import { type FormEvent, useEffect, useMemo, useRef } from "react"; import { useAuth } from "../../context/AuthContext"; import { CreateFolderModal } from "../../components/platform/CreateFolderModal"; import { CreateScriptModal } from "../../components/platform/CreateScriptModal"; import Icon from "../../components/common/Icon"; import { PublishModal } from "../../components/platform/PublishModal"; import { ScriptExplorer } from "../../components/platform/ScriptExplorer"; import { TreeContextMenu } from "../../components/platform/TreeContextMenu"; import { VersionReceiptModal } from "../../components/platform/VersionReceiptModal"; import { getSessionCache, useScriptWorkspaceStore } from "./state/scriptWorkspaceStore"; import { useUiStore } from "./state/uiStore"; import { ScriptWorkspace } from "./ScriptWorkspace"; export default function ScriptsPage() { const { currentWorkspace, user } = useAuth(); const uploadInputRef = useRef(null); // store state const scripts = useScriptWorkspaceStore((s) => s.scripts); const directories = useScriptWorkspaceStore((s) => s.directories); const selectedId = useScriptWorkspaceStore((s) => s.selectedId); const openTabIds = useScriptWorkspaceStore((s) => s.openTabIds); const keyword = useScriptWorkspaceStore((s) => s.keyword); const loading = useScriptWorkspaceStore((s) => s.loading); const refreshing = useScriptWorkspaceStore((s) => s.refreshing); const editSession = useScriptWorkspaceStore((s) => s.editSession); const embeddedJupyterUrl = useScriptWorkspaceStore((s) => s.embeddedJupyterUrl); const editBusy = useScriptWorkspaceStore((s) => s.editBusy); const editorOpenError = useScriptWorkspaceStore((s) => s.editorOpenError); const latestVersion = useScriptWorkspaceStore((s) => s.latestVersion); const latestVersionLoading = useScriptWorkspaceStore((s) => s.latestVersionLoading); const pythonEditorBuffers = useScriptWorkspaceStore((s) => s.pythonEditorBuffers); // store actions const setKeyword = useScriptWorkspaceStore((s) => s.setKeyword); const load = useScriptWorkspaceStore((s) => s.load); const reset = useScriptWorkspaceStore((s) => s.reset); const selectScript = useScriptWorkspaceStore((s) => s.selectScript); const openTab = useScriptWorkspaceStore((s) => s.openTab); const closeTab = useScriptWorkspaceStore((s) => s.closeTab); const switchTab = useScriptWorkspaceStore((s) => s.switchTab); const openScriptEditor = useScriptWorkspaceStore((s) => s.openScriptEditor); const endEditing = useScriptWorkspaceStore((s) => s.endEditing); const openPythonEditor = useScriptWorkspaceStore((s) => s.openPythonEditor); const setPythonEditorContent = useScriptWorkspaceStore((s) => s.setPythonEditorContent); const savePythonEditor = useScriptWorkspaceStore((s) => s.savePythonEditor); const exitPythonEditor = useScriptWorkspaceStore((s) => s.exitPythonEditor); const loadLatestVersion = useScriptWorkspaceStore((s) => s.loadLatestVersion); const createScript = useScriptWorkspaceStore((s) => s.createScript); const uploadScripts = useScriptWorkspaceStore((s) => s.uploadScripts); const createFolder = useScriptWorkspaceStore((s) => s.createFolder); const deleteScript = useScriptWorkspaceStore((s) => s.deleteScript); const deleteDirectory = useScriptWorkspaceStore((s) => s.deleteDirectory); const toggleScriptLock = useScriptWorkspaceStore((s) => s.toggleScriptLock); const openPublishDialog = useScriptWorkspaceStore((s) => s.openPublishDialog); const submitPublish = useScriptWorkspaceStore((s) => s.submitPublish); const refreshReadOnlyContent = useScriptWorkspaceStore((s) => s.refreshReadOnlyContent); // ui store const pushToast = useUiStore((s) => s.pushToast); const createDialog = useUiStore((s) => s.createDialog); const setCreateForm = useUiStore((s) => s.setCreateForm); const closeCreateDialog = useUiStore((s) => s.closeCreateDialog); const folderDialog = useUiStore((s) => s.folderDialog); const setFolderName = useUiStore((s) => s.setFolderName); const closeFolderDialog = useUiStore((s) => s.closeFolderDialog); const contextMenu = useUiStore((s) => s.contextMenu); const closeContextMenu = useUiStore((s) => s.closeContextMenu); const showContextMenu = useUiStore((s) => s.showContextMenu); const upload = useUiStore((s) => s.upload); const openCreateDialog = useUiStore((s) => s.openCreateDialog); const openFolderDialog = useUiStore((s) => s.openFolderDialog); const chooseUpload = useUiStore((s) => s.chooseUpload); const publish = useUiStore((s) => s.publish); const setReleaseNote = useUiStore((s) => s.setReleaseNote); const setPublishVisibility = useUiStore((s) => s.setPublishVisibility); const closePublishDialog = useUiStore((s) => s.closePublishDialog); const clearPublishedVersion = useUiStore((s) => s.clearPublishedVersion); const workspaceId = currentWorkspace?.workspace_id; // 1) 初始加载 + workspace 切换时重置 useEffect(() => { void load(); }, [load]); useEffect(() => { reset(); // 刷新前递增版本号,触发已打开标签页的只读内容刷新 refreshReadOnlyContent(); void load(); }, [reset, load, workspaceId, refreshReadOnlyContent]); // 2) 选中文件变更时加载最新版本 useEffect(() => { if (!selectedId) { useScriptWorkspaceStore.setState({ latestVersion: null }); return; } let ignore = false; useScriptWorkspaceStore.setState({ latestVersionLoading: true }); void loadLatestVersion(selectedId).then(() => { if (!ignore) { useScriptWorkspaceStore.setState({ latestVersionLoading: false }); } }); return () => { ignore = true; }; }, [selectedId, loadLatestVersion]); const selected = useMemo( () => scripts.find((item) => item.script_id === selectedId) ?? null, [scripts, selectedId], ); const filteredScripts = useMemo(() => { const normalized = keyword.trim().toLocaleLowerCase(); if (!normalized) return scripts; return scripts.filter((item) => item.script_name.toLocaleLowerCase().includes(normalized), ); }, [keyword, scripts]); // 3) 自动打开 notebook 编辑器 useEffect(() => { if ( !selected || selected.script_type !== "notebook" || editBusy || (editSession?.script_id === selected.script_id && embeddedJupyterUrl) || editorOpenError?.scriptId === selected.script_id ) { return; } const cached = getSessionCache().get(selected.script_id); if (cached) { useScriptWorkspaceStore.setState({ editSession: cached.session, embeddedJupyterUrl: cached.jupyterUrl, }); return; } void openScriptEditor(selected, false); }, [ selected, editBusy, editSession?.script_id, embeddedJupyterUrl, editorOpenError?.scriptId, openScriptEditor, ]); // 4) context menu 外部点击关闭 useEffect(() => { if (!contextMenu) return; const close = () => closeContextMenu(); const closeOnEscape = (event: KeyboardEvent) => { if (event.key === "Escape") close(); }; window.addEventListener("pointerdown", close); window.addEventListener("blur", close); window.addEventListener("resize", close); window.addEventListener("scroll", close, true); window.addEventListener("keydown", closeOnEscape); return () => { window.removeEventListener("pointerdown", close); window.removeEventListener("blur", close); window.removeEventListener("resize", close); window.removeEventListener("scroll", close, true); window.removeEventListener("keydown", closeOnEscape); }; }, [contextMenu, closeContextMenu]); // python editor handlers with scriptId forwarding const handleSetPythonEditorContent = (scriptId: string, value: string) => { setPythonEditorContent(scriptId, value); }; const handleSavePythonEditor = (scriptId: string) => { void savePythonEditor(scriptId); }; const handleExitPythonEditor = (scriptId: string) => { exitPythonEditor(scriptId); }; const handleClosePythonTab = (scriptId: string) => { void closeTab(scriptId); }; // 5) handlers const handleUpload = (event: React.ChangeEvent) => { const files = Array.from(event.target.files ?? []); event.target.value = ""; if (files.length === 0) return; void uploadScripts(files, upload.parentPath); }; const handleCreateSubmit = (event: FormEvent) => { event.preventDefault(); void createScript(createDialog.form); }; const handleFolderSubmit = (event: FormEvent) => { event.preventDefault(); void createFolder(folderDialog.name, folderDialog.parentPath); }; const handlePublishSubmit = (event: FormEvent) => { event.preventDefault(); void submitPublish(publish.releaseNote, publish.visibility); }; const triggerUpload = (parentPath?: string) => { chooseUpload(parentPath); uploadInputRef.current?.click(); }; return (
void load(true)} onUpload={() => triggerUpload("")} onOpenCreateDialog={(parentPath, scriptType) => openCreateDialog(parentPath, scriptType)} onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)} onChooseUpload={(parentPath) => triggerUpload(parentPath)} onContextMenu={showContextMenu} onSelect={openTab} uploadInputRef={uploadInputRef} onHandleUpload={handleUpload} />
{selected ? ( { const s = scripts.find((item) => item.script_id === id); return { scriptId: id, scriptName: s?.script_name ?? "未知", scriptType: s?.script_type ?? "notebook", }; })} onOpenEditor={() => { if (selected.script_type === "python") { void openPythonEditor(selected); } else { void openScriptEditor(selected); } }} onEndEditing={() => { if (selected && pythonEditorBuffers[selected.script_id]) { exitPythonEditor(selected.script_id); } else { void endEditing(); } }} onClose={(scriptId, event) => void closeTab(scriptId, event)} onSwitchTab={switchTab} onNewTab={() => openCreateDialog("")} onPublish={() => openPublishDialog(selected)} scripts={scripts} pythonEditorBuffers={pythonEditorBuffers} onOpenPythonEditor={() => void openPythonEditor(selected)} onSetPythonEditorContent={handleSetPythonEditorContent} onSavePythonEditor={handleSavePythonEditor} onExitPythonEditor={handleExitPythonEditor} onClosePythonTab={handleClosePythonTab} onInfo={(t) => pushToast(t)} /> ) : (
构建脚本工作台

创建你的第一个模型脚本

通过 Notebook 完成数据探索,或使用 Python 脚本构建可调度的处理任务。

)}
({ script_type: s.script_type, script_name: s.script_name, }))} onFormChange={setCreateForm} onSubmit={handleCreateSubmit} onClose={closeCreateDialog} /> { selectScript(scriptId); closeContextMenu(); }} onRemoveScript={(s) => void deleteScript(s)} onToggleLock={(s) => void toggleScriptLock(s)} onOpenCreateDialog={(parentPath, scriptType) => openCreateDialog(parentPath, scriptType)} onOpenFolderDialog={(parentPath) => openFolderDialog(parentPath)} onChooseUpload={(parentPath) => chooseUpload(parentPath)} onRemoveDirectory={(p) => void deleteDirectory(p)} onClose={closeContextMenu} /> pushToast({ tone: "success", message })} />
); }