@tanstack/ai-preact 0.3.0 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  import { AnyClientTool, ModelMessage } from '@tanstack/ai';
2
- import { ChatClientOptions, ChatRequestBody, UIMessage } from '@tanstack/ai-client';
3
- export type { UIMessage, ChatRequestBody };
2
+ import { ChatClientOptions, ChatClientState, ChatRequestBody, MultimodalContent, UIMessage } from '@tanstack/ai-client';
3
+ export type { ChatRequestBody, MultimodalContent, UIMessage };
4
4
  /**
5
5
  * Options for the useChat hook.
6
6
  *
@@ -9,6 +9,7 @@ export type { UIMessage, ChatRequestBody };
9
9
  * - `onMessagesChange` - Managed by Preact state (exposed as `messages`)
10
10
  * - `onLoadingChange` - Managed by Preact state (exposed as `isLoading`)
11
11
  * - `onErrorChange` - Managed by Preact state (exposed as `error`)
12
+ * - `onStatusChange` - Managed by Preact state (exposed as `status`)
12
13
  *
13
14
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
14
15
  * passed through to the underlying ChatClient and can be used for side effects.
@@ -16,16 +17,17 @@ export type { UIMessage, ChatRequestBody };
16
17
  * Note: Connection and body changes will recreate the ChatClient instance.
17
18
  * To update these options, remount the component or use a key prop.
18
19
  */
19
- export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'>;
20
+ export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange'>;
20
21
  export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
21
22
  /**
22
23
  * Current messages in the conversation
23
24
  */
24
25
  messages: Array<UIMessage<TTools>>;
25
26
  /**
26
- * Send a message and get a response
27
+ * Send a message and get a response.
28
+ * Can be a simple string or multimodal content with images, audio, etc.
27
29
  */
28
- sendMessage: (content: string) => Promise<void>;
30
+ sendMessage: (content: string | MultimodalContent) => Promise<void>;
29
31
  /**
30
32
  * Append a message to the conversation
31
33
  */
@@ -71,4 +73,8 @@ export interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any
71
73
  * Clear all messages
72
74
  */
73
75
  clear: () => void;
76
+ /**
77
+ * Current generation status
78
+ */
79
+ status: ChatClientState;
74
80
  }
