From 6965a54f48c2518778c273ccb29ee44b0b841ce8 Mon Sep 17 00:00:00 2001 From: Dayuan Jiang <34411969+DayuanJiang@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:40:11 +0900 Subject: [PATCH] feat: upgrade @ai-sdk/react to 2.0.107 and migrate to new API (#126) - Upgrade @ai-sdk/react from 2.0.22 to 2.0.107 - Migrate from addToolResult to addToolOutput (new API) - Add output-error state for proper error signaling to model - Add sendAutomaticallyWhen for auto-retry on tool errors - Add stop function ref for potential future use Co-authored-by: dayuan.jiang --- components/chat-panel.tsx | 223 ++++++++++++++++++++++---------------- package-lock.json | 192 ++++---------------------------- package.json | 2 +- 3 files changed, 153 insertions(+), 264 deletions(-) diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx index f24c5e4..5906c31 100644 --- a/components/chat-panel.tsx +++ b/components/chat-panel.tsx @@ -1,7 +1,10 @@ "use client" import { useChat } from "@ai-sdk/react" -import { DefaultChatTransport } from "ai" +import { + DefaultChatTransport, + lastAssistantMessageIsCompleteWithToolCalls, +} from "ai" import { CheckCircle, PanelRightClose, @@ -105,83 +108,109 @@ export default function ChatPanel({ chartXMLRef.current = chartXML }, [chartXML]) - const { messages, sendMessage, addToolResult, status, error, setMessages } = - useChat({ - transport: new DefaultChatTransport({ - api: "/api/chat", - }), - async onToolCall({ toolCall }) { - if (toolCall.toolName === "display_diagram") { - const { xml } = toolCall.input as { xml: string } + // Ref to hold stop function for use in onToolCall (avoids stale closure) + const stopRef = useRef<(() => void) | null>(null) - const validationError = validateMxCellStructure(xml) + const { + messages, + sendMessage, + addToolOutput, + stop, + status, + error, + setMessages, + } = useChat({ + transport: new DefaultChatTransport({ + api: "/api/chat", + }), + async onToolCall({ toolCall }) { + if (toolCall.toolName === "display_diagram") { + const { xml } = toolCall.input as { xml: string } - if (validationError) { - addToolResult({ - tool: "display_diagram", - toolCallId: toolCall.toolCallId, - output: validationError, - }) + // Validate the final XML result + const validationError = validateMxCellStructure(xml) + + if (validationError) { + console.warn( + "[display_diagram] Validation error:", + validationError, + ) + // Return error to model - sendAutomaticallyWhen will trigger retry + const errorMessage = `${validationError} + +Please fix the XML issues and call display_diagram again with corrected XML. + +Your failed XML: +\`\`\`xml +${xml} +\`\`\`` + addToolOutput({ + tool: "display_diagram", + toolCallId: toolCall.toolCallId, + state: "output-error", + errorText: errorMessage, + }) + } else { + // Success - diagram will be rendered by chat-message-display + addToolOutput({ + tool: "display_diagram", + toolCallId: toolCall.toolCallId, + output: "Successfully displayed the diagram.", + }) + } + } else if (toolCall.toolName === "edit_diagram") { + const { edits } = toolCall.input as { + edits: Array<{ search: string; replace: string }> + } + + let currentXml = "" + try { + console.log("[edit_diagram] Starting...") + // Use chartXML from ref directly - more reliable than export + // especially on Vercel where DrawIO iframe may have latency issues + // Using ref to avoid stale closure in callback + const cachedXML = chartXMLRef.current + if (cachedXML) { + currentXml = cachedXML + console.log( + "[edit_diagram] Using cached chartXML, length:", + currentXml.length, + ) } else { - addToolResult({ - tool: "display_diagram", - toolCallId: toolCall.toolCallId, - output: "Successfully displayed the diagram.", - }) - } - } else if (toolCall.toolName === "edit_diagram") { - const { edits } = toolCall.input as { - edits: Array<{ search: string; replace: string }> + // Fallback to export only if no cached XML + console.log( + "[edit_diagram] No cached XML, fetching from DrawIO...", + ) + currentXml = await onFetchChart(false) + console.log( + "[edit_diagram] Got XML from export, length:", + currentXml.length, + ) } - let currentXml = "" - try { - console.log("[edit_diagram] Starting...") - // Use chartXML from ref directly - more reliable than export - // especially on Vercel where DrawIO iframe may have latency issues - // Using ref to avoid stale closure in callback - const cachedXML = chartXMLRef.current - if (cachedXML) { - currentXml = cachedXML - console.log( - "[edit_diagram] Using cached chartXML, length:", - currentXml.length, - ) - } else { - // Fallback to export only if no cached XML - console.log( - "[edit_diagram] No cached XML, fetching from DrawIO...", - ) - currentXml = await onFetchChart(false) - console.log( - "[edit_diagram] Got XML from export, length:", - currentXml.length, - ) - } + const { replaceXMLParts } = await import("@/lib/utils") + const editedXml = replaceXMLParts(currentXml, edits) - const { replaceXMLParts } = await import("@/lib/utils") - const editedXml = replaceXMLParts(currentXml, edits) + onDisplayChart(editedXml) - onDisplayChart(editedXml) + addToolOutput({ + tool: "edit_diagram", + toolCallId: toolCall.toolCallId, + output: `Successfully applied ${edits.length} edit(s) to the diagram.`, + }) + console.log("[edit_diagram] Success") + } catch (error) { + console.error("[edit_diagram] Failed:", error) - addToolResult({ - tool: "edit_diagram", - toolCallId: toolCall.toolCallId, - output: `Successfully applied ${edits.length} edit(s) to the diagram.`, - }) - console.log("[edit_diagram] Success") - } catch (error) { - console.error("[edit_diagram] Failed:", error) + const errorMessage = + error instanceof Error ? error.message : String(error) - const errorMessage = - error instanceof Error - ? error.message - : String(error) - - addToolResult({ - tool: "edit_diagram", - toolCallId: toolCall.toolCallId, - output: `Edit failed: ${errorMessage} + // Use addToolOutput with state: 'output-error' for proper error signaling + addToolOutput({ + tool: "edit_diagram", + toolCallId: toolCall.toolCallId, + state: "output-error", + errorText: `Edit failed: ${errorMessage} Current diagram XML: \`\`\`xml @@ -189,34 +218,40 @@ ${currentXml || "No XML available"} \`\`\` Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`, - }) - } - } - }, - onError: (error) => { - // Silence access code error in console since it's handled by UI - if (!error.message.includes("Invalid or missing access code")) { - console.error("Chat error:", error) + }) } + } + }, + onError: (error) => { + // Silence access code error in console since it's handled by UI + if (!error.message.includes("Invalid or missing access code")) { + console.error("Chat error:", error) + } - // Add system message for error so it can be cleared - setMessages((currentMessages) => { - const errorMessage = { - id: `error-${Date.now()}`, - role: "system" as const, - content: error.message, - parts: [{ type: "text" as const, text: error.message }], - } - return [...currentMessages, errorMessage] - }) - - if (error.message.includes("Invalid or missing access code")) { - // Show settings button and open dialog to help user fix it - setAccessCodeRequired(true) - setShowSettingsDialog(true) + // Add system message for error so it can be cleared + setMessages((currentMessages) => { + const errorMessage = { + id: `error-${Date.now()}`, + role: "system" as const, + content: error.message, + parts: [{ type: "text" as const, text: error.message }], } - }, - }) + return [...currentMessages, errorMessage] + }) + + if (error.message.includes("Invalid or missing access code")) { + // Show settings button and open dialog to help user fix it + setAccessCodeRequired(true) + setShowSettingsDialog(true) + } + }, + // Auto-resubmit when all tool results are available (including errors) + // This enables the model to retry when a tool returns an error + sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls, + }) + + // Update stopRef so onToolCall can access it + stopRef.current = stop const messagesEndRef = useRef(null) diff --git a/package-lock.json b/package-lock.json index be9eb24..9a834b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@ai-sdk/deepseek": "^1.0.30", "@ai-sdk/google": "^2.0.0", "@ai-sdk/openai": "^2.0.19", - "@ai-sdk/react": "^2.0.22", + "@ai-sdk/react": "^2.0.107", "@aws-sdk/credential-providers": "^3.943.0", "@langfuse/client": "^4.4.9", "@langfuse/otel": "^4.4.4", @@ -90,23 +90,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/amazon-bedrock/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", - "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/anthropic": { "version": "2.0.50", "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-2.0.50.tgz", @@ -123,23 +106,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/anthropic/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", - "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/azure": { "version": "2.0.69", "resolved": "https://registry.npmjs.org/@ai-sdk/azure/-/azure-2.0.69.tgz", @@ -191,32 +157,15 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/deepseek/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", - "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/gateway": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.7.tgz", - "integrity": "sha512-/AI5AKi4vOK9SEb8Z1dfXkhsJ5NAfWsoJQc96B/mzn2KIrjw5occOjIwD06scuhV9xWlghCoXJT1sQD9QH/tyg==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.18.tgz", + "integrity": "sha512-sDQcW+6ck2m0pTIHW6BPHD7S125WD3qNkx/B8sEzJp/hurocmJ5Cni0ybExg6sQMGo+fr/GWOwpHF1cmCdg5rQ==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.16", - "@vercel/oidc": "3.0.3" + "@ai-sdk/provider-utils": "3.0.18", + "@vercel/oidc": "3.0.5" }, "engines": { "node": ">=18" @@ -300,23 +249,6 @@ "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", - "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider-utils": { "version": "3.0.17", "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", @@ -347,9 +279,9 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.16.tgz", - "integrity": "sha512-lsWQY9aDXHitw7C1QRYIbVGmgwyT98TF3MfM8alNIXKpdJdi+W782Rzd9f1RyOfgRmZ08gJ2EYNDhWNK7RqpEA==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.18.tgz", + "integrity": "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.0", @@ -364,13 +296,13 @@ } }, "node_modules/@ai-sdk/react": { - "version": "2.0.22", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.22.tgz", - "integrity": "sha512-nJt2U0ZDjpdPEIHCEWlxOixUhQyA/teQ0y9gz66mYW40OhBjSsZjcEAYhbS05mvy+NMVqzlE3sVu54DqzjR68w==", + "version": "2.0.107", + "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.107.tgz", + "integrity": "sha512-rv0u+tAi2r2zJu2uSLXcC3TBgGrkQIWXRM+i6us6qcGmYQ2kOu2VYg+lxviOSGPhL9PVebvTlN5x8mf3rDqX+w==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider-utils": "3.0.5", - "ai": "5.0.22", + "@ai-sdk/provider-utils": "3.0.18", + "ai": "5.0.107", "swr": "^2.2.5", "throttleit": "2.1.0" }, @@ -379,7 +311,7 @@ }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", - "zod": "^3.25.76 || ^4" + "zod": "^3.25.76 || ^4.1.8" }, "peerDependenciesMeta": { "zod": { @@ -387,67 +319,6 @@ } } }, - "node_modules/@ai-sdk/react/node_modules/@ai-sdk/gateway": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.11.tgz", - "integrity": "sha512-ErwWS3sPOuWy42eE3AVxlKkTa1XjjKBEtNCOylVKMO5KNyz5qie8QVlLYbULOG56dtxX4zTKX3rQNJudplhcmQ==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4" - } - }, - "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.5.tgz", - "integrity": "sha512-HliwB/yzufw3iwczbFVE2Fiwf1XqROB/I6ng8EKUsPM5+2wnIa8f4VbljZcDx+grhFrPV+PnRZH7zBqi8WZM7Q==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.3", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4" - } - }, - "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils/node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, - "node_modules/@ai-sdk/react/node_modules/ai": { - "version": "5.0.22", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.22.tgz", - "integrity": "sha512-RZiYhj7Ux7hrLtXkHPcxzdiSZt4NOiC69O5AkNfMCsz3twwz/KRkl9ASptosoOsg833s5yRcTSdIu5z53Sl6Pw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/gateway": "1.0.11", - "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.5", - "@opentelemetry/api": "1.9.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -5999,9 +5870,9 @@ } }, "node_modules/@vercel/oidc": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.3.tgz", - "integrity": "sha512-yNEQvPcVrK9sIe637+I0jD6leluPxzwJKx/Haw6F4H77CdDsszUn5V3o96LPziXkSNE2B83+Z3mjqGKBK/R6Gg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz", + "integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==", "license": "Apache-2.0", "engines": { "node": ">= 20" @@ -6049,14 +5920,14 @@ } }, "node_modules/ai": { - "version": "5.0.90", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.90.tgz", - "integrity": "sha512-bawNN10N2cXzFedbDdNUZo8KkcGp12VX1b+mCL5dfllh6WmLsIYYME7GVxsRJvHvPP7xRhuds5fn0jtLyxGnZw==", + "version": "5.0.107", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.107.tgz", + "integrity": "sha512-laZlS9ZC/DZfSaxPgrBqI4mM+kxRvTPBBQfa74ceBFskkunZKEsaGVFNEs4cfyGa3nCCCl1WO/fjxixp4V8Zag==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "2.0.7", + "@ai-sdk/gateway": "2.0.18", "@ai-sdk/provider": "2.0.0", - "@ai-sdk/provider-utils": "3.0.16", + "@ai-sdk/provider-utils": "3.0.18", "@opentelemetry/api": "1.9.0" }, "engines": { @@ -10743,23 +10614,6 @@ "zod": "^4.0.16" } }, - "node_modules/ollama-ai-provider-v2/node_modules/@ai-sdk/provider-utils": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz", - "integrity": "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "2.0.0", - "@standard-schema/spec": "^1.0.0", - "eventsource-parser": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", diff --git a/package.json b/package.json index f2feeb7..69c767e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@ai-sdk/deepseek": "^1.0.30", "@ai-sdk/google": "^2.0.0", "@ai-sdk/openai": "^2.0.19", - "@ai-sdk/react": "^2.0.22", + "@ai-sdk/react": "^2.0.107", "@aws-sdk/credential-providers": "^3.943.0", "@langfuse/client": "^4.4.9", "@langfuse/otel": "^4.4.4",