From 94cda183e489c3d3c5ff08a9aa132ee109f53009 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 04:38:34 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20optimize=20chat=20messa?= =?UTF-8?q?ge=20re-renders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Memoize MessageBubble and ThinkingIndicator components - Optimize props to localize re-renders to affected items only - Reduce ReactMarkdown re-rendering overhead during streaming and copy actions --- src/components/chat/client.tsx | 9 +- src/components/chat/message-bubble.tsx | 198 +++++++++++++------------ 2 files changed, 108 insertions(+), 99 deletions(-) diff --git a/src/components/chat/client.tsx b/src/components/chat/client.tsx index 6a3333e..8cd0f83 100644 --- a/src/components/chat/client.tsx +++ b/src/components/chat/client.tsx @@ -90,11 +90,14 @@ export default function ChatWidgetClient() { ))} {status === "submitted" && diff --git a/src/components/chat/message-bubble.tsx b/src/components/chat/message-bubble.tsx index 954de0d..ac8fc9c 100644 --- a/src/components/chat/message-bubble.tsx +++ b/src/components/chat/message-bubble.tsx @@ -3,6 +3,7 @@ import { Check, Copy, Pencil } from "lucide-react"; import { UIMessage } from "ai"; +import React, { memo } from "react"; import ReactMarkdown from "react-markdown"; import { Button, Tooltip } from "@heroui/react"; @@ -94,125 +95,128 @@ const MessageContent = ({ return {text}; }; -export const MessageBubble = ({ - message, - isLoading, - isLast, - onEdit, - onCopy, - copiedId, -}: { - message: UIMessage; - isLoading: boolean; - isLast: boolean; - onEdit: (text: string) => void; - onCopy: (id: string, text: string) => void; - copiedId: string | null; -}) => { - const showTypingIndicator = - isLast && message.role === "assistant" && isLoading; - const messageText = - message.parts?.reduce( - (acc, p) => (p.type === "text" ? acc + p.text : acc), - "", - ) || ""; +// ⚡ Optimization: Memoize the MessageBubble to prevent expensive re-renders of the entire message list, +// especially during streaming or when copying messages. +export const MessageBubble = memo( + ({ + message, + isTyping, + onEdit, + onCopy, + isCopied, + }: { + message: UIMessage; + isTyping: boolean; + onEdit: (text: string) => void; + onCopy: (id: string, text: string) => void; + isCopied: boolean; + }) => { + const messageText = + message.parts?.reduce( + (acc, p) => (p.type === "text" ? acc + p.text : acc), + "", + ) || ""; - return ( -
+ return (
{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - selectElementText(e.currentTarget); - } - }} - onDoubleClick={(e) => { - selectElementText(e.currentTarget); - }} - className={`chat-message-bubble group ${ - message.role === "user" - ? "chat-message-user" - : "chat-message-assistant" - }`} + className={`flex ${message.role === "user" ? "justify-end" : "justify-start"}`} > - {/* Floating Message Actions */}
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + selectElementText(e.currentTarget); + } + }} + onDoubleClick={(e) => { + selectElementText(e.currentTarget); + }} + className={`chat-message-bubble group ${ message.role === "user" - ? "chat-message-actions-user" - : "chat-message-actions-assistant" + ? "chat-message-user" + : "chat-message-assistant" }`} > - {message.role === "user" && ( + {/* Floating Message Actions */} +
+ {message.role === "user" && ( + + + + + + Edit question + + + + )} - Edit question + {message.role === "user" ? "Copy question" : "Copy answer"} - )} - - - - - - {message.role === "user" ? "Copy question" : "Copy answer"} - - - -
+
-
- - {showTypingIndicator && ( -
- Generating... -
-
-
-
- )} +
+ + {isTyping && ( +
+ Generating... +
+
+
+
+ )} +
-
- ); -}; + ); + }, +); + +MessageBubble.displayName = "MessageBubble"; -export const ThinkingIndicator = () => ( +// ⚡ Optimization: Memoize the thinking indicator for minor performance gains during state updates. +export const ThinkingIndicator = memo(() => (
Thinking @@ -221,4 +225,6 @@ export const ThinkingIndicator = () => (
-); +)); + +ThinkingIndicator.displayName = "ThinkingIndicator"; From c7d2edfba4383f641d23a60eedbe12561bdee691 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:55:59 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20refactor=20chat=20messa?= =?UTF-8?q?ges=20for=20better=20performance=20and=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Memoize MessageBubble and ThinkingIndicator components - Optimize props to localize re-renders to affected items - Extract message actions into sub-components (EditAction, CopyAction) - Integrate HeroUI components for message actions - Use descriptive prop names (isGenerating) and remove unnecessary comments --- src/components/chat/client.tsx | 2 +- src/components/chat/message-bubble.tsx | 171 ++++++++++++++++--------- 2 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/components/chat/client.tsx b/src/components/chat/client.tsx index 8cd0f83..059ba06 100644 --- a/src/components/chat/client.tsx +++ b/src/components/chat/client.tsx @@ -90,7 +90,7 @@ export default function ChatWidgetClient() { {text}; }; -// ⚡ Optimization: Memoize the MessageBubble to prevent expensive re-renders of the entire message list, -// especially during streaming or when copying messages. +const EditAction = ({ + onEdit, + text, +}: { + onEdit: (text: string) => void; + text: string; +}) => ( + + + + + + Edit question + + + +); + +const CopyAction = ({ + onCopy, + id, + text, + isCopied, + role, +}: { + onCopy: (id: string, text: string) => void; + id: string; + text: string; + isCopied: boolean; + role: string; +}) => ( + + + + + + {role === "user" ? "Copy question" : "Copy answer"} + + + +); + +const MessageActions = ({ + role, + messageId, + messageText, + isCopied, + onEdit, + onCopy, +}: { + role: string; + messageId: string; + messageText: string; + isCopied: boolean; + onEdit: (text: string) => void; + onCopy: (id: string, text: string) => void; +}) => ( +
+ {role === "user" && } + +
+); + export const MessageBubble = memo( ({ message, - isTyping, + isGenerating, onEdit, onCopy, isCopied, }: { message: UIMessage; - isTyping: boolean; + isGenerating: boolean; onEdit: (text: string) => void; onCopy: (id: string, text: string) => void; isCopied: boolean; @@ -140,65 +237,18 @@ export const MessageBubble = memo( : "chat-message-assistant" }`} > - {/* Floating Message Actions */} -
- {message.role === "user" && ( - - - - - - Edit question - - - - )} - - - - - - {message.role === "user" ? "Copy question" : "Copy answer"} - - - -
+
- {isTyping && ( + {isGenerating && (
Generating...
@@ -215,7 +265,6 @@ export const MessageBubble = memo( MessageBubble.displayName = "MessageBubble"; -// ⚡ Optimization: Memoize the thinking indicator for minor performance gains during state updates. export const ThinkingIndicator = memo(() => (
From 977eb32fda566e38be91e8559c611be5a4cedb08 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:32:09 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9A=A1=20Bolt:=20refactor=20chat=20messa?= =?UTF-8?q?ge=20bubbles=20with=20CSS=20data=20variants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor MessageBubble to use data-role attributes - Use Tailwind data- modifiers for conditional alignment and styling - Improve code cleanliness by removing ternary operators in className strings - Maintain performance optimizations (memoization and localized props) --- src/components/chat/message-bubble.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/components/chat/message-bubble.tsx b/src/components/chat/message-bubble.tsx index 5515ef5..c2c3ef6 100644 --- a/src/components/chat/message-bubble.tsx +++ b/src/components/chat/message-bubble.tsx @@ -177,11 +177,8 @@ const MessageActions = ({ onCopy: (id: string, text: string) => void; }) => (
{role === "user" && }
{ if (e.key === "Enter" || e.key === " ") { @@ -231,11 +230,7 @@ export const MessageBubble = memo( onDoubleClick={(e) => { selectElementText(e.currentTarget); }} - className={`chat-message-bubble group ${ - message.role === "user" - ? "chat-message-user" - : "chat-message-assistant" - }`} + className="chat-message-bubble group data-[role=user]:chat-message-user data-[role=assistant]:chat-message-assistant" > Date: Tue, 7 Jul 2026 06:52:58 -0400 Subject: [PATCH 4/8] refactor: centralize chat styles into CSS classes --- src/app/globals.css | 61 +++++++++++++++++++++++--- src/components/chat/message-bubble.tsx | 43 +++++++----------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index ad2cb62..a5ca445 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -233,15 +233,27 @@ } /* Chat Widget Message Bubbles */ +.chat-message-wrapper { + @apply flex; +} + +.chat-message-wrapper[data-role="user"] { + @apply justify-end; +} + +.chat-message-wrapper[data-role="assistant"] { + @apply justify-start; +} + .chat-message-bubble { @apply relative max-w-[80%] rounded-2xl px-4 py-2.5 text-sm shadow-sm select-text cursor-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary; } -.chat-message-user { +.chat-message-bubble[data-role="user"] { @apply bg-gradient-to-tr from-indigo-600 to-violet-500 dark:from-indigo-500 dark:to-purple-500 text-white; } -.chat-message-assistant { +.chat-message-bubble[data-role="assistant"] { @apply bg-slate-100 dark:bg-zinc-800/80 border border-slate-200/50 dark:border-zinc-700/50 text-foreground; } @@ -249,16 +261,55 @@ @apply absolute top-1/2 -translate-y-1/2 flex items-center gap-1 bg-white dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 border border-zinc-200 dark:border-zinc-700 shadow-md px-1.5 py-0.5 rounded-full opacity-90 md:opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-200 z-10; } -.chat-message-actions-user { +.chat-message-actions[data-role="user"] { @apply left-0 -translate-x-[calc(100%+8px)]; } -.chat-message-actions-assistant { +.chat-message-actions[data-role="assistant"] { @apply right-0 translate-x-[calc(100%+8px)]; } .chat-message-action-btn { - @apply p-1.5 hover:text-indigo-600 dark:hover:text-indigo-400 hover:scale-105 transition-all min-w-0 h-fit; + @apply p-1.5 hover:text-indigo-600 dark:hover:text-indigo-400 hover:scale-105 transition-all min-w-0 h-fit focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-full; +} +.chat-message-markdown-link { + @apply text-indigo-600 dark:text-indigo-400 hover:underline font-semibold transition-colors; +} + +.chat-message-markdown-p { + @apply mb-2 last:mb-0 leading-relaxed; +} + +.chat-message-markdown-ul { + @apply list-disc pl-4 mb-2 space-y-1; +} + +.chat-message-markdown-ol { + @apply list-decimal pl-4 mb-2 space-y-1; +} + +.chat-message-markdown-li { + @apply mb-0.5; +} + +.chat-message-markdown-strong { + @apply font-bold text-indigo-950 dark:text-white; +} + +.chat-reasoning-text { + @apply mb-2 italic text-zinc-500 dark:text-zinc-400 border-l-2 border-zinc-300 dark:border-zinc-700 pl-2 text-xs; +} + +.chat-user-text { + @apply whitespace-pre-wrap break-words; +} + +.chat-thinking-wrapper { + @apply flex justify-start animate-in fade-in duration-200; +} + +.chat-action-btn-success { + @apply text-green-500 animate-pulse; } .chat-typing-indicator { diff --git a/src/components/chat/message-bubble.tsx b/src/components/chat/message-bubble.tsx index c2c3ef6..c75eab9 100644 --- a/src/components/chat/message-bubble.tsx +++ b/src/components/chat/message-bubble.tsx @@ -25,28 +25,26 @@ const markdownComponents = { href={href} target={isHash ? undefined : "_blank"} rel={isHash ? undefined : "noopener noreferrer"} - className="text-indigo-600 dark:text-indigo-400 hover:underline font-semibold transition-colors" + className="chat-message-markdown-link" > {children} ); }, p: ({ children }: { children?: React.ReactNode }) => ( -

{children}

+

{children}

), ul: ({ children }: { children?: React.ReactNode }) => ( -
    {children}
+
    {children}
), ol: ({ children }: { children?: React.ReactNode }) => ( -
    {children}
+
    {children}
), li: ({ children }: { children?: React.ReactNode }) => ( -
  • {children}
  • +
  • {children}
  • ), strong: ({ children }: { children?: React.ReactNode }) => ( - - {children} - + {children} ), }; @@ -63,7 +61,7 @@ const MessageContent = ({ if (part.type === "text") { if (message.role === "user") { return ( -
    +
    {part.text}
    ); @@ -76,10 +74,7 @@ const MessageContent = ({ } if (part.type === "reasoning") { return ( -
    +
    Thinking: {part.text}
    ); @@ -89,7 +84,7 @@ const MessageContent = ({ } if (message.role === "user") { - return
    {text}
    ; + return
    {text}
    ; } return {text}; @@ -108,7 +103,7 @@ const EditAction = ({ isIconOnly variant="ghost" onPress={() => onEdit(text)} - className="chat-message-action-btn focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-full" + className="chat-message-action-btn" aria-label="Edit question" >