@tanstack/ai-solid 0.0.1
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/LICENSE +21 -0
- package/README.md +437 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +80 -0
- package/dist/use-chat.d.ts +8 -0
- package/dist/use-chat.js +85 -0
- package/dist/use-chat.js.map +1 -0
- package/package.json +61 -0
- package/src/index.ts +18 -0
- package/src/types.ts +102 -0
- package/src/use-chat.ts +133 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Tanner Linsley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# @tanstack/ai-react
|
|
2
|
+
|
|
3
|
+
React hooks for building AI chat interfaces with TanStack AI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tanstack/ai-react @tanstack/ai-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## useChat Hook
|
|
12
|
+
|
|
13
|
+
The `useChat` hook manages chat state, handles streaming responses, and provides a complete chat interface in a single hook.
|
|
14
|
+
|
|
15
|
+
**Design Philosophy (v5 API):**
|
|
16
|
+
|
|
17
|
+
- You control input state
|
|
18
|
+
- Just call `sendMessage()` when ready
|
|
19
|
+
- No form-centric API - use buttons, keyboard events, or any trigger
|
|
20
|
+
- More flexible and less opinionated
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
|
|
26
|
+
import { useState } from "react";
|
|
27
|
+
|
|
28
|
+
function ChatComponent() {
|
|
29
|
+
const { messages, sendMessage, isLoading } = useChat({
|
|
30
|
+
connection: fetchServerSentEvents("/api/chat"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const [input, setInput] = useState("");
|
|
34
|
+
|
|
35
|
+
const handleSend = () => {
|
|
36
|
+
sendMessage(input);
|
|
37
|
+
setInput("");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
{messages.map((m) => (
|
|
43
|
+
<div key={m.id}>
|
|
44
|
+
<strong>{m.role}:</strong> {m.content}
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
|
|
48
|
+
<input
|
|
49
|
+
value={input}
|
|
50
|
+
onChange={(e) => setInput(e.target.value)}
|
|
51
|
+
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
|
52
|
+
disabled={isLoading}
|
|
53
|
+
/>
|
|
54
|
+
<button onClick={handleSend} disabled={isLoading || !input.trim()}>
|
|
55
|
+
Send
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### API
|
|
63
|
+
|
|
64
|
+
#### Options
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
interface UseChatOptions {
|
|
68
|
+
// Connection adapter (required)
|
|
69
|
+
connection: ConnectionAdapter
|
|
70
|
+
|
|
71
|
+
// Configuration
|
|
72
|
+
initialMessages?: UIMessage[] // Starting messages
|
|
73
|
+
id?: string // Unique chat ID
|
|
74
|
+
body?: Record<string, any> // Extra data to send
|
|
75
|
+
|
|
76
|
+
// Callbacks
|
|
77
|
+
onResponse?: (response?: Response) => void
|
|
78
|
+
onChunk?: (chunk: StreamChunk) => void
|
|
79
|
+
onFinish?: (message: UIMessage) => void
|
|
80
|
+
onError?: (error: Error) => void
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Return Value
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface UseChatReturn {
|
|
88
|
+
messages: UIMessage[] // Current conversation
|
|
89
|
+
sendMessage: (content: string) => Promise<void> // Send a message
|
|
90
|
+
append: (message) => Promise<void> // Add message programmatically
|
|
91
|
+
reload: () => Promise<void> // Reload last response
|
|
92
|
+
stop: () => void // Stop current generation
|
|
93
|
+
isLoading: boolean // Is generating a response
|
|
94
|
+
error: Error | undefined // Current error
|
|
95
|
+
setMessages: (messages) => void // Set messages manually
|
|
96
|
+
clear: () => void // Clear all messages
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Connection Adapters
|
|
101
|
+
|
|
102
|
+
Connection adapters provide flexible streaming for different scenarios. See the complete guides:
|
|
103
|
+
|
|
104
|
+
- 📖 [Connection Adapters Guide](../../docs/CONNECTION_ADAPTERS_GUIDE.md) - Complete guide with examples
|
|
105
|
+
- 📖 [Connection Adapters API](../ai-client/CONNECTION_ADAPTERS.md) - API reference
|
|
106
|
+
|
|
107
|
+
### Quick Examples
|
|
108
|
+
|
|
109
|
+
**SSE (Most Common):**
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
113
|
+
|
|
114
|
+
const chat = useChat({
|
|
115
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Server Functions:**
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { useChat, stream } from '@tanstack/ai-react'
|
|
123
|
+
|
|
124
|
+
const chat = useChat({
|
|
125
|
+
connection: stream((messages) => serverChatFunction({ messages })),
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Custom (e.g., WebSockets):**
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { useChat } from '@tanstack/ai-react'
|
|
133
|
+
import type { ConnectionAdapter } from '@tanstack/ai-client'
|
|
134
|
+
|
|
135
|
+
const wsAdapter: ConnectionAdapter = {
|
|
136
|
+
async *connect(messages) {
|
|
137
|
+
// Your WebSocket logic
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const chat = useChat({ connection: wsAdapter })
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Backend Endpoint
|
|
145
|
+
|
|
146
|
+
Your backend should use the `chat()` method which **automatically handles tool execution in a loop**:
|
|
147
|
+
|
|
148
|
+
1. Receive POST requests with this body:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
{
|
|
152
|
+
messages: Message[];
|
|
153
|
+
data?: Record<string, any>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
2. Use `chat()` to stream responses (with automatic tool execution):
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { chat, toStreamResponse } from '@tanstack/ai'
|
|
161
|
+
import { openai } from '@tanstack/ai-openai'
|
|
162
|
+
|
|
163
|
+
export async function POST(request: Request) {
|
|
164
|
+
const { messages } = await request.json()
|
|
165
|
+
|
|
166
|
+
const stream = chat({
|
|
167
|
+
adapter: openai(),
|
|
168
|
+
model: 'gpt-4o',
|
|
169
|
+
messages,
|
|
170
|
+
tools: [weatherTool], // Optional: auto-executed in loop
|
|
171
|
+
agentLoopStrategy: maxIterations(5), // Optional: control loop
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Convert to HTTP streaming response with SSE headers
|
|
175
|
+
return toStreamResponse(stream)
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The response streams `StreamChunk` objects as Server-Sent Events:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
data: {"type":"content","delta":"Hello","content":"Hello",...}
|
|
183
|
+
data: {"type":"tool_call","toolCall":{...},...}
|
|
184
|
+
data: {"type":"tool_result","toolCallId":"...","content":"...",...}
|
|
185
|
+
data: {"type":"content","delta":" world","content":"Hello world",...}
|
|
186
|
+
data: {"type":"done","finishReason":"stop","usage":{...}}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Note:** The `chat()` method automatically executes tools and emits `tool_result` chunks - you don't need to handle tool execution manually!
|
|
190
|
+
|
|
191
|
+
### Advanced Usage
|
|
192
|
+
|
|
193
|
+
#### With Callbacks
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
197
|
+
|
|
198
|
+
const { messages, sendMessage } = useChat({
|
|
199
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
200
|
+
onChunk: (chunk) => {
|
|
201
|
+
if (chunk.type === 'content') {
|
|
202
|
+
console.log('New token:', chunk.delta)
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
onFinish: (message) => {
|
|
206
|
+
console.log('Final message:', message)
|
|
207
|
+
// Save to database, log analytics, etc.
|
|
208
|
+
},
|
|
209
|
+
onError: (error) => {
|
|
210
|
+
console.error('Chat error:', error)
|
|
211
|
+
// Show toast notification, log error, etc.
|
|
212
|
+
},
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
// Send messages programmatically
|
|
216
|
+
await sendMessage('Tell me a joke')
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### Flexible Triggering
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
|
|
223
|
+
|
|
224
|
+
const { sendMessage, isLoading } = useChat({
|
|
225
|
+
connection: fetchServerSentEvents("/api/chat")
|
|
226
|
+
});
|
|
227
|
+
const [input, setInput] = useState("");
|
|
228
|
+
|
|
229
|
+
// Button click
|
|
230
|
+
<button onClick={() => sendMessage(input)}>Send</button>
|
|
231
|
+
|
|
232
|
+
// Enter key
|
|
233
|
+
<input onKeyDown={(e) => e.key === "Enter" && sendMessage(input)} />
|
|
234
|
+
|
|
235
|
+
// Voice input
|
|
236
|
+
<button onClick={async () => {
|
|
237
|
+
const transcript = await voiceToText();
|
|
238
|
+
sendMessage(transcript);
|
|
239
|
+
}}>🎤 Speak</button>
|
|
240
|
+
|
|
241
|
+
// Predefined prompts
|
|
242
|
+
<button onClick={() => sendMessage("Explain quantum computing")}>
|
|
243
|
+
Ask about quantum computing
|
|
244
|
+
</button>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### With Custom Headers
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
251
|
+
|
|
252
|
+
const chat = useChat({
|
|
253
|
+
connection: fetchServerSentEvents('/api/chat', {
|
|
254
|
+
headers: {
|
|
255
|
+
Authorization: `Bearer ${token}`,
|
|
256
|
+
'X-Custom-Header': 'value',
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
body: {
|
|
260
|
+
userId: '123',
|
|
261
|
+
sessionId: 'abc',
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Programmatic Control
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const { messages, sendMessage, append, reload, stop, clear } = useChat()
|
|
270
|
+
|
|
271
|
+
// Send a simple message
|
|
272
|
+
await sendMessage('Hello!')
|
|
273
|
+
|
|
274
|
+
// Add a message with more control
|
|
275
|
+
await append({
|
|
276
|
+
role: 'user',
|
|
277
|
+
content: 'Hello!',
|
|
278
|
+
id: 'custom-id',
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Reload the last AI response
|
|
282
|
+
await reload()
|
|
283
|
+
|
|
284
|
+
// Stop the current generation
|
|
285
|
+
stop()
|
|
286
|
+
|
|
287
|
+
// Clear all messages
|
|
288
|
+
clear()
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Multiple Chats
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
|
|
295
|
+
|
|
296
|
+
function App() {
|
|
297
|
+
const chat1 = useChat({
|
|
298
|
+
id: 'chat-1',
|
|
299
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
300
|
+
})
|
|
301
|
+
const chat2 = useChat({
|
|
302
|
+
id: 'chat-2',
|
|
303
|
+
connection: fetchServerSentEvents('/api/chat'),
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Each hook manages independent state
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Example Backend (Node.js/Express)
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import express from 'express'
|
|
314
|
+
import { AI, toStreamResponse } from '@tanstack/ai'
|
|
315
|
+
import { OpenAIAdapter } from '@tanstack/ai-openai'
|
|
316
|
+
|
|
317
|
+
const app = express()
|
|
318
|
+
app.use(express.json())
|
|
319
|
+
|
|
320
|
+
const ai = new AI(new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY }))
|
|
321
|
+
|
|
322
|
+
app.post('/api/chat', async (req, res) => {
|
|
323
|
+
const { messages } = req.body
|
|
324
|
+
|
|
325
|
+
// One line to create streaming response!
|
|
326
|
+
const stream = ai.streamChat({
|
|
327
|
+
model: 'gpt-3.5-turbo',
|
|
328
|
+
messages,
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
const response = toStreamResponse(stream)
|
|
332
|
+
|
|
333
|
+
// Copy headers and stream to Express response
|
|
334
|
+
response.headers.forEach((value, key) => {
|
|
335
|
+
res.setHeader(key, value)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const reader = response.body?.getReader()
|
|
339
|
+
if (reader) {
|
|
340
|
+
while (true) {
|
|
341
|
+
const { done, value } = await reader.read()
|
|
342
|
+
if (done) break
|
|
343
|
+
res.write(value)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
res.end()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
app.listen(3000)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Example Backend (Next.js App Router)
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// app/api/chat/route.ts
|
|
356
|
+
import { AI, toStreamResponse } from '@tanstack/ai'
|
|
357
|
+
import { OpenAIAdapter } from '@tanstack/ai-openai'
|
|
358
|
+
|
|
359
|
+
export const runtime = 'edge'
|
|
360
|
+
|
|
361
|
+
const ai = new AI(new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY }))
|
|
362
|
+
|
|
363
|
+
export async function POST(req: Request) {
|
|
364
|
+
const { messages } = await req.json()
|
|
365
|
+
|
|
366
|
+
// One line!
|
|
367
|
+
return toStreamResponse(
|
|
368
|
+
ai.streamChat({
|
|
369
|
+
model: 'gpt-3.5-turbo',
|
|
370
|
+
messages,
|
|
371
|
+
}),
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Example Backend (TanStack Start)
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
380
|
+
import { AI, toStreamResponse } from '@tanstack/ai'
|
|
381
|
+
import { AnthropicAdapter } from '@tanstack/ai-anthropic'
|
|
382
|
+
|
|
383
|
+
const ai = new AI(
|
|
384
|
+
new AnthropicAdapter({ apiKey: process.env.ANTHROPIC_API_KEY }),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
export const Route = createFileRoute('/api/chat')({
|
|
388
|
+
server: {
|
|
389
|
+
handlers: {
|
|
390
|
+
POST: async ({ request }) => {
|
|
391
|
+
const { messages } = await request.json()
|
|
392
|
+
|
|
393
|
+
// One line with automatic tool execution!
|
|
394
|
+
return toStreamResponse(
|
|
395
|
+
ai.streamChat({
|
|
396
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
397
|
+
messages,
|
|
398
|
+
tools, // Tools with execute functions
|
|
399
|
+
}),
|
|
400
|
+
)
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## TypeScript Types
|
|
408
|
+
|
|
409
|
+
All types are fully exported:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import type {
|
|
413
|
+
UIMessage,
|
|
414
|
+
UseChatOptions,
|
|
415
|
+
UseChatReturn,
|
|
416
|
+
ChatRequestBody,
|
|
417
|
+
} from '@tanstack/ai-react'
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Features
|
|
421
|
+
|
|
422
|
+
- ✅ Automatic message state management
|
|
423
|
+
- ✅ Streaming response handling
|
|
424
|
+
- ✅ Loading and error states
|
|
425
|
+
- ✅ Simple `sendMessage()` API (v5 style)
|
|
426
|
+
- ✅ You control input state (flexible)
|
|
427
|
+
- ✅ Abort/stop generation
|
|
428
|
+
- ✅ Reload last response
|
|
429
|
+
- ✅ Clear conversation
|
|
430
|
+
- ✅ Custom headers and body data (via connection adapter options)
|
|
431
|
+
- ✅ Callback hooks for lifecycle events
|
|
432
|
+
- ✅ Multiple concurrent chats
|
|
433
|
+
- ✅ Full TypeScript support
|
|
434
|
+
|
|
435
|
+
## License
|
|
436
|
+
|
|
437
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ChatRequestBody, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
|
|
2
|
+
import { useChat } from "./use-chat.js";
|
|
3
|
+
import { ConnectionAdapter, FetchConnectionOptions, InferChatMessages, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
|
|
4
|
+
export { type ChatRequestBody, type ConnectionAdapter, type FetchConnectionOptions, type InferChatMessages, type UIMessage, type UseChatOptions, type UseChatReturn, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat };
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Accessor } from "solid-js";
|
|
2
|
+
import { ChatClientOptions, ChatRequestBody, UIMessage } from "@tanstack/ai-client";
|
|
3
|
+
import { AnyClientTool, ModelMessage } from "@tanstack/ai";
|
|
4
|
+
|
|
5
|
+
//#region src/types.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for the useChat hook.
|
|
9
|
+
*
|
|
10
|
+
* This extends ChatClientOptions but omits the state change callbacks that are
|
|
11
|
+
* managed internally by Solid signals:
|
|
12
|
+
* - `onMessagesChange` - Managed by Solid signal (exposed as `messages`)
|
|
13
|
+
* - `onLoadingChange` - Managed by Solid signal (exposed as `isLoading`)
|
|
14
|
+
* - `onErrorChange` - Managed by Solid signal (exposed as `error`)
|
|
15
|
+
*
|
|
16
|
+
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
17
|
+
* passed through to the underlying ChatClient and can be used for side effects.
|
|
18
|
+
*
|
|
19
|
+
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
20
|
+
* To update these options, remount the component or use a key prop.
|
|
21
|
+
*/
|
|
22
|
+
type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'>;
|
|
23
|
+
interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
24
|
+
/**
|
|
25
|
+
* Current messages in the conversation
|
|
26
|
+
*/
|
|
27
|
+
messages: Accessor<Array<UIMessage<TTools>>>;
|
|
28
|
+
/**
|
|
29
|
+
* Send a message and get a response
|
|
30
|
+
*/
|
|
31
|
+
sendMessage: (content: string) => Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Append a message to the conversation
|
|
34
|
+
*/
|
|
35
|
+
append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Add the result of a client-side tool execution
|
|
38
|
+
*/
|
|
39
|
+
addToolResult: (result: {
|
|
40
|
+
toolCallId: string;
|
|
41
|
+
tool: string;
|
|
42
|
+
output: any;
|
|
43
|
+
state?: 'output-available' | 'output-error';
|
|
44
|
+
errorText?: string;
|
|
45
|
+
}) => Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Respond to a tool approval request
|
|
48
|
+
*/
|
|
49
|
+
addToolApprovalResponse: (response: {
|
|
50
|
+
id: string;
|
|
51
|
+
approved: boolean;
|
|
52
|
+
}) => Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Reload the last assistant message
|
|
55
|
+
*/
|
|
56
|
+
reload: () => Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Stop the current response generation
|
|
59
|
+
*/
|
|
60
|
+
stop: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Whether a response is currently being generated
|
|
63
|
+
*/
|
|
64
|
+
isLoading: Accessor<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Current error, if any
|
|
67
|
+
*/
|
|
68
|
+
error: Accessor<Error | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Set messages manually
|
|
71
|
+
*/
|
|
72
|
+
setMessages: (messages: Array<UIMessage<TTools>>) => void;
|
|
73
|
+
/**
|
|
74
|
+
* Clear all messages
|
|
75
|
+
*/
|
|
76
|
+
clear: () => void;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { type ChatRequestBody, type UIMessage, UseChatOptions, UseChatReturn };
|
|
80
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { UseChatOptions, UseChatReturn } from "./types.js";
|
|
2
|
+
import { AnyClientTool } from "@tanstack/ai";
|
|
3
|
+
|
|
4
|
+
//#region src/use-chat.d.ts
|
|
5
|
+
declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(options?: UseChatOptions<TTools>): UseChatReturn<TTools>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { useChat };
|
|
8
|
+
//# sourceMappingURL=use-chat.d.ts.map
|
package/dist/use-chat.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createEffect, createMemo, createSignal, createUniqueId } from "solid-js";
|
|
2
|
+
import { ChatClient } from "@tanstack/ai-client";
|
|
3
|
+
|
|
4
|
+
//#region src/use-chat.ts
|
|
5
|
+
function useChat(options = {}) {
|
|
6
|
+
const hookId = createUniqueId();
|
|
7
|
+
const clientId = options.id || hookId;
|
|
8
|
+
const [messages, setMessages] = createSignal(options.initialMessages || []);
|
|
9
|
+
const [isLoading, setIsLoading] = createSignal(false);
|
|
10
|
+
const [error, setError] = createSignal(void 0);
|
|
11
|
+
const client = createMemo(() => {
|
|
12
|
+
return new ChatClient({
|
|
13
|
+
connection: options.connection,
|
|
14
|
+
id: clientId,
|
|
15
|
+
initialMessages: options.initialMessages,
|
|
16
|
+
body: options.body,
|
|
17
|
+
onResponse: options.onResponse,
|
|
18
|
+
onChunk: options.onChunk,
|
|
19
|
+
onFinish: options.onFinish,
|
|
20
|
+
onError: options.onError,
|
|
21
|
+
tools: options.tools,
|
|
22
|
+
streamProcessor: options.streamProcessor,
|
|
23
|
+
onMessagesChange: (newMessages) => {
|
|
24
|
+
setMessages(newMessages);
|
|
25
|
+
},
|
|
26
|
+
onLoadingChange: (newIsLoading) => {
|
|
27
|
+
setIsLoading(newIsLoading);
|
|
28
|
+
},
|
|
29
|
+
onErrorChange: (newError) => {
|
|
30
|
+
setError(newError);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}, [clientId]);
|
|
34
|
+
createEffect(() => {
|
|
35
|
+
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
36
|
+
if (messages().length === 0) client().setMessagesManually(options.initialMessages);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
createEffect(() => {
|
|
40
|
+
return () => {
|
|
41
|
+
if (isLoading()) client().stop();
|
|
42
|
+
};
|
|
43
|
+
}, [client, isLoading]);
|
|
44
|
+
const sendMessage = async (content) => {
|
|
45
|
+
await client().sendMessage(content);
|
|
46
|
+
};
|
|
47
|
+
const append = async (message) => {
|
|
48
|
+
await client().append(message);
|
|
49
|
+
};
|
|
50
|
+
const reload = async () => {
|
|
51
|
+
await client().reload();
|
|
52
|
+
};
|
|
53
|
+
const stop = () => {
|
|
54
|
+
client().stop();
|
|
55
|
+
};
|
|
56
|
+
const clear = () => {
|
|
57
|
+
client().clear();
|
|
58
|
+
};
|
|
59
|
+
const setMessagesManually = (newMessages) => {
|
|
60
|
+
client().setMessagesManually(newMessages);
|
|
61
|
+
};
|
|
62
|
+
const addToolResult = async (result) => {
|
|
63
|
+
await client().addToolResult(result);
|
|
64
|
+
};
|
|
65
|
+
const addToolApprovalResponse = async (response) => {
|
|
66
|
+
await client().addToolApprovalResponse(response);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
messages,
|
|
70
|
+
sendMessage,
|
|
71
|
+
append,
|
|
72
|
+
reload,
|
|
73
|
+
stop,
|
|
74
|
+
isLoading,
|
|
75
|
+
error,
|
|
76
|
+
setMessages: setMessagesManually,
|
|
77
|
+
clear,
|
|
78
|
+
addToolResult,
|
|
79
|
+
addToolApprovalResponse
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
//#endregion
|
|
84
|
+
export { useChat };
|
|
85
|
+
//# sourceMappingURL=use-chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n} from 'solid-js'\nimport { ChatClient } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\nimport type { UIMessage, UseChatOptions, UseChatReturn } from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\n const hookId = createUniqueId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n\n // Create ChatClient instance with callbacks to sync state\n // Note: Options are captured at client creation time.\n // The connection adapter can use functions for dynamic values (url, headers, etc.)\n // which are evaluated lazily on each request.\n const client = createMemo(() => {\n return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n onResponse: options.onResponse,\n onChunk: options.onChunk,\n onFinish: options.onFinish,\n onError: options.onError,\n tools: options.tools,\n streamProcessor: options.streamProcessor,\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n setIsLoading(newIsLoading)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n })\n // Only recreate when clientId changes\n // Connection and other options are captured at creation time\n }, [clientId])\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Cleanup on unmount: stop any in-flight requests\n createEffect(() => {\n return () => {\n // Stop any active generation when component unmounts\n if (isLoading()) {\n client().stop()\n }\n }\n }, [client, isLoading])\n\n // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)\n // are captured at client creation time. Changes to these callbacks require\n // remounting the component or changing the connection to recreate the client.\n\n const sendMessage = async (content: string) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n }\n\n return {\n messages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n setMessages: setMessagesManually,\n clear,\n addToolResult,\n addToolApprovalResponse,\n }\n}\n"],"mappings":";;;;AAUA,SAAgB,QACd,UAAkC,EAAE,EACb;CACvB,MAAM,SAAS,gBAAgB;CAC/B,MAAM,WAAW,QAAQ,MAAM;CAE/B,MAAM,CAAC,UAAU,eAAe,aAC9B,QAAQ,mBAAmB,EAAE,CAC9B;CACD,MAAM,CAAC,WAAW,gBAAgB,aAAa,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,aAAgC,OAAU;CAMpE,MAAM,SAAS,iBAAiB;AAC9B,SAAO,IAAI,WAAW;GACpB,YAAY,QAAQ;GACpB,IAAI;GACJ,iBAAiB,QAAQ;GACzB,MAAM,QAAQ;GACd,YAAY,QAAQ;GACpB,SAAS,QAAQ;GACjB,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,OAAO,QAAQ;GACf,iBAAiB,QAAQ;GACzB,mBAAmB,gBAA0C;AAC3D,gBAAY,YAAY;;GAE1B,kBAAkB,iBAA0B;AAC1C,iBAAa,aAAa;;GAE5B,gBAAgB,aAAgC;AAC9C,aAAS,SAAS;;GAErB,CAAC;IAGD,CAAC,SAAS,CAAC;AAKd,oBAAmB;AACjB,MAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAE9D;OAAI,UAAU,CAAC,WAAW,EACxB,SAAQ,CAAC,oBAAoB,QAAQ,gBAAgB;;GAGzD;AAGF,oBAAmB;AACjB,eAAa;AAEX,OAAI,WAAW,CACb,SAAQ,CAAC,MAAM;;IAGlB,CAAC,QAAQ,UAAU,CAAC;CAMvB,MAAM,cAAc,OAAO,YAAoB;AAC7C,QAAM,QAAQ,CAAC,YAAY,QAAQ;;CAGrC,MAAM,SAAS,OAAO,YAA8C;AAClE,QAAM,QAAQ,CAAC,OAAO,QAAQ;;CAGhC,MAAM,SAAS,YAAY;AACzB,QAAM,QAAQ,CAAC,QAAQ;;CAGzB,MAAM,aAAa;AACjB,UAAQ,CAAC,MAAM;;CAGjB,MAAM,cAAc;AAClB,UAAQ,CAAC,OAAO;;CAGlB,MAAM,uBAAuB,gBAA0C;AACrE,UAAQ,CAAC,oBAAoB,YAAY;;CAG3C,MAAM,gBAAgB,OAAO,WAMvB;AACJ,QAAM,QAAQ,CAAC,cAAc,OAAO;;CAGtC,MAAM,0BAA0B,OAAO,aAGjC;AACJ,QAAM,QAAQ,CAAC,wBAAwB,SAAS;;AAGlD,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACD"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-solid",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Solid hooks for TanStack AI",
|
|
5
|
+
"author": "",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TanStack/ai.git",
|
|
10
|
+
"directory": "packages/typescript/ai-solid"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ai",
|
|
27
|
+
"solidjs",
|
|
28
|
+
"hooks",
|
|
29
|
+
"tanstack",
|
|
30
|
+
"chat",
|
|
31
|
+
"streaming"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"zod": "^4.1.13",
|
|
35
|
+
"@tanstack/ai": "0.0.1",
|
|
36
|
+
"@tanstack/ai-client": "0.0.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@solidjs/testing-library": "^0.8.10",
|
|
40
|
+
"@types/node": "^24.10.1",
|
|
41
|
+
"@vitest/coverage-v8": "4.0.14",
|
|
42
|
+
"jsdom": "^27.2.0",
|
|
43
|
+
"solid-js": "^1.9.10",
|
|
44
|
+
"tsdown": "^0.17.0-beta.6",
|
|
45
|
+
"typescript": "5.9.3",
|
|
46
|
+
"vitest": "^4.0.14"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"solid-js": ">=1.9.10",
|
|
50
|
+
"@tanstack/ai": "0.0.1",
|
|
51
|
+
"@tanstack/ai-client": "0.0.1"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"clean": "premove ./build ./dist",
|
|
55
|
+
"test:eslint": "eslint ./src",
|
|
56
|
+
"test:lib": "vitest run",
|
|
57
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
58
|
+
"test:types": "tsc",
|
|
59
|
+
"build": "tsdown"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { useChat } from './use-chat'
|
|
2
|
+
export type {
|
|
3
|
+
UseChatOptions,
|
|
4
|
+
UseChatReturn,
|
|
5
|
+
UIMessage,
|
|
6
|
+
ChatRequestBody,
|
|
7
|
+
} from './types'
|
|
8
|
+
|
|
9
|
+
// Re-export from ai-client for convenience
|
|
10
|
+
export {
|
|
11
|
+
fetchServerSentEvents,
|
|
12
|
+
fetchHttpStream,
|
|
13
|
+
stream,
|
|
14
|
+
createChatClientOptions,
|
|
15
|
+
type ConnectionAdapter,
|
|
16
|
+
type FetchConnectionOptions,
|
|
17
|
+
type InferChatMessages,
|
|
18
|
+
} from '@tanstack/ai-client'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Accessor } from 'solid-js'
|
|
2
|
+
import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
|
|
3
|
+
import type {
|
|
4
|
+
ChatClientOptions,
|
|
5
|
+
ChatRequestBody,
|
|
6
|
+
UIMessage,
|
|
7
|
+
} from '@tanstack/ai-client'
|
|
8
|
+
|
|
9
|
+
// Re-export types from ai-client
|
|
10
|
+
export type { UIMessage, ChatRequestBody }
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for the useChat hook.
|
|
14
|
+
*
|
|
15
|
+
* This extends ChatClientOptions but omits the state change callbacks that are
|
|
16
|
+
* managed internally by Solid signals:
|
|
17
|
+
* - `onMessagesChange` - Managed by Solid signal (exposed as `messages`)
|
|
18
|
+
* - `onLoadingChange` - Managed by Solid signal (exposed as `isLoading`)
|
|
19
|
+
* - `onErrorChange` - Managed by Solid signal (exposed as `error`)
|
|
20
|
+
*
|
|
21
|
+
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
22
|
+
* passed through to the underlying ChatClient and can be used for side effects.
|
|
23
|
+
*
|
|
24
|
+
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
25
|
+
* To update these options, remount the component or use a key prop.
|
|
26
|
+
*/
|
|
27
|
+
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
|
|
28
|
+
Omit<
|
|
29
|
+
ChatClientOptions<TTools>,
|
|
30
|
+
'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'
|
|
31
|
+
>
|
|
32
|
+
|
|
33
|
+
export interface UseChatReturn<
|
|
34
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
35
|
+
> {
|
|
36
|
+
/**
|
|
37
|
+
* Current messages in the conversation
|
|
38
|
+
*/
|
|
39
|
+
messages: Accessor<Array<UIMessage<TTools>>>
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Send a message and get a response
|
|
43
|
+
*/
|
|
44
|
+
sendMessage: (content: string) => Promise<void>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Append a message to the conversation
|
|
48
|
+
*/
|
|
49
|
+
append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add the result of a client-side tool execution
|
|
53
|
+
*/
|
|
54
|
+
addToolResult: (result: {
|
|
55
|
+
toolCallId: string
|
|
56
|
+
tool: string
|
|
57
|
+
output: any
|
|
58
|
+
state?: 'output-available' | 'output-error'
|
|
59
|
+
errorText?: string
|
|
60
|
+
}) => Promise<void>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Respond to a tool approval request
|
|
64
|
+
*/
|
|
65
|
+
addToolApprovalResponse: (response: {
|
|
66
|
+
id: string // approval.id, not toolCallId
|
|
67
|
+
approved: boolean
|
|
68
|
+
}) => Promise<void>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reload the last assistant message
|
|
72
|
+
*/
|
|
73
|
+
reload: () => Promise<void>
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop the current response generation
|
|
77
|
+
*/
|
|
78
|
+
stop: () => void
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether a response is currently being generated
|
|
82
|
+
*/
|
|
83
|
+
isLoading: Accessor<boolean>
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Current error, if any
|
|
87
|
+
*/
|
|
88
|
+
error: Accessor<Error | undefined>
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Set messages manually
|
|
92
|
+
*/
|
|
93
|
+
setMessages: (messages: Array<UIMessage<TTools>>) => void
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear all messages
|
|
97
|
+
*/
|
|
98
|
+
clear: () => void
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client
|
|
102
|
+
// and re-exported from there for convenience
|
package/src/use-chat.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
createMemo,
|
|
4
|
+
createSignal,
|
|
5
|
+
createUniqueId,
|
|
6
|
+
} from 'solid-js'
|
|
7
|
+
import { ChatClient } from '@tanstack/ai-client'
|
|
8
|
+
import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
|
|
9
|
+
import type { UIMessage, UseChatOptions, UseChatReturn } from './types'
|
|
10
|
+
|
|
11
|
+
export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
12
|
+
options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,
|
|
13
|
+
): UseChatReturn<TTools> {
|
|
14
|
+
const hookId = createUniqueId()
|
|
15
|
+
const clientId = options.id || hookId
|
|
16
|
+
|
|
17
|
+
const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(
|
|
18
|
+
options.initialMessages || [],
|
|
19
|
+
)
|
|
20
|
+
const [isLoading, setIsLoading] = createSignal(false)
|
|
21
|
+
const [error, setError] = createSignal<Error | undefined>(undefined)
|
|
22
|
+
|
|
23
|
+
// Create ChatClient instance with callbacks to sync state
|
|
24
|
+
// Note: Options are captured at client creation time.
|
|
25
|
+
// The connection adapter can use functions for dynamic values (url, headers, etc.)
|
|
26
|
+
// which are evaluated lazily on each request.
|
|
27
|
+
const client = createMemo(() => {
|
|
28
|
+
return new ChatClient({
|
|
29
|
+
connection: options.connection,
|
|
30
|
+
id: clientId,
|
|
31
|
+
initialMessages: options.initialMessages,
|
|
32
|
+
body: options.body,
|
|
33
|
+
onResponse: options.onResponse,
|
|
34
|
+
onChunk: options.onChunk,
|
|
35
|
+
onFinish: options.onFinish,
|
|
36
|
+
onError: options.onError,
|
|
37
|
+
tools: options.tools,
|
|
38
|
+
streamProcessor: options.streamProcessor,
|
|
39
|
+
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
40
|
+
setMessages(newMessages)
|
|
41
|
+
},
|
|
42
|
+
onLoadingChange: (newIsLoading: boolean) => {
|
|
43
|
+
setIsLoading(newIsLoading)
|
|
44
|
+
},
|
|
45
|
+
onErrorChange: (newError: Error | undefined) => {
|
|
46
|
+
setError(newError)
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
// Only recreate when clientId changes
|
|
50
|
+
// Connection and other options are captured at creation time
|
|
51
|
+
}, [clientId])
|
|
52
|
+
|
|
53
|
+
// Sync initial messages on mount only
|
|
54
|
+
// Note: initialMessages are passed to ChatClient constructor, but we also
|
|
55
|
+
// set them here to ensure React state is in sync
|
|
56
|
+
createEffect(() => {
|
|
57
|
+
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
58
|
+
// Only set if current messages are empty (initial state)
|
|
59
|
+
if (messages().length === 0) {
|
|
60
|
+
client().setMessagesManually(options.initialMessages)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}) // Only run on mount - initialMessages are handled by ChatClient constructor
|
|
64
|
+
|
|
65
|
+
// Cleanup on unmount: stop any in-flight requests
|
|
66
|
+
createEffect(() => {
|
|
67
|
+
return () => {
|
|
68
|
+
// Stop any active generation when component unmounts
|
|
69
|
+
if (isLoading()) {
|
|
70
|
+
client().stop()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}, [client, isLoading])
|
|
74
|
+
|
|
75
|
+
// Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)
|
|
76
|
+
// are captured at client creation time. Changes to these callbacks require
|
|
77
|
+
// remounting the component or changing the connection to recreate the client.
|
|
78
|
+
|
|
79
|
+
const sendMessage = async (content: string) => {
|
|
80
|
+
await client().sendMessage(content)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const append = async (message: ModelMessage | UIMessage<TTools>) => {
|
|
84
|
+
await client().append(message)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const reload = async () => {
|
|
88
|
+
await client().reload()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const stop = () => {
|
|
92
|
+
client().stop()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const clear = () => {
|
|
96
|
+
client().clear()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {
|
|
100
|
+
client().setMessagesManually(newMessages)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const addToolResult = async (result: {
|
|
104
|
+
toolCallId: string
|
|
105
|
+
tool: string
|
|
106
|
+
output: any
|
|
107
|
+
state?: 'output-available' | 'output-error'
|
|
108
|
+
errorText?: string
|
|
109
|
+
}) => {
|
|
110
|
+
await client().addToolResult(result)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const addToolApprovalResponse = async (response: {
|
|
114
|
+
id: string
|
|
115
|
+
approved: boolean
|
|
116
|
+
}) => {
|
|
117
|
+
await client().addToolApprovalResponse(response)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
messages,
|
|
122
|
+
sendMessage,
|
|
123
|
+
append,
|
|
124
|
+
reload,
|
|
125
|
+
stop,
|
|
126
|
+
isLoading,
|
|
127
|
+
error,
|
|
128
|
+
setMessages: setMessagesManually,
|
|
129
|
+
clear,
|
|
130
|
+
addToolResult,
|
|
131
|
+
addToolApprovalResponse,
|
|
132
|
+
}
|
|
133
|
+
}
|