From b54ce1b87ed8047f4b9be4bb6758ecb1bc1ea674 Mon Sep 17 00:00:00 2001 From: Sma1lboy <541898146chen@gmail.com> Date: Sun, 19 Oct 2025 00:16:14 -0700 Subject: [PATCH 1/6] feat: Integrate Sandpack code editor into RightPanel; add file management and editing capabilities with project structure visibility --- app/api/project/file/route.ts | 43 + app/api/project/files/route.ts | 35 + app/layout.tsx | 11 +- bun.lock | 103 +- components/preview/code-editor/index.ts | 3 + .../preview/code-editor/sandpack-editor.tsx | 298 ++++ components/preview/code-editor/types.ts | 11 + components/preview/right-panel.tsx | 9 +- components/providers.tsx | 28 + docs/2025-10-18/code_editor_integration.md | 410 ++++++ .../code_editor_solutions_comparison_cn.md | 1206 +++++++++++++++++ package.json | 1 + 12 files changed, 2147 insertions(+), 11 deletions(-) create mode 100644 app/api/project/file/route.ts create mode 100644 app/api/project/files/route.ts create mode 100644 components/preview/code-editor/index.ts create mode 100644 components/preview/code-editor/sandpack-editor.tsx create mode 100644 components/preview/code-editor/types.ts create mode 100644 components/providers.tsx create mode 100644 docs/2025-10-18/code_editor_integration.md create mode 100644 docs/2025-10-18/code_editor_solutions_comparison_cn.md diff --git a/app/api/project/file/route.ts b/app/api/project/file/route.ts new file mode 100644 index 0000000..64a6018 --- /dev/null +++ b/app/api/project/file/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ProjectManager } from '@/lib/project-manager'; + +const projectManager = ProjectManager.getInstance(); + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const projectId = searchParams.get('projectId'); + const filePath = searchParams.get('filePath'); + + if (!projectId || typeof projectId !== 'string') { + return NextResponse.json( + { success: false, error: 'projectId is required' }, + { status: 400 } + ); + } + + if (!filePath || typeof filePath !== 'string') { + return NextResponse.json( + { success: false, error: 'filePath is required' }, + { status: 400 } + ); + } + + const content = await projectManager.readFile(projectId, filePath); + + return NextResponse.json({ + success: true, + content + }); + } catch (error) { + console.error('[api/project/file] Error reading file:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} + diff --git a/app/api/project/files/route.ts b/app/api/project/files/route.ts new file mode 100644 index 0000000..ae21b5a --- /dev/null +++ b/app/api/project/files/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ProjectManager } from '@/lib/project-manager'; + +const projectManager = ProjectManager.getInstance(); + +export async function GET(req: NextRequest) { + try { + const { searchParams } = new URL(req.url); + const projectId = searchParams.get('projectId'); + + if (!projectId || typeof projectId !== 'string') { + return NextResponse.json( + { success: false, error: 'projectId is required' }, + { status: 400 } + ); + } + + const files = await projectManager.listFiles(projectId); + + return NextResponse.json({ + success: true, + files + }); + } catch (error) { + console.error('[api/project/files] Error listing files:', error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } +} + diff --git a/app/layout.tsx b/app/layout.tsx index 44a9227..d9e1dd7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { Toaster } from "@/components/ui/sonner"; -import { ThemeProvider } from "@/components/theme-provider"; +import { Providers } from "@/components/providers"; import "./globals.css"; const geistSans = Geist({ @@ -29,15 +29,10 @@ export default function RootLayout({ - + {children} - + ); diff --git a/bun.lock b/bun.lock index ff86733..df06193 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@ai-sdk/openai": "^2.0.49", "@ai-sdk/react": "^2.0.68", + "@codesandbox/sandpack-react": "^2.20.0", "@hookform/resolvers": "^5.2.2", "@openrouter/ai-sdk-provider": "^1.2.0", "@radix-ui/react-accordion": "^1.2.12", @@ -90,6 +91,30 @@ "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@codemirror/autocomplete": ["@codemirror/autocomplete@6.19.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-61Hfv3cF07XvUxNeC3E7jhG8XNi1Yom1G0lRC936oLnlF+jrbrv8rc/J98XlYzcsAoTVupfsf5fLej1aI8kyIg=="], + + "@codemirror/commands": ["@codemirror/commands@6.9.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-454TVgjhO6cMufsyyGN70rGIfJxJEjcqjBG2x2Y03Y/+Fm99d3O/Kv1QDYWuG6hvxsgmjXmBuATikIIYvERX+w=="], + + "@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="], + + "@codemirror/lang-html": ["@codemirror/lang-html@6.4.11", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw=="], + + "@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.4", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA=="], + + "@codemirror/language": ["@codemirror/language@6.11.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA=="], + + "@codemirror/lint": ["@codemirror/lint@6.9.0", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-wZxW+9XDytH3SKvS8cQzMyQCaaazH8XL1EMHleHe00wVzsv7NBQKVW2yzEHrRhmM7ZOhVdItPbvlRBvMp9ej7A=="], + + "@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="], + + "@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="], + + "@codesandbox/nodebox": ["@codesandbox/nodebox@0.1.8", "", { "dependencies": { "outvariant": "^1.4.0", "strict-event-emitter": "^0.4.3" } }, "sha512-2VRS6JDSk+M+pg56GA6CryyUSGPjBEe8Pnae0QL3jJF1mJZJVMDKr93gJRtBbLkfZN6LD/DwMtf+2L0bpWrjqg=="], + + "@codesandbox/sandpack-client": ["@codesandbox/sandpack-client@2.19.8", "", { "dependencies": { "@codesandbox/nodebox": "0.1.8", "buffer": "^6.0.3", "dequal": "^2.0.2", "mime-db": "^1.52.0", "outvariant": "1.4.0", "static-browser-server": "1.0.3" } }, "sha512-CMV4nr1zgKzVpx4I3FYvGRM5YT0VaQhALMW9vy4wZRhEyWAtJITQIqZzrTGWqB1JvV7V72dVEUCUPLfYz5hgJQ=="], + + "@codesandbox/sandpack-react": ["@codesandbox/sandpack-react@2.20.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.4.0", "@codemirror/commands": "^6.1.3", "@codemirror/lang-css": "^6.0.1", "@codemirror/lang-html": "^6.4.0", "@codemirror/lang-javascript": "^6.1.2", "@codemirror/language": "^6.3.2", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.7.1", "@codesandbox/sandpack-client": "^2.19.8", "@lezer/highlight": "^1.1.3", "@react-hook/intersection-observer": "^3.1.1", "@stitches/core": "^1.2.6", "anser": "^2.1.1", "clean-set": "^1.1.2", "dequal": "^2.0.2", "escape-carriage": "^1.3.1", "lz-string": "^1.4.4", "react-devtools-inline": "4.4.0", "react-is": "^17.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19", "react-dom": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-takd1YpW/PMQ6KPQfvseWLHWklJovGY8QYj8MtWnskGKbjOGJ6uZfyZbcJ6aCFLQMpNyjTqz9AKNbvhCOZ1TUQ=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], "@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="], @@ -192,6 +217,20 @@ "@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=="], + "@lezer/common": ["@lezer/common@1.3.0", "", {}, "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ=="], + + "@lezer/css": ["@lezer/css@1.3.0", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw=="], + + "@lezer/highlight": ["@lezer/highlight@1.2.2", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-z8TQwaBXXQIvG6i2g3e9cgMwUUXu9Ib7jo2qRRggdhwKpM56Dw3PM3wmexn+EGaaOZ7az0K7sjc3/gcGW7sz7A=="], + + "@lezer/html": ["@lezer/html@1.3.12", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw=="], + + "@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="], + + "@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="], + + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@next/env": ["@next/env@15.5.4", "", {}, "sha512-27SQhYp5QryzIT5uO8hq99C69eLQ7qkzkDPsk3N+GuS2XgOgoYEeOav7Pf8Tn4drECOVDsDg8oj+/DVy8qQL2A=="], @@ -222,6 +261,8 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@1.2.0", "", { "peerDependencies": { "ai": "^5.0.0", "zod": "^3.24.1 || ^v4" } }, "sha512-stuIwq7Yb7DNmk3GuCtz+oS3nZOY4TXEV3V5KsknDGQN7Fpu3KRMQVWRc1J073xKdf0FC9EHOctSyzsACmp5Ag=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], @@ -334,6 +375,10 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@react-hook/intersection-observer": ["@react-hook/intersection-observer@3.1.2", "", { "dependencies": { "@react-hook/passive-layout-effect": "^1.2.0", "intersection-observer": "^0.10.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ=="], + + "@react-hook/passive-layout-effect": ["@react-hook/passive-layout-effect@1.2.1", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.13.0", "", {}, "sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw=="], @@ -342,6 +387,8 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@stitches/core": ["@stitches/core@1.2.8", "", {}, "sha512-Gfkvwk9o9kE9r9XNBmJRfV8zONvXThnm1tcuojL04Uy5uRyqg93DC83lDebl0rocZCfKSjUv+fWYtMQmEDJldg=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], @@ -490,6 +537,8 @@ "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "anser": ["anser@2.3.2", "", {}, "sha512-PMqBCBvrOVDRqLGooQb+z+t1Q0PiPyurUQeZRR5uHBOVZcW8B04KMmnT12USnhpNX2wCPagWzLVppQMUG3u0Dw=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], @@ -528,10 +577,14 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -558,6 +611,8 @@ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "clean-set": ["clean-set@1.1.2", "", {}, "sha512-cA8uCj0qSoG9e0kevyOWXwPaELRPVg5Pxp6WskLMwerx257Zfnh8Nl0JBH59d7wQzij2CK7qEfJQK3RjuKKIug=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], @@ -572,10 +627,14 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + "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=="], @@ -634,6 +693,8 @@ "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], @@ -662,6 +723,14 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], + + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], + + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], + + "escape-carriage": ["escape-carriage@1.3.1", "", {}, "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], @@ -686,6 +755,8 @@ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], @@ -698,10 +769,14 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -794,6 +869,8 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -808,6 +885,8 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "intersection-observer": ["intersection-observer@0.10.0", "", {}, "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ=="], + "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="], "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="], @@ -938,6 +1017,8 @@ "lucide-react": ["lucide-react@0.545.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw=="], + "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], + "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1004,6 +1085,8 @@ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -1024,6 +1107,8 @@ "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -1044,6 +1129,8 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "outvariant": ["outvariant@1.4.0", "", {}, "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw=="], + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -1084,11 +1171,13 @@ "react-day-picker": ["react-day-picker@9.11.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw=="], + "react-devtools-inline": ["react-devtools-inline@4.4.0", "", { "dependencies": { "es6-symbol": "^3" } }, "sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ=="], + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], "react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="], - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -1168,8 +1257,12 @@ "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + "static-browser-server": ["static-browser-server@1.0.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.1.0", "dotenv": "^16.0.3", "mime-db": "^1.52.0", "outvariant": "^1.3.0" } }, "sha512-ZUyfgGDdFRbZGGJQ1YhiM930Yczz5VlbJObrQLlk24+qNHVQx4OlLcYswEUo3bIyNAbQUIUR9Yr5/Hqjzqb4zA=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "strict-event-emitter": ["strict-event-emitter@0.4.6", "", {}, "sha512-12KWeb+wixJohmnwNFerbyiBrAlq5qJLwIt38etRtKtmmHyDSoGlIqFE9wx+4IwG0aDjI7GV8tc8ZccjWZZtTg=="], + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -1188,6 +1281,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], + "style-to-js": ["style-to-js@1.1.18", "", { "dependencies": { "style-to-object": "1.0.11" } }, "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg=="], "style-to-object": ["style-to-object@1.0.11", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow=="], @@ -1228,6 +1323,8 @@ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -1274,6 +1371,8 @@ "victory-vendor": ["victory-vendor@36.9.2", "", { "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-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], @@ -1350,6 +1449,8 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "recharts/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="], "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], diff --git a/components/preview/code-editor/index.ts b/components/preview/code-editor/index.ts new file mode 100644 index 0000000..948f56a --- /dev/null +++ b/components/preview/code-editor/index.ts @@ -0,0 +1,3 @@ +export { SandpackEditor } from './sandpack-editor'; +export type { SandpackEditorProps, FileNode } from './types'; + diff --git a/components/preview/code-editor/sandpack-editor.tsx b/components/preview/code-editor/sandpack-editor.tsx new file mode 100644 index 0000000..3302574 --- /dev/null +++ b/components/preview/code-editor/sandpack-editor.tsx @@ -0,0 +1,298 @@ +'use client'; + +import { + SandpackLayout, + SandpackCodeEditor, + SandpackFileExplorer, + useSandpack +} from "@codesandbox/sandpack-react"; +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; +import type { SandpackEditorProps } from "./types"; + +// Mock data for testing +const MOCK_FILES = { + '/app/page.tsx': `export default function Home() { + return ( +
+
+

+ Welcome to CodeFox +

+

+ Start building amazing things with AI +

+ +
+
+ ); +}`, + '/app/layout.tsx': `export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +}`, + '/components/Button.tsx': `interface ButtonProps { + children: React.ReactNode; + onClick?: () => void; + variant?: 'primary' | 'secondary'; +} + +export function Button({ children, onClick, variant = 'primary' }: ButtonProps) { + const baseStyles = 'px-4 py-2 rounded-lg font-medium transition-colors'; + const variantStyles = { + primary: 'bg-blue-600 text-white hover:bg-blue-700', + secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300' + }; + + return ( + + ); +}`, + '/lib/utils.ts': `export function cn(...classes: string[]) { + return classes.filter(Boolean).join(' '); +} + +export function formatDate(date: Date) { + return new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }).format(date); +}`, + '/styles/globals.css': `@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +}`, + '/README.md': `# CodeFox Local Project + +This is a demo project generated by CodeFox. + +## Features + +- โœจ Modern React with TypeScript +- ๐ŸŽจ Tailwind CSS styling +- ๐Ÿš€ Fast development with Vite +- ๐Ÿ“ฆ Component-based architecture + +## Get Started + +\`\`\`bash +bun install +bun dev +\`\`\` + +Built with โค๏ธ by CodeFox AI +`, + '/package.json': `{ + "name": "codefox-project", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "next": "^14.0.0" + }, + "devDependencies": { + "typescript": "^5.0.0", + "@types/react": "^18.2.0", + "@types/node": "^20.0.0", + "tailwindcss": "^3.3.0" + } +}` +}; + +export function SandpackEditor({ projectId }: SandpackEditorProps) { + const { sandpack } = useSandpack(); + const [files, setFiles] = useState>({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function loadProjectFiles() { + // If no projectId, use mock data for testing + if (!projectId) { + console.log('[SandpackEditor] No projectId, using mock data for testing'); + setFiles(MOCK_FILES); + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + try { + // Get file list + const listResponse = await fetch( + `/api/project/files?projectId=${projectId}` + ); + + if (!listResponse.ok) { + throw new Error('Failed to load file list'); + } + + const { files: fileList } = await listResponse.json(); + + // Load content for each file + const fileContents: Record = {}; + + for (const filePath of fileList) { + try { + const response = await fetch( + `/api/project/file?projectId=${projectId}&filePath=${encodeURIComponent(filePath)}` + ); + + if (response.ok) { + const data = await response.json(); + // Sandpack expects files to start with / + const sandpackPath = filePath.startsWith('/') ? filePath : `/${filePath}`; + fileContents[sandpackPath] = data.content || ''; + } + } catch (err) { + console.warn(`Failed to load file ${filePath}:`, err); + } + } + + if (Object.keys(fileContents).length === 0) { + // Use mock data if no files found + console.log('[SandpackEditor] No files in project, using mock data'); + setFiles(MOCK_FILES); + } else { + setFiles(fileContents); + } + } catch (err) { + console.error('Failed to load project files:', err); + // Fallback to mock data on error + console.log('[SandpackEditor] Error loading files, using mock data'); + setFiles(MOCK_FILES); + } finally { + setLoading(false); + } + } + + loadProjectFiles(); + }, [projectId]); + + // Update sandpack context when files change + useEffect(() => { + if (Object.keys(files).length > 0 && sandpack) { + // Get current files from sandpack + const currentFiles = sandpack.files; + const currentFilePaths = Object.keys(currentFiles); + const newFilePaths = Object.keys(files); + + // Only update if files actually changed + const filesChanged = + currentFilePaths.length !== newFilePaths.length || + newFilePaths.some(path => currentFiles[path]?.code !== files[path]); + + if (filesChanged) { + // Delete removed files + currentFilePaths.forEach(path => { + if (!files[path]) { + sandpack.deleteFile(path); + } + }); + + // Update or add new files + Object.entries(files).forEach(([path, code]) => { + if (!currentFiles[path] || currentFiles[path].code !== code) { + sandpack.updateFile(path, code); + } + }); + + // Set first file as active if no active file + if (!sandpack.activeFile) { + const firstFile = Object.keys(files)[0]; + if (firstFile) { + sandpack.openFile(firstFile); + } + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [files]); + + if (loading) { + return ( +
+ +

Loading project files...

+

+ This may take a moment +

+
+ ); + } + + if (error) { + return ( +
+
+

Failed to load project

+

{error}

+
+
+ ); + } + + if (Object.keys(files).length === 0) { + return ( +
+
+

No files found

+

+ Start chatting to generate project files +

+
+
+ ); + } + + return ( + + + + + ); +} + diff --git a/components/preview/code-editor/types.ts b/components/preview/code-editor/types.ts new file mode 100644 index 0000000..89cf51d --- /dev/null +++ b/components/preview/code-editor/types.ts @@ -0,0 +1,11 @@ +export interface FileNode { + name: string; + path: string; + type: 'file' | 'folder'; + children?: FileNode[]; +} + +export interface SandpackEditorProps { + projectId: string; +} + diff --git a/components/preview/right-panel.tsx b/components/preview/right-panel.tsx index 81c6212..62e27b4 100644 --- a/components/preview/right-panel.tsx +++ b/components/preview/right-panel.tsx @@ -9,6 +9,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"; import { useProjectStore } from "@/lib/store"; import { Input } from "@/components/ui/input"; +import { SandpackEditor } from "@/components/preview/code-editor"; interface RightPanelProps { generatedCode?: string; @@ -28,6 +29,7 @@ export const RightPanel = forwardRef( const devServer = useProjectStore((state) => state.devServer); const previewUrl = devServer.serverUrl; const serverStatus = devServer.status; + const currentProject = useProjectStore((state) => state.getCurrentProject()); useImperativeHandle(ref, () => ({ setUrl: (url: string) => { @@ -188,8 +190,11 @@ export const RightPanel = forwardRef( {/* Body: Code Tab */} - - {generatedCode ? ( + + {/* Always show Sandpack in testing mode, pass projectId if available */} + {currentProject || true ? ( + + ) : generatedCode ? ( + + {children} + + + ); +} + diff --git a/docs/2025-10-18/code_editor_integration.md b/docs/2025-10-18/code_editor_integration.md new file mode 100644 index 0000000..dea842f --- /dev/null +++ b/docs/2025-10-18/code_editor_integration.md @@ -0,0 +1,410 @@ +# Code Editor Integration - File Tree & Editor Feature + +**Created:** 2024-10-19 +**Status:** Planning +**Priority:** High + +## ๐Ÿ“‹ Overview + +Add file tree structure display and code editing capabilities to the `RightPanel` component's Code Tab, transforming it from a read-only HTML viewer into a fully functional code editor. + +--- + +## ๐ŸŽฏ Requirements + +### Current State +- Code Tab only displays `generatedCode` (single HTML string) +- Uses `react-syntax-highlighter` for read-only display +- No file management or editing capability +- No project structure visibility + +### Target State +- **Left Panel (30%)**: Interactive file tree + - Display project directory structure + - Expand/collapse folders + - File icons based on extensions + - Click to select and open files + +- **Right Panel (70%)**: Code editor + - View and edit file contents + - Syntax highlighting for multiple languages + - Save changes back to project + - Support for multiple file tabs (optional) + +### Expected User Experience +Similar to VS Code, StackBlitz, or CodeSandbox embedded IDEs. + +--- + +## ๐Ÿ—๏ธ Technical Solutions Comparison + +### Option 1: Sandpack (Recommended - First Attempt) โญ + +**Package:** `@codesandbox/sandpack-react` + +**Pros:** +- โœ… **All-in-one solution** - File tree + Editor + Preview bundled +- โœ… **Production-ready** - Built and maintained by CodeSandbox team +- โœ… **Beautiful UI** - Polished design out of the box +- โœ… **Fast integration** - Can be implemented in 1-2 hours +- โœ… **Built-in bundler** - Can run code in browser sandbox +- โœ… **Customizable** - Can hide/show different panels + +**Cons:** +- โš ๏ธ May need style adjustments to match our theme +- โš ๏ธ Has its own bundler (might be overkill if we just want file editing) +- โš ๏ธ ~4MB bundle size + +**Installation:** +```bash +bun add @codesandbox/sandpack-react +``` + +**Basic Usage:** +```tsx +import { Sandpack } from "@codesandbox/sandpack-react"; + +Hello }", + "/styles.css": "body { margin: 0; }" + }} + template="react" + theme="dark" + options={{ + showNavigator: true, + showTabs: true, + showLineNumbers: true, + editorHeight: 600 + }} +/> +``` + +--- + +### Option 2: Monaco Editor + react-arborist (Fallback) + +**Packages:** +- `@monaco-editor/react` - VS Code's editor +- `react-arborist` - Modern file tree component + +**Pros:** +- โœ… **Full control** - Complete customization of every aspect +- โœ… **Monaco = VS Code** - Best-in-class editor experience +- โœ… **Native API integration** - Works directly with our `ProjectManager` +- โœ… **Lightweight file tree** - Modern, performant component + +**Cons:** +- โš ๏ธ More integration work (~4-6 hours) +- โš ๏ธ Need to write glue code between components +- โš ๏ธ ~3.5MB bundle size + +**Installation:** +```bash +bun add @monaco-editor/react react-arborist +``` + +--- + +### Option 3: WebContainer API (Future - Cloud Deployment) + +**Package:** `@webcontainer/api` + +**Use Case:** When we want to run a complete Node.js environment in the browser + +**Pros:** +- โœ… **Real Node.js in browser** - Can run `npm install`, dev servers +- โœ… **No backend required** - Everything runs client-side +- โœ… **Free** - No cloud costs +- โœ… **Sandboxed** - Each user has isolated environment + +**Cons:** +- โš ๏ธ Slower initial boot (~10s) +- โš ๏ธ More complex to integrate +- โš ๏ธ Requires significant architecture changes + +**Note:** This is for future consideration when deploying to cloud. See "Future Architecture" section below. + +--- + +## ๐Ÿš€ Implementation Plan (Phase 1: Sandpack) + +### Step 1: Setup & Installation + +```bash +bun add @codesandbox/sandpack-react +``` + +### Step 2: Create Sandpack Integration Component + +**File:** `components/preview/code-editor/sandpack-editor.tsx` + +```tsx +import { Sandpack, SandpackProvider, SandpackLayout } from "@codesandbox/sandpack-react"; +import { useProjectStore } from "@/lib/store"; +import { useEffect, useState } from "react"; + +interface SandpackEditorProps { + projectId: string; +} + +export function SandpackEditor({ projectId }: SandpackEditorProps) { + const [files, setFiles] = useState>({}); + const [loading, setLoading] = useState(true); + + // Load project files from backend + useEffect(() => { + async function loadFiles() { + setLoading(true); + try { + // Fetch file tree + const response = await fetch( + `/api/project?action=getFileTree&projectId=${projectId}` + ); + const data = await response.json(); + + // Load file contents + const fileContents: Record = {}; + for (const file of data.files) { + const res = await fetch( + `/api/project?action=readFile&projectId=${projectId}&filePath=${file.path}` + ); + const content = await res.json(); + fileContents[file.path] = content.content; + } + + setFiles(fileContents); + } catch (error) { + console.error("Failed to load files:", error); + } finally { + setLoading(false); + } + } + + loadFiles(); + }, [projectId]); + + if (loading) { + return
Loading project files...
; + } + + return ( + + ); +} +``` + +### Step 3: Update API Routes + +**File:** `app/api/project/route.ts` + +Add new actions: + +```typescript +// Get file tree structure +case 'getFileTree': { + const { projectId } = await req.json(); + const files = await projectManager.listFiles(projectId); + + // Convert flat list to tree structure + const tree = buildFileTree(files); + + return NextResponse.json({ tree }); +} + +// Read single file +case 'readFile': { + const { projectId, filePath } = await req.json(); + const content = await projectManager.readFile(projectId, filePath); + + return NextResponse.json({ content }); +} + +// writeFile already exists - can be reused for saving +``` + +### Step 4: Update RightPanel Component + +**File:** `components/preview/right-panel.tsx` + +Replace Code TabContent: + +```tsx + + {currentProject ? ( + + ) : ( +
+

