@lantos1618/better-ui 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +537 -398
- package/dist/components/index.js +22 -8
- package/dist/components/index.mjs +22 -8
- package/dist/openapi/index.d.mts +33 -24
- package/dist/openapi/index.d.ts +33 -24
- package/dist/openapi/index.js +81 -2
- package/dist/openapi/index.mjs +79 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -6,565 +6,721 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@lantos1618/better-ui)
|
|
7
7
|
[](https://github.com/lantos1618/better-ui/actions/workflows/test.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
10
9
|
|
|
11
10
|
**[Guide](./GUIDE.md)** · **[API Reference](./docs/)** · **[Examples](./examples/)**
|
|
12
11
|
|
|
13
|
-
## The
|
|
12
|
+
## The idea
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
One tool definition = schema + server logic + view. Use the same tool in chat, as a React hook, as an MCP server, as an OpenAPI endpoint, or as an AG-UI server.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install @lantos1618/better-ui zod ai @ai-sdk/openai
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
## 1. Define tools
|
|
20
23
|
|
|
21
|
-
```
|
|
22
|
-
|
|
24
|
+
```tsx
|
|
25
|
+
// lib/tools.tsx
|
|
26
|
+
import { tool, Tool } from '@lantos1618/better-ui';
|
|
23
27
|
import { z } from 'zod';
|
|
24
28
|
|
|
25
|
-
const
|
|
29
|
+
export const weatherTool = tool({
|
|
26
30
|
name: 'weather',
|
|
27
|
-
description: 'Get weather for a city',
|
|
31
|
+
description: 'Get current weather for a city',
|
|
28
32
|
input: z.object({ city: z.string() }),
|
|
29
|
-
output: z.object({
|
|
33
|
+
output: z.object({
|
|
34
|
+
temp: z.number(),
|
|
35
|
+
city: z.string(),
|
|
36
|
+
condition: z.string(),
|
|
37
|
+
}),
|
|
30
38
|
});
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
weatherTool.server(async ({ city }, ctx) => {
|
|
41
|
+
// ctx.env, ctx.headers, ctx.user, ctx.session available here (stripped on client)
|
|
42
|
+
const res = await fetch(`https://wttr.in/${encodeURIComponent(city)}?format=j1`);
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return { temp: Number(data.current_condition[0].temp_C), city, condition: data.current_condition[0].weatherDesc[0].value };
|
|
35
45
|
});
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
weatherTool.view((data, state) => {
|
|
48
|
+
if (state?.loading) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="bg-zinc-800 rounded-xl p-4 text-zinc-400 text-sm animate-pulse">
|
|
51
|
+
Fetching weather...
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (state?.error) {
|
|
56
|
+
return <div className="bg-zinc-800 rounded-xl p-4 text-red-400 text-sm">{state.error.message}</div>;
|
|
57
|
+
}
|
|
58
|
+
if (!data) return null;
|
|
59
|
+
return (
|
|
60
|
+
<div className="bg-zinc-800 border border-zinc-700 rounded-xl p-4">
|
|
61
|
+
<p className="text-xs text-zinc-500 uppercase">{data.city}</p>
|
|
62
|
+
<p className="text-3xl font-light">{data.temp}°</p>
|
|
63
|
+
<p className="text-sm text-zinc-400">{data.condition}</p>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
});
|
|
48
67
|
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
// Export a registry for routes and components to share
|
|
69
|
+
export const tools = { weather: weatherTool } satisfies Record<string, Tool>;
|
|
51
70
|
```
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| **MCP server** | Expose any tool registry to Claude Desktop, Cursor, VS Code |
|
|
59
|
-
| **AG-UI protocol** | Compatible with CopilotKit, LangChain, Google ADK frontends |
|
|
60
|
-
| **Multi-provider** | OpenAI, Anthropic, Google Gemini, OpenRouter |
|
|
61
|
-
| **Streaming views** | Progressive partial data rendering |
|
|
62
|
-
| **Drop-in chat** | `<Chat />` component with automatic tool view rendering |
|
|
63
|
-
| **HITL confirmation** | Tools can require human approval before executing |
|
|
64
|
-
| **Auth helpers** | JWT, session cookies, BetterAuth integration |
|
|
65
|
-
| **Security** | Context stripping, input validation, output sanitization, rate limiting |
|
|
72
|
+
Key points:
|
|
73
|
+
- `.server(input, ctx)` runs only server-side. `ctx.env`, `ctx.headers`, `ctx.user` are available and auto-stripped on client.
|
|
74
|
+
- `.view(data, state)` — `state` has `loading`, `streaming`, `error`, and `onAction` (which takes the tool's **input** schema).
|
|
75
|
+
- Calling `.view()` also creates `tool.View` — a memoized React component you can render standalone anywhere.
|
|
76
|
+
- No `.client()`? Client auto-fetches to `/api/tools/execute`. Override with `clientFetch: { endpoint: '/my/path' }` in the tool config.
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
### 1. Define a Tool
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
import { tool } from '@lantos1618/better-ui';
|
|
73
|
-
import { z } from 'zod';
|
|
78
|
+
---
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
name: 'search',
|
|
77
|
-
description: 'Search the web',
|
|
78
|
-
input: z.object({ query: z.string().max(1000) }),
|
|
79
|
-
output: z.object({
|
|
80
|
-
results: z.array(z.object({
|
|
81
|
-
title: z.string(),
|
|
82
|
-
url: z.string(),
|
|
83
|
-
})),
|
|
84
|
-
}),
|
|
85
|
-
});
|
|
80
|
+
## 2. Wire up API routes (Next.js App Router)
|
|
86
81
|
|
|
87
|
-
|
|
88
|
-
const results = await searchAPI.search(query);
|
|
89
|
-
return { results };
|
|
90
|
-
});
|
|
82
|
+
### Chat route
|
|
91
83
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</ul>
|
|
98
|
-
));
|
|
99
|
-
```
|
|
84
|
+
```ts
|
|
85
|
+
// app/api/chat/route.ts
|
|
86
|
+
import { openai } from '@ai-sdk/openai';
|
|
87
|
+
import { streamText, convertToModelMessages } from 'ai';
|
|
88
|
+
import { weatherTool } from '@/lib/tools';
|
|
100
89
|
|
|
101
|
-
|
|
90
|
+
export async function POST(req: Request) {
|
|
91
|
+
const { messages } = await req.json();
|
|
102
92
|
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
const result = await streamText({
|
|
94
|
+
model: openai('gpt-4o'),
|
|
95
|
+
messages: convertToModelMessages(messages),
|
|
96
|
+
tools: {
|
|
97
|
+
weather: weatherTool.toAITool(),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
105
100
|
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<Chat
|
|
109
|
-
endpoint="/api/chat"
|
|
110
|
-
tools={{ weather, search }}
|
|
111
|
-
className="h-[600px]"
|
|
112
|
-
/>
|
|
113
|
-
);
|
|
101
|
+
return result.toUIMessageStreamResponse();
|
|
114
102
|
}
|
|
115
103
|
```
|
|
116
104
|
|
|
117
|
-
|
|
105
|
+
`.toAITool()` returns `{ description, inputSchema, execute }` — the format AI SDK v6 expects. The `execute` callback runs `tool.run(input, { isServer: true })` automatically. If the tool has `confirm: true`, `execute` is omitted so the SDK leaves the call at `state: 'input-available'` for HITL approval.
|
|
118
106
|
|
|
119
|
-
###
|
|
107
|
+
### Passing auth context to AI-called tools
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
// app/api/chat/route.ts (Next.js)
|
|
123
|
-
import { streamText, convertToModelMessages } from 'ai';
|
|
124
|
-
import { createProvider } from '@lantos1618/better-ui';
|
|
109
|
+
When the LLM calls a tool via `streamText`, the `execute` from `.toAITool()` runs server-side. To pass auth/request context into it, wrap the call:
|
|
125
110
|
|
|
126
|
-
|
|
111
|
+
```ts
|
|
112
|
+
// app/api/chat/route.ts
|
|
113
|
+
import { betterAuth } from '@lantos1618/better-ui/auth';
|
|
114
|
+
import { auth as authInstance } from '@/lib/auth'; // your BetterAuth instance
|
|
115
|
+
|
|
116
|
+
const auth = betterAuth(authInstance);
|
|
127
117
|
|
|
128
118
|
export async function POST(req: Request) {
|
|
119
|
+
const { user, session } = await auth(req.headers);
|
|
129
120
|
const { messages } = await req.json();
|
|
130
|
-
|
|
131
|
-
|
|
121
|
+
|
|
122
|
+
const result = await streamText({
|
|
123
|
+
model: openai('gpt-4o'),
|
|
132
124
|
messages: convertToModelMessages(messages),
|
|
133
125
|
tools: {
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
// Wrap toAITool's execute to inject auth context
|
|
127
|
+
weather: {
|
|
128
|
+
...weatherTool.toAITool(),
|
|
129
|
+
execute: async (input) => weatherTool.run(input, {
|
|
130
|
+
isServer: true, user, session, headers: req.headers,
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
136
133
|
},
|
|
137
134
|
});
|
|
135
|
+
|
|
138
136
|
return result.toUIMessageStreamResponse();
|
|
139
137
|
}
|
|
140
138
|
```
|
|
141
139
|
|
|
142
|
-
|
|
140
|
+
Now `ctx.user` and `ctx.session` are available inside `.server(input, ctx)`. Works the same with `jwtAuth()` or `sessionAuth()`.
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
import { createMCPServer } from '@lantos1618/better-ui/mcp';
|
|
142
|
+
### Tool execution route
|
|
146
143
|
|
|
147
|
-
|
|
148
|
-
name: 'my-tools',
|
|
149
|
-
version: '1.0.0',
|
|
150
|
-
tools: { weather, search },
|
|
151
|
-
});
|
|
144
|
+
Interactive views call tools directly (via `onAction`). This endpoint handles that:
|
|
152
145
|
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
```ts
|
|
147
|
+
// app/api/tools/execute/route.ts
|
|
148
|
+
import { tools } from '@/lib/tools';
|
|
149
|
+
import { betterAuth } from '@lantos1618/better-ui/auth';
|
|
150
|
+
import { auth as authInstance } from '@/lib/auth';
|
|
155
151
|
|
|
156
|
-
|
|
152
|
+
const auth = betterAuth(authInstance);
|
|
157
153
|
|
|
158
|
-
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
export async function POST(req: Request) {
|
|
155
|
+
const { user, session } = await auth(req.headers);
|
|
156
|
+
const { tool: name, input } = await req.json();
|
|
157
|
+
|
|
158
|
+
const t = tools[name];
|
|
159
|
+
if (!t) return Response.json({ error: 'Tool not found' }, { status: 404 });
|
|
160
|
+
|
|
161
|
+
// Pass the same auth context so ctx.user works in onAction calls too
|
|
162
|
+
const result = await t.run(input, { isServer: true, user, session, headers: req.headers });
|
|
163
|
+
return Response.json({ result });
|
|
166
164
|
}
|
|
167
165
|
```
|
|
168
166
|
|
|
169
|
-
|
|
167
|
+
### HITL confirmation route (only needed if you use `confirm: true`)
|
|
170
168
|
|
|
171
|
-
```
|
|
172
|
-
// app/api/
|
|
173
|
-
|
|
169
|
+
```ts
|
|
170
|
+
// app/api/tools/confirm/route.ts
|
|
171
|
+
import { tools } from '@/lib/tools';
|
|
172
|
+
|
|
173
|
+
export async function POST(req: Request) {
|
|
174
|
+
const { tool: name, input } = await req.json();
|
|
175
|
+
const t = tools[name];
|
|
176
|
+
if (!t) return Response.json({ error: 'Not found' }, { status: 404 });
|
|
177
|
+
const result = await t.run(input, { isServer: true });
|
|
178
|
+
return Response.json({ result });
|
|
179
|
+
}
|
|
174
180
|
```
|
|
175
181
|
|
|
176
182
|
---
|
|
177
183
|
|
|
178
|
-
##
|
|
179
|
-
|
|
180
|
-
### Object Config
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
const myTool = tool({
|
|
184
|
-
name: 'myTool',
|
|
185
|
-
description: 'What this tool does',
|
|
186
|
-
input: z.object({ query: z.string() }),
|
|
187
|
-
output: z.object({ results: z.array(z.string()) }),
|
|
188
|
-
tags: ['search'],
|
|
189
|
-
cache: { ttl: 60000 },
|
|
190
|
-
confirm: true, // require HITL confirmation
|
|
191
|
-
hints: { destructive: true }, // behavioral metadata
|
|
192
|
-
autoRespond: true, // auto-send state back to AI after user action
|
|
193
|
-
groupKey: (input) => input.query, // collapse related calls in thread
|
|
194
|
-
});
|
|
195
|
-
```
|
|
184
|
+
## 3. Chat UI
|
|
196
185
|
|
|
197
|
-
###
|
|
186
|
+
### Drop-in (simplest)
|
|
198
187
|
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
188
|
+
```tsx
|
|
189
|
+
// app/page.tsx
|
|
190
|
+
'use client';
|
|
191
|
+
import { Chat } from '@lantos1618/better-ui/components';
|
|
192
|
+
import { tools } from '@/lib/tools';
|
|
193
|
+
|
|
194
|
+
export default function Page() {
|
|
195
|
+
return (
|
|
196
|
+
<Chat
|
|
197
|
+
endpoint="/api/chat"
|
|
198
|
+
tools={tools}
|
|
199
|
+
className="h-screen"
|
|
200
|
+
placeholder="Ask something..."
|
|
201
|
+
suggestions={["What's the weather in Tokyo?"]}
|
|
202
|
+
/>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
207
205
|
```
|
|
208
206
|
|
|
209
|
-
|
|
207
|
+
`Chat` = `ChatProvider` + `Thread` + `Composer` in one component. Tool views render inline automatically.
|
|
210
208
|
|
|
211
|
-
|
|
212
|
-
// Server — runs in API routes, never on client
|
|
213
|
-
myTool.server(async (input, ctx) => {
|
|
214
|
-
// ctx.env, ctx.headers, ctx.cookies, ctx.user, ctx.session
|
|
215
|
-
return await db.query(input.query);
|
|
216
|
-
});
|
|
209
|
+
### Composable (full control)
|
|
217
210
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
211
|
+
```tsx
|
|
212
|
+
'use client';
|
|
213
|
+
import { ChatProvider, Thread, Composer, ChatPanel } from '@lantos1618/better-ui/components';
|
|
214
|
+
import { tools } from '@/lib/tools';
|
|
222
215
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
216
|
+
export default function Page() {
|
|
217
|
+
return (
|
|
218
|
+
<ChatProvider endpoint="/api/chat" tools={tools}>
|
|
219
|
+
<div className="flex h-screen">
|
|
220
|
+
<div className="flex-1 flex flex-col">
|
|
221
|
+
<Thread className="flex-1 overflow-y-auto" />
|
|
222
|
+
<Composer placeholder="Ask something..." />
|
|
223
|
+
</div>
|
|
224
|
+
{/* Side panel showing the latest tool result */}
|
|
225
|
+
<ChatPanel className="w-[500px] border-l border-zinc-800" />
|
|
226
|
+
</div>
|
|
227
|
+
</ChatProvider>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
230
231
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
Inside `ChatProvider`, use `useChatContext()` from a child component to access:
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
import { useChatContext } from '@lantos1618/better-ui/components';
|
|
236
|
+
|
|
237
|
+
const {
|
|
238
|
+
messages, // UIMessage[]
|
|
239
|
+
sendMessage, // (text: string) => void
|
|
240
|
+
isLoading, // boolean
|
|
241
|
+
status, // 'ready' | 'streaming' | 'submitted'
|
|
242
|
+
tools, // Record<string, Tool>
|
|
243
|
+
toolStateStore, // shared tool state
|
|
244
|
+
confirmTool, // HITL approve
|
|
245
|
+
rejectTool, // HITL reject
|
|
246
|
+
retryTool, // retry failed tool
|
|
247
|
+
// When persistence is configured:
|
|
248
|
+
threads, // Thread[]
|
|
249
|
+
threadId, // string
|
|
250
|
+
createThread, // (title?) => Promise<Thread>
|
|
251
|
+
switchThread, // (id) => Promise<void>
|
|
252
|
+
deleteThread, // (id) => Promise<void>
|
|
253
|
+
} = useChatContext();
|
|
238
254
|
```
|
|
239
255
|
|
|
240
|
-
|
|
256
|
+
---
|
|
241
257
|
|
|
242
|
-
|
|
243
|
-
// Server-side
|
|
244
|
-
const result = await myTool.run(input, { isServer: true });
|
|
258
|
+
## 4. Interactive views with onAction
|
|
245
259
|
|
|
246
|
-
|
|
247
|
-
const result = await myTool.run(input, { isServer: false });
|
|
260
|
+
Views can trigger tool re-execution. The result updates in-place and optionally syncs back to the AI:
|
|
248
261
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
```tsx
|
|
263
|
+
const counterTool = tool({
|
|
264
|
+
name: 'counter',
|
|
265
|
+
description: 'Manage a named counter',
|
|
266
|
+
input: z.object({
|
|
267
|
+
name: z.string(),
|
|
268
|
+
action: z.enum(['increment', 'decrement', 'reset', 'get']),
|
|
269
|
+
}),
|
|
270
|
+
output: z.object({ name: z.string(), value: z.number() }),
|
|
271
|
+
autoRespond: true, // auto-send updated state back to AI after user clicks
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// In-memory for demo only — use a real database in production (resets in serverless)
|
|
275
|
+
const counterStore: Record<string, number> = {};
|
|
254
276
|
|
|
255
|
-
|
|
256
|
-
|
|
277
|
+
counterTool.server(async ({ name, action }) => {
|
|
278
|
+
if (!(name in counterStore)) counterStore[name] = 0;
|
|
279
|
+
if (action === 'increment') counterStore[name]++;
|
|
280
|
+
if (action === 'decrement') counterStore[name]--;
|
|
281
|
+
return { name, value: counterStore[name] };
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
counterTool.view((data, state) => {
|
|
285
|
+
if (!data) return null;
|
|
286
|
+
return (
|
|
287
|
+
<div className="flex items-center gap-4 p-4 bg-zinc-800 rounded-xl">
|
|
288
|
+
<span>{data.name}: {data.value}</span>
|
|
289
|
+
<button onClick={() => state?.onAction?.({ name: data.name, action: 'increment' })}>+</button>
|
|
290
|
+
<button onClick={() => state?.onAction?.({ name: data.name, action: 'decrement' })}>-</button>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
});
|
|
257
294
|
```
|
|
258
295
|
|
|
296
|
+
`onAction` calls `/api/tools/execute` with the new input and updates the view. With `autoRespond: true`, the updated state is also sent to the AI as a hidden message so it stays in sync.
|
|
297
|
+
|
|
259
298
|
---
|
|
260
299
|
|
|
261
|
-
##
|
|
300
|
+
## 5. HITL (human-in-the-loop)
|
|
262
301
|
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
|
|
302
|
+
```tsx
|
|
303
|
+
const sendEmailTool = tool({
|
|
304
|
+
name: 'sendEmail',
|
|
305
|
+
description: 'Send an email',
|
|
306
|
+
input: z.object({
|
|
307
|
+
to: z.string().email(),
|
|
308
|
+
subject: z.string(),
|
|
309
|
+
body: z.string(),
|
|
310
|
+
}),
|
|
311
|
+
output: z.object({ sent: z.boolean(), messageId: z.string() }),
|
|
312
|
+
confirm: true, // always show Approve/Reject before executing
|
|
313
|
+
});
|
|
266
314
|
|
|
267
|
-
|
|
315
|
+
// Or conditional:
|
|
316
|
+
const deleteTool = tool({
|
|
317
|
+
name: 'delete',
|
|
318
|
+
input: z.object({ id: z.string(), permanent: z.boolean() }),
|
|
319
|
+
confirm: (input) => input.permanent === true, // only confirm permanent deletes
|
|
320
|
+
// ...
|
|
321
|
+
});
|
|
268
322
|
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
323
|
+
// Or via hints (destructive auto-implies confirmation):
|
|
324
|
+
const dropTool = tool({
|
|
325
|
+
name: 'dropTable',
|
|
326
|
+
hints: { destructive: true },
|
|
327
|
+
// ...
|
|
274
328
|
});
|
|
275
329
|
```
|
|
276
330
|
|
|
277
|
-
|
|
331
|
+
The chat UI automatically shows an Approve/Reject card. Approved tools hit `/api/tools/confirm`.
|
|
278
332
|
|
|
279
|
-
|
|
280
|
-
const { data, finalData, streaming, loading, error, execute, reset } = useToolStream(myTool);
|
|
281
|
-
```
|
|
333
|
+
---
|
|
282
334
|
|
|
283
|
-
|
|
335
|
+
## 6. Streaming
|
|
284
336
|
|
|
285
|
-
|
|
286
|
-
|
|
337
|
+
Use `.stream()` instead of (or alongside) `.server()` when you need partial updates before the final result:
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
const analysisTool = tool({
|
|
341
|
+
name: 'analyze',
|
|
342
|
+
input: z.object({ data: z.string() }),
|
|
343
|
+
output: z.object({ status: z.string(), result: z.string() }),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
analysisTool.stream(async (input, { stream }) => {
|
|
347
|
+
stream({ status: 'Parsing...' });
|
|
348
|
+
const parsed = JSON.parse(input.data); // your parsing logic
|
|
349
|
+
stream({ status: 'Analyzing...' });
|
|
350
|
+
const result = `Processed ${Object.keys(parsed).length} fields`; // your analysis
|
|
351
|
+
return { status: 'Done', result };
|
|
352
|
+
});
|
|
287
353
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
354
|
+
analysisTool.view((data, state) => {
|
|
355
|
+
if (state?.streaming) return <p>{data?.status}</p>;
|
|
356
|
+
return <p>{data?.result}</p>;
|
|
357
|
+
});
|
|
291
358
|
```
|
|
292
359
|
|
|
293
|
-
|
|
360
|
+
`state.streaming` is true while partials arrive.
|
|
294
361
|
|
|
295
|
-
|
|
362
|
+
---
|
|
296
363
|
|
|
297
|
-
|
|
298
|
-
import { Chat, ChatProvider, Thread, Composer, Message, ToolResult } from '@lantos1618/better-ui/components';
|
|
299
|
-
```
|
|
364
|
+
## 7. React hooks (outside chat)
|
|
300
365
|
|
|
301
|
-
|
|
366
|
+
Use tools directly in any React component:
|
|
302
367
|
|
|
303
368
|
```tsx
|
|
304
|
-
|
|
369
|
+
import { useTool } from '@lantos1618/better-ui/react';
|
|
370
|
+
|
|
371
|
+
function WeatherWidget() {
|
|
372
|
+
const { data, loading, error, execute } = useTool(weatherTool);
|
|
373
|
+
|
|
374
|
+
return (
|
|
375
|
+
<div>
|
|
376
|
+
<button onClick={() => execute({ city: 'Tokyo' })}>Get Weather</button>
|
|
377
|
+
{loading && <p>Loading...</p>}
|
|
378
|
+
{error && <p>Error: {error.message}</p>}
|
|
379
|
+
{data && <weatherTool.View data={data} />}
|
|
380
|
+
</div>
|
|
381
|
+
);
|
|
382
|
+
}
|
|
305
383
|
```
|
|
306
384
|
|
|
307
|
-
|
|
385
|
+
Calling `.view()` on a tool also creates `tool.View` — a memoized React component you can render standalone, outside of chat.
|
|
386
|
+
|
|
387
|
+
### useToolStream
|
|
308
388
|
|
|
309
389
|
```tsx
|
|
310
|
-
|
|
311
|
-
<div className="flex flex-col h-screen">
|
|
312
|
-
<Thread className="flex-1 overflow-y-auto" />
|
|
313
|
-
<Composer placeholder="Type a message..." />
|
|
314
|
-
</div>
|
|
315
|
-
</ChatProvider>
|
|
316
|
-
```
|
|
390
|
+
import { useToolStream } from '@lantos1618/better-ui/react';
|
|
317
391
|
|
|
318
|
-
|
|
392
|
+
const { data, finalData, streaming, execute } = useToolStream(analysisTool);
|
|
393
|
+
```
|
|
319
394
|
|
|
320
|
-
|
|
321
|
-
|-----------|-------------|
|
|
322
|
-
| `Chat` | All-in-one (ChatProvider + Thread + Composer) |
|
|
323
|
-
| `ChatProvider` | Context provider wrapping AI SDK's `useChat` |
|
|
324
|
-
| `Thread` | Message list with auto-scroll |
|
|
325
|
-
| `Message` | Single message with tool view rendering |
|
|
326
|
-
| `Composer` | Input form with send button |
|
|
327
|
-
| `ToolResult` | Renders a tool's `.view()` in chat context |
|
|
328
|
-
| `Panel` / `ChatPanel` | Sidebar panel for thread management |
|
|
329
|
-
| `Markdown` | Markdown renderer with syntax highlighting |
|
|
330
|
-
| `ThemeProvider` | Theme CSS variable provider |
|
|
395
|
+
### useTools (multiple)
|
|
331
396
|
|
|
332
|
-
|
|
397
|
+
```tsx
|
|
398
|
+
import { useTools } from '@lantos1618/better-ui/react';
|
|
333
399
|
|
|
334
|
-
|
|
400
|
+
function Dashboard() {
|
|
401
|
+
const t = useTools({ weather: weatherTool, search: searchTool });
|
|
335
402
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
403
|
+
return (
|
|
404
|
+
<div>
|
|
405
|
+
<button onClick={() => t.weather.execute({ city: 'London' })}>
|
|
406
|
+
{t.weather.loading ? 'Loading...' : t.weather.data?.temp ?? 'Get Weather'}
|
|
407
|
+
</button>
|
|
408
|
+
<button onClick={() => t.search.execute({ query: 'React' })}>
|
|
409
|
+
{t.search.loading ? 'Searching...' : 'Search'}
|
|
410
|
+
</button>
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
346
414
|
```
|
|
347
415
|
|
|
348
416
|
---
|
|
349
417
|
|
|
350
|
-
## MCP
|
|
418
|
+
## 8. MCP server
|
|
351
419
|
|
|
352
|
-
|
|
420
|
+
Expose the same tools to Claude Desktop, Cursor, VS Code:
|
|
353
421
|
|
|
354
|
-
```
|
|
422
|
+
```ts
|
|
423
|
+
// mcp-server.ts
|
|
355
424
|
import { createMCPServer } from '@lantos1618/better-ui/mcp';
|
|
425
|
+
import { weatherTool, searchTool } from './lib/tools';
|
|
356
426
|
|
|
357
427
|
const server = createMCPServer({
|
|
358
|
-
name: 'my-
|
|
428
|
+
name: 'my-tools',
|
|
359
429
|
version: '1.0.0',
|
|
360
|
-
tools: { weather, search
|
|
361
|
-
context: { env: process.env }, // passed to every tool execution
|
|
430
|
+
tools: { weather: weatherTool, search: searchTool },
|
|
362
431
|
});
|
|
432
|
+
|
|
433
|
+
server.start(); // stdio transport
|
|
363
434
|
```
|
|
364
435
|
|
|
365
|
-
|
|
436
|
+
```jsonc
|
|
437
|
+
// ~/.claude/claude_desktop_config.json
|
|
438
|
+
{
|
|
439
|
+
"mcpServers": {
|
|
440
|
+
"my-tools": { "command": "npx", "args": ["tsx", "mcp-server.ts"] }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
366
444
|
|
|
367
|
-
|
|
368
|
-
// stdio — for Claude Desktop, Cursor, VS Code extensions
|
|
369
|
-
server.start();
|
|
445
|
+
Or as an HTTP endpoint:
|
|
370
446
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
447
|
+
```ts
|
|
448
|
+
// app/api/mcp/route.ts
|
|
449
|
+
export const POST = server.httpHandler();
|
|
450
|
+
// or for SSE streaming:
|
|
451
|
+
export const POST = server.streamableHttpHandler();
|
|
374
452
|
```
|
|
375
453
|
|
|
376
|
-
|
|
454
|
+
---
|
|
377
455
|
|
|
378
|
-
|
|
379
|
-
// List tools with JSON schemas
|
|
380
|
-
const tools = server.listTools();
|
|
456
|
+
## 9. AG-UI server
|
|
381
457
|
|
|
382
|
-
|
|
383
|
-
const result = await server.callTool('weather', { city: 'Tokyo' });
|
|
384
|
-
// → { content: [{ type: 'text', text: '{"temp":21,"city":"Tokyo","condition":"sunny"}' }] }
|
|
458
|
+
Expose tools via AG-UI protocol (CopilotKit, LangChain, Google ADK):
|
|
385
459
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
460
|
+
```ts
|
|
461
|
+
// app/api/agui/route.ts
|
|
462
|
+
import { createAGUIServer } from '@lantos1618/better-ui/agui';
|
|
463
|
+
import { tools } from '@/lib/tools';
|
|
464
|
+
|
|
465
|
+
export const POST = createAGUIServer({
|
|
466
|
+
name: 'my-tools',
|
|
467
|
+
tools,
|
|
468
|
+
}).handler();
|
|
393
469
|
```
|
|
394
470
|
|
|
395
|
-
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## 10. Built-in view components
|
|
396
474
|
|
|
397
|
-
|
|
475
|
+
Pre-built views for common patterns. Use in your tool's `.view()`:
|
|
398
476
|
|
|
399
|
-
```
|
|
400
|
-
import {
|
|
477
|
+
```tsx
|
|
478
|
+
import {
|
|
479
|
+
QuestionView, // multiple choice / free-text
|
|
480
|
+
FormView, // dynamic forms from field definitions
|
|
481
|
+
DataTableView, // sortable paginated table
|
|
482
|
+
ProgressView, // step-by-step progress tracker
|
|
483
|
+
CodeBlockView, // syntax highlighted code with copy button
|
|
484
|
+
MediaDisplayView, // image/video grid or carousel
|
|
485
|
+
FileUploadView, // drag-and-drop file upload
|
|
486
|
+
} from '@lantos1618/better-ui/components';
|
|
487
|
+
|
|
488
|
+
// Example: question tool
|
|
489
|
+
questionTool.view((data, state) => (
|
|
490
|
+
<QuestionView
|
|
491
|
+
question={data.question}
|
|
492
|
+
options={data.options} // { label: string, value: string }[]
|
|
493
|
+
allowFreeText={true}
|
|
494
|
+
onSubmit={(answer) => state?.onAction?.({ ...data, answer })}
|
|
495
|
+
/>
|
|
496
|
+
));
|
|
497
|
+
|
|
498
|
+
// Example: form tool
|
|
499
|
+
formTool.view((data, state) => (
|
|
500
|
+
<FormView
|
|
501
|
+
title={data.title}
|
|
502
|
+
fields={data.fields} // { name, label, type, required, options? }[]
|
|
503
|
+
onSubmit={(values) => state?.onAction?.({ ...data, values })}
|
|
504
|
+
/>
|
|
505
|
+
));
|
|
401
506
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
507
|
+
// Example: data table
|
|
508
|
+
tableTool.view((data) => (
|
|
509
|
+
<DataTableView
|
|
510
|
+
columns={data.columns} // { key, label, sortable? }[]
|
|
511
|
+
rows={data.rows} // Record<string, unknown>[]
|
|
512
|
+
pageSize={10}
|
|
513
|
+
/>
|
|
514
|
+
));
|
|
408
515
|
```
|
|
409
516
|
|
|
410
517
|
---
|
|
411
518
|
|
|
412
|
-
##
|
|
519
|
+
## 11. Tool side effects
|
|
413
520
|
|
|
414
|
-
|
|
521
|
+
React to tool results outside the chat (e.g. open URLs, change themes):
|
|
415
522
|
|
|
416
|
-
```
|
|
417
|
-
import {
|
|
523
|
+
```tsx
|
|
524
|
+
import { useChatContext, useToolEffect } from '@lantos1618/better-ui/components';
|
|
418
525
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
tools: { weather, search },
|
|
422
|
-
});
|
|
526
|
+
function SideEffects() {
|
|
527
|
+
const { toolStateStore } = useChatContext();
|
|
423
528
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
529
|
+
useToolEffect(toolStateStore, 'navigate', (entry) => {
|
|
530
|
+
const data = entry.output as { url: string };
|
|
531
|
+
if (data?.url) window.open(data.url, '_blank');
|
|
532
|
+
});
|
|
427
533
|
|
|
428
|
-
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
```
|
|
429
537
|
|
|
430
538
|
---
|
|
431
539
|
|
|
432
|
-
##
|
|
540
|
+
## 12. Persistence
|
|
433
541
|
|
|
434
|
-
|
|
435
|
-
import { createProvider } from '@lantos1618/better-ui';
|
|
542
|
+
### In-memory (dev)
|
|
436
543
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
544
|
+
```tsx
|
|
545
|
+
import { createMemoryAdapter } from '@lantos1618/better-ui/persistence';
|
|
546
|
+
|
|
547
|
+
<ChatProvider
|
|
548
|
+
endpoint="/api/chat"
|
|
549
|
+
tools={tools}
|
|
550
|
+
persistence={createMemoryAdapter()}
|
|
551
|
+
>
|
|
441
552
|
```
|
|
442
553
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
554
|
+
### Database (production)
|
|
555
|
+
|
|
556
|
+
Implement `PersistenceAdapter`:
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
import type { PersistenceAdapter, Thread } from '@lantos1618/better-ui/persistence';
|
|
560
|
+
import type { UIMessage } from 'ai';
|
|
561
|
+
|
|
562
|
+
const persistence: PersistenceAdapter = {
|
|
563
|
+
listThreads(): Promise<Thread[]> { /* ... */ },
|
|
564
|
+
getThread(id: string): Promise<Thread | null> { /* ... */ },
|
|
565
|
+
createThread(title?: string): Promise<Thread> { /* ... */ },
|
|
566
|
+
deleteThread(id: string): Promise<void> { /* ... */ },
|
|
567
|
+
getMessages(threadId: string): Promise<UIMessage[]> { /* ... */ },
|
|
568
|
+
saveMessages(threadId: string, msgs: UIMessage[]): Promise<void> { /* ... */ },
|
|
569
|
+
};
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Messages auto-save when AI finishes responding. `useChatContext()` exposes `threads`, `createThread`, `switchThread`, `deleteThread`.
|
|
449
573
|
|
|
450
574
|
---
|
|
451
575
|
|
|
452
|
-
## Auth
|
|
576
|
+
## 13. Auth
|
|
453
577
|
|
|
454
|
-
```
|
|
578
|
+
```ts
|
|
455
579
|
import { jwtAuth, sessionAuth, betterAuth } from '@lantos1618/better-ui/auth';
|
|
456
580
|
|
|
457
|
-
// JWT Bearer tokens
|
|
458
|
-
const auth = jwtAuth({ secret: process.env.JWT_SECRET
|
|
581
|
+
// JWT Bearer tokens (uses jose)
|
|
582
|
+
const auth = jwtAuth({ secret: process.env.JWT_SECRET! });
|
|
459
583
|
|
|
460
|
-
// Cookie
|
|
461
|
-
const auth = sessionAuth({
|
|
584
|
+
// Cookie sessions
|
|
585
|
+
const auth = sessionAuth({
|
|
586
|
+
cookieName: 'session',
|
|
587
|
+
verify: async (token) => db.sessions.findUnique({ where: { token } }),
|
|
588
|
+
});
|
|
462
589
|
|
|
463
|
-
// BetterAuth
|
|
590
|
+
// BetterAuth
|
|
464
591
|
const auth = betterAuth(authInstance);
|
|
592
|
+
|
|
593
|
+
// Usage in a route:
|
|
594
|
+
const user = await auth(req.headers);
|
|
595
|
+
const result = await tool.run(input, { isServer: true, user });
|
|
465
596
|
```
|
|
466
597
|
|
|
467
598
|
---
|
|
468
599
|
|
|
469
|
-
##
|
|
600
|
+
## 14. Providers
|
|
470
601
|
|
|
471
|
-
```
|
|
472
|
-
import {
|
|
602
|
+
```ts
|
|
603
|
+
import { createProvider } from '@lantos1618/better-ui';
|
|
473
604
|
|
|
474
|
-
const
|
|
605
|
+
const p = createProvider({ provider: 'openai', model: 'gpt-4o' });
|
|
606
|
+
// or: 'anthropic' + 'claude-sonnet-4-5-20250929'
|
|
607
|
+
// or: 'google' + 'gemini-2.5-pro'
|
|
608
|
+
// or: 'openrouter' + 'anthropic/claude-sonnet-4-5-20250929' (needs apiKey)
|
|
475
609
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
await adapter.getMessages(threadId);
|
|
610
|
+
// Use in streamText:
|
|
611
|
+
streamText({ model: p.model(), tools: { ... } });
|
|
479
612
|
```
|
|
480
613
|
|
|
481
|
-
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## 15. Fluent builder (alternative syntax)
|
|
617
|
+
|
|
618
|
+
```tsx
|
|
619
|
+
const search = tool('search')
|
|
620
|
+
.description('Search the database')
|
|
621
|
+
.input(z.object({ query: z.string() }))
|
|
622
|
+
.output(z.object({ results: z.array(z.string()) }))
|
|
623
|
+
.cache({ ttl: 60_000 })
|
|
624
|
+
.hints({ readOnly: true })
|
|
625
|
+
.server(async ({ query }) => ({ results: await db.search(query) }))
|
|
626
|
+
.view((data) => <ul>{data.results.map((r, i) => <li key={i}>{r}</li>)}</ul>)
|
|
627
|
+
.build();
|
|
628
|
+
```
|
|
482
629
|
|
|
483
630
|
---
|
|
484
631
|
|
|
485
|
-
##
|
|
632
|
+
## 16. OpenAPI / Swagger
|
|
486
633
|
|
|
487
|
-
|
|
634
|
+
Auto-generate an OpenAPI 3.1 spec and callable REST endpoints from your tools:
|
|
488
635
|
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
636
|
+
```ts
|
|
637
|
+
import { toolRouter } from '@lantos1618/better-ui/openapi';
|
|
638
|
+
import { tools } from './tools';
|
|
639
|
+
|
|
640
|
+
// Next.js catch-all: app/api/tools/[...path]/route.ts
|
|
641
|
+
const router = toolRouter({ tools });
|
|
642
|
+
export const GET = router;
|
|
643
|
+
export const POST = router;
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
That gives you:
|
|
647
|
+
|
|
648
|
+
| Endpoint | What |
|
|
649
|
+
|---|---|
|
|
650
|
+
| `POST /api/tools/weather` | Execute tool, returns `{ result }` |
|
|
651
|
+
| `GET /api/tools` | OpenAPI 3.1 JSON spec |
|
|
652
|
+
| `GET /api/tools/docs` | Swagger UI |
|
|
653
|
+
|
|
654
|
+
With auth/rate-limiting:
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
const router = toolRouter({
|
|
658
|
+
tools,
|
|
659
|
+
onBeforeExecute: async (toolName, input, req) => {
|
|
660
|
+
const user = await auth(req.headers);
|
|
661
|
+
if (!user) throw new Error('Unauthorized');
|
|
662
|
+
},
|
|
497
663
|
});
|
|
498
664
|
```
|
|
499
665
|
|
|
500
|
-
|
|
666
|
+
Or just generate the spec without the router:
|
|
501
667
|
|
|
502
|
-
|
|
668
|
+
```ts
|
|
669
|
+
import { generateOpenAPISpec, openAPIHandler } from '@lantos1618/better-ui/openapi';
|
|
670
|
+
|
|
671
|
+
// Get the spec object
|
|
672
|
+
const spec = generateOpenAPISpec({ title: 'My API', version: '1.0.0', tools });
|
|
503
673
|
|
|
504
|
-
|
|
674
|
+
// Or serve it as a route
|
|
675
|
+
export const GET = openAPIHandler({ title: 'My API', version: '1.0.0', tools });
|
|
676
|
+
```
|
|
505
677
|
|
|
506
|
-
|
|
678
|
+
---
|
|
507
679
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
680
|
+
## Cheat sheet
|
|
681
|
+
|
|
682
|
+
| I want to... | Code |
|
|
683
|
+
|------------------------------|-------------------------------------------------|
|
|
684
|
+
| Define a tool | `tool({ name, input, output })` |
|
|
685
|
+
| Add server logic | `.server(async (input, ctx) => result)` |
|
|
686
|
+
| Add a view | `.view((data, state) => <JSX />)` |
|
|
687
|
+
| Drop into chat | `<Chat endpoint="..." tools={tools} />` |
|
|
688
|
+
| Composable chat | `<ChatProvider>` + `<Thread>` + `<Composer>` |
|
|
689
|
+
| Convert for AI SDK | `tool.toAITool()` |
|
|
690
|
+
| Run directly | `await tool.run(input, { isServer: true })` |
|
|
691
|
+
| Use as React hook | `useTool(tool)` / `useToolStream(tool)` |
|
|
692
|
+
| Render view standalone | `<tool.View data={data} />` |
|
|
693
|
+
| Require approval | `confirm: true` |
|
|
694
|
+
| Stream partial results | `.stream(async (input, { stream }) => ...)` |
|
|
695
|
+
| Sync UI actions back to AI | `autoRespond: true` |
|
|
696
|
+
| React to tool results | `useToolEffect(store, 'toolName', callback)` |
|
|
697
|
+
| Persist conversations | `persistence={adapter}` on `ChatProvider` |
|
|
698
|
+
| Expose via MCP | `createMCPServer({ tools }).start()` |
|
|
699
|
+
| Expose via AG-UI | `createAGUIServer({ tools }).handler()` |
|
|
700
|
+
| Add auth | `jwtAuth()` / `sessionAuth()` / `betterAuth()` |
|
|
701
|
+
| OpenAPI spec | `generateOpenAPISpec({ tools })` |
|
|
702
|
+
| Callable REST + Swagger UI | `toolRouter({ tools })` |
|
|
517
703
|
|
|
518
704
|
---
|
|
519
705
|
|
|
520
|
-
## Project
|
|
706
|
+
## Project structure
|
|
521
707
|
|
|
522
708
|
```
|
|
523
709
|
src/
|
|
524
710
|
tool.tsx Core tool() API — schema, handlers, view, streaming
|
|
525
711
|
index.ts Main exports (server-safe, no React)
|
|
526
|
-
react/
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
Composer.tsx Input form
|
|
535
|
-
ToolResult.tsx Tool view renderer
|
|
536
|
-
Panel.tsx Sidebar panel
|
|
537
|
-
Markdown.tsx Markdown renderer
|
|
538
|
-
Question.tsx Question view block
|
|
539
|
-
Form.tsx Form view block
|
|
540
|
-
DataTable.tsx Data table view block
|
|
541
|
-
Progress.tsx Progress view block
|
|
542
|
-
MediaDisplay.tsx Media view block
|
|
543
|
-
CodeBlock.tsx Code block view block
|
|
544
|
-
FileUpload.tsx File upload view block
|
|
545
|
-
Toast.tsx Toast notifications
|
|
546
|
-
ThemeProvider.tsx Theme CSS variables
|
|
547
|
-
providers/
|
|
548
|
-
openai.ts OpenAI adapter
|
|
549
|
-
anthropic.ts Anthropic adapter
|
|
550
|
-
google.ts Google Gemini adapter
|
|
551
|
-
openrouter.ts OpenRouter adapter
|
|
552
|
-
auth/
|
|
553
|
-
jwt.ts JWT auth helper
|
|
554
|
-
session.ts Session cookie auth
|
|
555
|
-
better-auth.ts BetterAuth integration
|
|
556
|
-
persistence/
|
|
557
|
-
types.ts PersistenceAdapter interface
|
|
558
|
-
memory.ts In-memory adapter
|
|
559
|
-
mcp/
|
|
560
|
-
server.ts MCP server (stdio + HTTP + SSE)
|
|
561
|
-
schema.ts Zod → JSON Schema converter
|
|
562
|
-
agui/
|
|
563
|
-
server.ts AG-UI protocol server (SSE)
|
|
712
|
+
react/ useTool, useTools, useToolStream hooks
|
|
713
|
+
components/ Chat, Thread, Composer, ToolResult, Panel, Markdown, Form, etc.
|
|
714
|
+
providers/ OpenAI, Anthropic, Google, OpenRouter adapters
|
|
715
|
+
auth/ JWT, session cookie, BetterAuth helpers
|
|
716
|
+
persistence/ PersistenceAdapter interface + in-memory adapter
|
|
717
|
+
mcp/ MCP server (stdio + HTTP + SSE)
|
|
718
|
+
agui/ AG-UI protocol server (SSE)
|
|
719
|
+
openapi/ OpenAPI spec generator + tool router
|
|
564
720
|
examples/
|
|
565
|
-
nextjs-demo/
|
|
566
|
-
vite-demo/
|
|
567
|
-
mcp-server/
|
|
721
|
+
nextjs-demo/ Full Next.js chat app
|
|
722
|
+
vite-demo/ Vite + React demo
|
|
723
|
+
mcp-server/ Standalone MCP server
|
|
568
724
|
```
|
|
569
725
|
|
|
570
726
|
## Development
|
|
@@ -572,27 +728,10 @@ examples/
|
|
|
572
728
|
```bash
|
|
573
729
|
npm install
|
|
574
730
|
npm run build # Build library
|
|
575
|
-
npm test # Run
|
|
731
|
+
npm test # Run 250 tests across 12 suites
|
|
576
732
|
npm run type-check # TypeScript check
|
|
577
733
|
```
|
|
578
734
|
|
|
579
|
-
## Deploy the Demo
|
|
580
|
-
|
|
581
|
-
The `examples/nextjs-demo/` is a full-featured chat app ready to deploy:
|
|
582
|
-
|
|
583
|
-
```bash
|
|
584
|
-
cd examples/nextjs-demo
|
|
585
|
-
npm install
|
|
586
|
-
# Set OPENAI_API_KEY in .env.local
|
|
587
|
-
npm run dev
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
To deploy on Vercel, set the **Root Directory** to `examples/nextjs-demo` in your project settings.
|
|
591
|
-
|
|
592
|
-
## Contributing
|
|
593
|
-
|
|
594
|
-
See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines.
|
|
595
|
-
|
|
596
735
|
## License
|
|
597
736
|
|
|
598
737
|
MIT
|