Skip to content

Commit 451c760

Browse files
committed
feat(core): react useChat example
1 parent aa2037c commit 451c760

File tree

10 files changed

+420
-0
lines changed

10 files changed

+420
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Copy this file to .env and fill in your credentials
2+
# Used by @ai-sdk/openai in the embedded VoltAgent dev server
3+
OPENAI_API_KEY=sk-...

examples/with-use-chat/README.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# with-use-chat (Plain React + built-in server)
2+
3+
This example shows how to build a simple chat UI in plain React using the AI SDK's `useChat` hook with VoltAgent's chat SSE endpoint.
4+
5+
It now includes a minimal VoltAgent dev server inside the same project, exposing:
6+
7+
POST http://localhost:3141/agents/:id/chat
8+
9+
The endpoint streams UI messages in the AI SDK format, compatible with `useChat` and `DefaultChatTransport`.
10+
11+
## Prerequisites
12+
13+
- Node.js 18+
14+
- OpenAI API key
15+
16+
Create a .env file from the provided template (recommended):
17+
18+
cp .env.example .env
19+
20+
# then edit .env and set OPENAI_API_KEY
21+
22+
Alternatively, you can export the variable in your shell:
23+
24+
export OPENAI_API_KEY=...
25+
26+
## Run
27+
28+
1. Install dependencies at the repo root:
29+
30+
pnpm install
31+
32+
2. Start the VoltAgent dev server (from this example directory):
33+
34+
cd examples/with-use-chat
35+
pnpm dev:server
36+
37+
This will start Hono at http://localhost:3141 with an agent `cerbai`.
38+
39+
Note: The React app now has a Port field (default 4310). If you run this embedded server, set the Port field to 3141 in the UI to match the server.
40+
41+
You can test it via cURL:
42+
43+
curl -N -X POST http://localhost:3141/agents/cerbai/chat \
44+
-H "Content-Type: application/json" \
45+
-d '{
46+
"input": "Tell me a story",
47+
"options": { "temperature": 0.7, "maxSteps": 10 }
48+
}'
49+
50+
3. In another terminal, start the React client:
51+
52+
cd examples/with-use-chat
53+
pnpm dev
54+
55+
4. Open the app:
56+
57+
http://localhost:5173
58+
59+
5. Type a message and press Send. You should see the assistant's response stream in real time. The default Agent ID is `cerbai` (you can change it in the UI).
60+
61+
## How it works
62+
63+
We configure a custom `DefaultChatTransport` so that the request body matches VoltAgent's expectations (last UI message as array plus options):
64+
65+
import { useChat } from "@ai-sdk/react";
66+
import { DefaultChatTransport } from "ai";
67+
68+
const transport = new DefaultChatTransport({
69+
api: `http://localhost:3141/agents/${agentId}/chat`,
70+
prepareSendMessagesRequest({ messages }) {
71+
const lastMessage = messages[messages.length - 1];
72+
return {
73+
body: {
74+
input: [lastMessage], // array of UIMessage
75+
options: {
76+
userId: "user-123",
77+
conversationId: "conv-456",
78+
temperature: 0.7,
79+
maxSteps: 10,
80+
},
81+
},
82+
};
83+
},
84+
});
85+
86+
const { messages, handleInputChange, handleSubmit, stop, status } = useChat({
87+
transport,
88+
});
89+
90+
This aligns with VoltAgent's AI SDK compatible chat stream endpoint.

examples/with-use-chat/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>VoltAgent useChat Example</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "voltagent-example-with-use-chat",
3+
"private": true,
4+
"license": "MIT",
5+
"version": "0.0.0",
6+
"description": "Plain React example using AI SDK useChat with VoltAgent /agents/:id/chat SSE endpoint",
7+
"keywords": ["agent", "ai", "useChat", "voltagent", "react"],
8+
"scripts": {
9+
"dev": "vite",
10+
"dev:client": "vite",
11+
"dev:server": "tsx src/server/index.ts",
12+
"build": "vite build",
13+
"preview": "vite preview --port 5173"
14+
},
15+
"dependencies": {
16+
"@ai-sdk/openai": "^2.0.52",
17+
"@ai-sdk/react": "^2.0.8",
18+
"@voltagent/core": "workspace:*",
19+
"@voltagent/logger": "^1.0.0",
20+
"@voltagent/libsql": "^1.0.0",
21+
"@voltagent/server-hono": "workspace:*",
22+
"ai": "^5.0.76",
23+
"react": "^19.0.0",
24+
"react-dom": "^19.0.0",
25+
"zod": "^3.25.76"
26+
},
27+
"devDependencies": {
28+
"@types/react": "^19",
29+
"@types/react-dom": "^19",
30+
"tsx": "^4.19.2",
31+
"typescript": "^5.8.2",
32+
"vite": "^5.4.10"
33+
}
34+
}

