@tanstack/ai-react 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 +131 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types.d.ts +74 -0
- package/dist/esm/use-chat.d.ts +3 -0
- package/dist/esm/use-chat.js +115 -0
- package/dist/esm/use-chat.js.map +1 -0
- package/package.json +61 -0
- package/src/index.ts +18 -0
- package/src/types.ts +101 -0
- package/src/use-chat.ts +162 -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,131 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="./media/header_ai.png" >
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<br />
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
<a href="https://npmjs.com/package/@tanstack/ai" target="\_parent">
|
|
9
|
+
<img alt="" src="https://img.shields.io/npm/dm/@tanstack/ai.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/TanStack/ai" target="\_parent">
|
|
12
|
+
<img alt="" src="https://img.shields.io/github/stars/TanStack/ai.svg?style=social&label=Star" alt="GitHub stars" />
|
|
13
|
+
</a>
|
|
14
|
+
<a href="https://bundlephobia.com/result?p=@tanstack/ai@latest" target="\_parent">
|
|
15
|
+
<img alt="" src="https://badgen.net/bundlephobia/minzip/@tanstack/ai@latest" />
|
|
16
|
+
</a>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div align="center">
|
|
20
|
+
<a href="#badge">
|
|
21
|
+
<img alt="semantic-release" src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg">
|
|
22
|
+
</a>
|
|
23
|
+
<a href="#badge">
|
|
24
|
+
<img src="https://img.shields.io/github/v/release/tanstack/ai" alt="Release"/>
|
|
25
|
+
</a>
|
|
26
|
+
<a href="https://twitter.com/tan_stack">
|
|
27
|
+
<img src="https://img.shields.io/twitter/follow/tan_stack.svg?style=social" alt="Follow @TanStack"/>
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
# TanStack AI
|
|
37
|
+
|
|
38
|
+
A powerful, type-safe AI SDK for building AI-powered applications.
|
|
39
|
+
|
|
40
|
+
- Provider-agnostic adapters (OpenAI, Anthropic, Gemini, Ollama, etc.)
|
|
41
|
+
- **Multimodal content support** - Send images, audio, video, and documents
|
|
42
|
+
- Chat completion, streaming, and agent loop strategies
|
|
43
|
+
- Headless chat state management with adapters (SSE, HTTP stream, custom)
|
|
44
|
+
- Isomorphic type-safe tools with server/client execution
|
|
45
|
+
- **Enhanced integration with TanStack Start** - Share implementations between AI tools and server functions
|
|
46
|
+
|
|
47
|
+
### <a href="https://tanstack.com/ai">Read the docs →</b></a>
|
|
48
|
+
|
|
49
|
+
## Bonus: TanStack Start Integration
|
|
50
|
+
|
|
51
|
+
TanStack AI works with **any** framework (Next.js, Express, Remix, etc.).
|
|
52
|
+
|
|
53
|
+
**With TanStack Start**, you get a bonus: share implementations between AI tools and server functions with `createServerFnTool`:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { createServerFnTool } from '@tanstack/ai-react'
|
|
57
|
+
|
|
58
|
+
// Define once, get AI tool AND server function (TanStack Start only)
|
|
59
|
+
const getProducts = createServerFnTool({
|
|
60
|
+
name: 'getProducts',
|
|
61
|
+
inputSchema: z.object({ query: z.string() }),
|
|
62
|
+
execute: async ({ query }) => db.products.search(query),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Use in AI chat
|
|
66
|
+
chat({ tools: [getProducts.server] })
|
|
67
|
+
|
|
68
|
+
// Call directly from components (no API endpoint needed!)
|
|
69
|
+
const products = await getProducts.serverFn({ query: 'laptop' })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No duplicate logic, full type safety, automatic validation. The `serverFn` feature requires TanStack Start. See [docs](https://tanstack.com/ai) for details.
|
|
73
|
+
|
|
74
|
+
## Get Involved
|
|
75
|
+
|
|
76
|
+
- We welcome issues and pull requests!
|
|
77
|
+
- Participate in [GitHub discussions](https://github.com/TanStack/ai/discussions)
|
|
78
|
+
- Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ)
|
|
79
|
+
- See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions
|
|
80
|
+
|
|
81
|
+
## Partners
|
|
82
|
+
|
|
83
|
+
<table align="center">
|
|
84
|
+
<tr>
|
|
85
|
+
<td>
|
|
86
|
+
<a href="https://www.coderabbit.ai/?via=tanstack&dub_id=aCcEEdAOqqutX6OS" >
|
|
87
|
+
<picture>
|
|
88
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/coderabbit-dark-CMcuvjEy.svg" height="40" />
|
|
89
|
+
<source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" />
|
|
90
|
+
<img src="https://tanstack.com/assets/coderabbit-light-DVMJ2jHi.svg" height="40" alt="CodeRabbit" />
|
|
91
|
+
</picture>
|
|
92
|
+
</a>
|
|
93
|
+
</td>
|
|
94
|
+
<td>
|
|
95
|
+
<a href="https://www.cloudflare.com?utm_source=tanstack">
|
|
96
|
+
<picture>
|
|
97
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://tanstack.com/assets/cloudflare-white-DQDB7UaL.svg" height="60" />
|
|
98
|
+
<source media="(prefers-color-scheme: light)" srcset="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" />
|
|
99
|
+
<img src="https://tanstack.com/assets/cloudflare-black-CPufaW0B.svg" height="60" alt="Cloudflare" />
|
|
100
|
+
</picture>
|
|
101
|
+
</a>
|
|
102
|
+
</td>
|
|
103
|
+
</tr>
|
|
104
|
+
</table>
|
|
105
|
+
|
|
106
|
+
<div align="center">
|
|
107
|
+
<img src="./media/partner_logo.svg" alt="AI & you?" height="65">
|
|
108
|
+
<p>
|
|
109
|
+
We're looking for TanStack AI Partners to join our mission! Partner with us to push the boundaries of TanStack AI and build amazing things together.
|
|
110
|
+
</p>
|
|
111
|
+
<a href="mailto:partners@tanstack.com?subject=TanStack AI Partnership"><b>LET'S CHAT</b></a>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
## Explore the TanStack Ecosystem
|
|
115
|
+
|
|
116
|
+
- <a href="https://github.com/tanstack/config"><b>TanStack Config</b></a> – Tooling for JS/TS packages
|
|
117
|
+
- <a href="https://github.com/tanstack/db"><b>TanStack DB</b></a> – Reactive sync client store
|
|
118
|
+
- <a href="https://github.com/tanstack/devtools">TanStack Devtools</a> – Unified devtools panel
|
|
119
|
+
- <a href="https://github.com/tanstack/form"><b>TanStack Form</b></a> – Type‑safe form state
|
|
120
|
+
- <a href="https://github.com/tanstack/pacer"><b>TanStack Pacer</b></a> – Debouncing, throttling, batching
|
|
121
|
+
- <a href="https://github.com/tanstack/query"><b>TanStack Query</b></a> – Async state & caching
|
|
122
|
+
- <a href="https://github.com/tanstack/ranger"><b>TanStack Ranger</b></a> – Range & slider primitives
|
|
123
|
+
- <a href="https://github.com/tanstack/router"><b>TanStack Router</b></a> – Type‑safe routing, caching & URL state
|
|
124
|
+
- <a href="https://github.com/tanstack/router"><b>TanStack Start</b></a> – Full‑stack SSR & streaming
|
|
125
|
+
- <a href="https://github.com/tanstack/store"><b>TanStack Store</b></a> – Reactive data store
|
|
126
|
+
- <a href="https://github.com/tanstack/table"><b>TanStack Table</b></a> – Headless datagrids
|
|
127
|
+
- <a href="https://github.com/tanstack/virtual"><b>TanStack Virtual</b></a> – Virtualized rendering
|
|
128
|
+
|
|
129
|
+
… and more at <a href="https://tanstack.com"><b>TanStack.com »</b></a>
|
|
130
|
+
|
|
131
|
+
<!-- USE THE FORCE LUKE -->
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { useChat } from './use-chat.js';
|
|
2
|
+
export type { UseChatOptions, UseChatReturn, UIMessage, ChatRequestBody, } from './types.js';
|
|
3
|
+
export { fetchServerSentEvents, fetchHttpStream, stream, createChatClientOptions, type ConnectionAdapter, type FetchConnectionOptions, type InferChatMessages, } from '@tanstack/ai-client';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useChat } from "./use-chat.js";
|
|
2
|
+
import { createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
|
|
3
|
+
export {
|
|
4
|
+
createChatClientOptions,
|
|
5
|
+
fetchHttpStream,
|
|
6
|
+
fetchServerSentEvents,
|
|
7
|
+
stream,
|
|
8
|
+
useChat
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AnyClientTool, ModelMessage } from '@tanstack/ai';
|
|
2
|
+
import { ChatClientOptions, ChatRequestBody, UIMessage } from '@tanstack/ai-client';
|
|
3
|
+
export type { UIMessage, ChatRequestBody };
|
|
4
|
+
/**
|
|
5
|
+
* Options for the useChat hook.
|
|
6
|
+
*
|
|
7
|
+
* This extends ChatClientOptions but omits the state change callbacks that are
|
|
8
|
+
* managed internally by React state:
|
|
9
|
+
* - `onMessagesChange` - Managed by React state (exposed as `messages`)
|
|
10
|
+
* - `onLoadingChange` - Managed by React state (exposed as `isLoading`)
|
|
11
|
+
* - `onErrorChange` - Managed by React state (exposed as `error`)
|
|
12
|
+
*
|
|
13
|
+
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
14
|
+
* passed through to the underlying ChatClient and can be used for side effects.
|
|
15
|
+
*
|
|
16
|
+
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
17
|
+
* To update these options, remount the component or use a key prop.
|
|
18
|
+
*/
|
|
19
|
+
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'>;
|
|
20
|
+
export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
21
|
+
/**
|
|
22
|
+
* Current messages in the conversation
|
|
23
|
+
*/
|
|
24
|
+
messages: Array<UIMessage<TTools>>;
|
|
25
|
+
/**
|
|
26
|
+
* Send a message and get a response
|
|
27
|
+
*/
|
|
28
|
+
sendMessage: (content: string) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Append a message to the conversation
|
|
31
|
+
*/
|
|
32
|
+
append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Add the result of a client-side tool execution
|
|
35
|
+
*/
|
|
36
|
+
addToolResult: (result: {
|
|
37
|
+
toolCallId: string;
|
|
38
|
+
tool: string;
|
|
39
|
+
output: any;
|
|
40
|
+
state?: 'output-available' | 'output-error';
|
|
41
|
+
errorText?: string;
|
|
42
|
+
}) => Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Respond to a tool approval request
|
|
45
|
+
*/
|
|
46
|
+
addToolApprovalResponse: (response: {
|
|
47
|
+
id: string;
|
|
48
|
+
approved: boolean;
|
|
49
|
+
}) => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Reload the last assistant message
|
|
52
|
+
*/
|
|
53
|
+
reload: () => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Stop the current response generation
|
|
56
|
+
*/
|
|
57
|
+
stop: () => void;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a response is currently being generated
|
|
60
|
+
*/
|
|
61
|
+
isLoading: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Current error, if any
|
|
64
|
+
*/
|
|
65
|
+
error: Error | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Set messages manually
|
|
68
|
+
*/
|
|
69
|
+
setMessages: (messages: Array<UIMessage<TTools>>) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Clear all messages
|
|
72
|
+
*/
|
|
73
|
+
clear: () => void;
|
|
74
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useId, useState, useRef, useEffect, useMemo, useCallback } from "react";
|
|
2
|
+
import { ChatClient } from "@tanstack/ai-client";
|
|
3
|
+
function useChat(options) {
|
|
4
|
+
const hookId = useId();
|
|
5
|
+
const clientId = options.id || hookId;
|
|
6
|
+
const [messages, setMessages] = useState(
|
|
7
|
+
options.initialMessages || []
|
|
8
|
+
);
|
|
9
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
10
|
+
const [error, setError] = useState(void 0);
|
|
11
|
+
const messagesRef = useRef(
|
|
12
|
+
options.initialMessages || []
|
|
13
|
+
);
|
|
14
|
+
const isFirstMountRef = useRef(true);
|
|
15
|
+
const optionsRef = useRef(options);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
messagesRef.current = messages;
|
|
18
|
+
}, [messages]);
|
|
19
|
+
optionsRef.current = options;
|
|
20
|
+
const client = useMemo(() => {
|
|
21
|
+
const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
|
|
22
|
+
isFirstMountRef.current = false;
|
|
23
|
+
return new ChatClient({
|
|
24
|
+
connection: optionsRef.current.connection,
|
|
25
|
+
id: clientId,
|
|
26
|
+
initialMessages: messagesToUse,
|
|
27
|
+
body: optionsRef.current.body,
|
|
28
|
+
onResponse: optionsRef.current.onResponse,
|
|
29
|
+
onChunk: optionsRef.current.onChunk,
|
|
30
|
+
onFinish: optionsRef.current.onFinish,
|
|
31
|
+
onError: optionsRef.current.onError,
|
|
32
|
+
tools: optionsRef.current.tools,
|
|
33
|
+
streamProcessor: options.streamProcessor,
|
|
34
|
+
onMessagesChange: (newMessages) => {
|
|
35
|
+
setMessages(newMessages);
|
|
36
|
+
},
|
|
37
|
+
onLoadingChange: (newIsLoading) => {
|
|
38
|
+
setIsLoading(newIsLoading);
|
|
39
|
+
},
|
|
40
|
+
onErrorChange: (newError) => {
|
|
41
|
+
setError(newError);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}, [clientId]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
47
|
+
if (messages.length === 0) {
|
|
48
|
+
client.setMessagesManually(options.initialMessages);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
return () => {
|
|
54
|
+
if (isLoading) {
|
|
55
|
+
client.stop();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}, [client, isLoading]);
|
|
59
|
+
const sendMessage = useCallback(
|
|
60
|
+
async (content) => {
|
|
61
|
+
await client.sendMessage(content);
|
|
62
|
+
},
|
|
63
|
+
[client]
|
|
64
|
+
);
|
|
65
|
+
const append = useCallback(
|
|
66
|
+
async (message) => {
|
|
67
|
+
await client.append(message);
|
|
68
|
+
},
|
|
69
|
+
[client]
|
|
70
|
+
);
|
|
71
|
+
const reload = useCallback(async () => {
|
|
72
|
+
await client.reload();
|
|
73
|
+
}, [client]);
|
|
74
|
+
const stop = useCallback(() => {
|
|
75
|
+
client.stop();
|
|
76
|
+
}, [client]);
|
|
77
|
+
const clear = useCallback(() => {
|
|
78
|
+
client.clear();
|
|
79
|
+
}, [client]);
|
|
80
|
+
const setMessagesManually = useCallback(
|
|
81
|
+
(newMessages) => {
|
|
82
|
+
client.setMessagesManually(newMessages);
|
|
83
|
+
},
|
|
84
|
+
[client]
|
|
85
|
+
);
|
|
86
|
+
const addToolResult = useCallback(
|
|
87
|
+
async (result) => {
|
|
88
|
+
await client.addToolResult(result);
|
|
89
|
+
},
|
|
90
|
+
[client]
|
|
91
|
+
);
|
|
92
|
+
const addToolApprovalResponse = useCallback(
|
|
93
|
+
async (response) => {
|
|
94
|
+
await client.addToolApprovalResponse(response);
|
|
95
|
+
},
|
|
96
|
+
[client]
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
messages,
|
|
100
|
+
sendMessage,
|
|
101
|
+
append,
|
|
102
|
+
reload,
|
|
103
|
+
stop,
|
|
104
|
+
isLoading,
|
|
105
|
+
error,
|
|
106
|
+
setMessages: setMessagesManually,
|
|
107
|
+
clear,
|
|
108
|
+
addToolResult,
|
|
109
|
+
addToolApprovalResponse
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
useChat
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=use-chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'\nimport { ChatClient } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\n\nimport type { UIMessage, UseChatOptions, UseChatReturn } from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\n const hookId = useId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | undefined>(undefined)\n\n // Track current messages in a ref to preserve them when client is recreated\n const messagesRef = useRef<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const isFirstMountRef = useRef(true)\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools>>(options)\n\n // Update ref whenever messages change\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n optionsRef.current = options\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 = useMemo(() => {\n // On first mount, use initialMessages. On subsequent recreations, preserve existing messages.\n const messagesToUse = isFirstMountRef.current\n ? options.initialMessages || []\n : messagesRef.current\n\n isFirstMountRef.current = false\n\n return new ChatClient({\n connection: optionsRef.current.connection,\n id: clientId,\n initialMessages: messagesToUse,\n body: optionsRef.current.body,\n onResponse: optionsRef.current.onResponse,\n onChunk: optionsRef.current.onChunk,\n onFinish: optionsRef.current.onFinish,\n onError: optionsRef.current.onError,\n tools: optionsRef.current.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 }, [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 useEffect(() => {\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 useEffect(() => {\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 = useCallback(\n async (content: string) => {\n await client.sendMessage(content)\n },\n [client],\n )\n\n const append = useCallback(\n async (message: ModelMessage | UIMessage) => {\n await client.append(message)\n },\n [client],\n )\n\n const reload = useCallback(async () => {\n await client.reload()\n }, [client])\n\n const stop = useCallback(() => {\n client.stop()\n }, [client])\n\n const clear = useCallback(() => {\n client.clear()\n }, [client])\n\n const setMessagesManually = useCallback(\n (newMessages: Array<UIMessage<TTools>>) => {\n client.setMessagesManually(newMessages)\n },\n [client],\n )\n\n const addToolResult = useCallback(\n 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 [client],\n )\n\n const addToolApprovalResponse = useCallback(\n async (response: { id: string; approved: boolean }) => {\n await client.addToolApprovalResponse(response)\n },\n [client],\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"],"names":[],"mappings":";;AAMO,SAAS,QACd,SACuB;AACvB,QAAM,SAAS,MAAA;AACf,QAAM,WAAW,QAAQ,MAAM;AAE/B,QAAM,CAAC,UAAU,WAAW,IAAI;AAAA,IAC9B,QAAQ,mBAAmB,CAAA;AAAA,EAAC;AAE9B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAA4B,MAAS;AAG/D,QAAM,cAAc;AAAA,IAClB,QAAQ,mBAAmB,CAAA;AAAA,EAAC;AAE9B,QAAM,kBAAkB,OAAO,IAAI;AAGnC,QAAM,aAAa,OAA+B,OAAO;AAGzD,YAAU,MAAM;AACd,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,QAAQ,CAAC;AAEb,aAAW,UAAU;AAMrB,QAAM,SAAS,QAAQ,MAAM;AAE3B,UAAM,gBAAgB,gBAAgB,UAClC,QAAQ,mBAAmB,CAAA,IAC3B,YAAY;AAEhB,oBAAgB,UAAU;AAE1B,WAAO,IAAI,WAAW;AAAA,MACpB,YAAY,WAAW,QAAQ;AAAA,MAC/B,IAAI;AAAA,MACJ,iBAAiB;AAAA,MACjB,MAAM,WAAW,QAAQ;AAAA,MACzB,YAAY,WAAW,QAAQ;AAAA,MAC/B,SAAS,WAAW,QAAQ;AAAA,MAC5B,UAAU,WAAW,QAAQ;AAAA,MAC7B,SAAS,WAAW,QAAQ;AAAA,MAC5B,OAAO,WAAW,QAAQ;AAAA,MAC1B,iBAAiB,QAAQ;AAAA,MACzB,kBAAkB,CAAC,gBAA0C;AAC3D,oBAAY,WAAW;AAAA,MACzB;AAAA,MACA,iBAAiB,CAAC,iBAA0B;AAC1C,qBAAa,YAAY;AAAA,MAC3B;AAAA,MACA,eAAe,CAAC,aAAgC;AAC9C,iBAAS,QAAQ;AAAA,MACnB;AAAA,IAAA,CACD;AAAA,EACH,GAAG,CAAC,QAAQ,CAAC;AAKb,YAAU,MAAM;AACd,QAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAAG;AAEjE,UAAI,SAAS,WAAW,GAAG;AACzB,eAAO,oBAAoB,QAAQ,eAAe;AAAA,MACpD;AAAA,IACF;AAAA,EACF,GAAG,CAAA,CAAE;AAGL,YAAU,MAAM;AACd,WAAO,MAAM;AAEX,UAAI,WAAW;AACb,eAAO,KAAA;AAAA,MACT;AAAA,IACF;AAAA,EACF,GAAG,CAAC,QAAQ,SAAS,CAAC;AAMtB,QAAM,cAAc;AAAA,IAClB,OAAO,YAAoB;AACzB,YAAM,OAAO,YAAY,OAAO;AAAA,IAClC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,SAAS;AAAA,IACb,OAAO,YAAsC;AAC3C,YAAM,OAAO,OAAO,OAAO;AAAA,IAC7B;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,OAAO,OAAA;AAAA,EACf,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,OAAO,YAAY,MAAM;AAC7B,WAAO,KAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,QAAQ,YAAY,MAAM;AAC9B,WAAO,MAAA;AAAA,EACT,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,sBAAsB;AAAA,IAC1B,CAAC,gBAA0C;AACzC,aAAO,oBAAoB,WAAW;AAAA,IACxC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,gBAAgB;AAAA,IACpB,OAAO,WAMD;AACJ,YAAM,OAAO,cAAc,MAAM;AAAA,IACnC;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,QAAM,0BAA0B;AAAA,IAC9B,OAAO,aAAgD;AACrD,YAAM,OAAO,wBAAwB,QAAQ;AAAA,IAC/C;AAAA,IACA,CAAC,MAAM;AAAA,EAAA;AAGT,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tanstack/ai-react",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "React 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-react"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"module": "./dist/esm/index.js",
|
|
14
|
+
"types": "./dist/esm/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"import": "./dist/esm/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"ai",
|
|
27
|
+
"react",
|
|
28
|
+
"hooks",
|
|
29
|
+
"tanstack",
|
|
30
|
+
"chat",
|
|
31
|
+
"streaming"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@tanstack/ai": "0.0.1",
|
|
35
|
+
"@tanstack/ai-client": "0.0.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@testing-library/react": "^16.3.0",
|
|
39
|
+
"@types/react": "^19.2.7",
|
|
40
|
+
"@vitest/coverage-v8": "4.0.14",
|
|
41
|
+
"jsdom": "^27.2.0",
|
|
42
|
+
"vite": "^7.2.4",
|
|
43
|
+
"zod": "^4.1.13"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@types/react": ">=18.0.0",
|
|
47
|
+
"react": ">=18.0.0",
|
|
48
|
+
"@tanstack/ai": "0.0.1",
|
|
49
|
+
"@tanstack/ai-client": "0.0.1"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"clean": "premove ./build ./dist",
|
|
53
|
+
"lint:fix": "eslint ./src --fix",
|
|
54
|
+
"test:eslint": "eslint ./src",
|
|
55
|
+
"test:lib": "vitest run",
|
|
56
|
+
"test:lib:dev": "pnpm test:lib --watch",
|
|
57
|
+
"test:types": "tsc",
|
|
58
|
+
"test:build": "publint --strict",
|
|
59
|
+
"build": "vite build"
|
|
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,101 @@
|
|
|
1
|
+
import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
|
|
2
|
+
import type {
|
|
3
|
+
ChatClientOptions,
|
|
4
|
+
ChatRequestBody,
|
|
5
|
+
UIMessage,
|
|
6
|
+
} from '@tanstack/ai-client'
|
|
7
|
+
|
|
8
|
+
// Re-export types from ai-client
|
|
9
|
+
export type { UIMessage, ChatRequestBody }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for the useChat hook.
|
|
13
|
+
*
|
|
14
|
+
* This extends ChatClientOptions but omits the state change callbacks that are
|
|
15
|
+
* managed internally by React state:
|
|
16
|
+
* - `onMessagesChange` - Managed by React state (exposed as `messages`)
|
|
17
|
+
* - `onLoadingChange` - Managed by React state (exposed as `isLoading`)
|
|
18
|
+
* - `onErrorChange` - Managed by React state (exposed as `error`)
|
|
19
|
+
*
|
|
20
|
+
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
21
|
+
* passed through to the underlying ChatClient and can be used for side effects.
|
|
22
|
+
*
|
|
23
|
+
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
24
|
+
* To update these options, remount the component or use a key prop.
|
|
25
|
+
*/
|
|
26
|
+
export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
|
|
27
|
+
Omit<
|
|
28
|
+
ChatClientOptions<TTools>,
|
|
29
|
+
'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
export interface UseChatReturn<
|
|
33
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
34
|
+
> {
|
|
35
|
+
/**
|
|
36
|
+
* Current messages in the conversation
|
|
37
|
+
*/
|
|
38
|
+
messages: Array<UIMessage<TTools>>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Send a message and get a response
|
|
42
|
+
*/
|
|
43
|
+
sendMessage: (content: string) => Promise<void>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Append a message to the conversation
|
|
47
|
+
*/
|
|
48
|
+
append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add the result of a client-side tool execution
|
|
52
|
+
*/
|
|
53
|
+
addToolResult: (result: {
|
|
54
|
+
toolCallId: string
|
|
55
|
+
tool: string
|
|
56
|
+
output: any
|
|
57
|
+
state?: 'output-available' | 'output-error'
|
|
58
|
+
errorText?: string
|
|
59
|
+
}) => Promise<void>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Respond to a tool approval request
|
|
63
|
+
*/
|
|
64
|
+
addToolApprovalResponse: (response: {
|
|
65
|
+
id: string // approval.id, not toolCallId
|
|
66
|
+
approved: boolean
|
|
67
|
+
}) => Promise<void>
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reload the last assistant message
|
|
71
|
+
*/
|
|
72
|
+
reload: () => Promise<void>
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop the current response generation
|
|
76
|
+
*/
|
|
77
|
+
stop: () => void
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether a response is currently being generated
|
|
81
|
+
*/
|
|
82
|
+
isLoading: boolean
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Current error, if any
|
|
86
|
+
*/
|
|
87
|
+
error: Error | undefined
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set messages manually
|
|
91
|
+
*/
|
|
92
|
+
setMessages: (messages: Array<UIMessage<TTools>>) => void
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clear all messages
|
|
96
|
+
*/
|
|
97
|
+
clear: () => void
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client
|
|
101
|
+
// and re-exported from there for convenience
|
package/src/use-chat.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { ChatClient } from '@tanstack/ai-client'
|
|
3
|
+
import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
|
|
4
|
+
|
|
5
|
+
import type { UIMessage, UseChatOptions, UseChatReturn } from './types'
|
|
6
|
+
|
|
7
|
+
export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
8
|
+
options: UseChatOptions<TTools>,
|
|
9
|
+
): UseChatReturn<TTools> {
|
|
10
|
+
const hookId = useId()
|
|
11
|
+
const clientId = options.id || hookId
|
|
12
|
+
|
|
13
|
+
const [messages, setMessages] = useState<Array<UIMessage<TTools>>>(
|
|
14
|
+
options.initialMessages || [],
|
|
15
|
+
)
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
17
|
+
const [error, setError] = useState<Error | undefined>(undefined)
|
|
18
|
+
|
|
19
|
+
// Track current messages in a ref to preserve them when client is recreated
|
|
20
|
+
const messagesRef = useRef<Array<UIMessage<TTools>>>(
|
|
21
|
+
options.initialMessages || [],
|
|
22
|
+
)
|
|
23
|
+
const isFirstMountRef = useRef(true)
|
|
24
|
+
|
|
25
|
+
// Track current options in a ref to avoid recreating client when options change
|
|
26
|
+
const optionsRef = useRef<UseChatOptions<TTools>>(options)
|
|
27
|
+
|
|
28
|
+
// Update ref whenever messages change
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
messagesRef.current = messages
|
|
31
|
+
}, [messages])
|
|
32
|
+
|
|
33
|
+
optionsRef.current = options
|
|
34
|
+
|
|
35
|
+
// Create ChatClient instance with callbacks to sync state
|
|
36
|
+
// Note: Options are captured at client creation time.
|
|
37
|
+
// The connection adapter can use functions for dynamic values (url, headers, etc.)
|
|
38
|
+
// which are evaluated lazily on each request.
|
|
39
|
+
const client = useMemo(() => {
|
|
40
|
+
// On first mount, use initialMessages. On subsequent recreations, preserve existing messages.
|
|
41
|
+
const messagesToUse = isFirstMountRef.current
|
|
42
|
+
? options.initialMessages || []
|
|
43
|
+
: messagesRef.current
|
|
44
|
+
|
|
45
|
+
isFirstMountRef.current = false
|
|
46
|
+
|
|
47
|
+
return new ChatClient({
|
|
48
|
+
connection: optionsRef.current.connection,
|
|
49
|
+
id: clientId,
|
|
50
|
+
initialMessages: messagesToUse,
|
|
51
|
+
body: optionsRef.current.body,
|
|
52
|
+
onResponse: optionsRef.current.onResponse,
|
|
53
|
+
onChunk: optionsRef.current.onChunk,
|
|
54
|
+
onFinish: optionsRef.current.onFinish,
|
|
55
|
+
onError: optionsRef.current.onError,
|
|
56
|
+
tools: optionsRef.current.tools,
|
|
57
|
+
streamProcessor: options.streamProcessor,
|
|
58
|
+
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
59
|
+
setMessages(newMessages)
|
|
60
|
+
},
|
|
61
|
+
onLoadingChange: (newIsLoading: boolean) => {
|
|
62
|
+
setIsLoading(newIsLoading)
|
|
63
|
+
},
|
|
64
|
+
onErrorChange: (newError: Error | undefined) => {
|
|
65
|
+
setError(newError)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}, [clientId])
|
|
69
|
+
|
|
70
|
+
// Sync initial messages on mount only
|
|
71
|
+
// Note: initialMessages are passed to ChatClient constructor, but we also
|
|
72
|
+
// set them here to ensure React state is in sync
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
75
|
+
// Only set if current messages are empty (initial state)
|
|
76
|
+
if (messages.length === 0) {
|
|
77
|
+
client.setMessagesManually(options.initialMessages)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}, []) // Only run on mount - initialMessages are handled by ChatClient constructor
|
|
81
|
+
|
|
82
|
+
// Cleanup on unmount: stop any in-flight requests
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
return () => {
|
|
85
|
+
// Stop any active generation when component unmounts
|
|
86
|
+
if (isLoading) {
|
|
87
|
+
client.stop()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, [client, isLoading])
|
|
91
|
+
|
|
92
|
+
// Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)
|
|
93
|
+
// are captured at client creation time. Changes to these callbacks require
|
|
94
|
+
// remounting the component or changing the connection to recreate the client.
|
|
95
|
+
|
|
96
|
+
const sendMessage = useCallback(
|
|
97
|
+
async (content: string) => {
|
|
98
|
+
await client.sendMessage(content)
|
|
99
|
+
},
|
|
100
|
+
[client],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const append = useCallback(
|
|
104
|
+
async (message: ModelMessage | UIMessage) => {
|
|
105
|
+
await client.append(message)
|
|
106
|
+
},
|
|
107
|
+
[client],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const reload = useCallback(async () => {
|
|
111
|
+
await client.reload()
|
|
112
|
+
}, [client])
|
|
113
|
+
|
|
114
|
+
const stop = useCallback(() => {
|
|
115
|
+
client.stop()
|
|
116
|
+
}, [client])
|
|
117
|
+
|
|
118
|
+
const clear = useCallback(() => {
|
|
119
|
+
client.clear()
|
|
120
|
+
}, [client])
|
|
121
|
+
|
|
122
|
+
const setMessagesManually = useCallback(
|
|
123
|
+
(newMessages: Array<UIMessage<TTools>>) => {
|
|
124
|
+
client.setMessagesManually(newMessages)
|
|
125
|
+
},
|
|
126
|
+
[client],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const addToolResult = useCallback(
|
|
130
|
+
async (result: {
|
|
131
|
+
toolCallId: string
|
|
132
|
+
tool: string
|
|
133
|
+
output: any
|
|
134
|
+
state?: 'output-available' | 'output-error'
|
|
135
|
+
errorText?: string
|
|
136
|
+
}) => {
|
|
137
|
+
await client.addToolResult(result)
|
|
138
|
+
},
|
|
139
|
+
[client],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const addToolApprovalResponse = useCallback(
|
|
143
|
+
async (response: { id: string; approved: boolean }) => {
|
|
144
|
+
await client.addToolApprovalResponse(response)
|
|
145
|
+
},
|
|
146
|
+
[client],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
messages,
|
|
151
|
+
sendMessage,
|
|
152
|
+
append,
|
|
153
|
+
reload,
|
|
154
|
+
stop,
|
|
155
|
+
isLoading,
|
|
156
|
+
error,
|
|
157
|
+
setMessages: setMessagesManually,
|
|
158
|
+
clear,
|
|
159
|
+
addToolResult,
|
|
160
|
+
addToolApprovalResponse,
|
|
161
|
+
}
|
|
162
|
+
}
|