mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
feat: auto-save and restore session state (#135)
- Save and restore chat messages, XML snapshots, session ID, and diagram XML to localStorage - Restore diagram when DrawIO becomes ready (using new onLoad callback) - Change close protection default to false since auto-save handles persistence - Clear localStorage when clearing chat - Handle edge cases: undefined edit fields, empty chartXML, missing access code header
This commit is contained in:
10
app/page.tsx
10
app/page.tsx
@@ -12,7 +12,7 @@ import {
|
|||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { drawioRef, handleDiagramExport } = useDiagram()
|
const { drawioRef, handleDiagramExport, onDrawioLoad } = useDiagram()
|
||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [isChatVisible, setIsChatVisible] = useState(true)
|
const [isChatVisible, setIsChatVisible] = useState(true)
|
||||||
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
const [drawioUi, setDrawioUi] = useState<"min" | "sketch">("min")
|
||||||
@@ -26,13 +26,14 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
setIsThemeLoaded(true)
|
setIsThemeLoaded(true)
|
||||||
}, [])
|
}, [])
|
||||||
const [closeProtection, setCloseProtection] = useState(true)
|
const [closeProtection, setCloseProtection] = useState(false)
|
||||||
|
|
||||||
// Load close protection setting from localStorage after mount
|
// Load close protection setting from localStorage after mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
|
const saved = localStorage.getItem(STORAGE_CLOSE_PROTECTION_KEY)
|
||||||
if (saved === "false") {
|
// Default to false since auto-save handles persistence
|
||||||
setCloseProtection(false)
|
if (saved === "true") {
|
||||||
|
setCloseProtection(true)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
const chatPanelRef = useRef<ImperativePanelHandle>(null)
|
||||||
@@ -107,6 +108,7 @@ export default function Home() {
|
|||||||
key={drawioUi}
|
key={drawioUi}
|
||||||
ref={drawioRef}
|
ref={drawioRef}
|
||||||
onExport={handleDiagramExport}
|
onExport={handleDiagramExport}
|
||||||
|
onLoad={onDrawioLoad}
|
||||||
urlParameters={{
|
urlParameters={{
|
||||||
ui: drawioUi,
|
ui: drawioUi,
|
||||||
spin: true,
|
spin: true,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{edits.map((edit, index) => (
|
{edits.map((edit, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${edit.search.slice(0, 50)}-${edit.replace.slice(0, 50)}-${index}`}
|
key={`${(edit.search || "").slice(0, 50)}-${(edit.replace || "").slice(0, 50)}-${index}`}
|
||||||
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
className="rounded-lg border border-border/50 overflow-hidden bg-background/50"
|
||||||
>
|
>
|
||||||
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
<div className="px-3 py-1.5 bg-muted/40 border-b border-border/30 flex items-center gap-2">
|
||||||
@@ -177,7 +177,10 @@ export function ChatMessageDisplay({
|
|||||||
const currentXml = xml || ""
|
const currentXml = xml || ""
|
||||||
const convertedXml = convertToLegalXml(currentXml)
|
const convertedXml = convertToLegalXml(currentXml)
|
||||||
if (convertedXml !== previousXML.current) {
|
if (convertedXml !== previousXML.current) {
|
||||||
const replacedXML = replaceNodes(chartXML, convertedXml)
|
// If chartXML is empty, use the converted XML directly
|
||||||
|
const replacedXML = chartXML
|
||||||
|
? replaceNodes(chartXML, convertedXml)
|
||||||
|
: convertedXml
|
||||||
|
|
||||||
const validationError = validateMxCellStructure(replacedXML)
|
const validationError = validateMxCellStructure(replacedXML)
|
||||||
if (!validationError) {
|
if (!validationError) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useCallback, useEffect, useRef, useState } from "react"
|
||||||
import { flushSync } from "react-dom"
|
import { flushSync } from "react-dom"
|
||||||
import { FaGithub } from "react-icons/fa"
|
import { FaGithub } from "react-icons/fa"
|
||||||
import { Toaster } from "sonner"
|
import { Toaster } from "sonner"
|
||||||
@@ -24,6 +24,13 @@ import {
|
|||||||
SettingsDialog,
|
SettingsDialog,
|
||||||
STORAGE_ACCESS_CODE_KEY,
|
STORAGE_ACCESS_CODE_KEY,
|
||||||
} from "@/components/settings-dialog"
|
} from "@/components/settings-dialog"
|
||||||
|
|
||||||
|
// localStorage keys for persistence
|
||||||
|
const STORAGE_MESSAGES_KEY = "next-ai-draw-io-messages"
|
||||||
|
const STORAGE_XML_SNAPSHOTS_KEY = "next-ai-draw-io-xml-snapshots"
|
||||||
|
const STORAGE_SESSION_ID_KEY = "next-ai-draw-io-session-id"
|
||||||
|
const STORAGE_DIAGRAM_XML_KEY = "next-ai-draw-io-diagram-xml"
|
||||||
|
|
||||||
import { useDiagram } from "@/contexts/diagram-context"
|
import { useDiagram } from "@/contexts/diagram-context"
|
||||||
import { formatXML, validateMxCellStructure } from "@/lib/utils"
|
import { formatXML, validateMxCellStructure } from "@/lib/utils"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display"
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
@@ -52,6 +59,7 @@ export default function ChatPanel({
|
|||||||
resolverRef,
|
resolverRef,
|
||||||
chartXML,
|
chartXML,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
|
isDrawioReady,
|
||||||
} = useDiagram()
|
} = useDiagram()
|
||||||
|
|
||||||
const onFetchChart = (saveToHistory = true) => {
|
const onFetchChart = (saveToHistory = true) => {
|
||||||
@@ -83,7 +91,7 @@ export default function ChatPanel({
|
|||||||
const [files, setFiles] = useState<File[]>([])
|
const [files, setFiles] = useState<File[]>([])
|
||||||
const [showHistory, setShowHistory] = useState(false)
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
const [showSettingsDialog, setShowSettingsDialog] = useState(false)
|
||||||
const [accessCodeRequired, setAccessCodeRequired] = useState(false)
|
const [, setAccessCodeRequired] = useState(false)
|
||||||
const [input, setInput] = useState("")
|
const [input, setInput] = useState("")
|
||||||
|
|
||||||
// Check if access code is required on mount
|
// Check if access code is required on mount
|
||||||
@@ -94,14 +102,21 @@ export default function ChatPanel({
|
|||||||
.catch(() => setAccessCodeRequired(false))
|
.catch(() => setAccessCodeRequired(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Generate a unique session ID for Langfuse tracing
|
// Generate a unique session ID for Langfuse tracing (restore from localStorage if available)
|
||||||
const [sessionId, setSessionId] = useState(
|
const [sessionId, setSessionId] = useState(() => {
|
||||||
() => `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
if (typeof window !== "undefined") {
|
||||||
)
|
const saved = localStorage.getItem(STORAGE_SESSION_ID_KEY)
|
||||||
|
if (saved) return saved
|
||||||
|
}
|
||||||
|
return `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
||||||
|
})
|
||||||
|
|
||||||
// Store XML snapshots for each user message (keyed by message index)
|
// Store XML snapshots for each user message (keyed by message index)
|
||||||
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map())
|
const xmlSnapshotsRef = useRef<Map<number, string>>(new Map())
|
||||||
|
|
||||||
|
// Flag to track if we've restored from localStorage
|
||||||
|
const hasRestoredRef = useRef(false)
|
||||||
|
|
||||||
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
|
// Ref to track latest chartXML for use in callbacks (avoids stale closure)
|
||||||
const chartXMLRef = useRef(chartXML)
|
const chartXMLRef = useRef(chartXML)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -253,14 +268,162 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
// Update stopRef so onToolCall can access it
|
// Update stopRef so onToolCall can access it
|
||||||
stopRef.current = stop
|
stopRef.current = stop
|
||||||
|
|
||||||
|
// Ref to track latest messages for unload persistence
|
||||||
|
const messagesRef = useRef(messages)
|
||||||
|
useEffect(() => {
|
||||||
|
messagesRef.current = messages
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Restore messages and XML snapshots from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasRestoredRef.current) return
|
||||||
|
hasRestoredRef.current = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Restore messages
|
||||||
|
const savedMessages = localStorage.getItem(STORAGE_MESSAGES_KEY)
|
||||||
|
if (savedMessages) {
|
||||||
|
const parsed = JSON.parse(savedMessages)
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
setMessages(parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore XML snapshots
|
||||||
|
const savedSnapshots = localStorage.getItem(
|
||||||
|
STORAGE_XML_SNAPSHOTS_KEY,
|
||||||
|
)
|
||||||
|
if (savedSnapshots) {
|
||||||
|
const parsed = JSON.parse(savedSnapshots)
|
||||||
|
xmlSnapshotsRef.current = new Map(parsed)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restore from localStorage:", error)
|
||||||
|
}
|
||||||
|
}, [setMessages])
|
||||||
|
|
||||||
|
// Restore diagram XML when DrawIO becomes ready
|
||||||
|
const hasDiagramRestoredRef = useRef(false)
|
||||||
|
const [canSaveDiagram, setCanSaveDiagram] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(
|
||||||
|
"[ChatPanel] isDrawioReady:",
|
||||||
|
isDrawioReady,
|
||||||
|
"hasDiagramRestored:",
|
||||||
|
hasDiagramRestoredRef.current,
|
||||||
|
)
|
||||||
|
if (!isDrawioReady || hasDiagramRestoredRef.current) return
|
||||||
|
hasDiagramRestoredRef.current = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedDiagramXml = localStorage.getItem(
|
||||||
|
STORAGE_DIAGRAM_XML_KEY,
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
"[ChatPanel] Restoring diagram, has saved XML:",
|
||||||
|
!!savedDiagramXml,
|
||||||
|
)
|
||||||
|
if (savedDiagramXml) {
|
||||||
|
console.log(
|
||||||
|
"[ChatPanel] Loading saved diagram XML, length:",
|
||||||
|
savedDiagramXml.length,
|
||||||
|
)
|
||||||
|
onDisplayChart(savedDiagramXml)
|
||||||
|
chartXMLRef.current = savedDiagramXml
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restore diagram from localStorage:", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow saving after restore is complete
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log("[ChatPanel] Enabling diagram save")
|
||||||
|
setCanSaveDiagram(true)
|
||||||
|
}, 500)
|
||||||
|
}, [isDrawioReady, onDisplayChart])
|
||||||
|
|
||||||
|
// Save messages to localStorage whenever they change
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasRestoredRef.current) return
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_MESSAGES_KEY, JSON.stringify(messages))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save messages to localStorage:", error)
|
||||||
|
}
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
// Save XML snapshots to localStorage whenever they change
|
||||||
|
const saveXmlSnapshots = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const snapshotsArray = Array.from(xmlSnapshotsRef.current.entries())
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_XML_SNAPSHOTS_KEY,
|
||||||
|
JSON.stringify(snapshotsArray),
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Failed to save XML snapshots to localStorage:",
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Save session ID to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
|
// Save current diagram XML to localStorage whenever it changes
|
||||||
|
// Only save after initial restore is complete and if it's not an empty diagram
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canSaveDiagram) return
|
||||||
|
// Don't save empty diagrams (check for minimal content)
|
||||||
|
if (chartXML && chartXML.length > 300) {
|
||||||
|
console.log(
|
||||||
|
"[ChatPanel] Saving diagram to localStorage, length:",
|
||||||
|
chartXML.length,
|
||||||
|
)
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, chartXML)
|
||||||
|
}
|
||||||
|
}, [chartXML, canSaveDiagram])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" })
|
||||||
}
|
}
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
// Save state right before page unload (refresh/close)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleBeforeUnload = () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_MESSAGES_KEY,
|
||||||
|
JSON.stringify(messagesRef.current),
|
||||||
|
)
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_XML_SNAPSHOTS_KEY,
|
||||||
|
JSON.stringify(
|
||||||
|
Array.from(xmlSnapshotsRef.current.entries()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const xml = chartXMLRef.current
|
||||||
|
if (xml && xml.length > 300) {
|
||||||
|
localStorage.setItem(STORAGE_DIAGRAM_XML_KEY, xml)
|
||||||
|
}
|
||||||
|
localStorage.setItem(STORAGE_SESSION_ID_KEY, sessionId)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to persist state before unload:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", handleBeforeUnload)
|
||||||
|
return () =>
|
||||||
|
window.removeEventListener("beforeunload", handleBeforeUnload)
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const isProcessing = status === "streaming" || status === "submitted"
|
const isProcessing = status === "streaming" || status === "submitted"
|
||||||
@@ -295,6 +458,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
// Save XML snapshot for this message (will be at index = current messages.length)
|
// Save XML snapshot for this message (will be at index = current messages.length)
|
||||||
const messageIndex = messages.length
|
const messageIndex = messages.length
|
||||||
xmlSnapshotsRef.current.set(messageIndex, chartXml)
|
xmlSnapshotsRef.current.set(messageIndex, chartXml)
|
||||||
|
saveXmlSnapshots()
|
||||||
|
|
||||||
const accessCode =
|
const accessCode =
|
||||||
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
@@ -373,6 +537,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xmlSnapshotsRef.current.delete(key)
|
xmlSnapshotsRef.current.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
saveXmlSnapshots()
|
||||||
|
|
||||||
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
// Remove the user message AND assistant message onwards (sendMessage will re-add the user message)
|
||||||
// Use flushSync to ensure state update is processed synchronously before sending
|
// Use flushSync to ensure state update is processed synchronously before sending
|
||||||
@@ -382,6 +547,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Now send the message after state is guaranteed to be updated
|
// Now send the message after state is guaranteed to be updated
|
||||||
|
const accessCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts: userParts },
|
{ parts: userParts },
|
||||||
{
|
{
|
||||||
@@ -389,6 +555,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xml: savedXml,
|
xml: savedXml,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -422,6 +591,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xmlSnapshotsRef.current.delete(key)
|
xmlSnapshotsRef.current.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
saveXmlSnapshots()
|
||||||
|
|
||||||
// Create new parts with updated text
|
// Create new parts with updated text
|
||||||
const newParts = message.parts?.map((part: any) => {
|
const newParts = message.parts?.map((part: any) => {
|
||||||
@@ -439,6 +609,7 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Now send the edited message after state is guaranteed to be updated
|
// Now send the edited message after state is guaranteed to be updated
|
||||||
|
const accessCode = localStorage.getItem(STORAGE_ACCESS_CODE_KEY) || ""
|
||||||
sendMessage(
|
sendMessage(
|
||||||
{ parts: newParts },
|
{ parts: newParts },
|
||||||
{
|
{
|
||||||
@@ -446,6 +617,9 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
xml: savedXml,
|
xml: savedXml,
|
||||||
sessionId,
|
sessionId,
|
||||||
},
|
},
|
||||||
|
headers: {
|
||||||
|
"x-access-code": accessCode,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -584,12 +758,19 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
onClearChat={() => {
|
onClearChat={() => {
|
||||||
setMessages([])
|
setMessages([])
|
||||||
clearDiagram()
|
clearDiagram()
|
||||||
setSessionId(
|
const newSessionId = `session-${Date.now()}-${Math.random()
|
||||||
`session-${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
.toString(36)
|
||||||
.slice(2, 9)}`,
|
.slice(2, 9)}`
|
||||||
)
|
setSessionId(newSessionId)
|
||||||
xmlSnapshotsRef.current.clear()
|
xmlSnapshotsRef.current.clear()
|
||||||
|
// Clear localStorage
|
||||||
|
localStorage.removeItem(STORAGE_MESSAGES_KEY)
|
||||||
|
localStorage.removeItem(STORAGE_XML_SNAPSHOTS_KEY)
|
||||||
|
localStorage.removeItem(STORAGE_DIAGRAM_XML_KEY)
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_SESSION_ID_KEY,
|
||||||
|
newSessionId,
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
files={files}
|
files={files}
|
||||||
onFileChange={handleFileChange}
|
onFileChange={handleFileChange}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ interface DiagramContextType {
|
|||||||
format: ExportFormat,
|
format: ExportFormat,
|
||||||
sessionId?: string,
|
sessionId?: string,
|
||||||
) => void
|
) => void
|
||||||
|
isDrawioReady: boolean
|
||||||
|
onDrawioLoad: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
const DiagramContext = createContext<DiagramContextType | undefined>(undefined)
|
||||||
@@ -32,10 +34,20 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [diagramHistory, setDiagramHistory] = useState<
|
const [diagramHistory, setDiagramHistory] = useState<
|
||||||
{ svg: string; xml: string }[]
|
{ svg: string; xml: string }[]
|
||||||
>([])
|
>([])
|
||||||
|
const [isDrawioReady, setIsDrawioReady] = useState(false)
|
||||||
|
const hasCalledOnLoadRef = useRef(false)
|
||||||
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
const drawioRef = useRef<DrawIoEmbedRef | null>(null)
|
||||||
const resolverRef = useRef<((value: string) => void) | null>(null)
|
const resolverRef = useRef<((value: string) => void) | null>(null)
|
||||||
// Track if we're expecting an export for history (user-initiated)
|
// Track if we're expecting an export for history (user-initiated)
|
||||||
const expectHistoryExportRef = useRef<boolean>(false)
|
const expectHistoryExportRef = useRef<boolean>(false)
|
||||||
|
|
||||||
|
const onDrawioLoad = () => {
|
||||||
|
// Only set ready state once to prevent infinite loops
|
||||||
|
if (hasCalledOnLoadRef.current) return
|
||||||
|
hasCalledOnLoadRef.current = true
|
||||||
|
console.log("[DiagramContext] DrawIO loaded, setting ready state")
|
||||||
|
setIsDrawioReady(true)
|
||||||
|
}
|
||||||
// Track if we're expecting an export for file save (stores raw export data)
|
// Track if we're expecting an export for file save (stores raw export data)
|
||||||
const saveResolverRef = useRef<{
|
const saveResolverRef = useRef<{
|
||||||
resolver: ((data: string) => void) | null
|
resolver: ((data: string) => void) | null
|
||||||
@@ -62,6 +74,9 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadDiagram = (chart: string) => {
|
const loadDiagram = (chart: string) => {
|
||||||
|
// Keep chartXML in sync even when diagrams are injected (e.g., display_diagram tool)
|
||||||
|
setChartXML(chart)
|
||||||
|
|
||||||
if (drawioRef.current) {
|
if (drawioRef.current) {
|
||||||
drawioRef.current.load({
|
drawioRef.current.load({
|
||||||
xml: chart,
|
xml: chart,
|
||||||
@@ -220,6 +235,8 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
|
|||||||
handleDiagramExport,
|
handleDiagramExport,
|
||||||
clearDiagram,
|
clearDiagram,
|
||||||
saveDiagramToFile,
|
saveDiagramToFile,
|
||||||
|
isDrawioReady,
|
||||||
|
onDrawioLoad,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -467,7 +467,9 @@ export function replaceXMLParts(
|
|||||||
i,
|
i,
|
||||||
i + searchLines.length,
|
i + searchLines.length,
|
||||||
)
|
)
|
||||||
const normalizedCandidate = normalizeWs(candidateLines.join(" "))
|
const normalizedCandidate = normalizeWs(
|
||||||
|
candidateLines.join(" "),
|
||||||
|
)
|
||||||
|
|
||||||
if (normalizedCandidate === normalizedSearch) {
|
if (normalizedCandidate === normalizedSearch) {
|
||||||
matchStartLine = i
|
matchStartLine = i
|
||||||
|
|||||||
Reference in New Issue
Block a user