"use client" import type { UIMessage } from "ai" import { Check, ChevronDown, ChevronUp, Copy, Cpu, Minus, Pencil, Plus, RotateCcw, ThumbsDown, ThumbsUp, X, } from "lucide-react" import Image from "next/image" import { useCallback, useEffect, useRef, useState } from "react" import ReactMarkdown from "react-markdown" import { ScrollArea } from "@/components/ui/scroll-area" import { convertToLegalXml, replaceNodes, validateMxCellStructure, } from "@/lib/utils" import ExamplePanel from "./chat-example-panel" import { CodeBlock } from "./code-block" interface EditPair { search: string replace: string } // Tool part interface for type safety interface ToolPartLike { type: string toolCallId: string state?: string input?: { xml?: string; edits?: EditPair[] } & Record output?: 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) => part.type === "text") .map((part) => (part as { text: string }).text) .join("\n") } interface ChatMessageDisplayProps { messages: UIMessage[] setInput: (input: string) => void setFiles: (files: File[]) => void sessionId?: string onRegenerate?: (messageIndex: number) => void onEditMessage?: (messageIndex: number, newText: string) => void } export function ChatMessageDisplay({ messages, 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< string | null >(null) const [feedback, setFeedback] = useState>({}) const [editingMessageId, setEditingMessageId] = useState( null, ) const editTextareaRef = useRef(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(() => { if (editingMessageId && editTextareaRef.current) { editTextareaRef.current.focus() } }, [editingMessageId]) useEffect(() => { messages.forEach((message) => { if (message.parts) { message.parts.forEach((part) => { if (part.type?.startsWith("tool-")) { const toolPart = part as ToolPartLike const { toolCallId, state, input } = toolPart if (state === "output-available") { setExpandedTools((prev) => ({ ...prev, [toolCallId]: false, })) } if ( part.type === "tool-display_diagram" && input?.xml ) { const xml = input.xml as string if ( state === "input-streaming" || state === "input-available" ) { handleDisplayChart(xml) } else if ( state === "output-available" && !processedToolCalls.current.has(toolCallId) ) { handleDisplayChart(xml) processedToolCalls.current.add(toolCallId) } } } }) } }) }, [messages, handleDisplayChart]) const renderToolPart = (part: ToolPartLike) => { 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" ? (