mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-08 10:47:48 +08:00
The canvas now fills the window and the chat floats on its right. draw.io is served from our own origin (public/drawio, downloaded at build time and checked against the release's SHA-256), so the app can drive the editor directly: - every AI change becomes a version card with a thumbnail; compare, restore or undo it, and Ctrl+Z on the canvas takes an AI change back in one step - shapes the AI just changed are highlighted on the canvas - selected shapes appear as a chip and are sent to the model - the app's own canvas toolbar, shape library and page tabs replace draw.io's chrome - new start screen, session rename and switcher, open .drawio files - settings split into Models & keys, Appearance, Advanced and About - phones switch between canvas and chat - dark mode keeps the diagram on a white sheet and no longer flashes With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the app falls back to draw.io's own toolbar and the features above that need same origin are off. The chat logic moved from the 1500-line chat panel into a chat engine plus small UI components; zustand holds canvas, versions, settings and UI state.
89 lines
3.9 KiB
TypeScript
89 lines
3.9 KiB
TypeScript
import { useEffect, useRef } from "react"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import { cn } from "@/lib/utils"
|
|
import { useUiStore } from "@/stores/ui-store"
|
|
import { useVersionsStore } from "@/stores/versions-store"
|
|
import { useChatEngine } from "./chat-engine"
|
|
|
|
/** Row of version thumbnails under the panel header; click to compare */
|
|
export function VersionStrip() {
|
|
const dict = useDictionary()
|
|
const versions = useVersionsStore((s) => s.versions)
|
|
const onCanvasVersionId = useVersionsStore((s) => s.onCanvasVersionId)
|
|
const openCompare = useUiStore((s) => s.openCompare)
|
|
const engine = useChatEngine()
|
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Keep the newest version in view
|
|
useEffect(() => {
|
|
const el = scrollRef.current
|
|
if (el) el.scrollTo({ left: el.scrollWidth, behavior: "smooth" })
|
|
}, [versions.length])
|
|
|
|
// Versions with no card in the chat: saved by older app versions, or
|
|
// from a turn that was retried
|
|
const hasCardless = versions.some(
|
|
(v) =>
|
|
!v.toolCallId ||
|
|
!engine.messages.some((m) =>
|
|
m.parts?.some(
|
|
(p) =>
|
|
(p as { toolCallId?: string }).toolCallId ===
|
|
v.toolCallId,
|
|
),
|
|
),
|
|
)
|
|
// One version with a card: the strip adds nothing to it
|
|
if (versions.length < 2 && !hasCardless) return null
|
|
|
|
return (
|
|
<div className="flex items-center gap-2 border-b border-border px-3.5 pt-1 pb-3">
|
|
<span className="shrink-0 text-xs text-muted-foreground">
|
|
{dict.versions.strip}
|
|
</span>
|
|
<div
|
|
ref={scrollRef}
|
|
className="flex min-w-0 flex-1 gap-1.5 overflow-x-auto p-1 scrollbar-thin"
|
|
>
|
|
{versions.map((version, index) => {
|
|
const isCurrent = version.id === onCanvasVersionId
|
|
return (
|
|
<button
|
|
key={version.id}
|
|
type="button"
|
|
onClick={() => openCompare(version.id)}
|
|
title={`v${index + 1}${version.id === onCanvasVersionId ? ` · ${dict.versions.currentCanvas}` : ""}`}
|
|
className={cn(
|
|
"sheet-light relative flex h-12 w-[72px] shrink-0 items-center justify-center overflow-hidden rounded-lg border border-border bg-white transition-colors hover:border-foreground/30",
|
|
// An outline with a gap reads on the white
|
|
// picture in both themes
|
|
isCurrent &&
|
|
"outline-2 outline-offset-2 outline-foreground",
|
|
)}
|
|
data-testid="version-thumb"
|
|
>
|
|
{version.svg && (
|
|
// biome-ignore lint/performance/noImgElement: data URL thumbnail
|
|
<img
|
|
src={version.svg}
|
|
alt=""
|
|
className="max-h-9 max-w-[62px] object-contain"
|
|
/>
|
|
)}
|
|
<span
|
|
className={cn(
|
|
"absolute bottom-0.5 left-1 text-[10px] font-medium tabular-nums text-neutral-500",
|
|
isCurrent &&
|
|
"font-semibold text-neutral-900",
|
|
)}
|
|
>
|
|
v{index + 1}
|
|
</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|