Skip to content

Commit 328db76

Browse files
committed
fix (chat): chatbubble fixes
1 parent e11d1ae commit 328db76

2 files changed

Lines changed: 163 additions & 149 deletions

File tree

src/client/app/chat/page.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,10 @@ export default function ChatPage() {
15561556
onReply={handleReply}
15571557
message={msg}
15581558
allMessages={displayedMessages}
1559+
isStreaming={
1560+
thinking &&
1561+
i === displayedMessages.length - 1
1562+
}
15591563
onDelete={handleDeleteMessage}
15601564
/>
15611565
</div>

src/client/components/ChatBubble.js

Lines changed: 159 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -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"
3333
import ReactMarkdown from "react-markdown"
3434
import remarkGfm from "remark-gfm"
3535
import IconGoogleDocs from "./icons/IconGoogleDocs"
@@ -38,6 +38,7 @@ import IconGoogleCalendar from "./icons/IconGoogleCalendar"
3838
import IconGoogleSlides from "./icons/IconGoogleSlides" // This is a custom one, not from tabler
3939
import IconGoogleMail from "./icons/IconGoogleMail"
4040
import IconGoogleDrive from "./icons/IconGoogleMail"
41+
import { Tooltip } from "react-tooltip"
4142
import toast from "react-hot-toast"
4243
import 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
208137
const 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

Comments
 (0)