mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 20:19:51 +08:00
The shell's VersionsSource (shell/node-versions-source.ts) reads GET /api/history through the sync and shows one version per distinct content (a restore's copy folds into the entry it copies; the blank page is none), numbered as they first appeared, with what changed since the one before. The entry the server says is on the canvas marks the version; undo and redo of the newest version restore the one before it, or it again, through POST /api/restore, so hand edits stay as versions of their own. The cards sit in a panel beside the canvas (toggle in the header); the strip and Compare come from the shared canvas components. The sync tells listeners once per server version, and when History changed without one, so the list is read again only then.
119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
import { useEffect, useRef, useState } from "react"
|
|
import { toast } from "sonner"
|
|
import { useDiagram } from "@/contexts/diagram-context"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import { diffDiagrams } from "@/lib/diagram-diff"
|
|
import { highlightChangedCells } from "@/lib/drawio/editor-bridge"
|
|
import { useCanvasStore } from "@/stores/canvas-store"
|
|
import { createMcpSync, type McpSync, type SyncStatus } from "./mcp-sync-core"
|
|
import type { ShellConfig } from "./runtime-config"
|
|
|
|
/**
|
|
* Keeps the canvas in step with the MCP server (mcp-sync-core.ts has the
|
|
* protocol): mounted once inside DiagramProvider. Server writes go on the
|
|
* canvas through loadDiagram, as one undo step when they can; the user's
|
|
* edits are pushed; the server's export and sync requests are answered.
|
|
* Returns the connection state for the status bar, and the sync itself for
|
|
* the version cards (History and restores go through it).
|
|
*/
|
|
export function useMcpSync(config: ShellConfig): {
|
|
status: SyncStatus
|
|
sync: McpSync | null
|
|
} {
|
|
const dict = useDictionary()
|
|
const diagram = useDiagram()
|
|
const [status, setStatus] = useState<SyncStatus>("waiting")
|
|
const [sync, setSync] = useState<McpSync | null>(null)
|
|
const syncRef = useRef<McpSync | null>(null)
|
|
// The provider's functions are new on every render; the sync (created
|
|
// once) reads the latest through refs
|
|
const diagramRef = useRef(diagram)
|
|
diagramRef.current = diagram
|
|
const dictRef = useRef(dict)
|
|
dictRef.current = dict
|
|
|
|
useEffect(() => {
|
|
if (!config.sessionId) return
|
|
const sync = createMcpSync({
|
|
sessionId: config.sessionId,
|
|
apiBase: config.apiBase,
|
|
token: config.token,
|
|
canvas: {
|
|
load: (xml, decision) =>
|
|
diagramRef.current.loadDiagram(
|
|
xml,
|
|
true,
|
|
decision.mode,
|
|
decision.mode === "commit"
|
|
? { pageId: decision.pageId }
|
|
: undefined,
|
|
),
|
|
showTransient: (xml) => diagramRef.current.showTransient(xml),
|
|
export: (request, timeoutMs) =>
|
|
diagramRef.current.requestExport(request, timeoutMs),
|
|
currentXml: () => diagramRef.current.chartXMLRef.current,
|
|
currentPageId: () => useCanvasStore.getState().currentPageId,
|
|
},
|
|
onNotice: (notice) =>
|
|
toast(dictRef.current.shell[notice], { duration: 8000 }),
|
|
onStatus: setStatus,
|
|
})
|
|
syncRef.current = sync
|
|
setSync(sync)
|
|
sync.setReady(diagramRef.current.isDrawioReady)
|
|
sync.start()
|
|
|
|
// draw.io's autosave and load events, as the classic page reads
|
|
// them: the frame component handles them for the canvas, the sync
|
|
// needs them for the protocol (an edit to push, a load done)
|
|
const drawioOrigin = new URL(config.drawioBaseUrl, location.href).origin
|
|
const onMessage = (event: MessageEvent) => {
|
|
if (event.source === window || event.origin !== drawioOrigin) {
|
|
return
|
|
}
|
|
if (typeof event.data !== "string") return
|
|
let message: { event?: string; xml?: unknown }
|
|
try {
|
|
message = JSON.parse(event.data)
|
|
} catch {
|
|
return
|
|
}
|
|
if (
|
|
message.event === "autosave" &&
|
|
typeof message.xml === "string"
|
|
) {
|
|
sync.onAutoSave(message.xml)
|
|
} else if (message.event === "load") {
|
|
sync.onDrawioLoad()
|
|
}
|
|
}
|
|
window.addEventListener("message", onMessage)
|
|
return () => {
|
|
window.removeEventListener("message", onMessage)
|
|
sync.stop()
|
|
syncRef.current = null
|
|
setSync(null)
|
|
}
|
|
}, [config])
|
|
|
|
useEffect(() => {
|
|
syncRef.current?.setReady(diagram.isDrawioReady)
|
|
}, [diagram.isDrawioReady])
|
|
|
|
// Mark what a server write changed on the page, as the web app does
|
|
// for its versions (use-versions.ts); any action on the canvas clears it
|
|
useEffect(() => {
|
|
diagram.setCommitHandler(({ beforeXml, afterXml, pageId }) => {
|
|
const { touchedIds, fromScratch } = diffDiagrams(
|
|
beforeXml,
|
|
afterXml,
|
|
pageId,
|
|
)
|
|
if (!fromScratch) highlightChangedCells(touchedIds)
|
|
})
|
|
return () => diagram.setCommitHandler(null)
|
|
}, [])
|
|
|
|
return { status, sync }
|
|
}
|