11"use client" ;
22
3- import { authClient } from "@databuddy/auth/client" ;
43import type { UIMessage } from "ai" ;
54import { useEffect , useState } from "react" ;
5+ import {
6+ ChainOfThought ,
7+ ChainOfThoughtContent ,
8+ ChainOfThoughtHeader ,
9+ ChainOfThoughtStep ,
10+ } from "@/components/ai-elements/chain-of-thought" ;
611import {
712 Message ,
8- MessageAvatar ,
913 MessageContent ,
14+ MessageResponse ,
1015} from "@/components/ai-elements/message" ;
1116import {
1217 Reasoning ,
1318 ReasoningContent ,
1419 ReasoningTrigger ,
1520} from "@/components/ai-elements/reasoning" ;
16- import { Response } from "@/components/ai-elements/response" ;
17- import {
18- Tool ,
19- ToolContent ,
20- ToolHeader ,
21- ToolInput ,
22- ToolOutput ,
23- } from "@/components/ai-elements/tool" ;
2421import { Avatar , AvatarFallback , AvatarImage } from "@/components/ui/avatar" ;
25- import { Skeleton } from "@/components/ui/skeleton" ;
2622import { cn } from "@/lib/utils" ;
2723
2824type AgentMessagesProps = {
@@ -34,24 +30,6 @@ type AgentMessagesProps = {
3430
3531type MessagePart = UIMessage [ "parts" ] [ number ] ;
3632
37- function isReasoningPart ( part : MessagePart ) : boolean {
38- return (
39- part . type === "reasoning" ||
40- part . type === "step-start" ||
41- part . type === "data-step-start" ||
42- part . type ?. includes ( "reasoning" ) ||
43- part . type ?. includes ( "step" )
44- ) ;
45- }
46-
47- function isTextPart ( part : MessagePart ) : boolean {
48- return part . type === "text" ;
49- }
50-
51- function isToolPart ( part : MessagePart ) : boolean {
52- return part . type ?. startsWith ( "tool" ) ?? false ;
53- }
54-
5533function getReasoningText ( part : MessagePart ) : string {
5634 const reasoning = part as {
5735 text ?: string ;
@@ -66,43 +44,44 @@ function getReasoningText(part: MessagePart): string {
6644 ) ;
6745}
6846
69- function getToolState ( part : MessagePart ) {
70- const tool = part as { errorText ?: string ; output ?: unknown } ;
71- if ( tool . errorText ) {
72- return "output-error" ;
47+ function formatToolOutput ( output : unknown ) {
48+ if ( output === undefined ) {
49+ return null ;
7350 }
74- if ( tool . output !== undefined ) {
75- return "output-available" ;
51+
52+ console . log ( output ) ;
53+
54+ if ( typeof output === "object" && "data" in output ) {
55+ return < p > Found { output . data . length } results.</ p > ;
7656 }
77- return "input-available" ;
78- }
7957
80- function formatToolOutput ( output : unknown , toolName ?: string ) {
81- if ( output === undefined ) {
82- return null ;
58+ if ( typeof output === "object" && "pages" in output ) {
59+ return < p > Found { output . pages . length } results.</ p > ;
8360 }
8461
85- if (
86- toolName === "web_search" &&
87- typeof output === "object" &&
88- output !== null
89- ) {
90- const webData = output as { data ?: unknown [ ] } ;
91- if ( Array . isArray ( webData . data ) ) {
92- return {
93- summary : `Scraped ${ webData . data . length } page(s)` ,
94- results : webData . data . map ( ( page , index ) => ( {
95- page : index + 1 ,
96- ...( typeof page === "object" ? page : { content : page } ) ,
97- } ) ) ,
98- } ;
99- }
62+ if ( typeof output === "object" && "errorText" in output ) {
63+ return < p > Error: { output . errorText } </ p > ;
10064 }
10165
102- if ( typeof output === "string" || typeof output === "object" ) {
103- return output as string | Record < string , unknown > ;
66+ if ( typeof output === "string" ) {
67+ const obj = JSON . parse ( output ) ;
68+
69+ if ( "data" in obj ) {
70+ return < p > Found { obj . data . length } results.</ p > ;
71+ }
72+
73+ if ( "pages" in obj ) {
74+ return < p > Found { obj . pages . length } results.</ p > ;
75+ }
76+
77+ if ( "errorText" in obj ) {
78+ return < p > Error: { obj . errorText } </ p > ;
79+ }
80+
81+ return < p > Found 0 results.</ p > ;
10482 }
105- return String ( output ) ;
83+
84+ return < p > Found 0 results.</ p > ;
10685}
10786
10887function ReasoningMessage ( {
@@ -128,61 +107,36 @@ function ReasoningMessage({
128107 ) ;
129108}
130109
131- function ToolMessage ( {
132- part,
133- partIndex,
134- isStreaming,
135- } : {
136- part : MessagePart ;
137- partIndex : number ;
138- isStreaming : boolean ;
139- } ) {
140- const toolPart = part as {
141- toolCallId ?: string ;
142- toolName ?: string ;
143- input ?: unknown ;
144- output ?: unknown ;
145- errorText ?: string ;
146- } ;
147-
148- const state = getToolState ( part ) ;
149- const isRunning = state === "input-available" ;
150- const hasCompleted = state === "output-available" || state === "output-error" ;
151-
152- const [ hasBeenStreaming , setHasBeenStreaming ] = useState (
153- isStreaming && isRunning
154- ) ;
155-
156- useEffect ( ( ) => {
157- if ( isStreaming && isRunning ) {
158- setHasBeenStreaming ( true ) ;
110+ function groupConsecutiveToolCalls ( parts : MessagePart [ ] ) {
111+ const grouped : Array < MessagePart | MessagePart [ ] > = [ ] ;
112+ let currentToolGroup : MessagePart [ ] = [ ] ;
113+
114+ for ( const part of parts ) {
115+ if ( part . type ?. includes ( "tool" ) ) {
116+ currentToolGroup . push ( part ) ;
117+ } else {
118+ if ( currentToolGroup . length > 0 ) {
119+ grouped . push (
120+ currentToolGroup . length === 1 ? currentToolGroup [ 0 ] : currentToolGroup
121+ ) ;
122+ currentToolGroup = [ ] ;
123+ }
124+ grouped . push ( part ) ;
159125 }
160- } , [ isStreaming , isRunning ] ) ;
126+ }
161127
162- const shouldBeOpen = hasBeenStreaming || hasCompleted ;
128+ // Don't forget the last group
129+ if ( currentToolGroup . length > 0 ) {
130+ grouped . push (
131+ currentToolGroup . length === 1 ? currentToolGroup [ 0 ] : currentToolGroup
132+ ) ;
133+ }
163134
164- return (
165- < Tool defaultOpen = { shouldBeOpen } >
166- < ToolHeader
167- state = { state }
168- type = {
169- ( toolPart . toolName as `tool-${string } `) ??
170- ( `tool-${ partIndex } ` as const )
171- }
172- />
173- < ToolContent >
174- { toolPart . input !== undefined && < ToolInput input = { toolPart . input } /> }
175- < ToolOutput
176- errorText = { toolPart . errorText }
177- output = { formatToolOutput ( toolPart . output , toolPart . toolName ) }
178- />
179- </ ToolContent >
180- </ Tool >
181- ) ;
135+ return grouped ;
182136}
183137
184138function renderMessagePart (
185- part : MessagePart ,
139+ part : MessagePart | MessagePart [ ] ,
186140 partIndex : number ,
187141 messageId : string ,
188142 isLastMessage : boolean ,
@@ -191,7 +145,27 @@ function renderMessagePart(
191145 const key = `${ messageId } -${ partIndex } ` ;
192146 const isCurrentlyStreaming = isLastMessage && isStreaming ;
193147
194- if ( isReasoningPart ( part ) ) {
148+ // Handle grouped tool calls
149+ if ( Array . isArray ( part ) ) {
150+ return (
151+ < ChainOfThought className = "my-4" defaultOpen key = { key } >
152+ < ChainOfThoughtHeader > Running { part . length } tools</ ChainOfThoughtHeader >
153+ < ChainOfThoughtContent >
154+ { part . map ( ( toolPart , idx ) => (
155+ < ChainOfThoughtStep
156+ key = { `${ key } -tool-${ idx } ` }
157+ label = { `Running ${ toolPart . type } ` }
158+ status = "complete"
159+ >
160+ { formatToolOutput ( toolPart . output ) }
161+ </ ChainOfThoughtStep >
162+ ) ) }
163+ </ ChainOfThoughtContent >
164+ </ ChainOfThought >
165+ ) ;
166+ }
167+
168+ if ( part . type === "reasoning" ) {
195169 return (
196170 < ReasoningMessage
197171 isStreaming = { isCurrentlyStreaming }
@@ -201,27 +175,32 @@ function renderMessagePart(
201175 ) ;
202176 }
203177
204- if ( isTextPart ( part ) ) {
178+ if ( part . type === "text" ) {
205179 const textPart = part as { text : string } ;
206180 if ( ! textPart . text ?. trim ( ) ) {
207181 return null ;
208182 }
209183
210184 return (
211- < Response isAnimating = { isCurrentlyStreaming } key = { key } >
185+ < MessageResponse isAnimating = { isCurrentlyStreaming } key = { key } >
212186 { textPart . text }
213- </ Response >
187+ </ MessageResponse >
214188 ) ;
215189 }
216190
217- if ( isToolPart ( part ) ) {
191+ console . log ( part ) ;
192+
193+ if ( part . type ?. includes ( "tool" ) ) {
218194 return (
219- < ToolMessage
220- isStreaming = { isCurrentlyStreaming }
221- key = { key }
222- part = { part }
223- partIndex = { partIndex }
224- />
195+ < ChainOfThought defaultOpen key = { key } >
196+ < ChainOfThoughtHeader />
197+ < ChainOfThoughtContent >
198+ < ChainOfThoughtStep
199+ label = { `Running ${ part . type } ` }
200+ status = "complete"
201+ />
202+ </ ChainOfThoughtContent >
203+ </ ChainOfThought >
225204 ) ;
226205 }
227206
@@ -245,29 +224,28 @@ export function AgentMessages({
245224 const showError =
246225 isLastMessage && hasError && message . role === "assistant" ;
247226
227+ const groupedParts = message . parts
228+ ? groupConsecutiveToolCalls ( message . parts )
229+ : [ ] ;
230+
248231 return (
249- < div className = "group" key = { message . id } >
250- < Message from = { message . role } >
251- < MessageContent className = "max-w-[80%]" variant = "flat" >
252- { message . parts ?. map ( ( part , partIndex ) =>
253- renderMessagePart (
254- part ,
255- partIndex ,
256- message . id ,
257- isLastMessage ,
258- isStreaming
259- )
260- ) }
261-
262- { showError && < ErrorMessage /> }
263- </ MessageContent >
264-
265- { message . role === "user" && < UserAvatar /> }
266- { message . role === "assistant" && (
267- < AssistantAvatar hasError = { hasError } />
232+ < Message from = { message . role } key = { message . id } >
233+ < MessageContent
234+ className = { cn ( message . role === "assistant" ? "w-full" : "" ) }
235+ >
236+ { groupedParts . map ( ( part , partIndex ) =>
237+ renderMessagePart (
238+ part ,
239+ partIndex ,
240+ message . id ,
241+ isLastMessage ,
242+ isStreaming
243+ )
268244 ) }
269- </ Message >
270- </ div >
245+
246+ { showError && < ErrorMessage /> }
247+ </ MessageContent >
248+ </ Message >
271249 ) ;
272250 } ) }
273251
@@ -319,35 +297,3 @@ function StreamingIndicator({ statusText }: { statusText?: string }) {
319297 </ div >
320298 ) ;
321299}
322-
323- function UserAvatar ( ) {
324- const { data : session , isPending } = authClient . useSession ( ) ;
325- const user = session ?. user ;
326-
327- if ( isPending ) {
328- return < Skeleton className = "size-8 shrink-0 rounded-full" /> ;
329- }
330-
331- return (
332- < MessageAvatar
333- name = { user ?. name || user ?. email || "User" }
334- src = { user ?. image || "" }
335- />
336- ) ;
337- }
338-
339- function AssistantAvatar ( { hasError = false } : { hasError ?: boolean } ) {
340- return (
341- < Avatar className = "size-8 shrink-0 ring-1 ring-border" >
342- < AvatarImage alt = "Databunny" src = "/databunny.webp" />
343- < AvatarFallback
344- className = { cn (
345- "bg-primary/10 font-semibold text-primary" ,
346- hasError && "bg-destructive/10 text-destructive"
347- ) }
348- >
349- DB
350- </ AvatarFallback >
351- </ Avatar >
352- ) ;
353- }
0 commit comments