"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
}
function EditDiffDisplay({ edits }: { edits: EditPair[] }) {
return (
{edits.map((edit, index) => (
Change {index + 1}
{/* Search (old) */}
{/* Replace (new) */}
))}
)
}
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[]
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 [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" ? (
) : (
/* Text content in bubble */
message.parts?.some(
(part: any) =>
part.type === "text" ||
part.type === "file",
) && (
{
if (
message.role ===
"user" &&
isLastUserMessage &&
onEditMessage
) {
setEditingMessageId(
message.id,
)
setEditText(
userMessageText,
)
}
}}
title={
message.role === "user" &&
isLastUserMessage &&
onEditMessage
? "Click to edit"
: undefined
}
>
{message.parts?.map(
(
part: any,
index: number,
) => {
switch (part.type) {
case "text":
return (
*:first-child]:mt-0 [&>*:last-child]:mb-0 ${
message.role ===
"user"
? "[&_*]:!text-primary-foreground prose-code:bg-white/20"
: "dark:prose-invert"
}`}
>
{
part.text
}
)
case "file":
return (
)
default:
return null
}
},
)}
)
)}
{/* Tool calls outside bubble */}
{message.parts?.map((part: any) => {
if (part.type?.startsWith("tool-")) {
return renderToolPart(part)
}
return null
})}
{/* Action buttons for assistant messages */}
{message.role === "assistant" && (
{/* Copy button */}
{/* Regenerate button - only on last assistant message */}
{onRegenerate &&
isLastAssistantMessage && (
)}
{/* Divider */}
{/* Thumbs up */}
{/* Thumbs down */}
)}
)
})}
)}
)
}