mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 04:29:51 +08:00
The canvas components will be reused by the MCP server's browser shell, which has no chat and no Next.js. They now read everything they need from small contexts and the canvas store instead of the chat engine: - components/canvas/versions-context.tsx: VersionsProvider and useVersionsContext give the version cards, the strip and the compare dialog a VersionsSource (versions, onCanvasId, undoneId, isBusy, canUndo, canRedo, restore, undo, redo). The chat fills it from the versions store and the engine in components/chat/chat-versions.tsx. - stores/canvas-store.ts: isBusy and busyReason; the chat engine sets them while a turn runs, SelectionAsk and the version UI read them. - components/canvas/version-card.tsx: the visual version card and the thumbnail, out of tool-activity.tsx; the chat's card composes it and adds its "Show XML" link and code panel. - compare-dialog.tsx and version-strip.tsx move to components/canvas; the strip takes the minimum number of versions to show as a prop, the chat panel computes it from the cards it has. - components/canvas/locale-context.tsx: LocaleProvider and useLocale, fed by the [lang] layout; CanvasStage no longer uses next/navigation. The overlay (SelectionAsk) is a slot and the wait for the saved language is a prop, so the shell can leave both out. - lib/version-text.ts: describeChanges is now describeChangeSummary, so it can be imported next to the MCP core's describeChanges.
78 lines
3.4 KiB
TypeScript
78 lines
3.4 KiB
TypeScript
import { useEffect, useRef } from "react"
|
|
import { useVersionsContext } from "@/components/canvas/versions-context"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import { cn } from "@/lib/utils"
|
|
import { useUiStore } from "@/stores/ui-store"
|
|
|
|
/** Row of version thumbnails under the panel header; click to compare */
|
|
export function VersionStrip({
|
|
minVersions = 2,
|
|
}: {
|
|
/** Hidden below this many versions: one version alone adds nothing to
|
|
* its card, unless the caller knows there is no card for it */
|
|
minVersions?: number
|
|
}) {
|
|
const dict = useDictionary()
|
|
const { versions, onCanvasId } = useVersionsContext()
|
|
const openCompare = useUiStore((s) => s.openCompare)
|
|
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])
|
|
|
|
if (versions.length < minVersions) 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) => {
|
|
const isCurrent = version.id === onCanvasId
|
|
return (
|
|
<button
|
|
key={version.id}
|
|
type="button"
|
|
onClick={() => openCompare(version.id)}
|
|
title={`v${version.number}${isCurrent ? ` · ${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{version.number}
|
|
</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|