import { useEffect, useMemo } from "react"; import { NodeInspector } from "./NodeInspector"; import { applyThemeFromQuery } from "./system-theme"; import { TopologyCanvas } from "./TopologyCanvas"; import { useAppStore } from "./store"; function readQuery() { try { return new URLSearchParams(window.location.search); } catch { return new URLSearchParams(); } } export function App() { const query = useMemo(() => readQuery(), []); const embed = query.get("embed") === "1" || query.get("embed") === "true"; const queryCanvasId = query.get("canvasId") || ""; const { canvases, canvasId, canvas, nodes, selectedIds, leftCollapsed, loadCanvases, createCanvas, openCanvas, undo, setCollapsedPanels, setPrefs, applyOps, trash, restoreFromTrash, } = useAppStore(); useEffect(() => { applyThemeFromQuery(); }, []); useEffect(() => { void (async () => { await loadCanvases(); if (queryCanvasId) { await openCanvas(queryCanvasId); return; } if (!useAppStore.getState().canvasId) { await createCanvas("未命名画布"); } })(); }, [loadCanvases, createCanvas, openCanvas, queryCanvasId]); useEffect(() => { const wsUrl = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`; let ws: WebSocket | null = null; let closed = false; let retryTimer: number | undefined; let refreshTimer: number | undefined; let retryMs = 800; const scheduleRefresh = (id: string, preserveUnpinned?: boolean) => { window.clearTimeout(refreshTimer); refreshTimer = window.setTimeout(async () => { if (useAppStore.getState().canvasId !== id) return; try { const data = await fetch(`/api/canvases/${id}`).then((r) => r.json()) as { canvas: import("@mind-map/shared").CanvasMeta; nodes: import("@mind-map/shared").MindNode[]; edges: import("@mind-map/shared").MindEdge[]; positions: import("./store").Positions; }; useAppStore.getState().applyCanvasPayload(data, { preserveUnpinned }); } catch { void useAppStore.getState().openCanvas(id); } }, 120); }; const connect = () => { if (closed) return; try { ws = new WebSocket(wsUrl); } catch { retryTimer = window.setTimeout(connect, retryMs); retryMs = Math.min(8000, retryMs * 1.6); return; } ws.onopen = () => { retryMs = 800; }; ws.onmessage = (ev) => { try { const msg = JSON.parse(String(ev.data)) as { type?: string; canvasId?: string; preserveUnpinned?: boolean; nodeIds?: string[]; edgeIds?: string[]; style?: { nodeColor?: string; nodeGlow?: boolean; edgeWidth?: number; edgeColor?: string; }; ttlMs?: number; fitViewport?: { x: number; y: number; zoom: number }; mirror?: { kind: "user" | "assistant" | "text-delta" | "tool" | "turn-end"; text?: string; name?: string; callId?: string; sessionId?: string; reason?: unknown; }; }; if ( msg.type === "canvas/event" && msg.canvasId && msg.canvasId === useAppStore.getState().canvasId ) { if (msg.fitViewport) { useAppStore.getState().requestFitViewport(msg.fitViewport); } scheduleRefresh(msg.canvasId, msg.preserveUnpinned === true); } if ( msg.type === "canvas/highlight" && msg.canvasId && msg.canvasId === useAppStore.getState().canvasId ) { const nodeIds = Array.isArray(msg.nodeIds) ? msg.nodeIds : []; const edgeIds = Array.isArray(msg.edgeIds) ? msg.edgeIds : []; useAppStore.getState().flashHighlight({ nodeIds, edgeIds, style: msg.style && typeof msg.style === "object" ? msg.style : null, ttlMs: typeof msg.ttlMs === "number" ? msg.ttlMs : edgeIds.length ? 5000 : 2200, }); } } catch { /* ignore */ } }; ws.onclose = () => { if (closed) return; retryTimer = window.setTimeout(connect, retryMs); retryMs = Math.min(8000, retryMs * 1.6); }; ws.onerror = () => { try { ws?.close(); } catch { /* ignore */ } }; }; connect(); return () => { closed = true; window.clearTimeout(retryTimer); window.clearTimeout(refreshTimer); try { ws?.close(); } catch { /* ignore */ } }; }, []); useEffect(() => { const id = canvasId; if (!id) return; const t = window.setTimeout(() => { void fetch(`/api/canvases/${id}/activity`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ selectionIds: selectedIds, focusNodeId: canvas?.focusNodeId ?? null, viewport: canvas?.viewport ?? { x: 0, y: 0, zoom: 1 }, }), }); }, 200); return () => window.clearTimeout(t); }, [canvasId, selectedIds, canvas?.focusNodeId, canvas?.viewport]); const focusId = canvas?.focusNodeId; const outline = nodes.filter((n) => !n.deletedAt); if (embed) { return (
); } return (
Mind Map
); }