"use client"; import { useRef, useEffect, useState, useCallback } from "react"; import Image from "next/image"; import { ScrollArea } from "@/components/ui/scroll-area"; import ExamplePanel from "./chat-example-panel"; import { UIMessage } from "ai"; import { convertToLegalXml, replaceNodes, validateMxCellStructure } from "@/lib/utils"; import { Copy, Check, X, ChevronDown, ChevronUp, Cpu, Minus, Plus, ThumbsUp, ThumbsDown, RotateCcw, Pencil } from "lucide-react"; import { CodeBlock } from "./code-block"; interface EditPair { search: string; replace: string; } function EditDiffDisplay({ edits }: { edits: EditPair[] }) { return (
{edits.map((edit, index) => (
Change {index + 1}
{/* Search (old) */}
Remove
                                {edit.search}
                            
{/* Replace (new) */}
Add
                                {edit.replace}
                            
))}
); } import { useDiagram } from "@/contexts/diagram-context"; const getMessageTextContent = (message: UIMessage): string => { if (!message.parts) return ""; return message.parts .filter((part: any) => part.type === "text") .map((part: any) => part.text) .join("\n"); }; interface ChatMessageDisplayProps { messages: UIMessage[]; error?: Error | null; setInput: (input: string) => void; setFiles: (files: File[]) => void; sessionId?: string; onRegenerate?: (messageIndex: number) => void; onEditMessage?: (messageIndex: number, newText: string) => void; } export function ChatMessageDisplay({ messages, error, setInput, setFiles, sessionId, onRegenerate, onEditMessage, }: ChatMessageDisplayProps) { const { chartXML, loadDiagram: onDisplayChart } = useDiagram(); const messagesEndRef = useRef(null); const previousXML = useRef(""); const processedToolCalls = useRef>(new Set()); const [expandedTools, setExpandedTools] = useState>( {} ); const [copiedMessageId, setCopiedMessageId] = useState(null); const [copyFailedMessageId, setCopyFailedMessageId] = useState(null); const [feedback, setFeedback] = useState>({}); const [editingMessageId, setEditingMessageId] = useState(null); const [editText, setEditText] = useState(""); const copyMessageToClipboard = async (messageId: string, text: string) => { try { await navigator.clipboard.writeText(text); setCopiedMessageId(messageId); setTimeout(() => setCopiedMessageId(null), 2000); } catch (err) { console.error("Failed to copy message:", err); setCopyFailedMessageId(messageId); setTimeout(() => setCopyFailedMessageId(null), 2000); } }; const submitFeedback = async (messageId: string, value: "good" | "bad") => { // Toggle off if already selected if (feedback[messageId] === value) { setFeedback((prev) => { const next = { ...prev }; delete next[messageId]; return next; }); return; } setFeedback((prev) => ({ ...prev, [messageId]: value })); try { await fetch("/api/log-feedback", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messageId, feedback: value, sessionId, }), }); } catch (error) { console.warn("Failed to log feedback:", error); } }; const handleDisplayChart = useCallback( (xml: string) => { const currentXml = xml || ""; const convertedXml = convertToLegalXml(currentXml); if (convertedXml !== previousXML.current) { const replacedXML = replaceNodes(chartXML, convertedXml); const validationError = validateMxCellStructure(replacedXML); if (!validationError) { previousXML.current = convertedXml; onDisplayChart(replacedXML); } else { console.log("[ChatMessageDisplay] XML validation failed:", validationError); } } }, [chartXML, onDisplayChart] ); useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages]); useEffect(() => { messages.forEach((message) => { if (message.parts) { message.parts.forEach((part: any) => { if (part.type?.startsWith("tool-")) { const { toolCallId, state } = part; if (state === "output-available") { setExpandedTools((prev) => ({ ...prev, [toolCallId]: false, })); } if ( part.type === "tool-display_diagram" && part.input?.xml ) { if ( state === "input-streaming" || state === "input-available" ) { handleDisplayChart(part.input.xml); } else if ( state === "output-available" && !processedToolCalls.current.has(toolCallId) ) { handleDisplayChart(part.input.xml); processedToolCalls.current.add(toolCallId); } } } }); } }); }, [messages, handleDisplayChart]); const renderToolPart = (part: any) => { const callId = part.toolCallId; const { state, input, output } = part; const isExpanded = expandedTools[callId] ?? true; const toolName = part.type?.replace("tool-", ""); const toggleExpanded = () => { setExpandedTools((prev) => ({ ...prev, [callId]: !isExpanded, })); }; const getToolDisplayName = (name: string) => { switch (name) { case "display_diagram": return "Generate Diagram"; case "edit_diagram": return "Edit Diagram"; default: return name; } }; return (
{getToolDisplayName(toolName)}
{state === "input-streaming" && (
)} {state === "output-available" && ( Complete )} {state === "output-error" && ( Error )} {input && Object.keys(input).length > 0 && ( )}
{input && isExpanded && (
{typeof input === "object" && input.xml ? ( ) : typeof input === "object" && input.edits && Array.isArray(input.edits) ? ( ) : typeof input === "object" && Object.keys(input).length > 0 ? ( ) : null}
)} {output && state === "output-error" && (
{output}
)}
); }; return ( {messages.length === 0 ? ( ) : (
{messages.map((message, messageIndex) => { const userMessageText = message.role === "user" ? getMessageTextContent(message) : ""; const isLastAssistantMessage = message.role === "assistant" && ( messageIndex === messages.length - 1 || messages.slice(messageIndex + 1).every(m => m.role !== "assistant") ); const isLastUserMessage = message.role === "user" && ( messageIndex === messages.length - 1 || messages.slice(messageIndex + 1).every(m => m.role !== "user") ); const isEditing = editingMessageId === message.id; return (
{message.role === "user" && userMessageText && !isEditing && (
{/* Edit button - only on last user message */} {onEditMessage && isLastUserMessage && ( )}
)}
{/* Edit mode for user messages */} {isEditing && message.role === "user" ? (