@@ -24,20 +24,8 @@ import IconGoogleDrive from "./icons/IconGoogleDrive" // Importing custom icon c
2424import IconGoogleMail from "./icons/IconGoogleMail" // Importing custom icon component for Google Mail
2525import toast from "react-hot-toast" // Importing toast for displaying toast notifications
2626
27- /**
28- * LinkButton Component - Renders a styled button that opens a link in a new tab.
29- *
30- * This component is used within chat messages to display URLs in a button format.
31- * It automatically detects the type of link (e.g., Google Docs, Gmail, generic link)
32- * and displays an appropriate icon and name. Clicking the button opens the link in a new tab.
33- *
34- * @param {object } props - Component props.
35- * @param {string } props.href - The URL to be opened when the button is clicked.
36- * @param {React.ReactNode } props.children - The display text for the link, used as fallback name if tool name not found.
37- * @returns {React.ReactNode } - The LinkButton component UI.
38- */
27+ // LinkButton component remains the same...
3928const LinkButton = ( { href, children } ) => {
40- // Mapping of domain names to their respective icons and names for tool identification
4129 const toolMapping = {
4230 "drive.google.com" : {
4331 icon : < IconGoogleDrive size = { 14 } className = "mr-1" /> ,
@@ -49,7 +37,7 @@ const LinkButton = ({ href, children }) => {
4937 } ,
5038 "gmail.com" : {
5139 icon : < IconGoogleMail size = { 14 } className = "mr-1" /> ,
52- name : children // Fallback name if specific tool name is not found
40+ name : children
5341 } ,
5442 "docs.google.com/spreadsheets" : {
5543 icon : < IconGoogleSheets /> ,
@@ -69,62 +57,46 @@ const LinkButton = ({ href, children }) => {
6957 } ,
7058 "external-mail" : {
7159 icon : < IconMail size = { 14 } className = "mr-1" /> ,
72- name : children // Uses children as name for external mail links
60+ name : children
7361 } ,
7462 default : {
7563 icon : < IconLink size = { 14 } className = "mr-1" /> ,
76- name : "Link" // Default name for generic links
64+ name : "Link"
7765 }
7866 }
7967
80- /**
81- * Determines the tool details (icon and name) based on the URL.
82- *
83- * Iterates through the `toolMapping` to find a matching domain in the given URL.
84- * If a match is found, returns the corresponding icon and name.
85- * If no specific domain is matched, defaults to a generic link icon and name.
86- *
87- * @function getToolDetails
88- * @param {string } url - The URL to analyze.
89- * @returns {{ icon: React.ReactNode, name: string } } - An object containing the icon and name for the tool.
90- */
9168 const getToolDetails = ( url ) => {
9269 for ( const domain in toolMapping ) {
9370 if ( url . includes ( domain ) ) {
94- return toolMapping [ domain ] // Return tool details if domain is found in URL
71+ return toolMapping [ domain ]
9572 } else if ( url . match ( / ^ [ ^ @ ] + @ [ \w . - ] + \. [ a - z ] { 2 , } $ / i) ) {
96- return toolMapping [ "external-mail" ] // Return external mail tool details if URL matches email format
73+ return toolMapping [ "external-mail" ]
9774 }
9875 }
99- return toolMapping [ "default" ] // Return default tool details for unmatched URLs
76+ return toolMapping [ "default" ]
10077 }
10178
102- const { icon, name } = getToolDetails ( href ) // Get tool icon and name based on href
79+ const { icon, name } = getToolDetails ( href )
10380
10481 return (
10582 < span
10683 onClick = { ( ) => window . open ( href , "_blank" , "noopener noreferrer" ) }
10784 className = "bg-[var(--color-primary-surface)] text-[var(--color-text-primary)] border border-[var(--color-primary-surface-elevated)] hover:border-[var(--color-accent-blue)] py-1 px-2 rounded-md items-center cursor-pointer inline-flex"
108- // Styling for the link button: background, text color, border, padding, rounded corners, cursor, inline-flex display
10985 style = { {
110- display : "inline-flex" , // Ensure inline-flex for proper alignment
111- verticalAlign : "middle" // Vertical alignment to middle
86+ display : "inline-flex" ,
87+ verticalAlign : "middle"
11288 } }
11389 >
114- { icon } { /* Render the icon determined by getToolDetails */ }
90+ { icon }
11591 < span > { name } </ span > { " " }
116- { /* Render the name determined by getToolDetails */ }
11792 </ span >
11893 )
11994}
12095
121- /**
122- * A dedicated component to render tool code blocks neatly.
123- */
96+ // ToolCodeBlock component remains the same...
12497const ToolCodeBlock = ( { name, code, isExpanded, onToggle } ) => {
12598 let formattedCode = code
12699 try {
127- // Pretty-print if it's a JSON string
128100 const parsed = JSON . parse ( code )
129101 formattedCode = JSON . stringify ( parsed , null , 2 )
130102 } catch ( e ) {
@@ -157,14 +129,10 @@ const ToolCodeBlock = ({ name, code, isExpanded, onToggle }) => {
157129 )
158130}
159131
160- /**
161- * A dedicated component to render tool result blocks neatly.
162- * NOTE: This component is now being used to display the tool result.
163- */
132+ // ToolResultBlock component remains the same...
164133const ToolResultBlock = ( { name, result, isExpanded, onToggle } ) => {
165134 let formattedResult = result
166135 try {
167- // Pretty-print if it's a JSON string
168136 const parsed = JSON . parse ( result )
169137 formattedResult = JSON . stringify ( parsed , null , 2 )
170138 } catch ( e ) {
@@ -197,47 +165,18 @@ const ToolResultBlock = ({ name, result, isExpanded, onToggle }) => {
197165 )
198166}
199167
200- /**
201- * ChatBubble Component - Displays a single chat message bubble.
202- *
203- * This component renders a chat message, distinguishing between user and AI messages with different styles.
204- * It supports rendering Markdown content, detects and renders URLs as LinkButtons, and provides functionality
205- * to copy message text to the clipboard. For AI messages, it also conditionally displays icons indicating
206- * if memory, agents, or internet were used in generating the response, along with tooltips for these icons.
207- *
208- * @param {object } props - Component props.
209- * @param {string } props.message - The text content of the chat message, can be Markdown or JSON.
210- * @param {boolean } props.isUser - Boolean indicating if the message is from the user or AI.
211- * @param {boolean } props.memoryUsed - Boolean indicating if memory was used to generate the response (AI messages only).
212- * @param {boolean } props.agentsUsed - Boolean indicating if agents were used (AI messages only).
213- * @param {boolean } props.boolean - Boolean indicating if internet was used (AI messages only).
214- * @returns {React.ReactNode } - The ChatBubble component UI.
215- */
216168const ChatBubble = ( {
217- message, // Text content of the message, can be Markdown or JSON - message: string
218- isUser, // Boolean, true if message is from user, false if from AI - isUser: boolean
219- memoryUsed, // Boolean, true if memory was used in response generation (AI only) - memoryUsed: boolean
220- agentsUsed, // Boolean, true if agents were used in response generation (AI only) - agentsUsed: boolean
221- internetUsed // Boolean, true if internet was used in response generation (AI only) - internetUsed: boolean
169+ message,
170+ isUser,
171+ memoryUsed,
172+ agentsUsed,
173+ internetUsed
222174} ) => {
223- // State to manage the 'copied' status for the copy button, indicating if the message text has been copied.
224175 const [ copied , setCopied ] = useState ( false )
225176 const [ expandedStates , setExpandedStates ] = useState ( { } )
226177
227- /**
228- * Handles copying the message text to the clipboard.
229- *
230- * When the copy button is clicked, this function attempts to write the message text to the clipboard.
231- * On success, it sets the 'copied' state to true to update the button icon to a checkmark,
232- * and then resets it back to false after a short delay (2 seconds) to revert the icon.
233- * If copying fails, it displays an error toast notification to inform the user.
234- *
235- * @function handleCopyToClipboard
236- * @returns {void }
237- */
238178 const handleCopyToClipboard = ( ) => {
239179 let textToCopy = message
240- // If message is a JSON string (e.g., from old tool results), stringify it for copying
241180 try {
242181 const parsed = JSON . parse ( message )
243182 textToCopy = JSON . stringify ( parsed , null , 2 )
@@ -258,16 +197,8 @@ const ChatBubble = ({
258197 setExpandedStates ( ( prev ) => ( { ...prev , [ id ] : ! prev [ id ] } ) )
259198 }
260199
261- /**
262- * Renders the content of the chat bubble.
263- *
264- * This function parses the incoming message string for special tags like <think>,
265- * <tool_code>, and <tool_result>. It splits the message into parts and renders
266- * each part accordingly: text as Markdown, and special tags as collapsible blocks.
267- */
268200 const renderMessageContent = ( ) => {
269201 if ( isUser || typeof message !== "string" || ! message ) {
270- // Fallback for user messages or non-string AI messages
271202 return (
272203 < ReactMarkdown
273204 className = "prose prose-invert"
@@ -287,26 +218,27 @@ const ChatBubble = ({
287218 / ( < 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 > ) / g
288219 let lastIndex = 0
289220
290- // --- START OF THE ROBUST FIX ---
291-
292- // Helper function to check for and filter out junk tokens
293- const isJunk = ( text ) => {
294- const trimmed = text . trim ( )
295- if ( trimmed === "" ) return true // Ignore whitespace-only strings
296-
297- // This regex identifies common junk patterns seen in logs.
298- // It looks for fragments of closing tags or orphaned tag-like words.
299- const junkRegex = / ^ ( < \/ ( \w + > ) ? | _ c o d e > | c o d e > | o d e > | > ) $ /
300- return junkRegex . test ( trimmed )
301- }
221+ // --- START OF THE ROBUST CONTEXTUAL FIX ---
302222
303223 for ( const match of message . matchAll ( regex ) ) {
304224 // 1. Process the text *before* the current valid tag
305225 if ( match . index > lastIndex ) {
306226 const textContent = message . substring ( lastIndex , match . index )
307- // Only add the text if it's NOT junk
308- if ( ! isJunk ( textContent ) ) {
309- contentParts . push ( { type : "text" , content : textContent } )
227+ const lastPart =
228+ contentParts . length > 0
229+ ? contentParts [ contentParts . length - 1 ]
230+ : null
231+
232+ // This is the key: Only add text if it's at the start of the message
233+ // or if it follows another text block. This filters out any text
234+ // sandwiched between special tags (e.g., </tool_code>...JUNK...<tool_result>).
235+ if ( ! lastPart || lastPart . type === "text" ) {
236+ if ( textContent . trim ( ) ) {
237+ contentParts . push ( {
238+ type : "text" ,
239+ content : textContent
240+ } )
241+ }
310242 }
311243 }
312244
@@ -319,54 +251,43 @@ const ChatBubble = ({
319251 / < t o o l _ c o d e n a m e = " ( [ ^ " ] + ) " > ( [ \s \S ] * ?) < \/ t o o l _ c o d e > /
320252 ) )
321253 ) {
322- const toolName = subMatch [ 1 ]
323- // Handle empty tool_code blocks gracefully
324- const toolCode = subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
325254 contentParts . push ( {
326255 type : "tool_code" ,
327- name : toolName ,
328- code : toolCode
256+ name : subMatch [ 1 ] ,
257+ code : subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
329258 } )
330259 } else if (
331260 ( subMatch = tag . match (
332261 / < 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 > /
333262 ) )
334263 ) {
335- const toolName = subMatch [ 1 ]
336- const toolResult = subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
337264 contentParts . push ( {
338265 type : "tool_result" ,
339- name : toolName ,
340- result : toolResult
266+ name : subMatch [ 1 ] ,
267+ result : subMatch [ 2 ] ? subMatch [ 2 ] . trim ( ) : "{}"
341268 } )
342269 } else if ( ( subMatch = tag . match ( / < t h i n k > ( [ \s \S ] * ?) < \/ t h i n k > / ) ) ) {
343270 const thinkContent = subMatch [ 1 ] . trim ( )
344271 if ( thinkContent ) {
345- contentParts . push ( {
346- type : "think" ,
347- content : thinkContent
348- } )
272+ contentParts . push ( { type : "think" , content : thinkContent } )
349273 }
350274 }
351275
352276 // 3. Update our position in the message string
353277 lastIndex = match . index + tag . length
354278 }
355279
356- // 4. Process any remaining text after the last valid tag
280+ // 4. Process any remaining text after the last valid tag.
281+ // This is often the final answer from the assistant.
357282 if ( lastIndex < message . length ) {
358283 const remainingText = message . substring ( lastIndex )
359- // Also check the final part for junk or incomplete streaming tags
360- const openBrackets = ( message . match ( / < / g) || [ ] ) . length
361- const closeBrackets = ( message . match ( / > / g) || [ ] ) . length
362-
363- if ( ! isJunk ( remainingText ) && openBrackets <= closeBrackets ) {
284+ if ( remainingText . trim ( ) ) {
364285 contentParts . push ( { type : "text" , content : remainingText } )
365286 }
366287 }
367- // --- END OF THE ROBUST FIX ---
368288
369- // The rest of the rendering logic remains the same
289+ // --- END OF THE ROBUST CONTEXTUAL FIX ---
290+
370291 return contentParts . map ( ( part , index ) => {
371292 const partId = `${ part . type } _${ index } `
372293 if ( part . type === "think" && part . content ) {
@@ -409,7 +330,6 @@ const ChatBubble = ({
409330 />
410331 )
411332 }
412- // ADDED THIS BLOCK TO RENDER THE TOOL RESULT
413333 if ( part . type === "tool_result" ) {
414334 return (
415335 < ToolResultBlock
@@ -501,4 +421,4 @@ const ChatBubble = ({
501421 )
502422}
503423
504- export default ChatBubble
424+ export default ChatBubble
0 commit comments