2025-03-23 12:48:31 +00:00
|
|
|
"use client";
|
2025-03-19 07:20:22 +00:00
|
|
|
|
2025-03-23 12:48:31 +00:00
|
|
|
import type React from "react";
|
|
|
|
|
import { useRef, useEffect, useState } from "react";
|
2025-04-03 15:29:26 +00:00
|
|
|
import { FaGithub } from "react-icons/fa";
|
2025-11-15 12:09:32 +09:00
|
|
|
import { PanelRightClose, PanelRightOpen } from "lucide-react";
|
2025-03-19 08:16:44 +00:00
|
|
|
|
2025-03-23 12:48:31 +00:00
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardFooter,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from "@/components/ui/card";
|
|
|
|
|
import { useChat } from "@ai-sdk/react";
|
2025-08-31 12:54:14 +09:00
|
|
|
import { DefaultChatTransport } from "ai";
|
2025-03-23 12:48:31 +00:00
|
|
|
import { ChatInput } from "@/components/chat-input";
|
2025-03-25 02:58:11 +00:00
|
|
|
import { ChatMessageDisplay } from "./chat-message-display";
|
2025-03-26 00:30:00 +00:00
|
|
|
import { useDiagram } from "@/contexts/diagram-context";
|
2025-08-31 20:52:04 +09:00
|
|
|
import { replaceNodes, formatXML } from "@/lib/utils";
|
2025-11-15 12:09:32 +09:00
|
|
|
import { ButtonWithTooltip } from "@/components/button-with-tooltip";
|
2025-03-26 00:30:00 +00:00
|
|
|
|
2025-11-15 12:09:32 +09:00
|
|
|
interface ChatPanelProps {
|
|
|
|
|
isVisible: boolean;
|
|
|
|
|
onToggleVisibility: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ChatPanel({ isVisible, onToggleVisibility }: ChatPanelProps) {
|
2025-03-26 00:30:00 +00:00
|
|
|
const {
|
|
|
|
|
loadDiagram: onDisplayChart,
|
|
|
|
|
handleExport: onExport,
|
|
|
|
|
resolverRef,
|
2025-04-03 15:10:53 +00:00
|
|
|
chartXML,
|
2025-03-27 08:09:22 +00:00
|
|
|
clearDiagram,
|
2025-03-26 00:30:00 +00:00
|
|
|
} = useDiagram();
|
2025-03-19 08:16:44 +00:00
|
|
|
|
2025-03-26 00:30:00 +00:00
|
|
|
const onFetchChart = () => {
|
2025-11-10 10:28:37 +09:00
|
|
|
return Promise.race([
|
|
|
|
|
new Promise<string>((resolve) => {
|
|
|
|
|
if (resolverRef && "current" in resolverRef) {
|
|
|
|
|
resolverRef.current = resolve;
|
|
|
|
|
}
|
|
|
|
|
onExport();
|
|
|
|
|
}),
|
|
|
|
|
new Promise<string>((_, reject) =>
|
|
|
|
|
setTimeout(() => reject(new Error("Chart export timed out after 10 seconds")), 10000)
|
|
|
|
|
)
|
|
|
|
|
]);
|
2025-03-26 00:30:00 +00:00
|
|
|
};
|
2025-03-22 16:03:03 +00:00
|
|
|
// Add a step counter to track updates
|
2025-03-25 04:23:38 +00:00
|
|
|
|
2025-03-23 11:03:25 +00:00
|
|
|
// Add state for file attachments
|
2025-03-27 08:02:03 +00:00
|
|
|
const [files, setFiles] = useState<File[]>([]);
|
2025-03-23 13:54:21 +00:00
|
|
|
// Add state for showing the history dialog
|
|
|
|
|
const [showHistory, setShowHistory] = useState(false);
|
2025-03-23 11:03:25 +00:00
|
|
|
|
2025-03-27 08:02:03 +00:00
|
|
|
// Convert File[] to FileList for experimental_attachments
|
|
|
|
|
const createFileList = (files: File[]): FileList => {
|
|
|
|
|
const dt = new DataTransfer();
|
|
|
|
|
files.forEach((file) => dt.items.add(file));
|
|
|
|
|
return dt.files;
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-31 12:54:14 +09:00
|
|
|
// Add state for input management
|
|
|
|
|
const [input, setInput] = useState("");
|
|
|
|
|
|
2025-03-22 16:03:03 +00:00
|
|
|
// Remove the currentXmlRef and related useEffect
|
2025-08-31 12:54:14 +09:00
|
|
|
const { messages, sendMessage, addToolResult, status, error, setMessages } =
|
|
|
|
|
useChat({
|
|
|
|
|
transport: new DefaultChatTransport({
|
|
|
|
|
api: "/api/chat",
|
|
|
|
|
}),
|
|
|
|
|
async onToolCall({ toolCall }) {
|
|
|
|
|
if (toolCall.toolName === "display_diagram") {
|
2025-11-10 11:27:25 +09:00
|
|
|
// Diagram is handled streamingly in the ChatMessageDisplay component
|
2025-08-31 12:54:14 +09:00
|
|
|
addToolResult({
|
|
|
|
|
tool: "display_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
2025-11-10 11:27:25 +09:00
|
|
|
output: "Successfully displayed the diagram.",
|
2025-08-31 12:54:14 +09:00
|
|
|
});
|
2025-08-31 20:52:04 +09:00
|
|
|
} else if (toolCall.toolName === "edit_diagram") {
|
|
|
|
|
const { edits } = toolCall.input as {
|
|
|
|
|
edits: Array<{ search: string; replace: string }>;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-10 11:27:25 +09:00
|
|
|
let currentXml = '';
|
2025-08-31 20:52:04 +09:00
|
|
|
try {
|
|
|
|
|
// Fetch current chart XML
|
2025-11-10 11:27:25 +09:00
|
|
|
currentXml = await onFetchChart();
|
2025-08-31 20:52:04 +09:00
|
|
|
|
|
|
|
|
// Apply edits using the utility function
|
|
|
|
|
const { replaceXMLParts } = await import("@/lib/utils");
|
|
|
|
|
const editedXml = replaceXMLParts(currentXml, edits);
|
|
|
|
|
|
|
|
|
|
// Load the edited diagram
|
|
|
|
|
onDisplayChart(editedXml);
|
|
|
|
|
|
|
|
|
|
addToolResult({
|
|
|
|
|
tool: "edit_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
|
|
|
|
output: `Successfully applied ${edits.length} edit(s) to the diagram.`,
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2025-11-10 11:27:25 +09:00
|
|
|
console.error("Edit diagram failed:", error);
|
|
|
|
|
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
2025-11-13 22:27:11 +09:00
|
|
|
// Provide detailed error with current diagram XML
|
2025-08-31 20:52:04 +09:00
|
|
|
addToolResult({
|
|
|
|
|
tool: "edit_diagram",
|
|
|
|
|
toolCallId: toolCall.toolCallId,
|
2025-11-13 22:27:11 +09:00
|
|
|
output: `Edit failed: ${errorMessage}
|
|
|
|
|
|
|
|
|
|
Current diagram XML:
|
|
|
|
|
\`\`\`xml
|
|
|
|
|
${currentXml}
|
|
|
|
|
\`\`\`
|
|
|
|
|
|
|
|
|
|
Please retry with an adjusted search pattern or use display_diagram if retries are exhausted.`,
|
2025-08-31 20:52:04 +09:00
|
|
|
});
|
|
|
|
|
}
|
2025-08-31 12:54:14 +09:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
console.error("Chat error:", error);
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-03-23 12:48:31 +00:00
|
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
2025-03-19 07:20:22 +00:00
|
|
|
// Scroll to bottom when messages change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (messagesEndRef.current) {
|
2025-03-23 12:48:31 +00:00
|
|
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
2025-03-19 07:20:22 +00:00
|
|
|
}
|
2025-03-23 12:48:31 +00:00
|
|
|
}, [messages]);
|
2025-03-19 07:20:22 +00:00
|
|
|
|
2025-03-22 16:03:03 +00:00
|
|
|
const onFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
2025-03-23 12:48:31 +00:00
|
|
|
e.preventDefault();
|
2025-03-22 13:26:14 +00:00
|
|
|
if (input.trim() && status !== "streaming") {
|
2025-03-22 16:03:03 +00:00
|
|
|
try {
|
2025-08-31 12:54:14 +09:00
|
|
|
// Fetch chart data before sending message
|
2025-08-31 20:52:04 +09:00
|
|
|
let chartXml = await onFetchChart();
|
|
|
|
|
|
|
|
|
|
// Format the XML to ensure consistency
|
|
|
|
|
chartXml = formatXML(chartXml);
|
2025-08-31 12:54:14 +09:00
|
|
|
|
|
|
|
|
// Create message parts
|
|
|
|
|
const parts: any[] = [{ type: "text", text: input }];
|
|
|
|
|
|
|
|
|
|
// Add file parts if files exist
|
|
|
|
|
if (files.length > 0) {
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
const dataUrl = await new Promise<string>((resolve) => {
|
|
|
|
|
reader.onload = () =>
|
|
|
|
|
resolve(reader.result as string);
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
parts.push({
|
|
|
|
|
type: "file",
|
|
|
|
|
url: dataUrl,
|
|
|
|
|
mediaType: file.type,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendMessage(
|
|
|
|
|
{ parts },
|
|
|
|
|
{
|
|
|
|
|
body: {
|
|
|
|
|
xml: chartXml,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Clear input and files after submission
|
|
|
|
|
setInput("");
|
2025-03-27 08:02:03 +00:00
|
|
|
setFiles([]);
|
2025-03-22 16:03:03 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error fetching chart data:", error);
|
|
|
|
|
}
|
2025-03-19 07:20:22 +00:00
|
|
|
}
|
2025-03-23 12:48:31 +00:00
|
|
|
};
|
2025-03-19 07:20:22 +00:00
|
|
|
|
2025-08-31 12:54:14 +09:00
|
|
|
// Handle input change
|
|
|
|
|
const handleInputChange = (
|
|
|
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
|
|
|
) => {
|
|
|
|
|
setInput(e.target.value);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-23 11:03:25 +00:00
|
|
|
// Helper function to handle file changes
|
2025-03-27 08:02:03 +00:00
|
|
|
const handleFileChange = (newFiles: File[]) => {
|
2025-03-23 11:03:25 +00:00
|
|
|
setFiles(newFiles);
|
2025-03-23 12:48:31 +00:00
|
|
|
};
|
2025-03-23 13:54:21 +00:00
|
|
|
|
2025-11-15 12:09:32 +09:00
|
|
|
// Collapsed view when chat is hidden
|
|
|
|
|
if (!isVisible) {
|
|
|
|
|
return (
|
|
|
|
|
<Card className="h-full flex flex-col rounded-none py-0 gap-0 items-center justify-start pt-4">
|
|
|
|
|
<ButtonWithTooltip
|
|
|
|
|
tooltipContent="Show chat panel (Ctrl+B)"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={onToggleVisibility}
|
|
|
|
|
>
|
|
|
|
|
<PanelRightOpen className="h-5 w-5" />
|
|
|
|
|
</ButtonWithTooltip>
|
|
|
|
|
<div
|
|
|
|
|
className="text-sm text-gray-500 mt-8"
|
|
|
|
|
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
|
|
|
|
|
>
|
|
|
|
|
Chat
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Full view when chat is visible
|
2025-03-19 07:20:22 +00:00
|
|
|
return (
|
2025-03-23 14:36:21 +00:00
|
|
|
<Card className="h-full flex flex-col rounded-none py-0 gap-0">
|
2025-11-15 12:09:32 +09:00
|
|
|
<CardHeader className="p-4 flex flex-row justify-between items-center">
|
2025-03-23 12:48:31 +00:00
|
|
|
<CardTitle>Next-AI-Drawio</CardTitle>
|
2025-11-15 12:09:32 +09:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<ButtonWithTooltip
|
|
|
|
|
tooltipContent="Hide chat panel (Ctrl+B)"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={onToggleVisibility}
|
|
|
|
|
>
|
|
|
|
|
<PanelRightClose className="h-5 w-5" />
|
|
|
|
|
</ButtonWithTooltip>
|
|
|
|
|
<a
|
|
|
|
|
href="https://github.com/DayuanJiang/next-ai-draw-io"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="text-gray-600 hover:text-gray-900 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<FaGithub className="w-6 h-6" />
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
2025-03-19 07:20:22 +00:00
|
|
|
</CardHeader>
|
2025-03-23 14:36:21 +00:00
|
|
|
<CardContent className="flex-grow overflow-hidden px-2">
|
2025-03-25 02:58:11 +00:00
|
|
|
<ChatMessageDisplay
|
|
|
|
|
messages={messages}
|
|
|
|
|
error={error}
|
|
|
|
|
setInput={setInput}
|
|
|
|
|
setFiles={handleFileChange}
|
|
|
|
|
/>
|
2025-03-19 07:20:22 +00:00
|
|
|
</CardContent>
|
2025-03-23 13:15:28 +00:00
|
|
|
|
2025-03-22 13:15:51 +00:00
|
|
|
<CardFooter className="p-2">
|
|
|
|
|
<ChatInput
|
|
|
|
|
input={input}
|
2025-03-22 13:26:14 +00:00
|
|
|
status={status}
|
2025-03-22 13:15:51 +00:00
|
|
|
onSubmit={onFormSubmit}
|
|
|
|
|
onChange={handleInputChange}
|
2025-03-27 08:09:22 +00:00
|
|
|
onClearChat={() => {
|
|
|
|
|
setMessages([]);
|
|
|
|
|
clearDiagram();
|
|
|
|
|
}}
|
2025-03-23 11:03:25 +00:00
|
|
|
files={files}
|
|
|
|
|
onFileChange={handleFileChange}
|
2025-03-23 13:54:21 +00:00
|
|
|
showHistory={showHistory}
|
2025-03-27 07:48:19 +00:00
|
|
|
onToggleHistory={setShowHistory}
|
2025-03-22 13:15:51 +00:00
|
|
|
/>
|
2025-03-19 07:20:22 +00:00
|
|
|
</CardFooter>
|
|
|
|
|
</Card>
|
2025-03-23 12:48:31 +00:00
|
|
|
);
|
2025-03-19 07:20:22 +00:00
|
|
|
}
|