@@ -1,5 +1,5 @@
1
- import { useId, useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
2
1
  import { ChatClient } from "@tanstack/ai-client";
2
+ import { useId, useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks";
3
3
  function useChat(options) {
4
4
  const hookId = useId();
5
5
  const clientId = options.id || hookId;
@@ -8,6 +8,7 @@ function useChat(options) {
8
8
  );
9
9
  const [isLoading, setIsLoading] = useState(false);
10
10
  const [error, setError] = useState(void 0);
11
+ const [status, setStatus] = useState("ready");
11
12
  const messagesRef = useRef(
12
13
  options.initialMessages || []
13
14
  );
@@ -27,8 +28,12 @@ function useChat(options) {
27
28
  body: optionsRef.current.body,
28
29
  onResponse: optionsRef.current.onResponse,
29
30
  onChunk: optionsRef.current.onChunk,
30
- onFinish: optionsRef.current.onFinish,
31
- onError: optionsRef.current.onError,
31
+ onFinish: (message) => {
32
+ optionsRef.current.onFinish?.(message);
33
+ },
34
+ onError: (err) => {
35
+ optionsRef.current.onError?.(err);
36
+ },
32
37
  tools: optionsRef.current.tools,
33
38
  streamProcessor: options.streamProcessor,
34
39
  onMessagesChange: (newMessages) => {
@@ -37,6 +42,9 @@ function useChat(options) {
37
42
  onLoadingChange: (newIsLoading) => {
38
43
  setIsLoading(newIsLoading);
39
44
  },
45
+ onStatusChange: (newStatus) => {
46
+ setStatus(newStatus);
47
+ },
40
48
  onErrorChange: (newError) => {
41
49
  setError(newError);
42
50
  }
@@ -102,6 +110,7 @@ function useChat(options) {
102
110
  stop,
103
111
  isLoading,
104
112
  error,
113
+ status,
105
114
  setMessages: setMessagesManually,
106
115
  clear,
107
116
  addToolResult,
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\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 const optionsRef = useRef<UseChatOptions<TTools>>(options)\n\n optionsRef.current = options\n\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\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 body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\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 Preact state is in sync\n useEffect(() => {\n if (\n options.initialMessages &&\n options.initialMessages.length &&\n !messages.length\n ) {\n client.setMessagesManually(options.initialMessages)\n }\n }, [])\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 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 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 { ChatClient } from '@tanstack/ai-client'\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type { ChatClientState } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\n\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} 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 const [status, setStatus] = useState<ChatClientState>('ready')\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 const optionsRef = useRef<UseChatOptions<TTools>>(options)\n\n optionsRef.current = options\n\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\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: (message) => {\n optionsRef.current.onFinish?.(message)\n },\n onError: (err) => {\n optionsRef.current.onError?.(err)\n },\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 onStatusChange: (newStatus: ChatClientState) => {\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n setError(newError)\n },\n })\n }, [clientId])\n\n // Sync body changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n useEffect(() => {\n client.updateOptions({ body: options.body })\n }, [client, options.body])\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 Preact state is in sync\n useEffect(() => {\n if (\n options.initialMessages &&\n options.initialMessages.length &&\n !messages.length\n ) {\n client.setMessagesManually(options.initialMessages)\n }\n }, [])\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 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 const sendMessage = useCallback(\n async (content: string | MultimodalContent) => {\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 status,\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-preact",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Preact hooks for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "streaming"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai-client": "0.3.0"
34
+ "@tanstack/ai-client": "0.4.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@testing-library/preact": "^3.2.4",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "preact": ">=10.11.0",
45
- "@tanstack/ai": "^0.3.0"
45
+ "@tanstack/ai": "^0.4.0"
46
46
  },
47
47
  "scripts": {
48
48
  "clean": "premove ./build ./dist",
package/src/types.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
2
2
  import type {
3
3
  ChatClientOptions,
4
+ ChatClientState,
4
5
  ChatRequestBody,
6
+ MultimodalContent,
5
7
  UIMessage,
6
8
  } from '@tanstack/ai-client'
7
9
 
8
10
  // Re-export types from ai-client
9
- export type { UIMessage, ChatRequestBody }
11
+ export type { ChatRequestBody, MultimodalContent, UIMessage }
10
12
 
11
13
  /**
12
14
  * Options for the useChat hook.
@@ -16,6 +18,7 @@ export type { UIMessage, ChatRequestBody }
16
18
  * - `onMessagesChange` - Managed by Preact state (exposed as `messages`)
17
19
  * - `onLoadingChange` - Managed by Preact state (exposed as `isLoading`)
18
20
  * - `onErrorChange` - Managed by Preact state (exposed as `error`)
21
+ * - `onStatusChange` - Managed by Preact state (exposed as `status`)
19
22
  *
20
23
  * All other callbacks (onResponse, onChunk, onFinish, onError) are
21
24
  * passed through to the underlying ChatClient and can be used for side effects.
@@ -26,7 +29,7 @@ export type { UIMessage, ChatRequestBody }
26
29
  export type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> =
27
30
  Omit<
28
31
  ChatClientOptions<TTools>,
29
- 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange'
32
+ 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange'
30
33
  >
31
34
 
32
35
  export interface UseChatReturn<
@@ -38,9 +41,10 @@ export interface UseChatReturn<
38
41
  messages: Array<UIMessage<TTools>>
39
42
 
40
43
  /**
41
- * Send a message and get a response
44
+ * Send a message and get a response.
45
+ * Can be a simple string or multimodal content with images, audio, etc.
42
46
  */
43
- sendMessage: (content: string) => Promise<void>
47
+ sendMessage: (content: string | MultimodalContent) => Promise<void>
44
48
 
45
49
  /**
46
50
  * Append a message to the conversation
@@ -95,4 +99,9 @@ export interface UseChatReturn<
95
99
  * Clear all messages
96
100
  */
97
101
  clear: () => void
102
+
103
+ /**
104
+ * Current generation status
105
+ */
106
+ status: ChatClientState
98
107
  }
package/src/use-chat.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ChatClient } from '@tanstack/ai-client'
1
2
  import {
2
3
  useCallback,
3
4
  useEffect,
@@ -6,10 +7,15 @@ import {
6
7
  useRef,
7
8
  useState,
8
9
  } from 'preact/hooks'
9
- import { ChatClient } from '@tanstack/ai-client'
10
+ import type { ChatClientState } from '@tanstack/ai-client'
10
11
  import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
11
12
 
12
- import type { UIMessage, UseChatOptions, UseChatReturn } from './types'
13
+ import type {
14
+ MultimodalContent,
15
+ UIMessage,
16
+ UseChatOptions,
17
+ UseChatReturn,
18
+ } from './types'
13
19
 
14
20
  export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
15
21
  options: UseChatOptions<TTools>,
@@ -22,6 +28,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
22
28
  )
23
29
  const [isLoading, setIsLoading] = useState(false)
24
30
  const [error, setError] = useState<Error | undefined>(undefined)
31
+ const [status, setStatus] = useState<ChatClientState>('ready')
25
32
 
26
33
  // Track current messages in a ref to preserve them when client is recreated
27
34
  const messagesRef = useRef<Array<UIMessage<TTools>>>(
@@ -51,8 +58,12 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
51
58
  body: optionsRef.current.body,
52
59
  onResponse: optionsRef.current.onResponse,
53
60
  onChunk: optionsRef.current.onChunk,
54
- onFinish: optionsRef.current.onFinish,
55
- onError: optionsRef.current.onError,
61
+ onFinish: (message) => {
62
+ optionsRef.current.onFinish?.(message)
63
+ },
64
+ onError: (err) => {
65
+ optionsRef.current.onError?.(err)
66
+ },
56
67
  tools: optionsRef.current.tools,
57
68
  streamProcessor: options.streamProcessor,
58
69
  onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
@@ -61,6 +72,9 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
61
72
  onLoadingChange: (newIsLoading: boolean) => {
62
73
  setIsLoading(newIsLoading)
63
74
  },
75
+ onStatusChange: (newStatus: ChatClientState) => {
76
+ setStatus(newStatus)
77
+ },
64
78
  onErrorChange: (newError: Error | undefined) => {
65
79
  setError(newError)
66
80
  },
@@ -100,7 +114,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
100
114
  // are captured at client creation time. Changes to these callbacks require
101
115
  // remounting the component or changing the connection to recreate the client.
102
116
  const sendMessage = useCallback(
103
- async (content: string) => {
117
+ async (content: string | MultimodalContent) => {
104
118
  await client.sendMessage(content)
105
119
  },
106
120
  [client],
@@ -160,6 +174,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
160
174
  stop,
161
175
  isLoading,
162
176
  error,
177
+ status,
163
178
  setMessages: setMessagesManually,
164
179
  clear,
165
180
  addToolResult,