diff --git a/components/ai-chat/chat-messages.tsx b/components/ai-chat/chat-messages.tsx index d6786fe..0321b06 100644 --- a/components/ai-chat/chat-messages.tsx +++ b/components/ai-chat/chat-messages.tsx @@ -4,6 +4,7 @@ import { MessageSquare, Loader2, Brain } from "lucide-react" import type { UIMessage } from "ai" import ReactMarkdown from "react-markdown" import Image from "next/image" +import { memo, useMemo } from "react" /** * Sanitize image URLs to prevent XSS attacks @@ -44,6 +45,90 @@ interface ChatMessagesProps { isLoading: boolean } +function getRenderablePartSignature(message: UIMessage): string { + if (!message.parts || message.parts.length === 0) return "" + + return message.parts + .filter((part) => part.type === "text" || part.type === "file") + .map((part) => { + if (part.type === "text") { + return `text:${part.text}` + } + + if (part.type === "file") { + return `file:${part.url || ""}:${part.filename || ""}:${part.mediaType || ""}` + } + + return "" + }) + .join("|") +} + +const AssistantMarkdown = memo( + function AssistantMarkdown({ text }: { text: string }) { + return ( +
+ {text} +
+ ) + }, + (prev, next) => prev.text === next.text, +) + +type MessageRowProps = { + message: UIMessage + signature: string +} + +const MessageRow = memo( + function MessageRow({ message }: MessageRowProps) { + return ( +
+
+ {message.parts?.map((part, index) => { + if (part.type === "text") { + return message.role === "assistant" ? ( + + ) : ( +

+ {part.text} +

+ ) + } + + if (part.type === "file") { + const sanitizedUrl = sanitizeImageUrl(part.url) + return ( +
+ {part.filename +
+ ) + } + + if (part.type.startsWith("tool-")) { + return null + } + + return null + })} +
+
+ ) + }, + (prev, next) => prev.signature === next.signature && prev.message.role === next.message.role, +) + function getToolStatusMessage(toolName: string): string { const toolMessages: Record = { getCanvasState: "Checking Canvas", @@ -64,6 +149,15 @@ function getToolStatusMessage(toolName: string): string { } export function ChatMessages({ messages, isLoading }: ChatMessagesProps) { + const messageRows = useMemo( + () => + messages.map((message) => ({ + message, + signature: getRenderablePartSignature(message), + })), + [messages], + ) + const lastMessage = messages[messages.length - 1] const isAgentWorking = isLoading && lastMessage?.role === "assistant" @@ -90,55 +184,8 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) { )} - {messages.map((message) => ( -
-
- {message.parts?.map((part, index) => { - // Handle text parts - if (part.type === "text") { - return message.role === "assistant" ? ( -
- {part.text} -
- ) : ( -

- {part.text} -

- ) - } - - // Handle file parts (images) - if (part.type === "file") { - const sanitizedUrl = sanitizeImageUrl(part.url) - return ( -
- {part.filename -
- ) - } - - if (part.type.startsWith("tool-")) { - return null - } - - return null - })} -
-
+ {messageRows.map(({ message, signature }) => ( + ))} {runningTools.length > 0 && (