@tanstack/ai-react 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,9 @@
38
38
  A powerful, type-safe AI SDK for building AI-powered applications.
39
39
 
40
40
  - Provider-agnostic adapters (OpenAI, Anthropic, Gemini, Ollama, etc.)
41
+ - **Tree-shakeable adapters** - Import only what you need for smaller bundles
41
42
  - **Multimodal content support** - Send images, audio, video, and documents
43
+ - **Image generation** - Generate images with OpenAI DALL-E/GPT-Image and Gemini Imagen
42
44
  - Chat completion, streaming, and agent loop strategies
43
45
  - Headless chat state management with adapters (SSE, HTTP stream, custom)
44
46
  - Isomorphic type-safe tools with server/client execution
@@ -46,6 +48,30 @@ A powerful, type-safe AI SDK for building AI-powered applications.
46
48
 
47
49
  ### <a href="https://tanstack.com/ai">Read the docs →</b></a>
48
50
 
51
+ ## Tree-Shakeable Adapters
52
+
53
+ Import only the functionality you need for smaller bundle sizes:
54
+
55
+ ```typescript
56
+ // Only chat functionality - no summarization code bundled
57
+ import { openaiText } from '@tanstack/ai-openai/adapters'
58
+ import { generate } from '@tanstack/ai'
59
+
60
+ const textAdapter = openaiText()
61
+
62
+ const result = generate({
63
+ adapter: textAdapter,
64
+ model: 'gpt-4o',
65
+ messages: [{ role: 'user', content: [{ type: 'text', content: 'Hello!' }] }],
66
+ })
67
+
68
+ for await (const chunk of result) {
69
+ console.log(chunk)
70
+ }
71
+ ```
72
+
73
+ Available adapters: `openaiText`, `openaiEmbed`, `openaiSummarize`, `anthropicText`, `geminiText`, `ollamaText`, and more.
74
+
49
75
  ## Bonus: TanStack Start Integration
50
76
 
51
77
  TanStack AI works with **any** framework (Next.js, Express, Remix, etc.).
@@ -12,10 +12,10 @@ function useChat(options) {
12
12
  options.initialMessages || []
13
13
  );
14
14
  const isFirstMountRef = useRef(true);
15
- const optionsRef = useRef(options);
16
15
  useEffect(() => {
17
16
  messagesRef.current = messages;
18
17
  }, [messages]);
18
+ const optionsRef = useRef(options);
19
19
  optionsRef.current = options;
20
20
  const client = useMemo(() => {
21
21
  const messagesToUse = isFirstMountRef.current ? options.initialMessages || [] : messagesRef.current;
@@ -51,11 +51,9 @@ function useChat(options) {
51
51
  }, []);
52
52
  useEffect(() => {
53
53
  return () => {
54
- if (isLoading) {
55
- client.stop();
56
- }
54
+ client.stop();
57
55
  };
58
- }, [client, isLoading]);
56
+ }, [client]);
59
57
  const sendMessage = useCallback(
60
58
  async (content) => {
61
59
  await client.sendMessage(content);
@@ -1 +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":"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"}
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 // Update ref whenever messages change\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n // Track current options in a ref to avoid recreating client when options change\n const optionsRef = useRef<UseChatOptions<TTools>>(options)\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 // Note: We only cleanup when client changes or component unmounts.\n // DO NOT include isLoading in dependencies - that would cause the cleanup\n // to run when isLoading changes, aborting continuation requests.\n useEffect(() => {\n return () => {\n // Stop any active generation when component unmounts or client changes\n client.stop()\n }\n }, [client])\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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-react",
3
- "version": "0.0.3",
3
+ "version": "0.1.0",
4
4
  "description": "React hooks for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,22 +31,22 @@
31
31
  "streaming"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai": "0.0.3",
35
- "@tanstack/ai-client": "0.0.3"
34
+ "@tanstack/ai": "0.1.0",
35
+ "@tanstack/ai-client": "0.1.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@testing-library/react": "^16.3.0",
39
39
  "@types/react": "^19.2.7",
40
40
  "@vitest/coverage-v8": "4.0.14",
41
41
  "jsdom": "^27.2.0",
42
- "vite": "^7.2.4",
42
+ "vite": "^7.2.7",
43
43
  "zod": "^4.1.13"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@types/react": ">=18.0.0",
47
47
  "react": ">=18.0.0",
48
- "@tanstack/ai": "0.0.3",
49
- "@tanstack/ai-client": "0.0.3"
48
+ "@tanstack/ai": "0.1.0",
49
+ "@tanstack/ai-client": "0.1.0"
50
50
  },
51
51
  "scripts": {
52
52
  "clean": "premove ./build ./dist",
package/src/use-chat.ts CHANGED
@@ -22,14 +22,13 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
22
22
  )
23
23
  const isFirstMountRef = useRef(true)
24
24
 
25
- // Track current options in a ref to avoid recreating client when options change
26
- const optionsRef = useRef<UseChatOptions<TTools>>(options)
27
-
28
25
  // Update ref whenever messages change
29
26
  useEffect(() => {
30
27
  messagesRef.current = messages
31
28
  }, [messages])
32
29
 
30
+ // Track current options in a ref to avoid recreating client when options change
31
+ const optionsRef = useRef<UseChatOptions<TTools>>(options)
33
32
  optionsRef.current = options
34
33
 
35
34
  // Create ChatClient instance with callbacks to sync state
@@ -80,14 +79,15 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
80
79
  }, []) // Only run on mount - initialMessages are handled by ChatClient constructor
81
80
 
82
81
  // Cleanup on unmount: stop any in-flight requests
82
+ // Note: We only cleanup when client changes or component unmounts.
83
+ // DO NOT include isLoading in dependencies - that would cause the cleanup
84
+ // to run when isLoading changes, aborting continuation requests.
83
85
  useEffect(() => {
84
86
  return () => {
85
- // Stop any active generation when component unmounts
86
- if (isLoading) {
87
- client.stop()
88
- }
87
+ // Stop any active generation when component unmounts or client changes
88
+ client.stop()
89
89
  }
90
- }, [client, isLoading])
90
+ }, [client])
91
91
 
92
92
  // Note: Callback options (onResponse, onChunk, onFinish, onError, onToolCall)
93
93
  // are captured at client creation time. Changes to these callbacks require