@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 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
@@ -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
@@ -0,0 +1,4 @@
1
+ import { useChat } from "./use-chat.js";
2
+ import { createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
3
+
4
+ export { createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat };
@@ -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
@@ -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
@@ -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
+ }