Skip to content

Commit 217e946

Browse files
Update Agent (#215)
* Install missing better-auth dep * Add working better auth secret * Delete AI Elements * Add new AI Elements * Remove avatars, fix reasoning * Remove group * Remove conversation copy * Update agent-page-content.tsx * Update globals.css * Fix sizing * Rework tool calls
1 parent 5967cda commit 217e946

28 files changed

+3708
-3717
lines changed

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ REDIS_URL="redis://localhost:6379"
66
AI_API_KEY=""
77

88
BETTER_AUTH_URL="http://localhost:3000"
9-
BETTER_AUTH_SECRET="your_bcrypt_secret"
9+
BETTER_AUTH_SECRET="wSl7AHwHRxm6HVi0rSLcvFnn0SiZG+thg9IAF/vBkHs="
1010

1111
# OpenPageRank key, not needed for most operations
1212
OPR_API_KEY=""

apps/dashboard/app/(main)/websites/[id]/agent/_components/agent-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function AgentInput() {
5252

5353
return (
5454
<div className="shrink-0 border-t bg-sidebar/30 backdrop-blur-sm">
55-
<div className="mx-auto max-w-2xl p-4">
55+
<div className="mx-auto max-w-4xl p-4">
5656
<div className="relative">
5757
<AgentCommandMenu />
5858

Lines changed: 117 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
"use client";
22

3-
import { authClient } from "@databuddy/auth/client";
43
import type { UIMessage } from "ai";
54
import { useEffect, useState } from "react";
5+
import {
6+
ChainOfThought,
7+
ChainOfThoughtContent,
8+
ChainOfThoughtHeader,
9+
ChainOfThoughtStep,
10+
} from "@/components/ai-elements/chain-of-thought";
611
import {
712
Message,
8-
MessageAvatar,
913
MessageContent,
14+
MessageResponse,
1015
} from "@/components/ai-elements/message";
1116
import {
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";
2421
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
25-
import { Skeleton } from "@/components/ui/skeleton";
2622
import { cn } from "@/lib/utils";
2723

2824
type AgentMessagesProps = {
@@ -34,24 +30,6 @@ type AgentMessagesProps = {
3430

3531
type 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-
5533
function 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

10887
function 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

184138
function 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

Comments
 (0)