@tanstack/ai-preact 0.8.2 → 0.9.2
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/dist/esm/use-chat.js +51 -11
- package/dist/esm/use-chat.js.map +1 -1
- package/package.json +3 -3
- package/src/use-chat.ts +53 -24
package/dist/esm/use-chat.js
CHANGED
|
@@ -17,17 +17,21 @@ function useChat(options) {
|
|
|
17
17
|
options.initialMessages || []
|
|
18
18
|
);
|
|
19
19
|
const isFirstMountRef = useRef(true);
|
|
20
|
+
const activeClientRef = useRef(null);
|
|
21
|
+
const cleanupInvalidationRef = useRef(
|
|
22
|
+
null
|
|
23
|
+
);
|
|
20
24
|
const optionsRef = useRef(options);
|
|
21
25
|
optionsRef.current = options;
|
|
22
26
|
useEffect(() => {
|
|
23
27
|
messagesRef.current = messages;
|
|
24
28
|
}, [messages]);
|
|
25
29
|
const client = useMemo(() => {
|
|
26
|
-
const messagesToUse =
|
|
30
|
+
const messagesToUse = options.initialMessages || [];
|
|
27
31
|
isFirstMountRef.current = false;
|
|
28
32
|
const initialOptions = optionsRef.current;
|
|
29
33
|
const transport = initialOptions.connection ? { connection: initialOptions.connection } : { fetcher: initialOptions.fetcher };
|
|
30
|
-
|
|
34
|
+
const instance = new ChatClient({
|
|
31
35
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
32
36
|
...transport,
|
|
33
37
|
id: clientId,
|
|
@@ -36,6 +40,9 @@ function useChat(options) {
|
|
|
36
40
|
...initialOptions.forwardedProps !== void 0 && {
|
|
37
41
|
forwardedProps: initialOptions.forwardedProps
|
|
38
42
|
},
|
|
43
|
+
...initialOptions.persistence !== void 0 && {
|
|
44
|
+
persistence: initialOptions.persistence
|
|
45
|
+
},
|
|
39
46
|
...initialOptions.context !== void 0 && {
|
|
40
47
|
context: initialOptions.context
|
|
41
48
|
},
|
|
@@ -48,15 +55,26 @@ function useChat(options) {
|
|
|
48
55
|
// Wrap every callback so the latest options are read at call time.
|
|
49
56
|
// Capturing the function reference directly would freeze it to whatever
|
|
50
57
|
// the parent passed on the first render.
|
|
51
|
-
onResponse: (response) =>
|
|
52
|
-
|
|
58
|
+
onResponse: (response) => {
|
|
59
|
+
if (activeClientRef.current !== instance) return;
|
|
60
|
+
return optionsRef.current.onResponse?.(response);
|
|
61
|
+
},
|
|
62
|
+
onChunk: (chunk) => {
|
|
63
|
+
if (activeClientRef.current !== instance) return;
|
|
64
|
+
optionsRef.current.onChunk?.(chunk);
|
|
65
|
+
},
|
|
53
66
|
onFinish: (message) => {
|
|
67
|
+
if (activeClientRef.current !== instance) return;
|
|
54
68
|
optionsRef.current.onFinish?.(message);
|
|
55
69
|
},
|
|
56
70
|
onError: (err) => {
|
|
71
|
+
if (activeClientRef.current !== instance) return;
|
|
57
72
|
optionsRef.current.onError?.(err);
|
|
58
73
|
},
|
|
59
|
-
onCustomEvent: (eventType, data, context) =>
|
|
74
|
+
onCustomEvent: (eventType, data, context) => {
|
|
75
|
+
if (activeClientRef.current !== instance) return;
|
|
76
|
+
optionsRef.current.onCustomEvent?.(eventType, data, context);
|
|
77
|
+
},
|
|
60
78
|
...initialOptions.tools !== void 0 && {
|
|
61
79
|
tools: initialOptions.tools
|
|
62
80
|
},
|
|
@@ -64,28 +82,43 @@ function useChat(options) {
|
|
|
64
82
|
streamProcessor: options.streamProcessor
|
|
65
83
|
},
|
|
66
84
|
onMessagesChange: (newMessages) => {
|
|
85
|
+
if (activeClientRef.current !== instance) return;
|
|
67
86
|
setMessages(newMessages);
|
|
68
87
|
},
|
|
69
88
|
onLoadingChange: (newIsLoading) => {
|
|
89
|
+
if (activeClientRef.current !== instance) return;
|
|
70
90
|
setIsLoading(newIsLoading);
|
|
71
91
|
},
|
|
72
92
|
onStatusChange: (newStatus) => {
|
|
93
|
+
if (activeClientRef.current !== instance) return;
|
|
73
94
|
setStatus(newStatus);
|
|
74
95
|
},
|
|
75
96
|
onErrorChange: (newError) => {
|
|
97
|
+
if (activeClientRef.current !== instance) return;
|
|
76
98
|
setError(newError);
|
|
77
99
|
},
|
|
78
100
|
onSubscriptionChange: (nextIsSubscribed) => {
|
|
101
|
+
if (activeClientRef.current !== instance) return;
|
|
79
102
|
setIsSubscribed(nextIsSubscribed);
|
|
80
103
|
},
|
|
81
104
|
onConnectionStatusChange: (nextStatus) => {
|
|
105
|
+
if (activeClientRef.current !== instance) return;
|
|
82
106
|
setConnectionStatus(nextStatus);
|
|
83
107
|
},
|
|
84
108
|
onSessionGeneratingChange: (isGenerating) => {
|
|
109
|
+
if (activeClientRef.current !== instance) return;
|
|
85
110
|
setSessionGenerating(isGenerating);
|
|
86
111
|
}
|
|
87
112
|
});
|
|
113
|
+
activeClientRef.current = instance;
|
|
114
|
+
return instance;
|
|
88
115
|
}, [clientId]);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const clientMessages = client.getMessages();
|
|
118
|
+
if (clientMessages !== messagesRef.current) {
|
|
119
|
+
setMessages(clientMessages);
|
|
120
|
+
}
|
|
121
|
+
}, [client]);
|
|
89
122
|
useEffect(() => {
|
|
90
123
|
client.updateOptions({
|
|
91
124
|
body: options.body,
|
|
@@ -95,11 +128,6 @@ function useChat(options) {
|
|
|
95
128
|
context: options.context
|
|
96
129
|
});
|
|
97
130
|
}, [client, options.body, options.forwardedProps, options.context]);
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (options.initialMessages && options.initialMessages.length && !messages.length) {
|
|
100
|
-
client.setMessagesManually(options.initialMessages);
|
|
101
|
-
}
|
|
102
|
-
}, []);
|
|
103
131
|
useEffect(() => {
|
|
104
132
|
if (options.live) {
|
|
105
133
|
client.subscribe();
|
|
@@ -108,8 +136,19 @@ function useChat(options) {
|
|
|
108
136
|
}
|
|
109
137
|
}, [client, options.live]);
|
|
110
138
|
useEffect(() => {
|
|
139
|
+
if (cleanupInvalidationRef.current) {
|
|
140
|
+
clearTimeout(cleanupInvalidationRef.current);
|
|
141
|
+
cleanupInvalidationRef.current = null;
|
|
142
|
+
}
|
|
143
|
+
activeClientRef.current = client;
|
|
111
144
|
client.mountDevtools();
|
|
112
145
|
return () => {
|
|
146
|
+
cleanupInvalidationRef.current = setTimeout(() => {
|
|
147
|
+
if (activeClientRef.current === client) {
|
|
148
|
+
activeClientRef.current = null;
|
|
149
|
+
}
|
|
150
|
+
cleanupInvalidationRef.current = null;
|
|
151
|
+
}, 0);
|
|
113
152
|
if (optionsRef.current.live) {
|
|
114
153
|
client.unsubscribe();
|
|
115
154
|
} else {
|
|
@@ -157,8 +196,9 @@ function useChat(options) {
|
|
|
157
196
|
},
|
|
158
197
|
[client]
|
|
159
198
|
);
|
|
199
|
+
const renderedMessages = client.getMessages();
|
|
160
200
|
return {
|
|
161
|
-
messages,
|
|
201
|
+
messages: renderedMessages,
|
|
162
202
|
sendMessage,
|
|
163
203
|
append,
|
|
164
204
|
reload,
|
package/dist/esm/use-chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat.js","sources":["../../src/use-chat.ts"],"sourcesContent":["import { ChatClient } from '@tanstack/ai-client'\nimport { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n} 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<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n>(options: UseChatOptions<TTools, TContext>): 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 const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\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, TContext>>(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 // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n return new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'preact',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => optionsRef.current.onResponse?.(response),\n onChunk: (chunk) => optionsRef.current.onChunk?.(chunk),\n onFinish: (message) => {\n optionsRef.current.onFinish?.(message)\n },\n onError: (err) => {\n optionsRef.current.onError?.(err)\n },\n onCustomEvent: (eventType, data, context) =>\n optionsRef.current.onCustomEvent?.(eventType, data, context),\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\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 onSubscriptionChange: (nextIsSubscribed: boolean) => {\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n setSessionGenerating(isGenerating)\n },\n })\n }, [clientId])\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n })\n }, [client, options.body, options.forwardedProps, options.context])\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 useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\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 client.mountDevtools()\n\n return () => {\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating 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: unknown\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 isSubscribed,\n connectionStatus,\n sessionGenerating,\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 { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'preact/hooks'\nimport type {\n ChatClientState,\n ConnectionStatus,\n InferredClientContext,\n} 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<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TContext = InferredClientContext<TTools>,\n>(options: UseChatOptions<TTools, TContext>): 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 const [isSubscribed, setIsSubscribed] = useState(false)\n const [connectionStatus, setConnectionStatus] =\n useState<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = useState(false)\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 activeClientRef = useRef<ChatClient | null>(null)\n const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)\n\n optionsRef.current = options\n\n useEffect(() => {\n messagesRef.current = messages\n }, [messages])\n\n const client = useMemo(() => {\n const messagesToUse = options.initialMessages || []\n isFirstMountRef.current = false\n\n // Build options with conditional spreads for fields whose source\n // type is `T | undefined` but the ChatClient target uses a strict\n // optional (`field?: T`) — `exactOptionalPropertyTypes` rejects\n // assigning `undefined` to those, so we omit the key when absent.\n const initialOptions = optionsRef.current\n const transport = initialOptions.connection\n ? { connection: initialOptions.connection }\n : { fetcher: initialOptions.fetcher }\n\n const instance = new ChatClient<TTools, TContext>({\n devtoolsBridgeFactory: createChatDevtoolsBridge,\n ...transport,\n id: clientId,\n initialMessages: messagesToUse,\n ...(initialOptions.body !== undefined && { body: initialOptions.body }),\n ...(initialOptions.forwardedProps !== undefined && {\n forwardedProps: initialOptions.forwardedProps,\n }),\n ...(initialOptions.persistence !== undefined && {\n persistence: initialOptions.persistence,\n }),\n ...(initialOptions.context !== undefined && {\n context: initialOptions.context,\n }),\n devtools: {\n ...initialOptions.devtools,\n framework: 'preact',\n hookName: 'useChat',\n outputKind: initialOptions.outputSchema ? 'structured' : 'chat',\n },\n // Wrap every callback so the latest options are read at call time.\n // Capturing the function reference directly would freeze it to whatever\n // the parent passed on the first render.\n onResponse: (response) => {\n if (activeClientRef.current !== instance) return\n return optionsRef.current.onResponse?.(response)\n },\n onChunk: (chunk) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onChunk?.(chunk)\n },\n onFinish: (message) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onFinish?.(message)\n },\n onError: (err) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onError?.(err)\n },\n onCustomEvent: (eventType, data, context) => {\n if (activeClientRef.current !== instance) return\n optionsRef.current.onCustomEvent?.(eventType, data, context)\n },\n ...(initialOptions.tools !== undefined && {\n tools: initialOptions.tools,\n }),\n ...(options.streamProcessor !== undefined && {\n streamProcessor: options.streamProcessor,\n }),\n onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {\n if (activeClientRef.current !== instance) return\n setMessages(newMessages)\n },\n onLoadingChange: (newIsLoading: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsLoading(newIsLoading)\n },\n onStatusChange: (newStatus: ChatClientState) => {\n if (activeClientRef.current !== instance) return\n setStatus(newStatus)\n },\n onErrorChange: (newError: Error | undefined) => {\n if (activeClientRef.current !== instance) return\n setError(newError)\n },\n onSubscriptionChange: (nextIsSubscribed: boolean) => {\n if (activeClientRef.current !== instance) return\n setIsSubscribed(nextIsSubscribed)\n },\n onConnectionStatusChange: (nextStatus: ConnectionStatus) => {\n if (activeClientRef.current !== instance) return\n setConnectionStatus(nextStatus)\n },\n onSessionGeneratingChange: (isGenerating: boolean) => {\n if (activeClientRef.current !== instance) return\n setSessionGenerating(isGenerating)\n },\n })\n activeClientRef.current = instance\n return instance\n }, [clientId])\n\n useEffect(() => {\n const clientMessages = client.getMessages()\n if (clientMessages !== messagesRef.current) {\n setMessages(clientMessages)\n }\n }, [client])\n\n // Sync body / forwardedProps changes to the client.\n // Both populate the same wire payload; `forwardedProps` is preferred\n // and `body` is deprecated but still supported.\n useEffect(() => {\n // Conditional spread: `updateOptions` declares strict-optional\n // fields and rejects explicit `undefined` under EOPT.\n client.updateOptions({\n body: options.body,\n ...(options.forwardedProps !== undefined && {\n forwardedProps: options.forwardedProps,\n }),\n context: options.context,\n })\n }, [client, options.body, options.forwardedProps, options.context])\n\n useEffect(() => {\n if (options.live) {\n client.subscribe()\n } else {\n client.unsubscribe()\n }\n }, [client, options.live])\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 if (cleanupInvalidationRef.current) {\n clearTimeout(cleanupInvalidationRef.current)\n cleanupInvalidationRef.current = null\n }\n activeClientRef.current = client\n client.mountDevtools()\n\n return () => {\n cleanupInvalidationRef.current = setTimeout(() => {\n if (activeClientRef.current === client) {\n activeClientRef.current = null\n }\n cleanupInvalidationRef.current = null\n }, 0)\n // Subscribe/unsubscribe on `options.live` is owned by the dedicated\n // effect above. This cleanup only fires on unmount or client swap,\n // so read `live` through the ref to avoid disposing the client every\n // time `live` toggles.\n if (optionsRef.current.live) {\n client.unsubscribe()\n } else {\n client.stop()\n }\n client.dispose()\n }\n }, [client])\n\n // All callback options are read through optionsRef at call time, so fresh\n // closures from each render are picked up without recreating 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: unknown\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 const renderedMessages = client.getMessages()\n\n return {\n messages: renderedMessages,\n sendMessage,\n append,\n reload,\n stop,\n isLoading,\n error,\n status,\n isSubscribed,\n connectionStatus,\n sessionGenerating,\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
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Preact hooks for TanStack AI streaming chat and typed messages.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"tool-calling"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@tanstack/ai-client": "0.
|
|
37
|
+
"@tanstack/ai-client": "0.16.2"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@testing-library/preact": "^3.2.4",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"preact": ">=10.11.0",
|
|
48
|
-
"@tanstack/ai": "^0.
|
|
48
|
+
"@tanstack/ai": "^0.27.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"clean": "premove ./build ./dist",
|
package/src/use-chat.ts
CHANGED
|
@@ -45,6 +45,10 @@ export function useChat<
|
|
|
45
45
|
options.initialMessages || [],
|
|
46
46
|
)
|
|
47
47
|
const isFirstMountRef = useRef(true)
|
|
48
|
+
const activeClientRef = useRef<ChatClient | null>(null)
|
|
49
|
+
const cleanupInvalidationRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
50
|
+
null,
|
|
51
|
+
)
|
|
48
52
|
const optionsRef = useRef<UseChatOptions<TTools, TContext>>(options)
|
|
49
53
|
|
|
50
54
|
optionsRef.current = options
|
|
@@ -54,11 +58,7 @@ export function useChat<
|
|
|
54
58
|
}, [messages])
|
|
55
59
|
|
|
56
60
|
const client = useMemo(() => {
|
|
57
|
-
|
|
58
|
-
const messagesToUse = isFirstMountRef.current
|
|
59
|
-
? options.initialMessages || []
|
|
60
|
-
: messagesRef.current
|
|
61
|
-
|
|
61
|
+
const messagesToUse = options.initialMessages || []
|
|
62
62
|
isFirstMountRef.current = false
|
|
63
63
|
|
|
64
64
|
// Build options with conditional spreads for fields whose source
|
|
@@ -70,7 +70,7 @@ export function useChat<
|
|
|
70
70
|
? { connection: initialOptions.connection }
|
|
71
71
|
: { fetcher: initialOptions.fetcher }
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
const instance = new ChatClient<TTools, TContext>({
|
|
74
74
|
devtoolsBridgeFactory: createChatDevtoolsBridge,
|
|
75
75
|
...transport,
|
|
76
76
|
id: clientId,
|
|
@@ -79,6 +79,9 @@ export function useChat<
|
|
|
79
79
|
...(initialOptions.forwardedProps !== undefined && {
|
|
80
80
|
forwardedProps: initialOptions.forwardedProps,
|
|
81
81
|
}),
|
|
82
|
+
...(initialOptions.persistence !== undefined && {
|
|
83
|
+
persistence: initialOptions.persistence,
|
|
84
|
+
}),
|
|
82
85
|
...(initialOptions.context !== undefined && {
|
|
83
86
|
context: initialOptions.context,
|
|
84
87
|
}),
|
|
@@ -91,16 +94,26 @@ export function useChat<
|
|
|
91
94
|
// Wrap every callback so the latest options are read at call time.
|
|
92
95
|
// Capturing the function reference directly would freeze it to whatever
|
|
93
96
|
// the parent passed on the first render.
|
|
94
|
-
onResponse: (response) =>
|
|
95
|
-
|
|
97
|
+
onResponse: (response) => {
|
|
98
|
+
if (activeClientRef.current !== instance) return
|
|
99
|
+
return optionsRef.current.onResponse?.(response)
|
|
100
|
+
},
|
|
101
|
+
onChunk: (chunk) => {
|
|
102
|
+
if (activeClientRef.current !== instance) return
|
|
103
|
+
optionsRef.current.onChunk?.(chunk)
|
|
104
|
+
},
|
|
96
105
|
onFinish: (message) => {
|
|
106
|
+
if (activeClientRef.current !== instance) return
|
|
97
107
|
optionsRef.current.onFinish?.(message)
|
|
98
108
|
},
|
|
99
109
|
onError: (err) => {
|
|
110
|
+
if (activeClientRef.current !== instance) return
|
|
100
111
|
optionsRef.current.onError?.(err)
|
|
101
112
|
},
|
|
102
|
-
onCustomEvent: (eventType, data, context) =>
|
|
103
|
-
|
|
113
|
+
onCustomEvent: (eventType, data, context) => {
|
|
114
|
+
if (activeClientRef.current !== instance) return
|
|
115
|
+
optionsRef.current.onCustomEvent?.(eventType, data, context)
|
|
116
|
+
},
|
|
104
117
|
...(initialOptions.tools !== undefined && {
|
|
105
118
|
tools: initialOptions.tools,
|
|
106
119
|
}),
|
|
@@ -108,29 +121,45 @@ export function useChat<
|
|
|
108
121
|
streamProcessor: options.streamProcessor,
|
|
109
122
|
}),
|
|
110
123
|
onMessagesChange: (newMessages: Array<UIMessage<TTools>>) => {
|
|
124
|
+
if (activeClientRef.current !== instance) return
|
|
111
125
|
setMessages(newMessages)
|
|
112
126
|
},
|
|
113
127
|
onLoadingChange: (newIsLoading: boolean) => {
|
|
128
|
+
if (activeClientRef.current !== instance) return
|
|
114
129
|
setIsLoading(newIsLoading)
|
|
115
130
|
},
|
|
116
131
|
onStatusChange: (newStatus: ChatClientState) => {
|
|
132
|
+
if (activeClientRef.current !== instance) return
|
|
117
133
|
setStatus(newStatus)
|
|
118
134
|
},
|
|
119
135
|
onErrorChange: (newError: Error | undefined) => {
|
|
136
|
+
if (activeClientRef.current !== instance) return
|
|
120
137
|
setError(newError)
|
|
121
138
|
},
|
|
122
139
|
onSubscriptionChange: (nextIsSubscribed: boolean) => {
|
|
140
|
+
if (activeClientRef.current !== instance) return
|
|
123
141
|
setIsSubscribed(nextIsSubscribed)
|
|
124
142
|
},
|
|
125
143
|
onConnectionStatusChange: (nextStatus: ConnectionStatus) => {
|
|
144
|
+
if (activeClientRef.current !== instance) return
|
|
126
145
|
setConnectionStatus(nextStatus)
|
|
127
146
|
},
|
|
128
147
|
onSessionGeneratingChange: (isGenerating: boolean) => {
|
|
148
|
+
if (activeClientRef.current !== instance) return
|
|
129
149
|
setSessionGenerating(isGenerating)
|
|
130
150
|
},
|
|
131
151
|
})
|
|
152
|
+
activeClientRef.current = instance
|
|
153
|
+
return instance
|
|
132
154
|
}, [clientId])
|
|
133
155
|
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const clientMessages = client.getMessages()
|
|
158
|
+
if (clientMessages !== messagesRef.current) {
|
|
159
|
+
setMessages(clientMessages)
|
|
160
|
+
}
|
|
161
|
+
}, [client])
|
|
162
|
+
|
|
134
163
|
// Sync body / forwardedProps changes to the client.
|
|
135
164
|
// Both populate the same wire payload; `forwardedProps` is preferred
|
|
136
165
|
// and `body` is deprecated but still supported.
|
|
@@ -146,19 +175,6 @@ export function useChat<
|
|
|
146
175
|
})
|
|
147
176
|
}, [client, options.body, options.forwardedProps, options.context])
|
|
148
177
|
|
|
149
|
-
// Sync initial messages on mount only
|
|
150
|
-
// Note: initialMessages are passed to ChatClient constructor, but we also
|
|
151
|
-
// set them here to ensure Preact state is in sync
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
if (
|
|
154
|
-
options.initialMessages &&
|
|
155
|
-
options.initialMessages.length &&
|
|
156
|
-
!messages.length
|
|
157
|
-
) {
|
|
158
|
-
client.setMessagesManually(options.initialMessages)
|
|
159
|
-
}
|
|
160
|
-
}, [])
|
|
161
|
-
|
|
162
178
|
useEffect(() => {
|
|
163
179
|
if (options.live) {
|
|
164
180
|
client.subscribe()
|
|
@@ -172,9 +188,20 @@ export function useChat<
|
|
|
172
188
|
// DO NOT include isLoading in dependencies - that would cause the cleanup
|
|
173
189
|
// to run when isLoading changes, aborting continuation requests.
|
|
174
190
|
useEffect(() => {
|
|
191
|
+
if (cleanupInvalidationRef.current) {
|
|
192
|
+
clearTimeout(cleanupInvalidationRef.current)
|
|
193
|
+
cleanupInvalidationRef.current = null
|
|
194
|
+
}
|
|
195
|
+
activeClientRef.current = client
|
|
175
196
|
client.mountDevtools()
|
|
176
197
|
|
|
177
198
|
return () => {
|
|
199
|
+
cleanupInvalidationRef.current = setTimeout(() => {
|
|
200
|
+
if (activeClientRef.current === client) {
|
|
201
|
+
activeClientRef.current = null
|
|
202
|
+
}
|
|
203
|
+
cleanupInvalidationRef.current = null
|
|
204
|
+
}, 0)
|
|
178
205
|
// Subscribe/unsubscribe on `options.live` is owned by the dedicated
|
|
179
206
|
// effect above. This cleanup only fires on unmount or client swap,
|
|
180
207
|
// so read `live` through the ref to avoid disposing the client every
|
|
@@ -243,8 +270,10 @@ export function useChat<
|
|
|
243
270
|
[client],
|
|
244
271
|
)
|
|
245
272
|
|
|
273
|
+
const renderedMessages = client.getMessages()
|
|
274
|
+
|
|
246
275
|
return {
|
|
247
|
-
messages,
|
|
276
|
+
messages: renderedMessages,
|
|
248
277
|
sendMessage,
|
|
249
278
|
append,
|
|
250
279
|
reload,
|