import { BookOpen, Check, ChevronRight, ExternalLink, FileCode, Info, Lock, Plus, RefreshCw, X, } from "lucide-react"; import type { ActiveEditSession, LatestVersion, ScriptItem, ScriptType, } from "~/services/api"; import { scriptIcon } from "./WorkspaceTree"; import type { MouseEvent as ReactMouseEvent } from "react"; import Editor from "@monaco-editor/react"; import { useRef, useLayoutEffect, useState, useEffect } from "react"; import {type PythonEditorBuffer, useScriptWorkspaceStore} from "./state/scriptWorkspaceStore"; import { useAuth } from "../../context/AuthContext"; import { getScriptContent } from "../../services/api"; import {PythonEditor} from "~/features/platform/PythonEditor"; function fileIconTone(type: ScriptType | "data") { if (type === "notebook") return "text-[#e15e50] bg-[#fff0ed]"; if (type === "data") return "text-[#5a8f6a] bg-[#eef8f1]"; return "text-[#2e73c6] bg-[#eaf3ff]"; } const toolbarBtnClass = "h-[31px] cursor-pointer rounded-[5px] border border-[#d9e3ec] bg-white px-[11px] text-[11px] text-[#52708d] hover:border-[#9dbbd8] hover:bg-[#f7fbff] disabled:cursor-wait disabled:opacity-70"; const endEditBtnClass = "h-[31px] cursor-pointer rounded-[5px] border border-[#e1b8b8] bg-[#fff8f8] px-[11px] text-[11px] text-[#a34c4c] hover:border-[#9dbbd8] hover:bg-[#f7fbff] disabled:cursor-wait disabled:opacity-70"; const releaseBtnClass = "h-[31px] cursor-pointer rounded-[5px] border border-[#e7c689] bg-[#fffbf3] px-[11px] text-[11px] text-[#9b6a18] hover:border-[#9dbbd8] hover:bg-[#f7fbff]"; const openEditorBtnClass = "inline-flex h-[37px] cursor-pointer items-center justify-center gap-[7px] rounded-md border border-[#b9d4ef] bg-[#f4f9ff] px-3.5 text-[11px] font-[650] text-[#176cc0] hover:border-[#7eafe0] hover:bg-[#eaf4ff] disabled:cursor-wait disabled:opacity-70"; const openEditorEditingBtnClass = "inline-flex h-[37px] cursor-pointer items-center justify-center gap-[7px] rounded-md border border-[#69b79a] bg-[#effaf5] px-3.5 text-[11px] font-[650] text-[#137a55] hover:border-[#7eafe0] hover:bg-[#eaf4ff] disabled:cursor-wait disabled:opacity-70"; const saveBtnClass = "inline-flex h-[31px] cursor-pointer items-center justify-center gap-1.5 rounded-[5px] border border-[#b8dec1] bg-[#f1faf3] px-[11px] text-[11px] font-semibold text-[#1f6d3a] disabled:cursor-not-allowed disabled:opacity-50"; // 模块级变量存储刷新版本号,用于检测只读内容刷新 let _lastRefreshVersion = 0; type Notice = { tone: "success" | "error" | "info"; message: string; }; // 缓存的会话类型(多 iframe 共存方案) interface CachedSession { session: ActiveEditSession; jupyterUrl: string; lastActiveTime: number; } type ScriptWorkspaceProps = { script: ScriptItem; scripts: ScriptItem[]; sessionCache: Map; editSession: ActiveEditSession | null; jupyterUrl: string | null; editBusy: boolean; openError: string | null; latestVersion: LatestVersion | null; versionsLoading: boolean; openTabs: Array<{ scriptId: string; scriptName: string; scriptType: ScriptType }>; onOpenEditor: () => void; onEndEditing: () => void; onClose: (scriptId: string, event?: ReactMouseEvent) => void; onSwitchTab: (scriptId: string) => void; onNewTab: () => void; onPublish: () => void; onInfo: (toast: Notice) => void; pythonEditorBuffers: Record; onOpenPythonEditor: () => void; onSetPythonEditorContent: (scriptId: string, v: string) => void; onSavePythonEditor: (scriptId: string) => void; onExitPythonEditor: (scriptId: string) => void; onClosePythonTab: (scriptId: string) => void; }; function formatTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, }).format(new Date(value)); } function formatBytes(value: number) { if (value < 1024) return `${value} B`; return `${(value / 1024).toFixed(1)} KB`; } function shortHash(value: string) { return value ? `${value.slice(0, 8)}…${value.slice(-6)}` : "—"; } // Notebook 单元格类型 type NotebookCell = { cell_type: "code" | "markdown" | "raw"; source: string[] | string; execution_count?: number | null; outputs?: Array<{ name?: "stdout" | "stderr" | string; output_type: "stream" | "execute_result" | "display_data" | "error"; text?: string[] | string; data?: Record; }>; }; // Notebook Viewer 组件 function NotebookViewer({ content }: { content: object }) { const notebook = content as { cells?: NotebookCell[]; metadata?: Record; nbformat?: number; nbformat_minor?: number; }; const cells = notebook.cells ?? []; return (
{cells.map((cell, index) => { const sourceText = Array.isArray(cell.source) ? cell.source.join("") : (cell.source ?? ""); if (cell.cell_type === "markdown") { return (
{renderMarkdown(sourceText)}
); } if (cell.cell_type === "code") { const hasSource = sourceText.trim().length > 0; return (
In [{cell.execution_count ?? " "}]:
                    {sourceText}
                  
{cell.outputs && cell.outputs.length > 0 && (
{cell.outputs.map((output, outputIndex) => { if (output.output_type === "stream" && output.text) { const text = Array.isArray(output.text) ? output.text.join("") : output.text; return (
                              {text}
                            
); } if (output.output_type === "execute_result" || output.output_type === "display_data") { const text = output.data?.["text/plain"]; if (text) { const textStr = Array.isArray(text) ? text.join("") : String(text); return (
                                {textStr}
                              
); } } return null; })}
)}
); } return null; })}
); } // 简单的 markdown 渲染函数 function renderMarkdown(text: string) { const lines = text.split("\n"); return lines.map((line, i) => { if (line.startsWith("# ")) { return

