mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 12:39:52 +08:00
feat(ui): canvas-first redesign with a self-hosted draw.io
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.
This commit is contained in:
@@ -14,18 +14,30 @@ import {
|
||||
isIndexedDBAvailable,
|
||||
migrateFromLocalStorage,
|
||||
readSessionCount,
|
||||
renameSession as renameSessionInDB,
|
||||
type SessionMetadata,
|
||||
type StoredMessage,
|
||||
saveSession,
|
||||
} from "@/lib/session-storage"
|
||||
import { STORAGE_KEYS } from "@/lib/storage"
|
||||
import {
|
||||
type DiagramVersion,
|
||||
versionsFromLegacyHistory,
|
||||
} from "@/stores/versions-store"
|
||||
|
||||
export interface SessionData {
|
||||
messages: StoredMessage[]
|
||||
xmlSnapshots: [number, string][]
|
||||
diagramXml: string
|
||||
thumbnailDataUrl?: string
|
||||
diagramHistory?: { svg: string; xml: string }[]
|
||||
versions?: DiagramVersion[]
|
||||
/** Title for a session created by this save (default: first message) */
|
||||
title?: string
|
||||
}
|
||||
|
||||
/** Versions of a stored session, converting the old history format */
|
||||
export function getSessionVersions(session: ChatSession): DiagramVersion[] {
|
||||
return session.versions ?? versionsFromLegacyHistory(session.diagramHistory)
|
||||
}
|
||||
|
||||
// Taken right before a save's data is read: the chat on screen then, and
|
||||
@@ -58,6 +70,7 @@ export interface UseSessionManagerReturn {
|
||||
clearCurrentSession: () => void
|
||||
getChatGeneration: () => number
|
||||
getSaveTicket: () => SaveTicket
|
||||
renameSession: (id: string, title: string) => Promise<void>
|
||||
}
|
||||
|
||||
// Reading the session list loads every stored session in full, and window
|
||||
@@ -267,7 +280,7 @@ export function useSessionManager(
|
||||
xmlSnapshots: session.xmlSnapshots,
|
||||
diagramXml: session.diagramXml,
|
||||
thumbnailDataUrl: session.thumbnailDataUrl,
|
||||
diagramHistory: session.diagramHistory,
|
||||
versions: getSessionVersions(session),
|
||||
}
|
||||
},
|
||||
[currentSessionId, changeChat],
|
||||
@@ -319,8 +332,8 @@ export function useSessionManager(
|
||||
xmlSnapshots: data.xmlSnapshots,
|
||||
diagramXml: data.diagramXml,
|
||||
thumbnailDataUrl: data.thumbnailDataUrl,
|
||||
diagramHistory: data.diagramHistory,
|
||||
title: extractTitle(data.messages),
|
||||
versions: data.versions,
|
||||
title: data.title || extractTitle(data.messages),
|
||||
}
|
||||
// Without a stored session, keep no session id (it would end
|
||||
// up in the URL and point to nothing after a reload)
|
||||
@@ -348,8 +361,9 @@ export function useSessionManager(
|
||||
thumbnailDataUrl:
|
||||
data.thumbnailDataUrl ??
|
||||
currentSession.thumbnailDataUrl,
|
||||
diagramHistory:
|
||||
data.diagramHistory ?? currentSession.diagramHistory,
|
||||
versions:
|
||||
data.versions ?? getSessionVersions(currentSession),
|
||||
diagramHistory: undefined,
|
||||
updatedAt: Date.now(),
|
||||
// Update title if it's still default and we have messages
|
||||
title:
|
||||
@@ -412,6 +426,36 @@ export function useSessionManager(
|
||||
[],
|
||||
)
|
||||
|
||||
const renameSession = useCallback(
|
||||
(id: string, title: string): Promise<void> => {
|
||||
const trimmed = title.trim().slice(0, 100)
|
||||
if (!trimmed) return Promise.resolve()
|
||||
// In the save queue, so a save running now cannot put the old
|
||||
// title back
|
||||
const run = async () => {
|
||||
const updated = await renameSessionInDB(id, trimmed)
|
||||
if (!updated) {
|
||||
notifySaveFailed(dict.errors.sessionSaveFailed)
|
||||
return
|
||||
}
|
||||
const current = currentSessionRef.current
|
||||
if (current?.id === id) {
|
||||
currentSessionRef.current = { ...current, title: trimmed }
|
||||
setCurrentSession(currentSessionRef.current)
|
||||
}
|
||||
setSessions((prev) =>
|
||||
prev.map((s) =>
|
||||
s.id === id ? { ...s, title: trimmed } : s,
|
||||
),
|
||||
)
|
||||
}
|
||||
const result = saveQueueRef.current.then(run)
|
||||
saveQueueRef.current = result.catch(() => {})
|
||||
return result
|
||||
},
|
||||
[dict],
|
||||
)
|
||||
|
||||
return {
|
||||
sessions,
|
||||
currentSessionId,
|
||||
@@ -425,5 +469,6 @@ export function useSessionManager(
|
||||
clearCurrentSession,
|
||||
getChatGeneration,
|
||||
getSaveTicket,
|
||||
renameSession,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user