From 9751f5a9066897a421c90d12a2cfe26bd7ad3ee0 Mon Sep 17 00:00:00 2001 From: jessiegibson Date: Wed, 21 Jan 2026 15:36:03 -0500 Subject: [PATCH 1/4] Updated Rust features in lib.rs and added the SQLNode.tsx --- bun.lock | 15 ++++ package.json | 1 + src-tauri/src/lib.rs | 6 +- src/App.css | 2 +- src/App.tsx | 157 ++++++++++++++++++++++++++++--------- src/components/SqlNode.tsx | 36 +++++++++ 6 files changed, 178 insertions(+), 39 deletions(-) create mode 100644 src/components/SqlNode.tsx diff --git a/bun.lock b/bun.lock index 0d2c223..c3d657c 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "local_data", "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.10.0", @@ -129,6 +130,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.2", "", { "os": "android", "cpu": "arm" }, "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA=="], @@ -235,6 +240,8 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="], @@ -277,6 +284,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], @@ -301,6 +310,10 @@ "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -329,6 +342,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], diff --git a/package.json b/package.json index d581e49..9b33eeb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "tauri": "tauri" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2", "@tauri-apps/plugin-opener": "^2", "@xyflow/react": "^12.10.0", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 665e597..9b521d7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,7 +18,7 @@ async fn get_csv_schema(path: String) -> Result { // 1. Open a temporary in-memory DuckDB connection let conn = Connection::open_in_memory().map_err(|e| e.to_string())?; - let query = format!("DESCRIBE SELECT * FROM read_csv_auto('{}' LIMIT 1", path); + let query = format!("DESCRIBE (SELECT * FROM read_csv_auto('{}'));", path); let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?; @@ -42,12 +42,12 @@ async fn get_csv_schema(path: String) -> Result { Ok(TableSchema { columns, column_types: types, - row_count_estimate: 0, \\ We can add count logic later + row_count_estimate: 0, // We can add count logic later }) } // Ensure the command is registered -/#[cfg_attr(mobile, tauri::mobile_entry_point)] +#[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) diff --git a/src/App.css b/src/App.css index f57cebb..1b66de8 100644 --- a/src/App.css +++ b/src/App.css @@ -4,7 +4,6 @@ :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - color-scheme: light; } body, html, #root { @@ -13,6 +12,7 @@ body, html, #root { width: 100vw; height: 100vh; overflow: hidden; /* This is crucial for the "Desktop" feel */ + background-color: #fff; } diff --git a/src/App.tsx b/src/App.tsx index fb1abe0..5d8360e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,9 +7,18 @@ import { addEdge, Connection, Edge, + BackgroundVariant, + Controls, } from '@xyflow/react'; -import { listen } from '@tauri-apps/api/event'; // Tauri's event listener +import { listen } from '@tauri-apps/api/event'; import '@xyflow/react/dist/style.css'; +import { invoke } from '@tauri-apps/api/core'; +import SqlNode from './components/SqlNode'; + +// 1. Fix: Ensure keys match the component names correctly +const nodeTypes = { + sqlNode: SqlNode, +}; const initialNodes: any[] = []; const initialEdges: Edge[] = []; @@ -18,64 +27,142 @@ export default function App() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - // 1. Function to create a node when a file is dropped - const addFileNode = (filePath: string, x: number, y: number) => { - const fileName = filePath.split('/').pop(); // Get 'data.csv' from the full path - + const addSqlNode = () => { const newNode = { - id: `node-${Date.now()}`, - type: 'default', - position: { x, y }, - data: { label: `📄 ${fileName}` }, - style: { - background: '#1e293b', - color: '#fff', - border: '1px solid #64748b', - padding: '10px', - borderRadius: '8px' - }, + // Fix: Use backticks for template literals + id: `sql-${Date.now()}`, + type: 'sqlNode', + position: { x: 500, y: 300 }, + data: { label: 'SQL Query' }, }; - setNodes((nds) => nds.concat(newNode)); }; - // 2. Listen for Native macOS File Drops - useEffect(() => { - // This listens to the "tauri://drag-drop" event emitted by the OS - const unlisten = listen<{ paths: string[], x: number, y: number }>('tauri://drag-drop', (event) => { - const { paths, x, y } = event.payload; + const addFileNode = async (filePath: string, x: number, y: number) => { + try { + const schema = await invoke<{ columns: string[], column_types: string[] }>( + 'get_csv_schema', + { path: filePath } + ); + + const fileName = filePath.split('/').pop(); - paths.forEach((path) => { - // Only accept data files for now - if (path.endsWith('.csv') || path.endsWith('.parquet') || path.endsWith('.json')) { - addFileNode(path, x, y); - console.log("File dropped at path:", path); - } + const newNode = { + id: `node-${Date.now()}`, + type: 'default', + position: { x, y }, + data: { + path: filePath, + label: ( +
+
📄 {fileName}
+
+ {schema.columns.map((col, i) => ( +
+ {col} + {schema.column_types[i]} +
+ ))} +
+
+ ) + }, + style: { + background: '#fff', + color: '#1e293b', + border: '1px solid #cbd5e1', + padding: '12px', + borderRadius: '8px', + width: 250, + boxShadow: "0 10px 15px -3px rgb(0 0 0 / 0.1)" // Soft Shadow + }, + }; + + setNodes((nds) => nds.concat(newNode)); + } catch (err) { + console.error("Failed to read CSV schema:", err); + } + }; + + useEffect(() => { + const listenToDrops = async () => { + const unlisten = await listen<{ paths: string[], x: number, y: number }>('tauri://drag-drop', (event) => { + const { paths, x, y } = event.payload; + paths.forEach((path) => { + if (path.endsWith('.csv') || path.endsWith('.parquet') || path.endsWith('.json')) { + addFileNode(path, x, y); + } + }); }); - }); + return unlisten; + }; + + const unlistenPromise = listenToDrops(); return () => { - unlisten.then((f) => f()); + unlistenPromise.then((f) => f()); }; }, []); const onConnect = useCallback( - (params: Connection) => setEdges((eds) => addEdge(params, eds)), - [setEdges] + (params: Connection) => { + setEdges((eds) => addEdge(params, eds)); + + const sourceNode = nodes.find((n) => n.id === params.source); + const targetNode = nodes.find((n) => n.id === params.target); + + // if we are connecting a File -> SQL Node + if (sourceNode && targetNode && targetNode.type === 'SqlNode') { + const filePath = sourceNode.data.path; // We need to make sure path is stored here + const fileName = filePath.split('/').pop().replace(/\.[^/.]+$/, ""); // Clean Name + + // Update the SQL Node's internal data + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + inputPath: filePath, + tableName: fileName, + }, + }; + } + return node; + }) + ); + console.log(`Linked ${fileName} to SQL Node ${params.target}`); + } + }, + [nodes, setNodes, setEdges] ); return ( -
+
+ - + +
diff --git a/src/components/SqlNode.tsx b/src/components/SqlNode.tsx new file mode 100644 index 0000000..fd124d6 --- /dev/null +++ b/src/components/SqlNode.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import Editor from '@monaco-editor/react'; + +export default function SqlNode({ data, id }: any) { + const [code, setCode] = useState("SELECT * FROM input LIMIT 10"); + + return ( + /* Added h-[300px] to give the node a rigid size so React Flow can calculate its position */ +
+ + +
+ SQL Transform + +
+ + {/* flex-grow ensures the editor fills the 300px height */} +
+ +
+ + +
+ ); +}; From 014f026b01310edf434d981758b8b43c1bd6481f Mon Sep 17 00:00:00 2001 From: jessiegibson Date: Wed, 21 Jan 2026 16:02:23 -0500 Subject: [PATCH 2/4] Updated the README.md file --- README.md | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 102e366..e527c61 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,28 @@ -# Tauri + React + Typescript +# Local Data Visualization Tool + +I am working on a MacOS desktop application for data cleaning and analytics. This data tool allows users to go from raw data to insights faster. This application is for anyone that builds decks for clients and would like better visualizations. + + +## Essential Visualizations + - Bar & Column Charts: Vertical and Horizontal, with support for stacking (stacked or 100% stacked_ and grouping. + - **Line & Area Charts:** Used for time-series and trend analysis. + - **Scatter Plots:** Used when showing relationships and distributions between two numeric variables. + - **Histograms:** For frequency distribution analysis. + - **Tables & Pivot Tables:** High-Performance grids for raw data exploration. + +2. Advanced & "Infinite Canvas" Visuals +- **Metric Trees / Maps:** Visualizing KPIs in a hierarchical flow (e.g., Revenue -> Orders -> AOV). +- **Funnel Charts:** Specifically designed for marketing and product onboarding flows (e.g., Visit -> Signup -> Purchase). +- **Heat Maps:** For matrix-style data density visualization. +- **Big Number (KPI) Cards:** Large, scaled-up numbers for top-level metrics. +- **Sankey / Flow Diagrams:** Showing the movement of data or users between states. + +3. Specialized Analytical Templates +- **Customer Journey Maps:** Combining charts with sticky notes and flow arrows. +- **A/B Test Visuals:** Specialized views to compare control vs. variant groups. +- **GA4 / Website Analytics Maps:* Pre-built flows for web traffic. +- **OKRs & Alignment Canvases:** Mixing data with strategic planning shapes. + -This template should help get you started developing with Tauri, React and Typescript in Vite. -## Recommended IDE Setup -- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) From f26d4f9a06ad3f78f0a88c0db73c3703f7e7de58 Mon Sep 17 00:00:00 2001 From: jessiegibson Date: Wed, 21 Jan 2026 16:13:39 -0500 Subject: [PATCH 3/4] Fixed formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e527c61..80634b4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ I am working on a MacOS desktop application for data cleaning and analytics. Thi ## Essential Visualizations - - Bar & Column Charts: Vertical and Horizontal, with support for stacking (stacked or 100% stacked_ and grouping. + - **Bar & Column Charts:** Vertical and Horizontal, with support for stacking (stacked or 100% stacked_ and grouping. - **Line & Area Charts:** Used for time-series and trend analysis. - **Scatter Plots:** Used when showing relationships and distributions between two numeric variables. - **Histograms:** For frequency distribution analysis. @@ -20,7 +20,7 @@ I am working on a MacOS desktop application for data cleaning and analytics. Thi 3. Specialized Analytical Templates - **Customer Journey Maps:** Combining charts with sticky notes and flow arrows. - **A/B Test Visuals:** Specialized views to compare control vs. variant groups. -- **GA4 / Website Analytics Maps:* Pre-built flows for web traffic. +- **GA4 / Website Analytics Maps:** Pre-built flows for web traffic. - **OKRs & Alignment Canvases:** Mixing data with strategic planning shapes. From acd6ab88e8833aacbbcd22fadb62b44f9a262291 Mon Sep 17 00:00:00 2001 From: jessiegibson Date: Thu, 22 Jan 2026 19:09:15 -0500 Subject: [PATCH 4/4] Add SQL execution and visualization features - Add execute_sql Rust command for running queries via DuckDB - Update SqlNode to execute queries and display results in table - Add ChartNode component with bar, line, area, pie, scatter charts - Configure Monaco Editor to load locally (offline support) - Add dynamic status bar showing canvas context - Wire up data flow from SQL nodes to Chart nodes Co-Authored-By: Claude Opus 4.5 --- bun.lock | 66 +++++++++++ package.json | 2 + src-tauri/src/lib.rs | 99 +++++++++++++++- src/App.tsx | 217 +++++++++++++++++++++++++++------- src/components/ChartNode.tsx | 221 +++++++++++++++++++++++++++++++++++ src/components/SqlNode.tsx | 170 ++++++++++++++++++++++----- src/main.tsx | 5 + 7 files changed, 704 insertions(+), 76 deletions(-) create mode 100644 src/components/ChartNode.tsx diff --git a/bun.lock b/bun.lock index c3d657c..98686f5 100644 --- a/bun.lock +++ b/bun.lock @@ -11,8 +11,10 @@ "@xyflow/react": "^12.10.0", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "monaco-editor": "^0.55.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.10", }, @@ -134,6 +136,8 @@ "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.2", "", { "os": "android", "cpu": "arm" }, "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA=="], @@ -186,6 +190,10 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@tauri-apps/api": ["@tauri-apps/api@2.9.1", "", {}, "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.9.6", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.9.6", "@tauri-apps/cli-darwin-x64": "2.9.6", "@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6", "@tauri-apps/cli-linux-arm64-gnu": "2.9.6", "@tauri-apps/cli-linux-arm64-musl": "2.9.6", "@tauri-apps/cli-linux-riscv64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-gnu": "2.9.6", "@tauri-apps/cli-linux-x64-musl": "2.9.6", "@tauri-apps/cli-win32-arm64-msvc": "2.9.6", "@tauri-apps/cli-win32-ia32-msvc": "2.9.6", "@tauri-apps/cli-win32-x64-msvc": "2.9.6" }, "bin": { "tauri": "tauri.js" } }, "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw=="], @@ -222,14 +230,28 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], @@ -242,6 +264,8 @@ "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="], @@ -264,6 +288,8 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], @@ -272,10 +298,22 @@ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], @@ -284,14 +322,20 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], "electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="], + "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -300,6 +344,10 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -332,8 +380,20 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], + "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], + + "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="], + + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "rollup": ["rollup@4.55.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.2", "@rollup/rollup-android-arm64": "4.55.2", "@rollup/rollup-darwin-arm64": "4.55.2", "@rollup/rollup-darwin-x64": "4.55.2", "@rollup/rollup-freebsd-arm64": "4.55.2", "@rollup/rollup-freebsd-x64": "4.55.2", "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", "@rollup/rollup-linux-arm-musleabihf": "4.55.2", "@rollup/rollup-linux-arm64-gnu": "4.55.2", "@rollup/rollup-linux-arm64-musl": "4.55.2", "@rollup/rollup-linux-loong64-gnu": "4.55.2", "@rollup/rollup-linux-loong64-musl": "4.55.2", "@rollup/rollup-linux-ppc64-gnu": "4.55.2", "@rollup/rollup-linux-ppc64-musl": "4.55.2", "@rollup/rollup-linux-riscv64-gnu": "4.55.2", "@rollup/rollup-linux-riscv64-musl": "4.55.2", "@rollup/rollup-linux-s390x-gnu": "4.55.2", "@rollup/rollup-linux-x64-gnu": "4.55.2", "@rollup/rollup-linux-x64-musl": "4.55.2", "@rollup/rollup-openbsd-x64": "4.55.2", "@rollup/rollup-openharmony-arm64": "4.55.2", "@rollup/rollup-win32-arm64-msvc": "4.55.2", "@rollup/rollup-win32-ia32-msvc": "4.55.2", "@rollup/rollup-win32-x64-gnu": "4.55.2", "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -348,6 +408,8 @@ "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], @@ -356,12 +418,16 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="], + "@reduxjs/toolkit/immer": ["immer@11.1.3", "", {}, "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="], + "@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], } } diff --git a/package.json b/package.json index 9b33eeb..b85f441 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "@xyflow/react": "^12.10.0", "clsx": "^2.1.1", "lucide-react": "^0.562.0", + "monaco-editor": "^0.55.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "recharts": "^3.7.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.10" }, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9b521d7..e914e1b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,17 +1,26 @@ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // -use duckdb::Connection; -use serde::{Deserialize, Serialize}; +use duckdb::{Connection, types::Value}; +use serde::Serialize; +use serde_json::json; // This is the data structure we send back to React. #[derive(Serialize)] - pub struct TableSchema { columns: Vec, column_types: Vec, row_count_estimate: usize, } +// Result structure for SQL query execution +#[derive(Serialize)] +pub struct QueryResult { + columns: Vec, + column_types: Vec, + rows: Vec>, + row_count: usize, +} + #[tauri::command] async fn get_csv_schema(path: String) -> Result { @@ -46,12 +55,94 @@ async fn get_csv_schema(path: String) -> Result { }) } +#[tauri::command] +async fn execute_sql(query: String, file_path: String, table_name: String) -> Result { + // Open in-memory DuckDB connection + let conn = Connection::open_in_memory().map_err(|e| e.to_string())?; + + // Register the CSV file as a table with the given name + let create_view = format!( + "CREATE VIEW {} AS SELECT * FROM read_csv_auto('{}');", + table_name, file_path + ); + conn.execute(&create_view, []).map_err(|e| e.to_string())?; + + // Execute the user's query + let mut stmt = conn.prepare(&query).map_err(|e| e.to_string())?; + + // Get column information + let column_count = stmt.column_count(); + let columns: Vec = (0..column_count) + .map(|i| stmt.column_name(i).map(|s| s.to_string()).unwrap_or_else(|_| "unknown".to_string())) + .collect(); + + // Execute and collect rows + let mut rows: Vec> = Vec::new(); + let mut result_rows = stmt.query([]).map_err(|e| e.to_string())?; + + // Collect column types from first row (we'll fill this in) + let mut column_types: Vec = vec!["unknown".to_string(); column_count]; + + while let Some(row) = result_rows.next().map_err(|e| e.to_string())? { + let mut row_data: Vec = Vec::new(); + + for i in 0..column_count { + let value: Value = row.get(i).unwrap_or(Value::Null); + let json_value = match &value { + Value::Null => serde_json::Value::Null, + Value::Boolean(b) => json!(*b), + Value::TinyInt(n) => json!(*n), + Value::SmallInt(n) => json!(*n), + Value::Int(n) => json!(*n), + Value::BigInt(n) => json!(*n), + Value::HugeInt(n) => json!(n.to_string()), + Value::UTinyInt(n) => json!(*n), + Value::USmallInt(n) => json!(*n), + Value::UInt(n) => json!(*n), + Value::UBigInt(n) => json!(*n), + Value::Float(f) => json!(*f), + Value::Double(d) => json!(*d), + Value::Text(s) => json!(s), + Value::Blob(b) => json!(format!("[blob: {} bytes]", b.len())), + _ => json!(format!("{:?}", value)), + }; + + // Update column type based on value + if rows.is_empty() { + column_types[i] = match &value { + Value::Null => "NULL".to_string(), + Value::Boolean(_) => "BOOLEAN".to_string(), + Value::TinyInt(_) | Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) => "INTEGER".to_string(), + Value::HugeInt(_) => "HUGEINT".to_string(), + Value::UTinyInt(_) | Value::USmallInt(_) | Value::UInt(_) | Value::UBigInt(_) => "UINTEGER".to_string(), + Value::Float(_) | Value::Double(_) => "DOUBLE".to_string(), + Value::Text(_) => "VARCHAR".to_string(), + Value::Blob(_) => "BLOB".to_string(), + _ => "UNKNOWN".to_string(), + }; + } + + row_data.push(json_value); + } + rows.push(row_data); + } + + let row_count = rows.len(); + + Ok(QueryResult { + columns, + column_types, + rows, + row_count, + }) +} + // Ensure the command is registered #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![get_csv_schema]) // Register here! + .invoke_handler(tauri::generate_handler![get_csv_schema, execute_sql]) // Register here! .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/App.tsx b/src/App.tsx index 5d8360e..3751b2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { ReactFlow, Background, @@ -14,10 +14,11 @@ import { listen } from '@tauri-apps/api/event'; import '@xyflow/react/dist/style.css'; import { invoke } from '@tauri-apps/api/core'; import SqlNode from './components/SqlNode'; +import ChartNode from './components/ChartNode'; -// 1. Fix: Ensure keys match the component names correctly const nodeTypes = { sqlNode: SqlNode, + chartNode: ChartNode, }; const initialNodes: any[] = []; @@ -27,13 +28,34 @@ export default function App() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + // Callback for SQL nodes to update their results in the node data + const updateNodeData = useCallback((nodeId: string, newData: Record) => { + setNodes((nds) => + nds.map((node) => { + if (node.id === nodeId) { + return { ...node, data: { ...node.data, ...newData } }; + } + return node; + }) + ); + }, [setNodes]); + const addSqlNode = () => { const newNode = { - // Fix: Use backticks for template literals id: `sql-${Date.now()}`, type: 'sqlNode', position: { x: 500, y: 300 }, - data: { label: 'SQL Query' }, + data: { label: 'SQL Query', updateNodeData }, + }; + setNodes((nds) => nds.concat(newNode)); + }; + + const addChartNode = () => { + const newNode = { + id: `chart-${Date.now()}`, + type: 'chartNode', + position: { x: 800, y: 300 }, + data: {}, }; setNodes((nds) => nds.concat(newNode)); }; @@ -104,48 +126,122 @@ export default function App() { }; }, []); + // Propagate query results from SQL nodes to connected chart nodes + useEffect(() => { + const chartNodes = nodes.filter((n) => n.type === 'chartNode' && n.data.sourceNodeId); + + chartNodes.forEach((chartNode) => { + const sourceNode = nodes.find((n) => n.id === chartNode.data.sourceNodeId); + if (sourceNode && sourceNode.data.queryResults) { + // Only update if results have changed + if (JSON.stringify(chartNode.data.queryResults) !== JSON.stringify(sourceNode.data.queryResults)) { + setNodes((nds) => + nds.map((node) => { + if (node.id === chartNode.id) { + return { + ...node, + data: { + ...node.data, + queryResults: sourceNode.data.queryResults, + }, + }; + } + return node; + }) + ); + } + } + }); + }, [nodes, setNodes]); + const onConnect = useCallback( - (params: Connection) => { - setEdges((eds) => addEdge(params, eds)); - - const sourceNode = nodes.find((n) => n.id === params.source); - const targetNode = nodes.find((n) => n.id === params.target); - - // if we are connecting a File -> SQL Node - if (sourceNode && targetNode && targetNode.type === 'SqlNode') { - const filePath = sourceNode.data.path; // We need to make sure path is stored here - const fileName = filePath.split('/').pop().replace(/\.[^/.]+$/, ""); // Clean Name - - // Update the SQL Node's internal data - setNodes((nds) => - nds.map((node) => { - if (node.id === params.target) { - return { - ...node, - data: { - ...node.data, - inputPath: filePath, - tableName: fileName, - }, - }; - } - return node; - }) - ); - console.log(`Linked ${fileName} to SQL Node ${params.target}`); - } - }, - [nodes, setNodes, setEdges] + (params: Connection) => { + setEdges((eds) => addEdge(params, eds)); + + const sourceNode = nodes.find((n) => n.id === params.source); + const targetNode = nodes.find((n) => n.id === params.target); + + // File -> SQL Node connection + if (sourceNode && targetNode && targetNode.type === 'sqlNode' && sourceNode.data.path) { + const filePath = sourceNode.data.path; + const fileName = filePath.split('/').pop().replace(/\.[^/.]+$/, ""); + + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + inputPath: filePath, + tableName: fileName, + }, + }; + } + return node; + }) + ); + console.log(`Linked ${fileName} to SQL Node ${params.target}`); + } + + // SQL Node -> Chart Node connection + if (sourceNode && targetNode && sourceNode.type === 'sqlNode' && targetNode.type === 'chartNode') { + // Pass query results from SQL node to Chart node + const queryResults = sourceNode.data.queryResults; + if (queryResults) { + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + queryResults, + sourceNodeId: params.source, + }, + }; + } + return node; + }) + ); + } + // Store the connection so we can update chart when SQL results change + setNodes((nds) => + nds.map((node) => { + if (node.id === params.target) { + return { + ...node, + data: { + ...node.data, + sourceNodeId: params.source, + }, + }; + } + return node; + }) + ); + console.log(`Linked SQL Node ${params.source} to Chart Node ${params.target}`); + } + }, + [nodes, setNodes, setEdges] ); return (
- +
+ + +
-
- Status: Listening for CSV drops... -
+ +
+ ); +} + +function StatusBar({ nodes }: { nodes: any[] }) { + const status = useMemo(() => { + const fileNodes = nodes.filter((n) => n.type === 'default'); + const sqlNodes = nodes.filter((n) => n.type === 'sqlNode'); + const chartNodes = nodes.filter((n) => n.type === 'chartNode'); + const sqlWithResults = sqlNodes.filter((n) => n.data.queryResults); + + if (nodes.length === 0) { + return { text: 'Drop a CSV file to get started', color: 'bg-slate-600' }; + } + + if (fileNodes.length > 0 && sqlNodes.length === 0) { + return { text: `${fileNodes.length} file(s) loaded - Add a SQL node to query`, color: 'bg-blue-600' }; + } + + if (sqlNodes.length > 0 && sqlWithResults.length === 0) { + return { text: `${sqlNodes.length} SQL node(s) - Connect a file and run a query`, color: 'bg-amber-600' }; + } + + const parts: string[] = []; + if (fileNodes.length > 0) parts.push(`${fileNodes.length} file(s)`); + if (sqlWithResults.length > 0) parts.push(`${sqlWithResults.length} query result(s)`); + if (chartNodes.length > 0) parts.push(`${chartNodes.length} chart(s)`); + + return { text: parts.join(' | '), color: 'bg-green-600' }; + }, [nodes]); + + return ( +
+ + {status.text}
); } diff --git a/src/components/ChartNode.tsx b/src/components/ChartNode.tsx new file mode 100644 index 0000000..a7fbba6 --- /dev/null +++ b/src/components/ChartNode.tsx @@ -0,0 +1,221 @@ +import { useState, useEffect } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import { + BarChart, + Bar, + LineChart, + Line, + AreaChart, + Area, + PieChart, + Pie, + ScatterChart, + Scatter, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from 'recharts'; + +type ChartType = 'bar' | 'line' | 'area' | 'pie' | 'scatter'; + +interface ChartNodeData { + queryResults?: { + columns: string[]; + rows: any[][]; + row_count: number; + }; +} + +const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']; + +export default function ChartNode({ data }: { data: ChartNodeData }) { + const [chartType, setChartType] = useState('bar'); + const [xColumn, setXColumn] = useState(''); + const [yColumn, setYColumn] = useState(''); + + const { queryResults } = data; + const columns = queryResults?.columns || []; + + // Auto-select columns when data arrives + useEffect(() => { + if (columns.length > 0 && !xColumn) { + setXColumn(columns[0]); + if (columns.length > 1) { + setYColumn(columns[1]); + } + } + }, [columns, xColumn]); + + // Transform row data to recharts format + const chartData = queryResults?.rows.map((row) => { + const obj: Record = {}; + columns.forEach((col, i) => { + obj[col] = row[i]; + }); + return obj; + }) || []; + + const hasData = chartData.length > 0 && xColumn && yColumn; + + const renderChart = () => { + if (!hasData) { + return ( +
+ {!queryResults ? 'Connect a SQL node with results' : 'Select X and Y columns'} +
+ ); + } + + const commonProps = { + data: chartData, + margin: { top: 10, right: 10, left: 0, bottom: 0 }, + }; + + switch (chartType) { + case 'bar': + return ( + + + + + + + + + + ); + + case 'line': + return ( + + + + + + + + + + ); + + case 'area': + return ( + + + + + + + + + + ); + + case 'pie': + return ( + + + name} + labelLine={{ stroke: '#94a3b8' }} + fontSize={9} + > + {chartData.map((_, index) => ( + + ))} + + + + + ); + + case 'scatter': + return ( + + + + + + + + + + ); + + default: + return null; + } + }; + + return ( +
+ + + {/* Header */} +
+ Chart +
+ {(['bar', 'line', 'area', 'pie', 'scatter'] as ChartType[]).map((type) => ( + + ))} +
+
+ + {/* Column selectors */} + {columns.length > 0 && ( +
+
+ + +
+
+ + +
+
+ )} + + {/* Chart area */} +
+ {renderChart()} +
+ + +
+ ); +} diff --git a/src/components/SqlNode.tsx b/src/components/SqlNode.tsx index fd124d6..954fdeb 100644 --- a/src/components/SqlNode.tsx +++ b/src/components/SqlNode.tsx @@ -1,36 +1,150 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { Handle, Position } from '@xyflow/react'; import Editor from '@monaco-editor/react'; +import { invoke } from '@tauri-apps/api/core'; -export default function SqlNode({ data, id }: any) { +interface QueryResult { + columns: string[]; + column_types: string[]; + rows: any[][]; + row_count: number; +} + +interface SqlNodeData { + inputPath?: string; + tableName?: string; + updateNodeData?: (nodeId: string, data: Record) => void; +} + +export default function SqlNode({ data, id }: { data: SqlNodeData; id: string }) { const [code, setCode] = useState("SELECT * FROM input LIMIT 10"); + const [results, setResults] = useState(null); + const [error, setError] = useState(null); + const [isRunning, setIsRunning] = useState(false); + + const handleRun = async () => { + if (!data.inputPath || !data.tableName) { + setError("Connect a data file to this node first"); + return; + } + + setIsRunning(true); + setError(null); + + try { + // Replace "input" in query with the actual table name + const processedQuery = code.replace(/\binput\b/gi, data.tableName); + + const result = await invoke('execute_sql', { + query: processedQuery, + filePath: data.inputPath, + tableName: data.tableName, + }); + + setResults(result); + + // Update node data so results can flow to connected chart nodes + if (data.updateNodeData) { + data.updateNodeData(id, { queryResults: result }); + } + } catch (err) { + setError(err as string); + setResults(null); + } finally { + setIsRunning(false); + } + }; + + const hasResults = results && results.rows.length > 0; + const nodeHeight = hasResults ? 'h-[500px]' : 'h-[300px]'; return ( - /* Added h-[300px] to give the node a rigid size so React Flow can calculate its position */ -
- - -
- SQL Transform - -
+
+ - {/* flex-grow ensures the editor fills the 300px height */} -
- -
+
+
+ SQL Transform + {data.tableName && ( + + {data.tableName} + + )} +
+ +
+ +
+ setCode(value || '')} + options={{ + minimap: { enabled: false }, + automaticLayout: true, + scrollBeyondLastLine: false, + fontSize: 12, + lineNumbers: 'off', + }} + /> +
- -
- ); -}; + {/* Results / Error area */} +
+ {error && ( +
+ {error} +
+ )} + + {hasResults && ( +
+ + + + {results.columns.map((col, i) => ( + + ))} + + + + {results.rows.map((row, rowIdx) => ( + + {row.map((cell, cellIdx) => ( + + ))} + + ))} + +
+ {col} +
+ {cell === null ? null : String(cell)} +
+
+ {results.row_count} row{results.row_count !== 1 ? 's' : ''} +
+
+ )} + + {!error && !hasResults && !isRunning && ( +
+ {data.inputPath ? 'Click RUN to execute query' : 'Connect a data file to get started'} +
+ )} +
+ + +
+ ); +} diff --git a/src/main.tsx b/src/main.tsx index 2be325e..367e7c5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,6 +1,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import { loader } from '@monaco-editor/react'; +import * as monaco from 'monaco-editor'; + +// Configure Monaco to use the local bundle instead of CDN +loader.config({ monaco }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(