From b44d057241fff842932d9d4384193a6bf8d8413c Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:02:16 +0800 Subject: [PATCH] feat: preview components --- .../app/features/platform/ScriptWorkspace.tsx | 89 ++++++++++++++----- .../platform/state/scriptWorkspaceStore.ts | 81 ++++++++++++++++- frontend/package.json | 3 +- frontend/pnpm-lock.yaml | 57 ++++++++++++ 4 files changed, 203 insertions(+), 27 deletions(-) diff --git a/frontend/app/features/platform/ScriptWorkspace.tsx b/frontend/app/features/platform/ScriptWorkspace.tsx index cce6111..d779be7 100644 --- a/frontend/app/features/platform/ScriptWorkspace.tsx +++ b/frontend/app/features/platform/ScriptWorkspace.tsx @@ -7,7 +7,10 @@ import type { } from "../../services/api"; import { scriptIcon } from "./WorkspaceTree"; import type { MouseEvent as ReactMouseEvent, RefObject } from "react"; -import { useRef, useEffect } from "react"; +import Editor from "@monaco-editor/react"; +import { useRef, useLayoutEffect } from "react"; + +import { useScriptWorkspaceStore } from "./state/scriptWorkspaceStore"; type ToastState = { tone: "success" | "error" | "info"; @@ -129,9 +132,9 @@ export function ScriptWorkspace({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth", }); - }; + }; - useEffect(() => { + useLayoutEffect(() => { const tabbar = tabbarRef.current; if (!tabbar) return; const activeTab = tabbar.querySelector(".editor-tab--active") as HTMLElement | null; @@ -232,13 +235,11 @@ export function ScriptWorkspace({ - {isEditing - ? isNotebook - ? "Demo 无锁模式 · Kernel 已连接" - : "Demo 无锁模式 · 编辑中" - : latestVersion + { + latestVersion ? `最新 ${latestVersion.version_label}` - : "工作副本已就绪"} + : "工作副本已就绪" + } @@ -378,13 +379,13 @@ export function ScriptWorkspace({ Python 预览 {isEditing ? "Jupyter 中可编辑" : "平台只读预览"} - + - {isEditing ? "Demo 无锁编辑会话已连接" : "工作副本已同步"} + {/*{isEditing ? "Demo 无锁编辑会话已连接" : "工作副本已同步"}*/} SHA-256 {shortHash(script.content_hash)} @@ -403,19 +404,59 @@ export function ScriptWorkspace({ ); } -function PythonPreview() { + +interface PythonPreviewProps { + workspaceId: string; + filePath: string; +} +export function PythonPreview({ + workspaceId, + filePath, +}: PythonPreviewProps) { + const previewKey = `${workspaceId}::${filePath}`; + const storeKey = useScriptWorkspaceStore((s) => s.previewKey); + const previewCode = useScriptWorkspaceStore((s) => s.previewCode); + const previewCodeSize = useScriptWorkspaceStore((s) => s.previewCodeSize); + const previewLoading = useScriptWorkspaceStore((s) => s.previewLoading); + const previewError = useScriptWorkspaceStore((s) => s.previewError); + const loadPreview = useScriptWorkspaceStore((s) => s.loadPreview); + + useLayoutEffect(() => { + void loadPreview(workspaceId, filePath); + }, [previewKey]); + + if (storeKey !== previewKey) { + return Loading...; + } + + if (previewLoading) { + return Loading...; + } + + if (previewError) { + return Failed to load: {previewError}; + } + return ( - - - 123456789 - - - """模型实验开发平台构建脚本。""" - {"\n\n"}def main() -> None: - {"\n"} print("Hello, Model Platform!") - {"\n\n\n"}if __name__ == "__main__": - {"\n"} main() - - + 1000, + }, + lineNumbers: "off", + folding: true, + wordWrap: "on", + contextmenu: false, + dragAndDrop: false, + automaticLayout: true, + renderLineHighlight: "none", + }} + /> ); } diff --git a/frontend/app/features/platform/state/scriptWorkspaceStore.ts b/frontend/app/features/platform/state/scriptWorkspaceStore.ts index 42cae9b..3d84283 100644 --- a/frontend/app/features/platform/state/scriptWorkspaceStore.ts +++ b/frontend/app/features/platform/state/scriptWorkspaceStore.ts @@ -8,7 +8,7 @@ import type { Visibility, WorkspaceBoundApi, WorkspaceDirectory, -} from "../../../services/api"; +} from "~/services/api"; import type { NewScriptForm } from "./uiStore"; import { useUiStore } from "./uiStore"; @@ -27,6 +27,8 @@ let _editSession: ActiveEditSession | null = null; let _editorOpening = false; let _editorOpenRequest = 0; let _api: WorkspaceBoundApi | null = null; +let _previewController: AbortController | null = null; +let _previewRequest = 0; export const bindScriptWorkspaceApi = (api: WorkspaceBoundApi | null) => { _api = api; @@ -60,6 +62,12 @@ type State = { latestVersion: LatestVersion | null; latestVersionLoading: boolean; + previewKey: string | null; + previewCode: string | null; + previewCodeSize: number | null; + previewLoading: boolean; + previewError: string | null; + // actions setApiOnline: (online: boolean) => void; setKeyword: (keyword: string) => void; @@ -72,6 +80,7 @@ type State = { openScriptEditor: (script: ScriptItem, showToast?: boolean) => Promise; endEditing: (closeTabFlag?: boolean, showToast?: boolean) => Promise; loadLatestVersion: (scriptId: string) => Promise; + loadPreview: (workspaceId: string, filePath: string) => Promise; createScript: (form: NewScriptForm) => Promise; uploadScripts: (files: File[], parentPath: string) => Promise; createFolder: (name: string, parentPath: string) => Promise; @@ -132,10 +141,21 @@ export const useScriptWorkspaceStore = create((set, get) => { latestVersion: null, latestVersionLoading: false, + previewKey: null, + previewCode: null, + previewCodeSize: null, + previewLoading: false, + previewError: null, + setApiOnline: (online) => set({ apiOnline: online }), setKeyword: (keyword) => set({ keyword }), reset: () => { + if (_previewController) { + _previewController.abort(); + _previewController = null; + } + _previewRequest += 1; _selectedId = null; _editSession = null; editSessionHandle.current = null; @@ -150,6 +170,11 @@ export const useScriptWorkspaceStore = create((set, get) => { editorOpenError: null, latestVersion: null, latestVersionLoading: false, + previewKey: null, + previewCode: null, + previewCodeSize: null, + previewLoading: false, + previewError: null, }); }, @@ -390,6 +415,58 @@ export const useScriptWorkspaceStore = create((set, get) => { } }, + loadPreview: async (workspaceId, filePath) => { + if (_previewController) { + _previewController.abort(); + } + _previewRequest += 1; + const requestId = _previewRequest; + const previewKey = `${workspaceId}::${filePath}`; + + set({ previewKey, previewLoading: true, previewError: null }); + + const controller = new AbortController(); + _previewController = controller; + + try { + const url = + `/jupyter/${workspaceId}/api/contents/${filePath}` + + `?type=file&content=1&hash=1&format=text`; + const response = await fetch(url, { + signal: controller.signal, + credentials: "include", + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`Failed to load file: ${response.status}`); + } + const data = await response.json(); + if (_previewRequest !== requestId) return; + set({ + previewKey, + previewCode: data.content ?? "", + previewCodeSize: data.size ?? 0, + previewLoading: false, + previewError: null, + }); + } catch (error) { + if (_previewRequest !== requestId) return; + if (error instanceof Error && error.name === "AbortError") return; + set({ + previewKey, + previewCode: null, + previewCodeSize: null, + previewLoading: false, + previewError: + error instanceof Error ? error.message : "加载预览失败", + }); + } finally { + if (_previewRequest === requestId) { + _previewController = null; + } + } + }, + createScript: async (form) => { const api = requireApi(); const suffix = form.scriptType === "notebook" ? ".ipynb" : ".py"; @@ -666,4 +743,4 @@ export const useScriptWorkspaceStore = create((set, get) => { } }, }; -}); \ No newline at end of file +}); diff --git a/frontend/package.json b/frontend/package.json index 2c97729..6ff0f4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "typecheck": "react-router typegen && tsc" }, "dependencies": { + "@monaco-editor/react": "^4.7.0", "@react-router/node": "^8", "@react-router/serve": "^8", "isbot": "^5.1.36", @@ -27,5 +28,5 @@ "typescript": "^5.9.3", "vite": "^8.0.3" }, - "packageManager": "pnpm@10.15.1" + "packageManager": "pnpm@11.21.0" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8b22ada..3ae96bc 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@monaco-editor/react': + specifier: ^4.7.0 + version: 4.7.0(monaco-editor@0.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@react-router/node': specifier: ^8 version: 8.3.0(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(typescript@5.9.3) @@ -228,6 +231,16 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@monaco-editor/loader@1.7.0': + resolution: {integrity: sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==} + + '@monaco-editor/react@4.7.0': + resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==} + 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 + '@napi-rs/wasm-runtime@1.2.0': resolution: {integrity: sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -499,6 +512,9 @@ packages: '@types/react@19.2.17': resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -619,6 +635,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dompurify@3.4.8: + resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -926,6 +945,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -946,6 +970,9 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + monaco-editor@0.56.0: + resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==} + morgan@1.11.0: resolution: {integrity: sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==} engines: {node: '>= 0.8.0'} @@ -1132,6 +1159,9 @@ packages: resolution: {integrity: sha512-mTozplhTX4tLKIHYji92OTZzVyZvi+Z1qRZDeBvQFI2XUB89wrRoj/xXad3c9NZ1GPJXXRvB+k41PQCPTMC+aA==} engines: {node: '>=0.10.0'} + state-local@1.0.7: + resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -1492,6 +1522,17 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.14 + '@monaco-editor/loader@1.7.0': + dependencies: + state-local: 1.0.7 + + '@monaco-editor/react@4.7.0(monaco-editor@0.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@monaco-editor/loader': 1.7.0 + monaco-editor: 0.56.0 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + '@napi-rs/wasm-runtime@1.2.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -1705,6 +1746,9 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/trusted-types@2.0.7': + optional: true + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -1815,6 +1859,10 @@ snapshots: detect-libc@2.1.2: {} + dompurify@3.4.8: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2075,6 +2123,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@14.0.0: {} + math-intrinsics@1.1.0: {} media-typer@1.1.1: {} @@ -2087,6 +2137,11 @@ snapshots: dependencies: mime-db: 1.54.0 + monaco-editor@0.56.0: + dependencies: + dompurify: 3.4.8 + marked: 14.0.0 + morgan@1.11.0: dependencies: basic-auth: 2.0.1 @@ -2291,6 +2346,8 @@ snapshots: source-map@0.6.0: {} + state-local@1.0.7: {} + statuses@2.0.2: {} tailwindcss@4.3.3: {}
- """模型实验开发平台构建脚本。""" - {"\n\n"}def main() -> None: - {"\n"} print("Hello, Model Platform!") - {"\n\n\n"}if __name__ == "__main__": - {"\n"} main() -