{line.slice(2)}

; } if (line.startsWith("## ")) { return

{line.slice(3)}

; } if (line.startsWith("### ")) { return

{line.slice(4)}

; } if (line.startsWith("- ") || line.startsWith("* ")) { return
  • {line.slice(2)}
  • ; } if (line.match(/^\d+\. /)) { return
  • {line.replace(/^\d+\. /, "")}
  • ; } if (line.trim() === "") { return
    ; } return

    {line}

    ; }); } function confineJupyterFrame(frame: HTMLIFrameElement, readOnly: boolean = false): void { try { const document = frame.contentDocument; if (!document?.documentElement) return; const keepInside = (): void => { // 隐藏 "Open in..." 按钮 document.querySelectorAll("button,[role='button']").forEach( (element) => { const label = `${element.getAttribute("aria-label") ?? ""} ${ element.getAttribute("title") ?? "" } ${element.textContent ?? ""}`.trim(); if (/^open in\b/i.test(label) || /\bopen in\.\.\./i.test(label)) { element.style.setProperty("display", "none", "important"); } // 只读模式下隐藏保存/编辑相关的按钮 if (readOnly) { if (/\bsave\b|\bedit\b|\brun\b/i.test(label)) { element.style.setProperty("display", "none", "important"); } } }, ); // 阻止新窗口打开链接 document.querySelectorAll("a[target]").forEach((link) => { if (["_blank", "_top", "_parent"].includes(link.target)) { link.target = "_self"; } }); }; keepInside(); new MutationObserver(keepInside).observe(document.documentElement, { childList: true, subtree: true, }); document.addEventListener("click", (event) => { const target = event.target as HTMLElement | null; const link = target?.closest?.("a") as HTMLAnchorElement | null; if (link && ["_blank", "_top", "_parent"].includes(link.target)) { link.target = "_self"; } }, true); } catch { // The iframe remains sandboxed even if its document is not yet accessible. } } export function ScriptWorkspace({ script, scripts, sessionCache, editSession, jupyterUrl, editBusy, openError, latestVersion, versionsLoading, openTabs, onOpenEditor, onEndEditing, onClose, onSwitchTab, onNewTab, onPublish, onInfo, pythonEditorBuffers, onOpenPythonEditor, onSetPythonEditorContent, onSavePythonEditor, onExitPythonEditor, onClosePythonTab, }: ScriptWorkspaceProps) { const { user } = useAuth(); const tabbarRef = useRef(null); const isNotebook = script.script_type === "notebook"; const isPython = script.script_type === "python"; // 订阅 store 的刷新版本号,用于触发只读内容刷新 const readOnlyRefreshVersion = useScriptWorkspaceStore((s) => s.readOnlyRefreshVersion); // 判断当前选中的文件是否正在编辑(需要同时检查 session_status 和 script_id) const isEditing = editSession?.session_status === "active" && editSession?.script_id === script.script_id; const activePythonBuf = isPython ? pythonEditorBuffers[script.script_id] : null; const isPythonEditing = isPython && !!activePythonBuf; const showSaveButton = isPythonEditing && activePythonBuf && (activePythonBuf.dirty || activePythonBuf.saving || activePythonBuf.initial); const saveDisabled = !activePythonBuf?.dirty || activePythonBuf?.saving; const pythonTabBuffers = openTabs.filter( (t) => t.scriptType === "python" && pythonEditorBuffers[t.scriptId], ); const hasEmbeddedContent = sessionCache.size > 0 || pythonTabBuffers.length > 0; // 只读模式状态(用于 Python 文件和 notebook 的 JSON 内容) const [readOnlyContent, setReadOnlyContent] = useState(null); const [readOnlyLoading, setReadOnlyLoading] = useState(false); const [readOnlyError, setReadOnlyError] = useState(null); // 判断是否启用只读模式:文件已锁定 + 非所有者 + 非管理员 const isReadOnlyMode = script.is_locked && user?.user_id !== script.owner_user_id && user?.role_code !== "admin"; const scroll = (direction: "left" | "right") => { const tabbar = tabbarRef.current; if (!tabbar) return; const scrollAmount = 200; tabbar.scrollBy({ left: direction === "left" ? -scrollAmount : scrollAmount, behavior: "smooth", }); }; useLayoutEffect(() => { const tabbar = tabbarRef.current; if (!tabbar) return; const activeTab = tabbar.querySelector('[data-active="true"]') as HTMLElement | null; if (activeTab) { const tabbarRect = tabbar.getBoundingClientRect(); const tabRect = activeTab.getBoundingClientRect(); if (tabRect.right > tabbarRect.right || tabRect.left < tabbarRect.left) { activeTab.scrollIntoView({ behavior: "smooth", inline: "center" }); } } }, [script.script_id]); // 加载只读内容(当处于只读模式时) useEffect(() => { if (!isReadOnlyMode) { setReadOnlyContent(null); setReadOnlyLoading(false); setReadOnlyError(null); return; } setReadOnlyLoading(true); setReadOnlyError(null); getScriptContent(script.workspace_id, script.script_id) .then((data) => { setReadOnlyContent(data.content); }) .catch((err) => { setReadOnlyError(err instanceof Error ? err.message : '加载失败'); }) .finally(() => { setReadOnlyLoading(false); }); }, [isReadOnlyMode, script.script_id, script.workspace_id]); // 监听只读内容刷新 useEffect(() => { if (readOnlyRefreshVersion > _lastRefreshVersion && isReadOnlyMode) { _lastRefreshVersion = readOnlyRefreshVersion; // 重新加载只读内容 setReadOnlyLoading(true); setReadOnlyError(null); getScriptContent(script.workspace_id, script.script_id) .then((data) => { setReadOnlyContent(data.content); }) .catch((err) => { setReadOnlyError(err instanceof Error ? err.message : '加载失败'); }) .finally(() => { setReadOnlyLoading(false); }); } }, [script.script_id, script.workspace_id, isReadOnlyMode, readOnlyRefreshVersion]); const ScriptTypeIcon = scriptIcon(script); return ( <>
    {openTabs.map((tab) => { const isActive = tab.scriptId === script.script_id; return (
    onSwitchTab(tab.scriptId)} > {tab.scriptType === "notebook" ? : } {tab.scriptName}
    ); })}
    工作副本 {script.script_name}
    {!isReadOnlyMode && (
    {isPythonEditing ? ( <> {showSaveButton && ( )} ) : isEditing ? ( ) : ( )} { latestVersion ? `最新 ${latestVersion.version_label}` : "工作副本已就绪" }
    )}
    {/* 本地编辑锁提示——只在非 Python 编辑时显示, 因为 Python 编辑走 pythonEditorBuffers,不走文件锁。 这个锁只在本浏览器当前 tab 内有效,不能阻止隐身模式 / 其它浏览器同时编辑。 */} {isEditing && (
    本地编辑锁——关闭标签页、刷新页面或换浏览器后失效,不会阻止他人同时编辑。
    )} {/* 编辑器画布区域 - 始终渲染,保证 iframe 不重新加载 */}
    {/* 只读模式内容 - 用 CSS 控制显示/隐藏 */}
    此文件已被锁定,您当前处于只读模式
    {readOnlyLoading ? (
    加载内容中...
    ) : readOnlyError ? (
    加载失败

    {readOnlyError}

    ) : script.script_type === 'notebook' && readOnlyContent && typeof readOnlyContent === 'object' ? ( ) : ( )}
    {/* 正常编辑模式 - 始终渲染,用 CSS 控制显示/隐藏 */}
    0 ? "overflow-hidden bg-white" : "editor-canvas-shell overflow-auto" }`} style={{ display: isReadOnlyMode ? 'none' : 'block', height: '100%' }} > {/* 多 PythonEditor 实例:每个有 buffer 的 python tab 都挂载,仅 active 可见 */} {pythonTabBuffers.map((tab) => { const tabScript = scripts.find((s) => s.script_id === tab.scriptId); if (!tabScript) return null; const buf = pythonEditorBuffers[tab.scriptId]; const isActive = tab.scriptId === script.script_id; return (
    ); })} {/* 渲染所有缓存的 iframe */} {Array.from(sessionCache.entries()).map(([scriptId, cached]) => { const isActive = scriptId === script.script_id; return (
    Workspace Jupyter Server {cached.session.session_status === "active" ? "Notebook Session · Python 3 Kernel" : "Jupyter 文本编辑器"} Runtime {cached.session.runtime_id.slice(-8)}