Show dependency-closed model prefixes before full canvas loading

This commit is contained in:
2026-11-09 02:33:02 +08:00
parent a92ae72c8b
commit 0a705f82ee
13 changed files with 270 additions and 39 deletions
+14 -7
View File
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useRef, useState } from "react";
import type {Viewport} from '@xyflow/react';
import type {ModelPreview} from '@/lib/canvas-runtime/model-preview';
import { useRouter, useSearchParams } from "next/navigation";
import { api, errorText } from "@/lib/api";
import { readDemoBoards } from "@/lib/demo-workspace";
@@ -15,6 +17,7 @@ import dynamic from "next/dynamic";
const GraphCanvas = dynamic(() => import("@/components/canvas/graph-canvas"), { ssr: false, loading: () => <p role="status">正在准备画布…</p> });
const HeadlessGraphCanvas = dynamic(() => import("@/components/canvas/headless-graph-canvas"), { ssr: false, loading: () => <p role="status">正在准备画布…</p> });
const LoadingGraphCanvas=dynamic(()=>import("@/components/canvas/loading-graph-canvas"),{ssr:false});
type Loaded = { board: Board; snapshot?: CanvasSnapshot; route: string; engine?: ProjectEngine };
export default function GraphPage({ workspaceId, boardId }: { workspaceId: string; boardId: string }) {
const renderers=useCanvasRenderers();
@@ -24,11 +27,13 @@ export default function GraphPage({ workspaceId, boardId }: { workspaceId: strin
const route = workspaceId + ":" + boardId;
const { board: activeBoard, setBoard: setActiveBoard } = useActiveBoard();
const [loaded, setLoaded] = useState<Loaded | null>(null);
const [preview,setPreview]=useState<{board:Board;value:ModelPreview;route:string}|null>(null);
const previewViewport=useRef<Viewport|undefined>(undefined);
const [error, setError] = useState("");
const [retry, setRetry] = useState(0);
const navigationGeneration=useRef(0);
useEffect(() => {
setLoaded(null); setError("");
setLoaded(null);setPreview(null);previewViewport.current=undefined; setError("");
if (demo) {
const item = readDemoBoards(workspaceId).find((board) => matchesRef(board, boardId));
if (item) setLoaded({ board: item, route });
@@ -39,25 +44,27 @@ export default function GraphPage({ workspaceId, boardId }: { workspaceId: strin
const controller = new AbortController();
const startingIdentity=runtimeHost.identity();let currentIdentity:(()=>boolean)|undefined;
loadCanvasEntry({ request: api, workspaceRef: workspaceId, boardRef: boardId, registry: runtimeHost.registry, signal: controller.signal,navigationGeneration:++navigationGeneration.current,
readModel:input=>{const identity=runtimeHost.identity();if(identity.authEpoch!==startingIdentity.authEpoch&&identity.accountId!==input.accountId)throw new Error('identity_changed');currentIdentity=runtimeHost.bindAccount(input.accountId);return runtimeHost.loadModel(input.accountId,input.workspaceId,input.canvasId,{signal:input.signal,navigationGeneration:input.navigationGeneration});},
onPreview:(value,board)=>{if(active&&(!currentIdentity||currentIdentity())&&value.document.canvases.has(board.id))setPreview({board,value,route});},
readModel:input=>{const identity=runtimeHost.identity();if(identity.authEpoch!==startingIdentity.authEpoch&&identity.accountId!==input.accountId)throw new Error('identity_changed');currentIdentity=runtimeHost.bindAccount(input.accountId);return runtimeHost.loadModel(input.accountId,input.workspaceId,input.canvasId,{signal:input.signal,navigationGeneration:input.navigationGeneration,onPreview:input.onPreview});},
}).then(async result => {
if (!active||currentIdentity&&!currentIdentity()) return;
if (result.mode === 'legacy') { setLoaded({ board: result.board, snapshot: result.snapshot, route }); return; }
if (result.mode === 'legacy') { setLoaded({ board: result.board, snapshot: result.snapshot, route });setPreview(null); return; }
const engine = await runtimeHost.workspace(result.profile.id, result.board.workspace_id).open(result.model.document, result.model.versions);
if (active&&(!currentIdentity||currentIdentity())) setLoaded({ board: result.board, engine, route });
if (active&&(!currentIdentity||currentIdentity())){setLoaded({ board: result.board, engine, route });setPreview(null);}
})
.catch((error) => { if (active) { setError(errorText(error)); if (error.message === "invalid_session") router.replace("/login"); } });
return () => { active = false; controller.abort(); };
}, [workspaceId, boardId, route, demo, retry, router, runtimeHost]);
const current = loaded?.route === route ? loaded : null;
const board = current?.board;
const currentPreview=preview?.route===route?preview:null;
const board = current?.board??currentPreview?.board;
useEffect(() => {
if (board) setActiveBoard({ id: board.id, slug: board.slug, workspace_id: board.workspace_id, workspace_ref: workspaceId, title: board.title });
return () => setActiveBoard(null);
}, [board, workspaceId, setActiveBoard]);
const displayBoard = board && activeBoard?.id === board.id && activeBoard.workspace_id === board.workspace_id ? { ...board, title: activeBoard.title } : board;
return <main className={styles.graph} aria-label={displayBoard ? displayBoard.title + " · 无限图谱" : "无限图谱"} aria-busy={!board && !error}>
{displayBoard && current?.engine ? <HeadlessGraphCanvas key={displayBoard.workspace_id + ":" + displayBoard.id} board={displayBoard} engine={current.engine} renderers={renderers} /> : displayBoard ? <GraphCanvas key={displayBoard.workspace_id + ":" + displayBoard.id} board={displayBoard} snapshot={current?.snapshot} /> : <div className={styles.graphSurface} aria-label="正在读取画布" />}
return <main className={styles.graph} aria-label={displayBoard ? displayBoard.title + " · 无限图谱" : "无限图谱"} aria-busy={!current && !error}>
{displayBoard && current?.engine ? <HeadlessGraphCanvas key={displayBoard.workspace_id + ":" + displayBoard.id} board={displayBoard} engine={current.engine} renderers={renderers} initialViewport={previewViewport.current} /> : displayBoard&&currentPreview ? <LoadingGraphCanvas key={displayBoard.workspace_id+":"+displayBoard.id} preview={currentPreview.value} canvasId={displayBoard.id} workspaceId={displayBoard.workspace_id} registry={runtimeHost.registry} renderers={renderers} onViewport={viewport=>{previewViewport.current=viewport;}} /> : displayBoard ? <GraphCanvas key={displayBoard.workspace_id + ":" + displayBoard.id} board={displayBoard} snapshot={current?.snapshot} /> : <div className={styles.graphSurface} aria-label="正在读取画布" />}
{error ? <div className={styles.graphStatus} role="alert">{error} <button onClick={() => setRetry((value) => value + 1)}>重新加载</button></div> : !board && <p className={styles.graphStatus} role="status">正在打开图谱…</p>}
</main>;
}