examples/with-use-chat/src/App.tsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { useChat } from "@ai-sdk/react";
2+
import type { ClientSideToolResult } from "@voltagent/core";
3+
import {
4+
type ChatOnToolCallCallback,
5+
DefaultChatTransport,
6+
type UIMessage,
7+
lastAssistantMessageIsCompleteWithToolCalls,
8+
} from "ai";
9+
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
10+
11+
function ChatMessageView({ m }: { m: UIMessage }) {
12+
const renderContent = (content: any) => {
13+
if (typeof content === "string") return content;
14+
if (Array.isArray(content)) {
15+
// Collect text from parts like { type: 'text', text: '...' } or strings
16+
const textParts: string[] = [];
17+
for (const part of content) {
18+
if (typeof part === "string") {
19+
textParts.push(part);
20+
} else if (part && typeof part === "object") {
21+
if (typeof (part as any).text === "string") {
22+
textParts.push((part as any).text);
23+
} else if (typeof (part as any).content === "string") {
24+
textParts.push((part as any).content);
25+
}
26+
}
27+
}
28+
if (textParts.length > 0) return textParts.join("");
29+
// Fallback: show structured content for non-text parts (e.g., tool calls)
30+
return <pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(content, null, 2)}</pre>;
31+
}
32+
// Unknown shape fallback
33+
return <pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(content, null, 2)}</pre>;
34+
};
35+
36+
return (
37+
<div
38+
style={{
39+
padding: "8px 12px",
40+
borderRadius: 8,
41+
background: m.role === "assistant" ? "#f4f6ff" : "#f6f6f6",
42+
marginBottom: 8,
43+
}}
44+
>
45+
<div style={{ fontSize: 12, color: "#666", marginBottom: 4 }}>{m.role}</div>
46+
<div>{renderContent(m.content || m.parts)}</div>
47+
</div>
48+
);
49+
}
50+
const agentId = "ai-agent";
51+
const port = "3141";
52+
53+
export default function App() {
54+
const [input, setInput] = useState("");
55+
const inputRef = useRef<HTMLInputElement | null>(null);
56+
const [userId] = useState("user-123");
57+
const [conversationId] = useState("test-conversation-10");
58+
const [result, setResult] = useState<ClientSideToolResult | null>(null);
59+
60+
const handleToolCall = useCallback<ChatOnToolCallCallback>(async ({ toolCall }) => {
61+
if (toolCall.toolName === "getLocation") {
62+
navigator.geolocation.getCurrentPosition(
63+
(position) => {
64+
setResult({
65+
tool: "getLocation",
66+
toolCallId: toolCall.toolCallId,
67+
output: {
68+
latitude: position.coords.latitude,
69+
longitude: position.coords.longitude,
70+
accuracy: position.coords.accuracy,
71+
},
72+
});
73+
},
74+
(error) => {
75+
setResult({
76+
state: "output-error",
77+
tool: "getLocation",
78+
toolCallId: toolCall.toolCallId,
79+
errorText: error.message,
80+
});
81+
},
82+
);
83+
}
84+
}, []);
85+
86+
const { messages, sendMessage, addToolResult } = useChat({
87+
transport: new DefaultChatTransport({
88+
api: `http://localhost:${port}/agents/${agentId}/chat`,
89+
prepareSendMessagesRequest({ messages }) {
90+
const input = [messages[messages.length - 1]];
91+
return {
92+
body: {
93+
input,
94+
options: {
95+
userId,
96+
conversationId,
97+
temperature: 0.7,
98+
maxSteps: 10,
99+
},
100+
},
101+
};
102+
},
103+
}),
104+
onToolCall: handleToolCall,
105+
onFinish: () => {
106+
console.log("Message completed");
107+
},
108+
onError: (error) => {
109+
console.error("Chat error:", error);
110+
},
111+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
112+
});
113+
114+
useEffect(() => {
115+
if (!result) return;
116+
console.log("Adding tool result:", result);
117+
addToolResult(result);
118+
}, [result, addToolResult]);
119+
120+
// use local state for input control
121+
const value = input;
122+
return (
123+
<div
124+
style={{
125+
fontFamily: "Inter, ui-sans-serif, system-ui",
126+
maxWidth: 720,
127+
margin: "40px auto",
128+
padding: 16,
129+
}}
130+
>
131+
<h1>VoltAgent + AI SDK useChat</h1>
132+
<p style={{ color: "#444" }}>
133+
This example streams UI messages from VoltAgent's SSE endpoint that is compatible with the
134+
AI SDK's useChat hook.
135+
</p>
136+
137+
<div>
138+
{messages.map((m) => (
139+
<ChatMessageView key={m.id} m={m as UIMessage} />
140+
))}
141+
</div>
142+
143+
<form
144+
onSubmit={(e) => {
145+
e.preventDefault();
146+
const text = value?.trim();
147+
if (text) {
148+
// AI SDK's sendMessage expects a UIMessage-like object in this version
149+
sendMessage({ role: "user", content: text } as any);
150+
setInput("");
151+
}
152+
// focus input afterwards
153+
requestAnimationFrame(() => inputRef.current?.focus());
154+
}}
155+
style={{ display: "flex", gap: 8, marginTop: 16 }}
156+
>
157+
<input
158+
ref={inputRef}
159+
name="input"
160+
value={value}
161+
onChange={(e) => {
162+
setInput(e.target.value);
163+
}}
164+
placeholder="Type a message..."
165+
style={{ flex: 1, padding: "10px 12px", borderRadius: 8, border: "1px solid #ddd" }}
166+
/>
167+
<button type="submit" disabled={!value?.trim()}>
168+
Send
169+
</button>
170+
</form>
171+
</div>
172+
);
173+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from "react";
2+
import { createRoot } from "react-dom/client";
3+
import App from "./App";
4+
5+
const root = createRoot(document.getElementById("root") as HTMLElement);
6+
root.render(
7+
<React.StrictMode>
8+
<App />
9+
</React.StrictMode>,
10+
);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import "dotenv/config";
2+
import { openai } from "@ai-sdk/openai";
3+
import { Agent, AiSdkEmbeddingAdapter, Memory, VoltAgent } from "@voltagent/core";
4+
import { LibSQLMemoryAdapter, LibSQLVectorAdapter } from "@voltagent/libsql";
5+
import { createPinoLogger } from "@voltagent/logger";
6+
import { honoServer } from "@voltagent/server-hono";
7+
import { getLocationTool, weatherTool } from "./tools";
8+
9+
// Create logger
10+
const logger = createPinoLogger({
11+
name: "with-use-chat",
12+
level: "info",
13+
});
14+
15+
// Basic memory setup (LibSQL adapters with defaults)
16+
const memory = new Memory({
17+
storage: new LibSQLMemoryAdapter({}),
18+
embedding: new AiSdkEmbeddingAdapter(openai.textEmbeddingModel("text-embedding-3-small")),
19+
vector: new LibSQLVectorAdapter(),
20+
});
21+
22+
// Create Agent: cerbai
23+
const agent = new Agent({
24+
name: "ai-agent",
25+
instructions: "A helpful assistant that can check weather and help with various tasks.",
26+
model: openai("gpt-4o-mini"),
27+
tools: [weatherTool, getLocationTool],
28+
memory,
29+
});
30+
31+
new VoltAgent({
32+
agents: {
33+
agent,
34+
},
35+
logger,
36+
server: honoServer({ port: 3141 }),
37+
});

0 commit comments

Comments
 (0)