mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 04:29:51 +08:00
fix(chat): keep the canvas after an unrelated error, and more review fixes
Found by the second PR review: - After a streamed edit, an older render of the stream stored the edit's original diagram again, and the next failed request (no quota, a lost connection) put that old diagram back on the canvas. The tool handler now marks its call as handled, so the preview code leaves it alone. - An edit applied before the UI showed an earlier broken edit's error was erased when that error undid its preview, or was built on that preview. The handler now starts from the diagram before all unhandled previews, and reads the diagram state that updates at once. - A failed or stopped display_diagram left its half drawn diagram on the canvas. Its preview is undone now, like an edit's. - "New chat" cleared a chat that could not be saved (storage full). - The settings dialog showed a model list, a fetch error or a test result on the provider that was opened after the request started, and marked a model id changed during the test as tested. - A tool call with broken JSON was shown as cut off by the output limit. - History entries and session thumbnails could pair with a later diagram when draw.io answered an export late. - A server model saved before non-ASCII provider names got into the id was reset to the default model.
This commit is contained in:
@@ -52,6 +52,8 @@ type ValidateDiagramFn = (
|
||||
interface UseDiagramToolHandlersParams {
|
||||
partialXmlRef: MutableRefObject<string>
|
||||
editDiagramOriginalXmlRef: MutableRefObject<Map<string, string>>
|
||||
// Tool calls the streaming preview must leave alone (shared with it)
|
||||
processedToolCallsRef: MutableRefObject<Set<string>>
|
||||
// Failed VLM validations in the current user turn (reset on each user message)
|
||||
validationRetryCountRef: MutableRefObject<number>
|
||||
chartXMLRef: MutableRefObject<string>
|
||||
@@ -78,6 +80,7 @@ interface UseDiagramToolHandlersParams {
|
||||
export function useDiagramToolHandlers({
|
||||
partialXmlRef,
|
||||
editDiagramOriginalXmlRef,
|
||||
processedToolCallsRef,
|
||||
validationRetryCountRef,
|
||||
chartXMLRef,
|
||||
onDisplayChart,
|
||||
@@ -118,10 +121,22 @@ export function useDiagramToolHandlers({
|
||||
)
|
||||
}
|
||||
|
||||
// Stored originals belong to previews not handled yet: this call's,
|
||||
// and those of earlier calls with invalid input, which never get
|
||||
// here. The first is the diagram before all of them. This call's
|
||||
// result replaces those previews, so the preview code must neither
|
||||
// draw them again nor undo them later.
|
||||
const [originalXml] = editDiagramOriginalXmlRef.current.values()
|
||||
for (const id of editDiagramOriginalXmlRef.current.keys()) {
|
||||
processedToolCallsRef.current.add(id)
|
||||
}
|
||||
processedToolCallsRef.current.add(toolCall.toolCallId)
|
||||
editDiagramOriginalXmlRef.current.clear()
|
||||
|
||||
if (toolCall.toolName === "display_diagram") {
|
||||
await handleDisplayDiagram(toolCall, addToolOutput)
|
||||
} else if (toolCall.toolName === "edit_diagram") {
|
||||
await handleEditDiagram(toolCall, addToolOutput)
|
||||
await handleEditDiagram(toolCall, addToolOutput, originalXml)
|
||||
} else if (toolCall.toolName === "append_diagram") {
|
||||
handleAppendDiagram(toolCall, addToolOutput)
|
||||
}
|
||||
@@ -369,20 +384,18 @@ ${finalXml}
|
||||
}
|
||||
}
|
||||
|
||||
// originalXml: the diagram before the streamed previews, if any were drawn.
|
||||
// Operations apply to it, the same base XML that streaming used.
|
||||
const handleEditDiagram = async (
|
||||
toolCall: ToolCall,
|
||||
addToolOutput: AddToolOutputFn,
|
||||
originalXml: string | undefined,
|
||||
) => {
|
||||
const { operations } = toolCall.input as {
|
||||
operations: DiagramOperation[]
|
||||
}
|
||||
|
||||
let currentXml = ""
|
||||
// Use the original XML captured during streaming (shared with chat-message-display)
|
||||
// This ensures we apply operations to the same base XML that streaming used
|
||||
const originalXml = editDiagramOriginalXmlRef.current.get(
|
||||
toolCall.toolCallId,
|
||||
)
|
||||
// On failure, undo the streaming preview so the canvas matches the XML
|
||||
// reported back to the model
|
||||
const restoreOriginal = () => {
|
||||
@@ -423,8 +436,6 @@ ${currentXml}
|
||||
|
||||
Please check the cell IDs and retry.`,
|
||||
})
|
||||
// Clean up the shared original XML ref
|
||||
editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -435,8 +446,6 @@ Please check the cell IDs and retry.`,
|
||||
toolCallId: toolCall.toolCallId,
|
||||
output: `Successfully applied ${outcome.applied} operation(s) to the diagram.`,
|
||||
})
|
||||
// Clean up the shared original XML ref
|
||||
editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)
|
||||
} catch (error) {
|
||||
console.error("[edit_diagram] Failed:", error)
|
||||
|
||||
@@ -457,8 +466,6 @@ ${currentXml || "No XML available"}
|
||||
|
||||
Please check cell IDs and retry, or use display_diagram to regenerate.`,
|
||||
})
|
||||
// Clean up the shared original XML ref even on error
|
||||
editDiagramOriginalXmlRef.current.delete(toolCall.toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,19 @@ export function useModelConfig(): UseModelConfigReturn {
|
||||
id?.startsWith("server:") &&
|
||||
!raw.some((m) => m.id === id)
|
||||
if (id && !isStale) return prev
|
||||
const fallback = defaultServerModelId(raw)
|
||||
// Saved before non-ASCII characters in provider names
|
||||
// got into the id: they were dropped from it
|
||||
const renamed = raw.filter(
|
||||
(m) =>
|
||||
`server:${m.providerLabel
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "")}:${m.modelId}` === id,
|
||||
)
|
||||
const fallback =
|
||||
renamed.length === 1
|
||||
? renamed[0].id
|
||||
: defaultServerModelId(raw)
|
||||
return fallback === id
|
||||
? prev
|
||||
: { ...prev, selectedModelId: fallback }
|
||||
|
||||
@@ -38,10 +38,11 @@ export interface UseSessionManagerReturn {
|
||||
switchSession: (id: string) => Promise<SessionData | null>
|
||||
deleteSession: (id: string) => Promise<{ wasCurrentSession: boolean }>
|
||||
// forSessionId: optional session ID to verify save targets correct session (prevents stale debounce writes)
|
||||
// Resolves to false when the save failed (the user was told)
|
||||
saveCurrentSession: (
|
||||
data: SessionData,
|
||||
forSessionId?: string | null,
|
||||
) => Promise<void>
|
||||
) => Promise<boolean>
|
||||
refreshSessions: () => Promise<void>
|
||||
clearCurrentSession: () => void
|
||||
}
|
||||
@@ -247,17 +248,17 @@ export function useSessionManager(
|
||||
async (
|
||||
data: SessionData,
|
||||
forSessionId?: string | null,
|
||||
): Promise<void> => {
|
||||
): Promise<boolean> => {
|
||||
// If forSessionId is provided, verify it matches current session
|
||||
// This prevents stale debounced saves from overwriting a newly switched session
|
||||
if (
|
||||
forSessionId !== undefined &&
|
||||
forSessionId !== currentSessionId
|
||||
) {
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Nothing can be stored without IndexedDB
|
||||
if (!isIndexedDBAvailable()) return
|
||||
if (!isIndexedDBAvailable()) return true
|
||||
|
||||
if (!currentSession) {
|
||||
// Create a new session if none exists
|
||||
@@ -274,13 +275,13 @@ export function useSessionManager(
|
||||
// up in the URL and point to nothing after a reload)
|
||||
if (!(await saveSession(newSession))) {
|
||||
notifySaveFailed(dict.errors.sessionSaveFailed)
|
||||
return
|
||||
return false
|
||||
}
|
||||
await enforceSessionLimit()
|
||||
setCurrentSession(newSession)
|
||||
setCurrentSessionId(newSession.id)
|
||||
await refreshSessions()
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
// Update existing session
|
||||
@@ -304,7 +305,7 @@ export function useSessionManager(
|
||||
|
||||
if (!(await saveSession(updatedSession))) {
|
||||
notifySaveFailed(dict.errors.sessionSaveFailed)
|
||||
return
|
||||
return false
|
||||
}
|
||||
setCurrentSession(updatedSession)
|
||||
|
||||
@@ -325,6 +326,7 @@ export function useSessionManager(
|
||||
: s,
|
||||
),
|
||||
)
|
||||
return true
|
||||
},
|
||||
[currentSession, currentSessionId, refreshSessions, dict],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user