mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 14:22:28 +08:00
fix: add service status indicator for ongoing issues (#95)
This commit is contained in:
@@ -4,7 +4,7 @@ import type React from "react";
|
|||||||
import { useRef, useEffect, useState } from "react";
|
import { useRef, useEffect, 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 { PanelRightClose, PanelRightOpen } from "lucide-react";
|
import { PanelRightClose, PanelRightOpen, AlertTriangle } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
@@ -72,75 +72,88 @@ export default function ChatPanel({
|
|||||||
chartXMLRef.current = chartXML;
|
chartXMLRef.current = chartXML;
|
||||||
}, [chartXML]);
|
}, [chartXML]);
|
||||||
|
|
||||||
const { messages, sendMessage, addToolResult, status, error, setMessages, stop } =
|
const {
|
||||||
useChat({
|
messages,
|
||||||
transport: new DefaultChatTransport({
|
sendMessage,
|
||||||
api: "/api/chat",
|
addToolResult,
|
||||||
}),
|
status,
|
||||||
async onToolCall({ toolCall }) {
|
error,
|
||||||
if (toolCall.toolName === "display_diagram") {
|
setMessages,
|
||||||
const { xml } = toolCall.input as { xml: string };
|
stop,
|
||||||
|
} = useChat({
|
||||||
|
transport: new DefaultChatTransport({
|
||||||
|
api: "/api/chat",
|
||||||
|
}),
|
||||||
|
async onToolCall({ toolCall }) {
|
||||||
|
if (toolCall.toolName === "display_diagram") {
|
||||||
|
const { xml } = toolCall.input as { xml: string };
|
||||||
|
|
||||||
const validationError = validateMxCellStructure(xml);
|
const validationError = validateMxCellStructure(xml);
|
||||||
|
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
addToolResult({
|
addToolResult({
|
||||||
tool: "display_diagram",
|
tool: "display_diagram",
|
||||||
toolCallId: toolCall.toolCallId,
|
toolCallId: toolCall.toolCallId,
|
||||||
output: validationError,
|
output: validationError,
|
||||||
});
|
});
|
||||||
|
} 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 }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
addToolResult({
|
// Fallback to export only if no cached XML
|
||||||
tool: "display_diagram",
|
console.log(
|
||||||
toolCallId: toolCall.toolCallId,
|
"[edit_diagram] No cached XML, fetching from DrawIO..."
|
||||||
output: "Successfully displayed the diagram.",
|
);
|
||||||
});
|
currentXml = await onFetchChart(false);
|
||||||
|
console.log(
|
||||||
|
"[edit_diagram] Got XML from export, length:",
|
||||||
|
currentXml.length
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (toolCall.toolName === "edit_diagram") {
|
|
||||||
const { edits } = toolCall.input as {
|
|
||||||
edits: Array<{ search: string; replace: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
let currentXml = "";
|
const { replaceXMLParts } = await import("@/lib/utils");
|
||||||
try {
|
const editedXml = replaceXMLParts(currentXml, edits);
|
||||||
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");
|
onDisplayChart(editedXml);
|
||||||
const editedXml = replaceXMLParts(currentXml, edits);
|
|
||||||
|
|
||||||
onDisplayChart(editedXml);
|
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);
|
||||||
|
|
||||||
addToolResult({
|
const errorMessage =
|
||||||
tool: "edit_diagram",
|
error instanceof Error ? error.message : String(error);
|
||||||
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 =
|
addToolResult({
|
||||||
error instanceof Error
|
tool: "edit_diagram",
|
||||||
? error.message
|
toolCallId: toolCall.toolCallId,
|
||||||
: String(error);
|
output: `Edit failed: ${errorMessage}
|
||||||
|
|
||||||
addToolResult({
|
|
||||||
tool: "edit_diagram",
|
|
||||||
toolCallId: toolCall.toolCallId,
|
|
||||||
output: `Edit failed: ${errorMessage}
|
|
||||||
|
|
||||||
Current diagram XML:
|
Current diagram XML:
|
||||||
\`\`\`xml
|
\`\`\`xml
|
||||||
@@ -148,15 +161,15 @@ ${currentXml || "No XML available"}
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
onError: (error) => {
|
},
|
||||||
console.error("Chat error:", error);
|
onError: (error) => {
|
||||||
setStreamingError(error);
|
console.error("Chat error:", error);
|
||||||
},
|
setStreamingError(error);
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Streaming timeout detection - detects when stream stalls mid-response (e.g., Bedrock 503)
|
// Streaming timeout detection - detects when stream stalls mid-response (e.g., Bedrock 503)
|
||||||
// This catches cases where onError doesn't fire because headers were already sent
|
// This catches cases where onError doesn't fire because headers were already sent
|
||||||
@@ -201,9 +214,13 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
messages.length === capturedMessageCount &&
|
messages.length === capturedMessageCount &&
|
||||||
newPartsCount === capturedPartsCount
|
newPartsCount === capturedPartsCount
|
||||||
) {
|
) {
|
||||||
console.error("[Streaming Timeout] No activity for 15s - forcing error state");
|
console.error(
|
||||||
|
"[Streaming Timeout] No activity for 15s - forcing error state"
|
||||||
|
);
|
||||||
setStreamingError(
|
setStreamingError(
|
||||||
new Error("Connection lost. The AI service may be temporarily unavailable. Please try again.")
|
new Error(
|
||||||
|
"Connection lost. The AI service may be temporarily unavailable. Please try again."
|
||||||
|
)
|
||||||
);
|
);
|
||||||
stop(); // Allow user to retry by transitioning status to "ready"
|
stop(); // Allow user to retry by transitioning status to "ready"
|
||||||
}
|
}
|
||||||
@@ -220,11 +237,12 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
}
|
}
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
|
||||||
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Allow retry if there's a streaming error (workaround for stop() not transitioning status)
|
// Allow retry if there's a streaming error (workaround for stop() not transitioning status)
|
||||||
const isProcessing = (status === "streaming" || status === "submitted") && !streamingError;
|
const isProcessing =
|
||||||
|
(status === "streaming" || status === "submitted") &&
|
||||||
|
!streamingError;
|
||||||
if (input.trim() && !isProcessing) {
|
if (input.trim() && !isProcessing) {
|
||||||
// Clear any previous streaming error before starting new request
|
// Clear any previous streaming error before starting new request
|
||||||
setStreamingError(null);
|
setStreamingError(null);
|
||||||
@@ -292,7 +310,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
|
|
||||||
// Find the user message before this assistant message
|
// Find the user message before this assistant message
|
||||||
let userMessageIndex = messageIndex - 1;
|
let userMessageIndex = messageIndex - 1;
|
||||||
while (userMessageIndex >= 0 && messages[userMessageIndex].role !== "user") {
|
while (
|
||||||
|
userMessageIndex >= 0 &&
|
||||||
|
messages[userMessageIndex].role !== "user"
|
||||||
|
) {
|
||||||
userMessageIndex--;
|
userMessageIndex--;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +329,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
// Get the saved XML snapshot for this user message
|
// Get the saved XML snapshot for this user message
|
||||||
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex);
|
const savedXml = xmlSnapshotsRef.current.get(userMessageIndex);
|
||||||
if (!savedXml) {
|
if (!savedXml) {
|
||||||
console.error("No saved XML snapshot for message index:", userMessageIndex);
|
console.error(
|
||||||
|
"No saved XML snapshot for message index:",
|
||||||
|
userMessageIndex
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +377,10 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
// Get the saved XML snapshot for this user message
|
// Get the saved XML snapshot for this user message
|
||||||
const savedXml = xmlSnapshotsRef.current.get(messageIndex);
|
const savedXml = xmlSnapshotsRef.current.get(messageIndex);
|
||||||
if (!savedXml) {
|
if (!savedXml) {
|
||||||
console.error("No saved XML snapshot for message index:", messageIndex);
|
console.error(
|
||||||
|
"No saved XML snapshot for message index:",
|
||||||
|
messageIndex
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +474,14 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
|
|||||||
>
|
>
|
||||||
About
|
About
|
||||||
</Link>
|
</Link>
|
||||||
|
<ButtonWithTooltip
|
||||||
|
tooltipContent="We're experiencing issues with diagram generation. Debugging is in progress."
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-amber-500 hover:text-amber-600"
|
||||||
|
>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
</ButtonWithTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<a
|
<a
|
||||||
|
|||||||
Reference in New Issue
Block a user