mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 20:19:51 +08:00
160 lines
6.9 KiB
TypeScript
160 lines
6.9 KiB
TypeScript
import { Download, History, Moon, Sun } from "lucide-react"
|
|
import { useMemo, useState } from "react"
|
|
import { CanvasStage } from "@/components/canvas/canvas-stage"
|
|
import { CompareDialog } from "@/components/canvas/compare-dialog"
|
|
import { VersionsProvider } from "@/components/canvas/versions-context"
|
|
import { type ExportFormat, SaveDialog } from "@/components/save-dialog"
|
|
import { AppToaster } from "@/components/workspace/app-toaster"
|
|
import { useDiagram } from "@/contexts/diagram-context"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import type { DrawioSource } from "@/lib/drawio/drawio-config"
|
|
import { cn } from "@/lib/utils"
|
|
import { useSettingsStore } from "@/stores/settings-store"
|
|
import { useNodeVersions } from "./node-versions-source"
|
|
import type { ShellConfig } from "./runtime-config"
|
|
import { setDark } from "./theme"
|
|
import { useMcpSync } from "./use-mcp-sync"
|
|
import { VersionsPanel } from "./versions-panel"
|
|
|
|
const iconButton =
|
|
"inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground [&_svg]:size-4"
|
|
|
|
/**
|
|
* The MCP preview: the web app's canvas without the chat. A slim bar names
|
|
* the session and the sync state; the version cards (from the server's
|
|
* History), Compare and the export dialog come from the shared canvas
|
|
* components. The cards sit in a side panel, so the web app's versions strip
|
|
* above the canvas is left out: it would show the same list twice.
|
|
*/
|
|
export function CanvasShell({ config }: { config: ShellConfig }) {
|
|
const dict = useDictionary()
|
|
const isDark = useSettingsStore((s) => s.isDark)
|
|
const { saveDiagramToFile, hasPendingLoad } = useDiagram()
|
|
const { status, sync } = useMcpSync(config)
|
|
const versions = useNodeVersions(sync)
|
|
const [panelOpen, setPanelOpen] = useState(true)
|
|
const [saveOpen, setSaveOpen] = useState(false)
|
|
const drawioSource = useMemo<DrawioSource>(
|
|
() => ({
|
|
editorUrl: config.drawioBaseUrl,
|
|
ui: config.drawioUi || undefined,
|
|
lang: config.drawioLang || undefined,
|
|
// As the classic preview page: the user's own shape libraries
|
|
libraries: true,
|
|
}),
|
|
[config],
|
|
)
|
|
const hasVersions = versions.versions.length > 0
|
|
const showPanel = panelOpen && hasVersions
|
|
|
|
// draw.io exports what it shows: while a page export's one-page
|
|
// projection is on the canvas, or a full load has yet to reach the
|
|
// editor, that is not the document. Both end within seconds (the
|
|
// export's deadline is 15 s); the save waits for them, up to 20 s
|
|
const save = (filename: string, format: ExportFormat, tries = 0) => {
|
|
const canvasBusy = sync?.read().projectionActive || hasPendingLoad()
|
|
if (canvasBusy && tries < 66) {
|
|
setTimeout(() => save(filename, format, tries + 1), 300)
|
|
return
|
|
}
|
|
saveDiagramToFile(
|
|
filename,
|
|
format,
|
|
undefined,
|
|
dict.save.savedSuccessfully,
|
|
)
|
|
}
|
|
|
|
return (
|
|
<VersionsProvider value={versions}>
|
|
<div className="flex h-dvh flex-col bg-background text-foreground">
|
|
<header className="flex h-11 shrink-0 items-center gap-3 border-b border-border bg-surface-0 px-3.5">
|
|
<span className="text-[13px] font-semibold tracking-[-0.01em]">
|
|
Next AI Draw.io
|
|
</span>
|
|
{config.sessionId && (
|
|
<span
|
|
className="rounded-md bg-muted px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground tabular-nums"
|
|
title={config.sessionId}
|
|
data-testid="shell-session"
|
|
>
|
|
{config.sessionId.slice(-8)}
|
|
</span>
|
|
)}
|
|
<span
|
|
className="truncate text-xs text-muted-foreground"
|
|
data-testid="shell-status"
|
|
data-status={status}
|
|
>
|
|
{dict.shell[status]}
|
|
</span>
|
|
{hasVersions && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setPanelOpen((open) => !open)}
|
|
aria-label={
|
|
panelOpen
|
|
? dict.shell.hideVersions
|
|
: dict.shell.showVersions
|
|
}
|
|
title={
|
|
panelOpen
|
|
? dict.shell.hideVersions
|
|
: dict.shell.showVersions
|
|
}
|
|
aria-pressed={panelOpen}
|
|
className={cn(
|
|
iconButton,
|
|
"ml-auto",
|
|
panelOpen && "bg-accent text-foreground",
|
|
)}
|
|
data-testid="versions-toggle"
|
|
>
|
|
<History />
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={() => setSaveOpen(true)}
|
|
aria-label={dict.workspace.export}
|
|
title={dict.workspace.export}
|
|
className={cn(iconButton, !hasVersions && "ml-auto")}
|
|
data-testid="download"
|
|
>
|
|
<Download />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDark(!isDark)}
|
|
aria-label={dict.shell.toggleTheme}
|
|
title={dict.shell.toggleTheme}
|
|
className={iconButton}
|
|
data-testid="theme-toggle"
|
|
>
|
|
{isDark ? <Sun /> : <Moon />}
|
|
</button>
|
|
</header>
|
|
<div className="flex min-h-0 flex-1">
|
|
<div className="relative min-w-0 flex-1">
|
|
<CanvasStage
|
|
className="inset-0"
|
|
drawioSource={drawioSource}
|
|
/>
|
|
</div>
|
|
{showPanel && (
|
|
<VersionsPanel versions={versions.versions} />
|
|
)}
|
|
</div>
|
|
<CompareDialog />
|
|
<SaveDialog
|
|
open={saveOpen}
|
|
onOpenChange={setSaveOpen}
|
|
onSave={save}
|
|
defaultFilename={`diagram-${new Date().toLocaleDateString("sv-SE")}`}
|
|
/>
|
|
<AppToaster />
|
|
</div>
|
|
</VersionsProvider>
|
|
)
|
|
}
|