No project loaded

+
+ )} +
+``` + +### Step 5: Handle File Changes + +Implement auto-save or manual save when user edits files in Sandpack: + +```tsx +// Listen to file changes in Sandpack + { + // Debounced save to backend + debouncedSaveFiles(projectId, files); + }} +/> +``` + +--- + +## ๐Ÿ“Š Implementation Checklist + +### Phase 1: Basic Integration +- [ ] Install Sandpack package +- [ ] Create `SandpackEditor` component +- [ ] Add `getFileTree` API endpoint +- [ ] Add `readFile` API endpoint +- [ ] Update `RightPanel` to use Sandpack +- [ ] Test with existing projects + +### Phase 2: Polish & Features +- [ ] Implement file save functionality +- [ ] Add loading states +- [ ] Handle errors gracefully +- [ ] Match Sandpack theme to app theme +- [ ] Add keyboard shortcuts (Cmd+S to save) +- [ ] Show unsaved changes indicator + +### Phase 3: Optimization +- [ ] Lazy load Sandpack (code splitting) +- [ ] Cache file contents +- [ ] Optimize file tree API (pagination) +- [ ] Add file search functionality + +--- + +## ๐ŸŽจ UI/UX Considerations + +### Layout +- Use existing `Tabs` component structure +- Sandpack fills entire Code tab content area +- Responsive design - collapse file tree on small screens + +### Theme Integration +- Configure Sandpack to use our dark/light theme +- Match colors to existing UI components +- Consistent font family (use our mono font) + +### Performance +- Lazy load Sandpack component (only when Code tab is active) +- Implement virtual scrolling for large file trees +- Debounce file save operations + +--- + +## ๐Ÿ”ฎ Future Enhancements (Phase 2+) + +### Short-term +- [ ] Multiple file tabs support +- [ ] File search (Cmd+P) +- [ ] Git integration indicator +- [ ] File/folder create/delete/rename +- [ ] Syntax error highlighting +- [ ] Code formatting (Prettier integration) + +### Long-term (Cloud Deployment) +- [ ] Migrate to WebContainer API for true Node.js support +- [ ] Real-time collaboration (multiplayer editing) +- [ ] Terminal integration +- [ ] NPM package installation UI +- [ ] Deploy button (Vercel/Netlify integration) + +--- + +## ๐ŸŒ Future Architecture: Cloud Deployment + +When moving to cloud/remote deployment, consider WebContainer: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Frontend (Vercel/Netlify) โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Chat โ”‚ WebContainer โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (Browser Sandbox) โ”‚ โ”‚ +โ”‚ โ”‚ AI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ†’ fs.writeFile() โ”‚ โ”‚ +โ”‚ โ”‚ generates โ”‚ npm install โ”‚ โ”‚ +โ”‚ โ”‚ code โ”‚ npm run dev โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + No backend server needed! +``` + +**Benefits:** +- Zero backend infrastructure costs +- Each user has isolated environment +- Real Node.js (npm, dev servers work) +- No server-side file system management + +**Migration Path:** +1. Replace `ProjectManager` calls with WebContainer API +2. Keep same chat interface +3. Files live in browser memory instead of server filesystem + +--- + +## โš ๏ธ Known Limitations & Considerations + +### Sandpack Limitations +1. **Bundle size**: ~4MB - may impact initial load time +2. **Customization**: Limited compared to building custom solution +3. **File operations**: May need to sync with backend filesystem + +### Technical Debt +- Current `generatedCode` prop in RightPanel will be deprecated +- Need to handle backwards compatibility during migration +- Consider how this affects existing projects in localStorage + +### Security +- Sandpack runs code in sandboxed iframe (secure) +- Still need to validate file paths on backend +- Prevent directory traversal attacks + +--- + +## ๐Ÿ“š Resources + +- [Sandpack Documentation](https://sandpack.codesandbox.io/) +- [Monaco Editor React](https://github.com/suren-atoyan/monaco-react) +- [react-arborist](https://github.com/brimdata/react-arborist) +- [WebContainer API](https://webcontainers.io/) + +--- + +## ๐Ÿค Decision Log + +**2024-10-19**: Decided to try Sandpack first +- Rationale: Fastest time to MVP, production-ready solution +- Fallback: Monaco + react-arborist if customization needed +- Future: Consider WebContainer for cloud deployment + +--- + +## ๐Ÿ“ Notes + +- This feature transforms CodeFox from a "HTML generator" to a "full-stack IDE" +- Aligns with our vision of AI-powered local development +- Key differentiator from competitors who only generate static code +- Opens door for future features: terminal, git integration, deploy + diff --git a/docs/2025-10-18/code_editor_solutions_comparison_cn.md b/docs/2025-10-18/code_editor_solutions_comparison_cn.md new file mode 100644 index 0000000..95f8697 --- /dev/null +++ b/docs/2025-10-18/code_editor_solutions_comparison_cn.md @@ -0,0 +1,1206 @@ +# ไปฃ็ ็ผ–่พ‘ๅ™จ้›†ๆˆๆ–นๆกˆๅฏนๆฏ”๏ผˆไธญๆ–‡่ฏฆ็ป†็‰ˆ๏ผ‰ + +**ๅˆ›ๅปบๆ—ถ้—ด๏ผš** 2024-10-19 +**็Šถๆ€๏ผš** ๆ–นๆกˆ่ฐƒ็ ” +**ไผ˜ๅ…ˆ็บง๏ผš** ้ซ˜ + +--- + +## ๐Ÿ“‹ ้œ€ๆฑ‚ๆฆ‚่ฟฐ + +ไธบ `RightPanel` ็ป„ไปถ็š„ Code Tab ๆทปๅŠ ๆ–‡ไปถๆ ‘ๅ’Œไปฃ็ ็ผ–่พ‘ๅŠŸ่ƒฝ๏ผŒไปŽๅช่ฏป็š„ HTML ๆŸฅ็œ‹ๅ™จๅ‡็บงไธบๅฎŒๆ•ด็š„ไปฃ็ ็ผ–่พ‘็Žฏๅขƒใ€‚ + +### ๆ ธๅฟƒ้œ€ๆฑ‚ +- **ๅทฆไพง๏ผˆ30%๏ผ‰**๏ผšๆ–‡ไปถๆ ‘๏ผˆๅฏๅฑ•ๅผ€/ๆ”ถ่ตท๏ผŒ็‚นๅ‡ปๆ‰“ๅผ€ๆ–‡ไปถ๏ผ‰ +- **ๅณไพง๏ผˆ70%๏ผ‰**๏ผšไปฃ็ ็ผ–่พ‘ๅ™จ๏ผˆๆŸฅ็œ‹ๅ’Œ็ผ–่พ‘๏ผŒไฟๅญ˜ๅˆฐ้กน็›ฎ๏ผ‰ +- **็›ฎๆ ‡ไฝ“้ชŒ**๏ผš็ฑปไผผ VS Codeใ€StackBlitzใ€CodeSandbox + +--- + +## ๐ŸŽฏ ไธ‰ๅคงๆŠ€ๆœฏๆ–นๆกˆๅฏนๆฏ” + +### ๆ–นๆกˆ 1๏ธโƒฃ๏ผšSandpack๏ผˆCodeSandbox ๅฎ˜ๆ–นๆ–นๆกˆ๏ผ‰ + +#### ๐Ÿ“ฆ ๅŸบๆœฌไฟกๆฏ + +```bash +npm install @codesandbox/sandpack-react +``` + +**ๅฎ˜็ฝ‘๏ผš** https://sandpack.codesandbox.io/ + +#### ๐Ÿ—๏ธ ๆžถๆž„ๅŽŸ็† + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ไฝ ็š„ React ็ป„ไปถ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Sandpack ๅ†…้ƒจ็ป„ไปถ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ File โ”‚ Editor โ”‚ Preview โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Tree โ”‚ (CM6) โ”‚ (iframe) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ ๅ†…็ฝฎ Bundler (ๅœจๆต่งˆๅ™จ่ฟ่กŒ) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**ๆ ธๅฟƒ็‰น็‚น๏ผš** +- ๐ŸŽ **ๅผ€็ฎฑๅณ็”จ**๏ผšๆ–‡ไปถๆ ‘ + ็ผ–่พ‘ๅ™จ + ้ข„่งˆๅ…จๅŒ…ๅซ +- ๐ŸŒ **ๆต่งˆๅ™จ็ผ–่ฏ‘**๏ผšไฝฟ็”จ่‡ชๅทฑ็š„ bundler ๅœจๆต่งˆๅ™จ็ผ–่ฏ‘ไปฃ็  +- ๐ŸŽจ **่ฎพ่ฎก็ฒพ็พŽ**๏ผšCodeSandbox ๅ›ข้˜Ÿ็ฒพๅฟƒๆ‰“็ฃจ็š„ UI +- โšก **ๅฎžๆ—ถ้ข„่งˆ**๏ผšไปฃ็ ๅ˜ๅŒ–็ซ‹ๅณๅๆ˜ ๅˆฐ้ข„่งˆ็ช—ๅฃ + +#### ๐Ÿ’ป ๅŸบ็ก€็”จๆณ• + +```tsx +import { Sandpack } from "@codesandbox/sandpack-react"; + +export function CodeEditor() { + return ( + Hello World + }`, + "/styles.css": `body { + margin: 0; + font-family: sans-serif; + }` + }} + template="react" + theme="dark" + options={{ + showNavigator: true, // ๆ˜พ็คบๆ–‡ไปถๅฏผ่ˆช + showTabs: true, // ๆ˜พ็คบๆ–‡ไปถๆ ‡็ญพ + showLineNumbers: true, // ๆ˜พ็คบ่กŒๅท + editorHeight: 600, // ็ผ–่พ‘ๅ™จ้ซ˜ๅบฆ + closableTabs: true, // ๅฏๅ…ณ้—ญๆ ‡็ญพ + }} + /> + ); +} +``` + +#### ๐Ÿ”„ ๅŠจๆ€ๆ›ดๆ–ฐๆ–‡ไปถ + +```tsx +import { + SandpackProvider, + SandpackLayout, + SandpackCodeEditor, + SandpackPreview, + useSandpack +} from "@codesandbox/sandpack-react"; + +function CodeEditorWithAI() { + const [files, setFiles] = useState({ + "/App.js": "// ๅˆๅง‹ไปฃ็ " + }); + + // AI ็”Ÿๆˆไปฃ็ ๅŽๆ›ดๆ–ฐ + const handleAIGenerate = (newCode: string) => { + setFiles({ + ...files, + "/App.js": newCode + }); + }; + + return ( + + + + + + + ); +} +``` + +#### โœ… ไผ˜็‚น + +1. **ๅฟซ้€Ÿ้›†ๆˆ** + - 1-2 ๅฐๆ—ถๅณๅฏๅฎŒๆˆๅŸบ็ก€้›†ๆˆ + - ๅ‡ ไนŽไธ้œ€่ฆ้…็ฝฎ + +2. **ๅŠŸ่ƒฝๅฎŒๆ•ด** + - ๆ–‡ไปถๆ ‘่‡ชๅŠจ็”Ÿๆˆ + - ๅคšๆ–‡ไปถๆ”ฏๆŒ + - ่ฏญๆณ•้ซ˜ไบฎ + - ้”™่ฏฏๆ็คบ + - ๅฎžๆ—ถ้ข„่งˆ + +3. **็”Ÿไบง็บง่ดจ้‡** + - CodeSandbox ๅ›ข้˜Ÿ็ปดๆŠค + - ่ขซๅคง้‡ๆ–‡ๆกฃ็ฝ‘็ซ™ไฝฟ็”จ๏ผˆReactใ€Vue ็ญ‰๏ผ‰ + - ็จณๅฎšๅฏ้  + +4. **ๅฏๅฎšๅˆถ** + - ๆ”ฏๆŒ่‡ชๅฎšไน‰ไธป้ข˜ + - ๅฏไปฅ้š่—/ๆ˜พ็คบไธๅŒ้ขๆฟ + - ๆ”ฏๆŒ่‡ชๅฎšไน‰ๆ–‡ไปถ็ณป็ปŸ + +#### โš ๏ธ ็ผบ็‚น + +1. **ไฝ“็งฏ่พƒๅคง** + - Bundle size: ~4MB + - ้ฆ–ๆฌกๅŠ ่ฝฝ่พƒๆ…ข + +2. **ๅ†…็ฝฎ Bundler** + - ๆœ‰่‡ชๅทฑ็š„็ผ–่ฏ‘้€ป่พ‘ + - ๅฆ‚ๆžœๅชๆƒณ็ผ–่พ‘ๆ–‡ไปถ๏ผˆไธ้œ€่ฆ้ข„่งˆ๏ผ‰ๅฏ่ƒฝ่ฟ‡ไบŽๅคๆ‚ + +3. **ๅฎšๅˆถๅ—้™** + - ๆทฑๅบฆๅฎšๅˆถๆฏ”่พƒๅ›ฐ้šพ + - UI ๆ ทๅผ่ฐƒๆ•ดๆœ‰้™ๅˆถ + +4. **ๆ–‡ไปถๅญ˜ๅ‚จ** + - ๆ–‡ไปถๅœจ Sandpack ๅ†…ๅญ˜ไธญ + - ้œ€่ฆๆ‰‹ๅŠจๅŒๆญฅๅˆฐๅŽ็ซฏ + +#### ๐ŸŽฏ ไธŽๆˆ‘ไปฌ้กน็›ฎ็š„้›†ๆˆ + +```tsx +// components/preview/code-editor/sandpack-editor.tsx +import { Sandpack } from "@codesandbox/sandpack-react"; +import { useEffect, useState } from "react"; + +interface SandpackEditorProps { + projectId: string; +} + +export function SandpackEditor({ projectId }: SandpackEditorProps) { + const [files, setFiles] = useState>({}); + + // 1. ไปŽๅŽ็ซฏๅŠ ่ฝฝๆ–‡ไปถ + useEffect(() => { + async function loadProjectFiles() { + const response = await fetch( + `/api/project?action=getFileTree&projectId=${projectId}` + ); + const data = await response.json(); + + // ๅŠ ่ฝฝๆ‰€ๆœ‰ๆ–‡ไปถๅ†…ๅฎน + const fileContents: Record = {}; + for (const file of data.files) { + const res = await fetch( + `/api/project?action=readFile&projectId=${projectId}&filePath=${file.path}` + ); + const content = await res.json(); + fileContents[file.path] = content.content; + } + + setFiles(fileContents); + } + + loadProjectFiles(); + }, [projectId]); + + // 2. ็›‘ๅฌๆ–‡ไปถๅ˜ๅŒ–๏ผŒไฟๅญ˜ๅˆฐๅŽ็ซฏ + const handleFileChange = async (updatedFiles: Record) => { + // ๆ‰พๅ‡บๅ˜ๅŒ–็š„ๆ–‡ไปถ + for (const [path, content] of Object.entries(updatedFiles)) { + if (files[path] !== content) { + // ไฟๅญ˜ๅˆฐๅŽ็ซฏ + await fetch('/api/project', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'writeFile', + projectId, + filePath: path, + content + }) + }); + } + } + setFiles(updatedFiles); + }; + + return ( + + ); +} +``` + +#### ๐Ÿ’ฐ ๆˆๆœฌ่ฏ„ไผฐ + +- **ๅผ€ๅ‘ๆˆๆœฌ**๏ผšโญ ้žๅธธไฝŽ๏ผˆ1-2 ๅฐๆ—ถ๏ผ‰ +- **็ปดๆŠคๆˆๆœฌ**๏ผšโญ ไฝŽ๏ผˆๅŸบๆœฌไธ้œ€่ฆ็ปดๆŠค๏ผ‰ +- **ๆ€ง่ƒฝๆˆๆœฌ**๏ผšโญโญโญ ไธญ๏ผˆ4MB bundle๏ผ‰ +- **็ตๆดปๆ€ง**๏ผšโญโญโญ ไธญ็ญ‰ + +--- + +### ๆ–นๆกˆ 2๏ธโƒฃ๏ผšWebContainer API๏ผˆStackBlitz ๆ–นๆกˆ๏ผ‰ + +#### ๐Ÿ“ฆ ๅŸบๆœฌไฟกๆฏ + +```bash +npm install @webcontainer/api +``` + +**ๅฎ˜็ฝ‘๏ผš** https://webcontainers.io/ + +#### ๐Ÿ—๏ธ ๆžถๆž„ๅŽŸ็† + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Browser Tab โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Main Thread (ไฝ ็š„ React ไปฃ็ ) โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ const container = โ”‚ โ”‚ +โ”‚ โ”‚ await WebContainer.boot() โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ await container.fs.writeFile(...) โ”‚ โ”‚ +โ”‚ โ”‚ await container.spawn('npm', [...]) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ postMessage โ”‚ +โ”‚ โ†“ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Web Worker (้š”็ฆป็บฟ็จ‹) โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿ—‚๏ธ Virtual File System (ๅ†…ๅญ˜) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ /app/page.tsx โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ /package.json โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ /node_modules/react/... โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐Ÿš€ Node.js Runtime (WASM) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - npm install โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - node ๅ‘ฝไปค โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - ๆจกๅ—่งฃๆž โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ ๐ŸŒ HTTP Server โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - Express/Vite dev server โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ””โ”€> localhost:3000 โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โš ๏ธ ๅ…จ้ƒจๅœจๅ†…ๅญ˜๏ผŒๅˆทๆ–ฐ้กต้ข = ๆ•ฐๆฎไธขๅคฑ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +#### ๐Ÿ”‘ ๆ ธๅฟƒ็‰น็‚น + +**่ฟ™ๆ˜ฏ็œŸๆญฃ็š„ Node.js ็Žฏๅขƒ๏ผ** +- โœ… ๅฏไปฅ่ฟ่กŒ `npm install` +- โœ… ๅฏไปฅๅฏๅŠจ dev server๏ผˆViteใ€Next.js ็ญ‰๏ผ‰ +- โœ… ๅฎŒๆ•ด็š„ๆ–‡ไปถ็ณป็ปŸ API +- โœ… ๅฏไปฅๆ‰ง่กŒไปปๆ„ shell ๅ‘ฝไปค +- โš ๏ธ ไฝ†ๆ˜ฏๅฎŒๅ…จ่ฟ่กŒๅœจๆต่งˆๅ™จไธญ๏ผ + +#### ๐Ÿ’ป ๅŸบ็ก€็”จๆณ• + +```typescript +import { WebContainer } from '@webcontainer/api'; + +// 1. ๅฏๅŠจ WebContainer๏ผˆ้ฆ–ๆฌก้œ€่ฆ 5-10 ็ง’๏ผ‰ +const container = await WebContainer.boot(); + +// 2. ๅˆ›ๅปบๆ–‡ไปถ +await container.fs.writeFile('/package.json', JSON.stringify({ + name: 'my-app', + scripts: { + dev: 'vite' + }, + dependencies: { + 'react': '^18.0.0', + 'vite': '^5.0.0' + } +})); + +await container.fs.writeFile('/index.html', ` + + + My App + +
+ + + +`); + +// 3. ๅฎ‰่ฃ…ไพ่ต– +const installProcess = await container.spawn('npm', ['install']); +await installProcess.exit; // ็ญ‰ๅพ…ๅฎ‰่ฃ…ๅฎŒๆˆ + +// 4. ๅฏๅŠจ dev server +const devProcess = await container.spawn('npm', ['run', 'dev']); + +// 5. ็›‘ๅฌๆœๅŠกๅ™จๅฏๅŠจ +container.on('server-ready', (port, url) => { + console.log(`Server running at ${url}`); + // ๅœจ iframe ไธญๆ˜พ็คบ + iframe.src = url; +}); +``` + +#### ๐Ÿ”„ ๅฎŒๆ•ด้›†ๆˆ็คบไพ‹ + +```tsx +// hooks/use-webcontainer.ts +import { WebContainer } from '@webcontainer/api'; +import { useEffect, useRef, useState } from 'react'; + +export function useWebContainer() { + const containerRef = useRef(null); + const [status, setStatus] = useState<'idle' | 'booting' | 'ready' | 'error'>('idle'); + const [serverUrl, setServerUrl] = useState(null); + + useEffect(() => { + async function initContainer() { + try { + setStatus('booting'); + + // ๅฏๅŠจ WebContainer + const container = await WebContainer.boot(); + containerRef.current = container; + + // ็›‘ๅฌๆœๅŠกๅ™จ + container.on('server-ready', (port, url) => { + setServerUrl(url); + }); + + setStatus('ready'); + } catch (error) { + console.error('Failed to boot WebContainer:', error); + setStatus('error'); + } + } + + initContainer(); + + return () => { + containerRef.current?.teardown(); + }; + }, []); + + return { + container: containerRef.current, + status, + serverUrl + }; +} + +// components/preview/webcontainer-editor.tsx +export function WebContainerEditor({ projectId }: { projectId: string }) { + const { container, status, serverUrl } = useWebContainer(); + const [files, setFiles] = useState>({}); + + // AI ็”Ÿๆˆไปฃ็ ๅŽๅ†™ๅ…ฅ + const handleAIGenerate = async (filePath: string, content: string) => { + if (!container) return; + + // 1. ๅ†™ๅ…ฅ WebContainer ๆ–‡ไปถ็ณป็ปŸ + await container.fs.writeFile(filePath, content); + + // 2. ๅŒๆญฅไฟๅญ˜ๅˆฐ IndexedDB๏ผˆๆŒไน…ๅŒ–๏ผ‰ + await saveToIndexedDB(projectId, filePath, content); + + // 3. ้˜ฒๆŠ–ๅŽไฟๅญ˜ๅˆฐๆœๅŠกๅ™จ๏ผˆๅฏ้€‰๏ผ‰ + debouncedSaveToBackend(projectId, filePath, content); + }; + + if (status === 'booting') { + return
ๆญฃๅœจๅฏๅŠจ WebContainer...
; + } + + if (status === 'error') { + return
ๅฏๅŠจๅคฑ่ดฅ๏ผŒ่ฏทๅˆทๆ–ฐ้กต้ข้‡่ฏ•
; + } + + return ( +
+ {/* ๆ–‡ไปถๆ ‘ */} + { + // ไปŽ WebContainer ่ฏปๅ–ๆ–‡ไปถ + container?.fs.readFile(path, 'utf-8').then(content => { + // ๆ˜พ็คบๅœจ็ผ–่พ‘ๅ™จ + }); + }} + /> + + {/* ็ผ–่พ‘ๅ™จ */} + { + handleAIGenerate(path, content); + }} + /> + + {/* ้ข„่งˆ */} + {serverUrl && ( +