Skip to content

Commit 9974c00

Browse files
committed
feat: upgrade to ai sdk 5
1 parent 52cf2ce commit 9974c00

File tree

14 files changed

+2601
-508
lines changed

14 files changed

+2601
-508
lines changed

client/dashboard/package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,36 @@
1212
},
1313
"dependencies": {
1414
"@ai-sdk/openai": "catalog:",
15-
"@ai-sdk/react": "^1.2.12",
15+
"@ai-sdk/react": "catalog:",
16+
"@ai-sdk/ui-utils": "catalog:",
1617
"@datadog/browser-rum": "^6.20.0",
1718
"@dnd-kit/core": "^6.3.1",
1819
"@dnd-kit/modifiers": "^9.0.0",
1920
"@dnd-kit/sortable": "^10.0.0",
2021
"@dnd-kit/utilities": "^3.2.2",
2122
"@gram/client": "workspace:*",
22-
"@openrouter/ai-sdk-provider": "0.7.5",
23+
"@openrouter/ai-sdk-provider": "^1.2.0",
2324
"@polar-sh/checkout": "^0.1.11",
2425
"@radix-ui/react-accordion": "^1.2.12",
2526
"@radix-ui/react-avatar": "^1.1.10",
2627
"@radix-ui/react-checkbox": "^1.3.3",
28+
"@radix-ui/react-collapsible": "^1.1.12",
2729
"@radix-ui/react-dialog": "^1.1.15",
2830
"@radix-ui/react-dropdown-menu": "^2.1.16",
2931
"@radix-ui/react-hover-card": "^1.1.15",
3032
"@radix-ui/react-label": "^2.1.7",
3133
"@radix-ui/react-popover": "^1.1.15",
34+
"@radix-ui/react-progress": "^1.1.7",
3235
"@radix-ui/react-radio-group": "^1.3.8",
36+
"@radix-ui/react-scroll-area": "^1.2.10",
3337
"@radix-ui/react-select": "^2.2.6",
3438
"@radix-ui/react-separator": "^1.1.7",
3539
"@radix-ui/react-slot": "^1.2.3",
3640
"@radix-ui/react-tabs": "^1.1.13",
3741
"@radix-ui/react-toggle": "^1.1.10",
3842
"@radix-ui/react-toggle-group": "^1.1.11",
3943
"@radix-ui/react-tooltip": "^1.2.8",
44+
"@radix-ui/react-use-controllable-state": "^1.2.2",
4045
"@react-three/drei": "^10.0.7",
4146
"@react-three/fiber": "^9.1.2",
4247
"@react-three/postprocessing": "^3.0.4",
@@ -45,6 +50,7 @@
4550
"@tailwindcss/vite": "^4.1.13",
4651
"@tanstack/react-query": "catalog:",
4752
"@tanstack/react-table": "^8.21.3",
53+
"@xyflow/react": "^12.8.6",
4854
"ai": "catalog:",
4955
"class-variance-authority": "^0.7.1",
5056
"clsx": "^2.1.1",
@@ -63,12 +69,16 @@
6369
"react-error-boundary": "^6.0.0",
6470
"react-merge-refs": "^3.0.2",
6571
"react-router": "^7.9.1",
72+
"react-syntax-highlighter": "^15.6.6",
6673
"sonner": "^2.0.7",
74+
"streamdown": "^1.3.0",
6775
"tailwind-merge": "^3.3.1",
6876
"tailwindcss": "^4.1.13",
6977
"three": "^0.176.0",
78+
"tokenlens": "^1.3.1",
7079
"tunnel-rat": "^0.1.2",
7180
"tw-animate-css": "^1.3.8",
81+
"use-stick-to-bottom": "^1.1.1",
7282
"uuid": "^13.0.0",
7383
"vaul": "^1.1.2",
7484
"zod": "^3.20.0",
@@ -79,6 +89,7 @@
7989
"@types/node": "^24.5.2",
8090
"@types/react": "catalog:",
8191
"@types/react-dom": "catalog:",
92+
"@types/react-syntax-highlighter": "^15.5.13",
8293
"@types/three": "^0.180.0",
8394
"@vitejs/plugin-react": "^5.0.3",
8495
"eslint": "^9.35.0",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Button } from "@/components/ui/button";
2+
import { cn } from "@/lib/utils";
3+
import { useState } from "react";
4+
5+
interface ChatInputProps {
6+
onSend: (message: string) => void;
7+
disabled?: boolean;
8+
placeholder?: string;
9+
className?: string;
10+
initialInput?: string;
11+
additionalActions?: React.ReactNode;
12+
}
13+
14+
export function ChatInput({
15+
onSend,
16+
disabled,
17+
placeholder = "Type a message...",
18+
className,
19+
initialInput,
20+
additionalActions,
21+
}: ChatInputProps) {
22+
const [input, setInput] = useState(initialInput || "");
23+
24+
const handleSubmit = (e: React.FormEvent) => {
25+
e.preventDefault();
26+
if (input.trim() && !disabled) {
27+
onSend(input);
28+
setInput("");
29+
}
30+
};
31+
32+
return (
33+
<form
34+
onSubmit={handleSubmit}
35+
className={cn("flex items-center gap-2 border-t p-4", className)}
36+
>
37+
<textarea
38+
value={input}
39+
onChange={(e) => setInput(e.target.value)}
40+
placeholder={placeholder}
41+
disabled={disabled}
42+
className="flex-1 resize-none rounded-lg border bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
43+
rows={1}
44+
onKeyDown={(e) => {
45+
if (e.key === "Enter" && !e.shiftKey) {
46+
e.preventDefault();
47+
handleSubmit(e);
48+
}
49+
}}
50+
/>
51+
{additionalActions}
52+
<Button type="submit" disabled={disabled || !input.trim()}>
53+
Send
54+
</Button>
55+
</form>
56+
);
57+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { UIMessage } from "ai";
2+
import { cn } from "@/lib/utils";
3+
import { Type } from "@/components/ui/type";
4+
import { useEffect, useRef } from "react";
5+
6+
interface ChatMessagesProps {
7+
messages: UIMessage[];
8+
isLoading?: boolean;
9+
renderMessage?: (message: UIMessage) => React.ReactNode;
10+
className?: string;
11+
}
12+
13+
export function ChatMessages({
14+
messages,
15+
isLoading,
16+
renderMessage,
17+
className,
18+
}: ChatMessagesProps) {
19+
const messagesEndRef = useRef<HTMLDivElement>(null);
20+
21+
useEffect(() => {
22+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
23+
}, [messages]);
24+
25+
return (
26+
<div className={cn("flex flex-col gap-4 p-4 overflow-y-auto", className)}>
27+
{messages.map((message) => (
28+
<div key={message.id}>
29+
{renderMessage ? (
30+
renderMessage(message)
31+
) : (
32+
<DefaultMessageRenderer message={message} />
33+
)}
34+
</div>
35+
))}
36+
{isLoading && (
37+
<div className="flex items-center gap-2 text-muted-foreground">
38+
<div className="h-2 w-2 animate-bounce rounded-full bg-current [animation-delay:-0.3s]" />
39+
<div className="h-2 w-2 animate-bounce rounded-full bg-current [animation-delay:-0.15s]" />
40+
<div className="h-2 w-2 animate-bounce rounded-full bg-current" />
41+
</div>
42+
)}
43+
<div ref={messagesEndRef} />
44+
</div>
45+
);
46+
}
47+
48+
function DefaultMessageRenderer({ message }: { message: UIMessage }) {
49+
return (
50+
<div
51+
className={cn(
52+
"flex flex-col gap-2 rounded-lg p-4",
53+
message.role === "user"
54+
? "ml-auto max-w-[80%] bg-primary text-primary-foreground"
55+
: "mr-auto max-w-[80%] bg-muted",
56+
)}
57+
>
58+
<Type variant="small" className="font-medium opacity-70">
59+
{message.role === "user" ? "You" : "Assistant"}
60+
</Type>
61+
<div className="whitespace-pre-wrap">
62+
{message.parts.map((part, index) => {
63+
if (part.type === "text") {
64+
return <span key={index}>{part.text}</span>;
65+
}
66+
return null;
67+
})}
68+
</div>
69+
</div>
70+
);
71+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { streamText, convertToModelMessages, UIMessage, smoothStream } from "ai";
2+
3+
export interface CustomChatTransportConfig {
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
model: any;
6+
temperature: number;
7+
getTools: (messages: UIMessage[]) => Promise<{
8+
tools: Record<string, { description?: string; inputSchema: unknown }>;
9+
systemPrompt: string;
10+
}>;
11+
onError?: (error: { error: unknown }) => void;
12+
}
13+
14+
export class CustomChatTransport {
15+
private config: CustomChatTransportConfig;
16+
17+
constructor(config: CustomChatTransportConfig) {
18+
this.config = config;
19+
}
20+
21+
async sendMessages({ messages }: { messages: UIMessage[] }) {
22+
// Get tools and system prompt dynamically per request
23+
const { tools, systemPrompt } = await this.config.getTools(messages);
24+
25+
console.log("CustomChatTransport: Sending request with tools:", Object.keys(tools));
26+
console.log("CustomChatTransport: Tool count:", Object.keys(tools).length);
27+
28+
const result = await streamText({
29+
model: this.config.model,
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
messages: convertToModelMessages(messages) as any,
32+
tools,
33+
temperature: this.config.temperature,
34+
system: systemPrompt,
35+
experimental_transform: smoothStream({ delayInMs: 15 }),
36+
maxSteps: 5,
37+
onError: this.config.onError,
38+
});
39+
40+
return result.toUIMessageStream();
41+
}
42+
43+
updateConfig(config: Partial<CustomChatTransportConfig>) {
44+
this.config = { ...this.config, ...config };
45+
}
46+
}

client/dashboard/src/pages/playground/Agentify.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,12 @@ export const AgentifyProvider = ({
106106
const example = await fetch(exampleUrl!).then((res) => res.text());
107107

108108
const result = await generateObject({
109-
model,
109+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
110+
model: model as any,
110111
mode: "json",
111112
prompt: `
112113
<instructions>
113-
You will be given a chat history, a statement of intent, and a basic skeleton of an agent.
114+
You will be given a chat history, a statement of intent, and a basic skeleton of an agent.
114115
Using the statement of intent and details from the chat history, produce a complete agent in ${lang} using ${framework} that performs the task.
115116
The agent should use LLM calls and toolsets to solve the generic version of the task as described in the statement of intent, not just the specific example given.
116117
Note that the toolset provides tools and handles their execution and authentication. For example, any tool call present in the chat history will be available to the agent.
@@ -134,7 +135,7 @@ export const AgentifyProvider = ({
134135
</values-to-use>
135136
136137
<chat-history>
137-
${messages.map((m) => `${m.role}: ${m.content}`).join("\n\t")}
138+
${messages.map((m) => `${m.role}: ${m.parts.map((p) => (p.type === "text" ? p.text : "")).join("")}`).join("\n\t")}
138139
</chat-history>
139140
140141
<example-agent>
@@ -227,18 +228,19 @@ export const AgentifyButton = ({
227228
});
228229

229230
generateObject({
230-
model: openrouter.chat("openai/gpt-4o-mini"),
231+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
232+
model: openrouter.chat("openai/gpt-4o-mini") as any,
231233
mode: "json",
232234
prompt: `
233235
<instructions>
234-
You will be given a chat history.
236+
You will be given a chat history.
235237
Your job is to distill the user's intent from the chat history to produce a few sentences that describe the function the agent should perform, based on the user's intent from the chat history.
236238
This prompt will be used to generate an agent that can reusably and extensibly solve the task.
237239
Phrase the prompt as instructions, e.g. "Find all new users from the last 30 days and send them a welcome email".
238240
</instructions>
239241
240242
<chat-history>
241-
${messages.map((m) => `${m.role}: ${m.content}`).join("\n\t")}
243+
${messages.map((m) => `${m.role}: ${m.parts.map((p) => (p.type === "text" ? p.text : "")).join("")}`).join("\n\t")}
242244
</chat-history>
243245
`,
244246
temperature: 0.5,

client/dashboard/src/pages/playground/ChatContext.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useRegisterChatTelemetry, useTelemetry } from "@/contexts/Telemetry";
2-
import { Message, UIMessage } from "ai";
2+
import { UIMessage } from "ai";
33
import { createContext, useContext, useRef, useState } from "react";
44
import { useSearchParams } from "react-router";
55
import { v7 as uuidv7 } from "uuid";
66

7-
type AppendFn = (message: Message) => void;
7+
type AppendFn = (message: { content: string }) => void;
88

99
const ChatContext = createContext<{
1010
id: string;

0 commit comments

Comments
 (0)