@@ -29,7 +29,7 @@ import {
2929 IconBrandGoogleDrive ,
3030 IconBrandLinkedin
3131} from "@tabler/icons-react"
32- import { Tooltip } from "react-tooltip "
32+ import { AnimatePresence , motion } from "framer-motion "
3333import ReactMarkdown from "react-markdown"
3434import remarkGfm from "remark-gfm"
3535import IconGoogleDocs from "./icons/IconGoogleDocs"
@@ -38,6 +38,7 @@ import IconGoogleCalendar from "./icons/IconGoogleCalendar"
3838import IconGoogleSlides from "./icons/IconGoogleSlides" // This is a custom one, not from tabler
3939import IconGoogleMail from "./icons/IconGoogleMail"
4040import IconGoogleDrive from "./icons/IconGoogleMail"
41+ import { Tooltip } from "react-tooltip"
4142import toast from "react-hot-toast"
4243import FileCard from "./FileCard"
4344
@@ -132,78 +133,6 @@ const LinkButton = ({ href, children }) => {
132133 )
133134}
134135
135- // ToolCodeBlock is no longer rendered, but we keep it for potential future use or debugging
136- const ToolCodeBlock = ( { name, code, isExpanded, onToggle } ) => {
137- let formattedCode = code
138- try {
139- const parsed = JSON . parse ( code )
140- formattedCode = JSON . stringify ( parsed , null , 2 )
141- } catch ( e ) {
142- // Not JSON, leave as is
143- }
144-
145- return (
146- < div className = "mb-4 border-l-2 border-green-500 pl-3" >
147- < button
148- onClick = { onToggle }
149- className = "flex w-full items-center justify-start gap-2 text-green-400 hover:text-green-300 text-sm font-semibold"
150- data-tooltip-id = "chat-bubble-tooltip"
151- data-tooltip-content = "Click to see the tool call details."
152- >
153- { isExpanded ? (
154- < IconChevronUp size = { 16 } />
155- ) : (
156- < IconChevronDown size = { 16 } />
157- ) }
158- Tool Call: { name }
159- </ button >
160- { isExpanded && (
161- < div className = "mt-2 p-3 bg-neutral-800/50 rounded-md" >
162- < pre className = "text-xs text-gray-300 whitespace-pre-wrap font-mono" >
163- < code > { formattedCode } </ code >
164- </ pre >
165- </ div >
166- ) }
167- </ div >
168- )
169- }
170-
171- // ToolResultBlock component to display tool results in a collapsible format
172- const ToolResultBlock = ( { name, result, isExpanded, onToggle } ) => {
173- let formattedResult = result
174- try {
175- const parsed = JSON . parse ( result )
176- formattedResult = JSON . stringify ( parsed , null , 2 )
177- } catch ( e ) {
178- // Not a valid JSON, leave as is
179- }
180-
181- return (
182- < div className = "mb-4 border-l-2 border-purple-500 pl-3" >
183- < button
184- onClick = { onToggle }
185- className = "flex w-full items-center justify-start gap-2 text-purple-400 hover:text-purple-300 text-sm font-semibold"
186- data-tooltip-id = "chat-bubble-tooltip"
187- data-tooltip-content = "Click to see the result from the tool."
188- >
189- { isExpanded ? (
190- < IconChevronUp size = { 16 } />
191- ) : (
192- < IconChevronDown size = { 16 } />
193- ) }
194- Tool Result: { name }
195- </ button >
196- { isExpanded && (
197- < div className = "mt-2 p-3 bg-neutral-800/50 rounded-md" >
198- < pre className = "text-xs text-gray-300 whitespace-pre-wrap font-mono" >
199- < code > { formattedResult } </ code >
200- </ pre >
201- </ div >
202- ) }
203- </ div >
204- )
205- }
206-
207136// Main ChatBubble component
208137const ChatBubble = ( {
209138 role,
@@ -215,7 +144,8 @@ const ChatBubble = ({
215144 onReply,
216145 onDelete,
217146 message,
218- allMessages = [ ]
147+ allMessages = [ ] ,
148+ isStreaming = false
219149} ) => {
220150 const [ copied , setCopied ] = useState ( false )
221151 const [ expandedStates , setExpandedStates ] = useState ( { } )
@@ -320,6 +250,12 @@ const ChatBubble = ({
320250 }
321251 } , [ processedContent . content , thoughts , tool_calls , tool_results ] )
322252
253+ const hasInternalMonologue =
254+ parsedThoughts . length > 0 ||
255+ parsedToolCalls . length > 0 ||
256+ parsedToolResults . length > 0 ||
257+ isStreaming
258+
323259 // Function to toggle expansion of collapsible sections
324260 const toggleExpansion = ( id ) => {
325261 setExpandedStates ( ( prev ) => ( { ...prev , [ id ] : ! prev [ id ] } ) )
@@ -331,7 +267,7 @@ const ChatBubble = ({
331267 const transformLinkUri = ( uri ) => {
332268 return uri . startsWith ( "file:" ) ? uri : uri // Let ReactMarkdown handle its default security
333269 }
334- const renderMessageContent = ( contentToRender ) => {
270+ const renderedContent = React . useMemo ( ( ) => {
335271 const markdownComponents = {
336272 a : ( { href, children } ) => {
337273 if ( href && href . startsWith ( "file:" ) ) {
@@ -348,7 +284,7 @@ const ChatBubble = ({
348284 < ReactMarkdown
349285 className = "prose prose-invert"
350286 remarkPlugins = { [ remarkGfm ] }
351- children = { contentToRender || "" }
287+ children = { finalContent || "" }
352288 urlTransform = { transformLinkUri }
353289 components = { markdownComponents }
354290 />
@@ -358,95 +294,169 @@ const ChatBubble = ({
358294 // Assistant message rendering now uses the structured props
359295 return (
360296 < >
361- { parsedThoughts . map ( ( thought , index ) => {
362- const partId = `thought_${ index } `
363- return (
364- < div
365- key = { partId }
366- className = "mb-4 border-l-2 border-yellow-500 pl-3"
297+ { hasInternalMonologue && (
298+ < div className = "mb-4 border-l-2 border-yellow-500 pl-3" >
299+ < button
300+ onClick = { ( ) =>
301+ toggleExpansion ( "agent_thought_process" )
302+ }
303+ className = "flex w-full items-center justify-start gap-2 text-yellow-400 hover:text-yellow-300 text-sm font-semibold"
367304 >
368- < button
369- onClick = { ( ) => toggleExpansion ( partId ) }
370- className = "flex items-center gap-2 text-yellow-400 hover:text-yellow-300 text-sm font-semibold"
371- >
372- { expandedStates [ partId ] ? (
373- < IconChevronUp size = { 16 } />
374- ) : (
375- < IconChevronDown size = { 16 } />
376- ) }
377- Agent's Thought Process
378- </ button >
379- { expandedStates [ partId ] && (
380- < div className = "mt-2 p-3 bg-neutral-800/50 rounded-md" >
381- < ReactMarkdown className = "prose prose-sm prose-invert text-gray-300 whitespace-pre-wrap" >
382- { thought }
383- </ ReactMarkdown >
384- </ div >
305+ { expandedStates [ "agent_thought_process" ] ? (
306+ < IconChevronUp size = { 16 } />
307+ ) : (
308+ < IconChevronDown size = { 16 } />
385309 ) }
386- </ div >
387- )
388- } ) }
389-
390- { parsedToolCalls . map ( ( call , index ) => {
391- const partId = `tool_call_${ index } `
392- return (
393- < ToolCodeBlock
394- key = { partId }
395- name = { call . tool_name }
396- code = { call . parameters }
397- isExpanded = { ! ! expandedStates [ partId ] }
398- onToggle = { ( ) => toggleExpansion ( partId ) }
399- />
400- )
401- } ) }
402-
403- { parsedToolResults . map ( ( res , index ) => {
404- const partId = `tool_result_${ index } `
405- return (
406- < ToolResultBlock
407- key = { partId }
408- name = { res . tool_name }
409- result = { res . result }
410- isExpanded = { ! ! expandedStates [ partId ] }
411- onToggle = { ( ) => toggleExpansion ( partId ) }
412- />
413- )
414- } ) }
310+ Agent's Thought Process
311+ </ button >
312+ < AnimatePresence >
313+ { expandedStates [ "agent_thought_process" ] && (
314+ < motion . div
315+ initial = { { height : 0 , opacity : 0 } }
316+ animate = { { height : "auto" , opacity : 1 } }
317+ exit = { { height : 0 , opacity : 0 } }
318+ transition = { {
319+ duration : 0.3 ,
320+ ease : "easeInOut"
321+ } }
322+ className = "overflow-hidden"
323+ >
324+ < div className = "mt-2 p-3 bg-neutral-800/50 rounded-md space-y-4" >
325+ { isStreaming ? (
326+ < pre className = "text-xs text-gray-300 whitespace-pre-wrap font-mono" >
327+ < code >
328+ { processedContent . content }
329+ </ code >
330+ </ pre >
331+ ) : (
332+ < >
333+ { parsedThoughts . map (
334+ ( thought , index ) => (
335+ < div
336+ key = { `thought_${ index } ` }
337+ >
338+ < ReactMarkdown className = "prose prose-sm prose-invert text-gray-300 whitespace-pre-wrap" >
339+ { thought }
340+ </ ReactMarkdown >
341+ </ div >
342+ )
343+ ) }
344+ { parsedToolCalls . map (
345+ ( call , index ) => {
346+ let formattedParams =
347+ call . parameters
348+ try {
349+ const parsed =
350+ JSON . parse (
351+ call . parameters
352+ )
353+ formattedParams =
354+ JSON . stringify (
355+ parsed ,
356+ null ,
357+ 2
358+ )
359+ } catch ( e ) {
360+ // not json, leave as is
361+ }
362+ return (
363+ < div
364+ key = { `tool_call_${ index } ` }
365+ >
366+ < p className = "text-xs font-semibold text-green-400 mb-1" >
367+ Tool Call:{ " " }
368+ {
369+ call . tool_name
370+ }
371+ </ p >
372+ < pre className = "text-xs text-gray-300 whitespace-pre-wrap font-mono" >
373+ < code >
374+ {
375+ formattedParams
376+ }
377+ </ code >
378+ </ pre >
379+ </ div >
380+ )
381+ }
382+ ) }
383+ { parsedToolResults . map (
384+ ( res , index ) => {
385+ let formattedResult =
386+ res . result
387+ try {
388+ const parsed =
389+ JSON . parse (
390+ res . result
391+ )
392+ formattedResult =
393+ JSON . stringify (
394+ parsed ,
395+ null ,
396+ 2
397+ )
398+ } catch ( e ) {
399+ // not json, leave as is
400+ }
401+ return (
402+ < div
403+ key = { `tool_result_${ index } ` }
404+ >
405+ < p className = "text-xs font-semibold text-purple-400 mb-1" >
406+ Tool Result:{ " " }
407+ {
408+ res . tool_name
409+ }
410+ </ p >
411+ < pre className = "text-xs text-gray-300 whitespace-pre-wrap font-mono" >
412+ < code >
413+ {
414+ formattedResult
415+ }
416+ </ code >
417+ </ pre >
418+ </ div >
419+ )
420+ }
421+ ) }
422+ </ >
423+ ) }
424+ </ div >
425+ </ motion . div >
426+ ) }
427+ </ AnimatePresence >
428+ </ div >
429+ ) }
415430
416431 { /* Render the final, clean content */ }
417- { contentToRender && (
432+ { ! isStreaming && finalContent && (
418433 < div
419- className = {
420- ( parsedThoughts . length > 0 ||
421- parsedToolCalls . length > 0 ||
422- parsedToolResults . length > 0 ) &&
423- "mt-4 pt-4 border-t border-neutral-700/50"
424- }
434+ className = { cn (
435+ hasInternalMonologue &&
436+ "mt-4 pt-4 border-t border-neutral-700/50"
437+ ) }
425438 >
426439 < ReactMarkdown
427440 className = "prose prose-invert"
428441 remarkPlugins = { [ remarkGfm ] }
429- children = { contentToRender }
442+ children = { finalContent }
430443 urlTransform = { transformLinkUri }
431444 components = { markdownComponents }
432445 />
433446 </ div >
434447 ) }
435448 </ >
436449 )
437- }
438-
439- const renderedContent = React . useMemo (
440- ( ) => renderMessageContent ( finalContent ) ,
441- [
442- finalContent ,
443- expandedStates ,
444- isUser ,
445- parsedThoughts ,
446- parsedToolCalls ,
447- parsedToolResults
448- ]
449- )
450+ } , [
451+ processedContent . content ,
452+ finalContent ,
453+ expandedStates ,
454+ isUser ,
455+ parsedThoughts ,
456+ parsedToolCalls ,
457+ parsedToolResults ,
458+ isStreaming
459+ ] )
450460
451461 // Function to copy message content to clipboard
452462 const handleCopyToClipboard = ( ) => {
0 commit comments