@@ -24,7 +24,7 @@ import IconGoogleDrive from "./icons/IconGoogleDrive"
2424import IconGoogleMail from "./icons/IconGoogleMail"
2525import toast from "react-hot-toast"
2626
27- // LinkButton component to handle different types of links with custom icons
27+ // LinkButton component (no changes needed)
2828const LinkButton = ( { href, children } ) => {
2929 const toolMapping = {
3030 "drive.google.com" : {
@@ -93,7 +93,7 @@ const LinkButton = ({ href, children }) => {
9393 )
9494}
9595
96- // ToolCodeBlock component to display tool calls in a collapsible format
96+ // ToolCodeBlock is no longer rendered, but we keep it for potential future use or debugging
9797const ToolCodeBlock = ( { name, code, isExpanded, onToggle } ) => {
9898 let formattedCode = code
9999 try {
@@ -171,20 +171,24 @@ const ChatBubble = ({
171171 isUser,
172172 memoryUsed,
173173 agentsUsed,
174- internetUsed,
175- isStreamDone
174+ internetUsed
176175} ) => {
177176 const [ copied , setCopied ] = useState ( false )
178177 const [ expandedStates , setExpandedStates ] = useState ( { } )
178+ const [ renderedContent , setRenderedContent ] = useState ( [ ] )
179+
180+ // Memoize the parsed content to avoid re-parsing on every render
181+ React . useEffect ( ( ) => {
182+ setRenderedContent ( renderMessageContent ( ) )
183+ } , [ message , expandedStates ] ) // Rerun parsing if message or expansion state changes
179184
180185 // Function to copy message content to clipboard
181186 const handleCopyToClipboard = ( ) => {
182- // Filter out the tags and copy the plain text content.
183- const plainText = message
184- . replace ( / < t h i n k > [ \s \S ] * ?< \/ t h i n k > / g, "" )
185- . replace ( / < t o o l _ c o d e [ ^ > ] * > [ \s \S ] * ?< \/ t o o l _ c o d e > / g, "" )
186- . replace ( / < t o o l _ r e s u l t [ ^ > ] * > [ \s \S ] * ?< \/ t o o l _ r e s u l t > / g, "" )
187- . replace ( / < a n s w e r > ( [ \s \S ] * ?) < \/ a n s w e r > / g, "$1" )
187+ // Build the text to copy from the parsed parts, ensuring we only copy the final answer
188+ const plainText = renderedContent
189+ . filter ( ( part ) => part . type === "answer" )
190+ . map ( ( part ) => part . props . children )
191+ . join ( "" )
188192 . trim ( )
189193
190194 navigator . clipboard
@@ -201,11 +205,14 @@ const ChatBubble = ({
201205 setExpandedStates ( ( prev ) => ( { ...prev , [ id ] : ! prev [ id ] } ) )
202206 }
203207
204- // Function to render message content, processing special tags and text
208+ // ***************************************************************
209+ // *** UPDATED LOGIC: Function to render message content ***
210+ // ***************************************************************
205211 const renderMessageContent = ( ) => {
206212 if ( isUser || typeof message !== "string" || ! message ) {
207- return (
213+ return [
208214 < ReactMarkdown
215+ key = "user-md"
209216 className = "prose prose-invert"
210217 remarkPlugins = { [ remarkGfm ] }
211218 children = { message || "" }
@@ -215,18 +222,24 @@ const ChatBubble = ({
215222 )
216223 } }
217224 />
218- )
225+ ]
219226 }
220227
221228 const contentParts = [ ]
222229 const regex =
223- / ( < t h i n k > [ \s \S ] * ?< \/ t h i n k > | < t o o l _ c o d e n a m e = " [ ^ " ] + " > [ \s \S ] * ?< \/ t o o l _ c o d e > | < t o o l _ r e s u l t t o o l _ n a m e = " [ ^ " ] + " > [ \s \S ] * ?< \/ t o o l _ r e s u l t > | < a n s w e r > [ \s \S ] * ?< \/ a n s w e r > ) / g
230+ / ( < t h i n k > [ \s \S ] * ?< \/ t h i n k > | < t o o l _ c o d e [ ^ > ] * > [ \s \S ] * ?< \/ t o o l _ c o d e > | < t o o l _ r e s u l t [ ^ > ] * > [ \s \S ] * ?< \/ t o o l _ r e s u l t > | < a n s w e r > [ \s \S ] * ?< \/ a n s w e r > ) / g
224231 let lastIndex = 0
232+ let inToolCallPhase = false // State to track if we are between a tool_code and tool_result
225233
226- // Parse the message into parts
227234 for ( const match of message . matchAll ( regex ) ) {
228- // By not capturing the text between matches, we ignore "junk tokens"
235+ const precedingText = message . substring ( lastIndex , match . index )
236+
237+ // 1. Add any text that came before the current tag, but only if we're not in the "ignore" phase
238+ if ( precedingText . trim ( ) && ! inToolCallPhase ) {
239+ contentParts . push ( { type : "answer" , content : precedingText } )
240+ }
229241
242+ // 2. Process the matched tag
230243 const tag = match [ 0 ]
231244 let subMatch
232245
@@ -237,43 +250,46 @@ const ChatBubble = ({
237250 }
238251 } else if (
239252 ( subMatch = tag . match (
240- / < t o o l _ c o d e n a m e = " ( [ ^ " ] + ) " > ( [ \s \S ] * ?) < \/ t o o l _ c o d e > /
253+ / < t o o l _ c o d e n a m e = " ( [ ^ " ] + ) " > [ \s \S ] * ?< \/ t o o l _ c o d e > /
241254 ) )
242255 ) {
243- contentParts . push ( {
244- type : "tool_code" ,
245- name : subMatch [ 1 ] ,
246- code : subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
247- } )
256+ // When we find a tool_code, we enter the "ignore" phase and do not render the code itself.
257+ inToolCallPhase = true
248258 } else if (
259+ // CORRECTED REGEX: Added ([\s\S]*?) to capture the result content
249260 ( subMatch = tag . match (
250- / < t o o l _ r e s u l t t o o l _ n a m e = " ( [ ^ " ] + ) " > [ \s \S ] * ?< \/ t o o l _ r e s u l t > /
261+ / < t o o l _ r e s u l t t o o l _ n a m e = " ( [ ^ " ] + ) " > ( [ \s \S ] * ?) < \/ t o o l _ r e s u l t > /
251262 ) )
252263 ) {
264+ // When we find a tool_result, we exit the "ignore" phase and render the result.
265+ inToolCallPhase = false
253266 contentParts . push ( {
254267 type : "tool_result" ,
255268 name : subMatch [ 1 ] ,
256269 result : subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
257270 } )
258271 } else if ( ( subMatch = tag . match ( / < a n s w e r > ( [ \s \S ] * ?) < \/ a n s w e r > / ) ) ) {
259272 const answerContent = subMatch [ 1 ]
260- // Don't trim, whitespace might be intentional
261273 if ( answerContent ) {
262274 contentParts . push ( {
263275 type : "answer" ,
264276 content : answerContent
265277 } )
266278 }
267279 }
268-
269280 lastIndex = match . index + tag . length
270281 }
271282
272- // By not capturing text after the last tag, we ignore trailing junk.
283+ // 3. Add any remaining text after the last tag (this is the final, streaming answer)
284+ const remainingText = message . substring ( lastIndex )
285+ if ( remainingText && ! inToolCallPhase ) {
286+ contentParts . push ( { type : "answer" , content : remainingText } )
287+ }
273288
274- // Render all parts
289+ // 4. Render all the collected parts into React components
275290 return contentParts . map ( ( part , index ) => {
276291 const partId = `${ part . type } _${ index } `
292+
277293 if ( part . type === "think" && part . content ) {
278294 return (
279295 < div
@@ -301,17 +317,6 @@ const ChatBubble = ({
301317 </ div >
302318 )
303319 }
304- if ( part . type === "tool_code" ) {
305- return (
306- < ToolCodeBlock
307- key = { partId }
308- name = { part . name }
309- code = { part . code }
310- isExpanded = { ! ! expandedStates [ partId ] }
311- onToggle = { ( ) => toggleExpansion ( partId ) }
312- />
313- )
314- }
315320 if ( part . type === "tool_result" ) {
316321 return (
317322 < ToolResultBlock
@@ -338,6 +343,7 @@ const ChatBubble = ({
338343 />
339344 )
340345 }
346+ // Note: tool_code parts are never rendered
341347 return null
342348 } )
343349 }
@@ -351,7 +357,7 @@ const ChatBubble = ({
351357 } mb-2 relative`}
352358 style = { { wordBreak : "break-word" } }
353359 >
354- { renderMessageContent ( ) }
360+ { renderedContent }
355361 { ! isUser && (
356362 < div className = "flex justify-start items-center space-x-4 mt-6" >
357363 < Tooltip id = "chat-bubble-tooltip" />
0 commit comments