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 (
+
+
+
+ )
+ }
+
+ 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 (
-
-
-
- )
- }
-
- if (part.type.startsWith("tool-")) {
- return null
- }
-
- return null
- })}
-
-
+ {messageRows.map(({ message, signature }) => (
+
))}
{runningTools.length > 0 && (