mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
fix: prevent diagram regeneration loop after successful display (#206)
- Changed sendAutomaticallyWhen to only auto-resubmit on tool errors - Extracted logic to shouldAutoResubmit() function with JSDoc - Added TypeScript interfaces for type safety (MessagePart, ChatMessage) - Wrapped debug logs with DEBUG flag for production readiness - Added TOOL_ERROR_STATE constant to avoid hardcoded strings Problem: AI was regenerating diagrams 3+ times even after successful display Root cause: lastAssistantMessageIsCompleteWithToolCalls auto-resubmits on both success AND error Solution: Custom logic that only auto-resubmits on errors, stops on success
This commit is contained in:
@@ -1,10 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useChat } from "@ai-sdk/react"
|
import { useChat } from "@ai-sdk/react"
|
||||||
import {
|
import { DefaultChatTransport } from "ai"
|
||||||
DefaultChatTransport,
|
|
||||||
lastAssistantMessageIsCompleteWithToolCalls,
|
|
||||||
} from "ai"
|
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
PanelRightClose,
|
PanelRightClose,
|
||||||
@@ -54,6 +51,20 @@ import {
|
|||||||
import { formatXML, wrapWithMxFile } from "@/lib/utils"
|
import { formatXML, wrapWithMxFile } from "@/lib/utils"
|
||||||
import { ChatMessageDisplay } from "./chat-message-display"
|
import { ChatMessageDisplay } from "./chat-message-display"
|
||||||
|
|
||||||
|
// Type for message parts (tool calls and their states)
|
||||||
|
interface MessagePart {
|
||||||
|
type: string
|
||||||
|
state?: string
|
||||||
|
toolName?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
role: string
|
||||||
|
parts?: MessagePart[]
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
interface ChatPanelProps {
|
interface ChatPanelProps {
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
onToggleVisibility: () => void
|
onToggleVisibility: () => void
|
||||||
@@ -65,6 +76,70 @@ interface ChatPanelProps {
|
|||||||
onCloseProtectionChange?: (enabled: boolean) => void
|
onCloseProtectionChange?: (enabled: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constants for tool states
|
||||||
|
const TOOL_ERROR_STATE = "output-error" as const
|
||||||
|
const DEBUG = process.env.NODE_ENV === "development"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom auto-resubmit logic for the AI chat.
|
||||||
|
*
|
||||||
|
* Strategy:
|
||||||
|
* - When tools return errors (e.g., invalid XML), automatically resubmit
|
||||||
|
* the conversation to let the AI retry with corrections
|
||||||
|
* - When tools succeed (e.g., diagram displayed), stop without AI acknowledgment
|
||||||
|
* to prevent unnecessary regeneration cycles
|
||||||
|
*
|
||||||
|
* This fixes the issue where successful diagrams were being regenerated
|
||||||
|
* multiple times because the previous logic (lastAssistantMessageIsCompleteWithToolCalls)
|
||||||
|
* auto-resubmitted on BOTH success and error.
|
||||||
|
*
|
||||||
|
* @param messages - Current conversation messages from AI SDK
|
||||||
|
* @returns true to auto-resubmit (for error recovery), false to stop
|
||||||
|
*/
|
||||||
|
function shouldAutoResubmit(messages: ChatMessage[]): boolean {
|
||||||
|
const lastMessage = messages[messages.length - 1]
|
||||||
|
if (!lastMessage || lastMessage.role !== "assistant") {
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
"[sendAutomaticallyWhen] No assistant message, returning false",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolParts =
|
||||||
|
(lastMessage.parts as MessagePart[] | undefined)?.filter((part) =>
|
||||||
|
part.type?.startsWith("tool-"),
|
||||||
|
) || []
|
||||||
|
|
||||||
|
if (toolParts.length === 0) {
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
"[sendAutomaticallyWhen] No tool parts, returning false",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only auto-resubmit if ANY tool has an error
|
||||||
|
const hasError = toolParts.some((part) => part.state === TOOL_ERROR_STATE)
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
if (hasError) {
|
||||||
|
console.log(
|
||||||
|
"[sendAutomaticallyWhen] Retrying due to errors in tools:",
|
||||||
|
toolParts
|
||||||
|
.filter((p) => p.state === TOOL_ERROR_STATE)
|
||||||
|
.map((p) => p.toolName),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.log("[sendAutomaticallyWhen] No errors, stopping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasError
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatPanel({
|
export default function ChatPanel({
|
||||||
isVisible,
|
isVisible,
|
||||||
onToggleVisibility,
|
onToggleVisibility,
|
||||||
@@ -369,8 +444,19 @@ export default function ChatPanel({
|
|||||||
api: "/api/chat",
|
api: "/api/chat",
|
||||||
}),
|
}),
|
||||||
async onToolCall({ toolCall }) {
|
async onToolCall({ toolCall }) {
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
`[onToolCall] Tool: ${toolCall.toolName}, CallId: ${toolCall.toolCallId}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (toolCall.toolName === "display_diagram") {
|
if (toolCall.toolName === "display_diagram") {
|
||||||
const { xml } = toolCall.input as { xml: string }
|
const { xml } = toolCall.input as { xml: string }
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
`[display_diagram] Received XML length: ${xml.length}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap raw XML with full mxfile structure for draw.io
|
// Wrap raw XML with full mxfile structure for draw.io
|
||||||
const fullXml = wrapWithMxFile(xml)
|
const fullXml = wrapWithMxFile(xml)
|
||||||
@@ -392,6 +478,11 @@ Your failed XML:
|
|||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
${xml}
|
${xml}
|
||||||
\`\`\``
|
\`\`\``
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
"[display_diagram] Adding tool output with state: output-error",
|
||||||
|
)
|
||||||
|
}
|
||||||
addToolOutput({
|
addToolOutput({
|
||||||
tool: "display_diagram",
|
tool: "display_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
@@ -400,11 +491,21 @@ ${xml}
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Success - diagram will be rendered by chat-message-display
|
// Success - diagram will be rendered by chat-message-display
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
"[display_diagram] Success! Adding tool output with state: output-available",
|
||||||
|
)
|
||||||
|
}
|
||||||
addToolOutput({
|
addToolOutput({
|
||||||
tool: "display_diagram",
|
tool: "display_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: "Successfully displayed the diagram.",
|
output: "Successfully displayed the diagram.",
|
||||||
})
|
})
|
||||||
|
if (DEBUG) {
|
||||||
|
console.log(
|
||||||
|
"[display_diagram] Tool output added. Diagram should be visible now.",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (toolCall.toolName === "edit_diagram") {
|
} else if (toolCall.toolName === "edit_diagram") {
|
||||||
const { edits } = toolCall.input as {
|
const { edits } = toolCall.input as {
|
||||||
@@ -549,9 +650,8 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Auto-resubmit when all tool results are available (including errors)
|
sendAutomaticallyWhen: ({ messages }) =>
|
||||||
// This enables the model to retry when a tool returns an error
|
shouldAutoResubmit(messages as unknown as ChatMessage[]),
|
||||||
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update stopRef so onToolCall can access it
|
// Update stopRef so onToolCall can access it
|
||||||
|
|||||||
Reference in New Issue
Block a user