@tanstack/ai-solid 0.9.0 → 0.10.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/dist/types.d.ts +12 -9
- package/dist/use-chat.js +26 -20
- package/dist/use-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/types.ts +18 -9
- package/src/use-chat.ts +47 -27
package/dist/types.d.ts
CHANGED
|
@@ -43,22 +43,25 @@ type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema e
|
|
|
43
43
|
* Discriminated return shape: when `outputSchema` is supplied, the hook adds
|
|
44
44
|
* typed `partial` / `final` accessors; otherwise the return is unchanged.
|
|
45
45
|
*/
|
|
46
|
-
type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools> & (TSchema extends SchemaInput ? {
|
|
46
|
+
type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools, TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown> & (TSchema extends SchemaInput ? {
|
|
47
47
|
/**
|
|
48
|
-
* Live progressively-parsed structured output.
|
|
48
|
+
* Live progressively-parsed structured output. Derived from the
|
|
49
|
+
* latest assistant message's structured-output part.
|
|
49
50
|
*/
|
|
50
51
|
partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>;
|
|
51
52
|
/**
|
|
52
|
-
* Final, schema-validated structured output. `null` until the
|
|
53
|
-
*
|
|
53
|
+
* Final, schema-validated structured output. `null` until the latest
|
|
54
|
+
* assistant turn's structured-output part transitions to `complete`.
|
|
54
55
|
*/
|
|
55
56
|
final: Accessor<InferSchemaType<TSchema> | null>;
|
|
56
57
|
} : Record<never, never>);
|
|
57
|
-
interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
58
|
+
interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
|
|
58
59
|
/**
|
|
59
|
-
* Current messages in the conversation
|
|
60
|
+
* Current messages in the conversation. When `outputSchema` is supplied,
|
|
61
|
+
* `messages()[i].parts.find(p => p.type === 'structured-output')` is typed
|
|
62
|
+
* by the schema — `data: T`, `partial: DeepPartial<T>`.
|
|
60
63
|
*/
|
|
61
|
-
messages: Accessor<Array<UIMessage<TTools>>>;
|
|
64
|
+
messages: Accessor<Array<UIMessage<TTools, TData>>>;
|
|
62
65
|
/**
|
|
63
66
|
* Send a message and get a response.
|
|
64
67
|
* Can be a simple string or multimodal content with images, audio, etc.
|
|
@@ -67,7 +70,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
|
67
70
|
/**
|
|
68
71
|
* Append a message to the conversation
|
|
69
72
|
*/
|
|
70
|
-
append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>;
|
|
73
|
+
append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>;
|
|
71
74
|
/**
|
|
72
75
|
* Add the result of a client-side tool execution
|
|
73
76
|
*/
|
|
@@ -104,7 +107,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
|
104
107
|
/**
|
|
105
108
|
* Set messages manually
|
|
106
109
|
*/
|
|
107
|
-
setMessages: (messages: Array<UIMessage<TTools>>) => void;
|
|
110
|
+
setMessages: (messages: Array<UIMessage<TTools, TData>>) => void;
|
|
108
111
|
/**
|
|
109
112
|
* Clear all messages
|
|
110
113
|
*/
|
package/dist/use-chat.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createEffect, createMemo, createSignal, createUniqueId, onCleanup } from "solid-js";
|
|
2
2
|
import { ChatClient } from "@tanstack/ai-client";
|
|
3
|
-
import { parsePartialJSON } from "@tanstack/ai";
|
|
4
3
|
|
|
5
4
|
//#region src/use-chat.ts
|
|
6
5
|
function useChat(options = {}) {
|
|
@@ -13,9 +12,6 @@ function useChat(options = {}) {
|
|
|
13
12
|
const [isSubscribed, setIsSubscribed] = createSignal(false);
|
|
14
13
|
const [connectionStatus, setConnectionStatus] = createSignal("disconnected");
|
|
15
14
|
const [sessionGenerating, setSessionGenerating] = createSignal(false);
|
|
16
|
-
const [partial, setPartial] = createSignal({});
|
|
17
|
-
const [final, setFinal] = createSignal(null);
|
|
18
|
-
let rawJson = "";
|
|
19
15
|
const client = createMemo(() => {
|
|
20
16
|
return new ChatClient({
|
|
21
17
|
connection: options.connection,
|
|
@@ -25,20 +21,6 @@ function useChat(options = {}) {
|
|
|
25
21
|
forwardedProps: options.forwardedProps,
|
|
26
22
|
onResponse: (response) => options.onResponse?.(response),
|
|
27
23
|
onChunk: (chunk) => {
|
|
28
|
-
if (options.outputSchema !== void 0) {
|
|
29
|
-
if (chunk.type === "RUN_STARTED") {
|
|
30
|
-
rawJson = "";
|
|
31
|
-
setPartial({});
|
|
32
|
-
setFinal(null);
|
|
33
|
-
} else if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) {
|
|
34
|
-
rawJson += chunk.delta;
|
|
35
|
-
const progressive = parsePartialJSON(rawJson);
|
|
36
|
-
if (progressive && typeof progressive === "object") setPartial(() => progressive);
|
|
37
|
-
} else if (chunk.type === "CUSTOM" && chunk.name === "structured-output.complete") {
|
|
38
|
-
const value = chunk.value;
|
|
39
|
-
setFinal(() => value.object);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
24
|
options.onChunk?.(chunk);
|
|
43
25
|
},
|
|
44
26
|
onFinish: (message) => {
|
|
@@ -118,6 +100,22 @@ function useChat(options = {}) {
|
|
|
118
100
|
const addToolApprovalResponse = async (response) => {
|
|
119
101
|
await client().addToolApprovalResponse(response);
|
|
120
102
|
};
|
|
103
|
+
const activeStructuredPart = createMemo(() => {
|
|
104
|
+
const list = messages();
|
|
105
|
+
let lastUserIndex = -1;
|
|
106
|
+
for (let i = list.length - 1; i >= 0; i--) if (list[i].role === "user") {
|
|
107
|
+
lastUserIndex = i;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (lastUserIndex === -1) return null;
|
|
111
|
+
for (let i = list.length - 1; i > lastUserIndex; i--) {
|
|
112
|
+
const m = list[i];
|
|
113
|
+
if (m.role !== "assistant") continue;
|
|
114
|
+
const part = m.parts.find((p) => p.type === "structured-output");
|
|
115
|
+
if (part) return part;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
});
|
|
121
119
|
return {
|
|
122
120
|
messages,
|
|
123
121
|
sendMessage,
|
|
@@ -134,8 +132,16 @@ function useChat(options = {}) {
|
|
|
134
132
|
clear,
|
|
135
133
|
addToolResult,
|
|
136
134
|
addToolApprovalResponse,
|
|
137
|
-
partial
|
|
138
|
-
|
|
135
|
+
partial: createMemo(() => {
|
|
136
|
+
const part = activeStructuredPart();
|
|
137
|
+
if (!part) return {};
|
|
138
|
+
return part.partial ?? part.data ?? {};
|
|
139
|
+
}),
|
|
140
|
+
final: createMemo(() => {
|
|
141
|
+
const part = activeStructuredPart();
|
|
142
|
+
if (!part || part.status !== "complete") return null;
|
|
143
|
+
return part.data;
|
|
144
|
+
})
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
147
|
|
package/dist/use-chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport { parsePartialJSON } from '@tanstack/ai'\nimport type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n>(\n options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<\n TTools,\n TSchema\n >,\n): UseChatReturn<TTools, TSchema> {\n const hookId = createUniqueId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n\n // Structured-output state. Runtime always tracks them — the conditional\n // return type hides them from callers that didn't supply `outputSchema`.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n const [partial, setPartial] = createSignal<Partial>({} as Partial)\n const [final, setFinal] = createSignal<Final | null>(null)\n // Raw JSON accumulator — kept outside the signal to avoid extra re-runs.\n let rawJson = ''\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\n return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n forwardedProps: options.forwardedProps,\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n if (options.outputSchema !== undefined) {\n if (chunk.type === 'RUN_STARTED') {\n rawJson = ''\n setPartial({} as Partial)\n setFinal(null)\n } else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {\n rawJson += chunk.delta\n const progressive = parsePartialJSON(rawJson)\n if (progressive && typeof progressive === 'object') {\n setPartial(() => progressive as Partial)\n }\n } else if (\n chunk.type === 'CUSTOM' &&\n chunk.name === 'structured-output.complete'\n ) {\n const value = chunk.value as { object: unknown }\n setFinal(() => value.object as Final)\n }\n }\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\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 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 // Only recreate when clientId changes\n // Connection and other options are captured at creation time\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 createEffect(() => {\n client().updateOptions({\n body: options.body,\n forwardedProps: options.forwardedProps,\n })\n })\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (content: string | MultimodalContent) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n }\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\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 partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-chat.js","names":[],"sources":["../src/use-chat.ts"],"sourcesContent":["import {\n createEffect,\n createMemo,\n createSignal,\n createUniqueId,\n onCleanup,\n} from 'solid-js'\n\nimport { ChatClient } from '@tanstack/ai-client'\nimport type {\n ChatClientState,\n ConnectionStatus,\n StructuredOutputPart,\n} from '@tanstack/ai-client'\nimport type {\n AnyClientTool,\n InferSchemaType,\n ModelMessage,\n SchemaInput,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n DeepPartial,\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TSchema extends SchemaInput | undefined = undefined,\n>(\n options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<\n TTools,\n TSchema\n >,\n): UseChatReturn<TTools, TSchema> {\n const hookId = createUniqueId()\n const clientId = options.id || hookId\n\n const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(\n options.initialMessages || [],\n )\n const [isLoading, setIsLoading] = createSignal(false)\n const [error, setError] = createSignal<Error | undefined>(undefined)\n const [status, setStatus] = createSignal<ChatClientState>('ready')\n const [isSubscribed, setIsSubscribed] = createSignal(false)\n const [connectionStatus, setConnectionStatus] =\n createSignal<ConnectionStatus>('disconnected')\n const [sessionGenerating, setSessionGenerating] = createSignal(false)\n\n // Structured-output `partial` / `final` are derived from `messages` —\n // specifically from the structured-output part on the latest assistant\n // message (the one after the most recent user message). Per-turn parts\n // keep history coherent without a separate reset signal.\n type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>\n type Final = InferSchemaType<NonNullable<TSchema>>\n\n // Create ChatClient instance with callbacks to sync state.\n // Every user-provided callback is wrapped so the LATEST `options.xxx` value\n // is read at call time. Direct assignment would freeze the callback to the\n // reference we saw at creation; the wrapper lets reactive `options` or\n // in-place mutations propagate. When the user clears a callback (sets it to\n // undefined), `?.` no-ops.\n const client = createMemo(() => {\n return new ChatClient({\n connection: options.connection,\n id: clientId,\n initialMessages: options.initialMessages,\n body: options.body,\n forwardedProps: options.forwardedProps,\n onResponse: (response) => options.onResponse?.(response),\n onChunk: (chunk: StreamChunk) => {\n options.onChunk?.(chunk)\n },\n onFinish: (message) => {\n options.onFinish?.(message)\n },\n onError: (err) => {\n options.onError?.(err)\n },\n tools: options.tools,\n onCustomEvent: (eventType, data, context) =>\n options.onCustomEvent?.(eventType, data, context),\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 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 // Only recreate when clientId changes\n // Connection and other options are captured at creation time\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 createEffect(() => {\n client().updateOptions({\n body: options.body,\n forwardedProps: options.forwardedProps,\n })\n })\n\n // Sync initial messages on mount only\n // Note: initialMessages are passed to ChatClient constructor, but we also\n // set them here to ensure React state is in sync\n createEffect(() => {\n if (options.initialMessages && options.initialMessages.length > 0) {\n // Only set if current messages are empty (initial state)\n if (messages().length === 0) {\n client().setMessagesManually(options.initialMessages)\n }\n }\n }) // Only run on mount - initialMessages are handled by ChatClient constructor\n\n // Apply initial live mode immediately on hook creation.\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n\n createEffect(() => {\n if (options.live) {\n client().subscribe()\n } else {\n client().unsubscribe()\n }\n })\n\n // Cleanup on unmount: stop any in-flight requests.\n onCleanup(() => {\n if (options.live) {\n client().unsubscribe()\n } else {\n client().stop()\n }\n })\n\n // Callback options are read through `options.xxx` at call time, so reactive\n // or mutated options propagate without recreating the client.\n\n const sendMessage = async (content: string | MultimodalContent) => {\n await client().sendMessage(content)\n }\n\n const append = async (message: ModelMessage | UIMessage<TTools>) => {\n await client().append(message)\n }\n\n const reload = async () => {\n await client().reload()\n }\n\n const stop = () => {\n client().stop()\n }\n\n const clear = () => {\n client().clear()\n }\n\n const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {\n client().setMessagesManually(newMessages)\n }\n\n const addToolResult = async (result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }) => {\n await client().addToolResult(result)\n }\n\n const addToolApprovalResponse = async (response: {\n id: string\n approved: boolean\n }) => {\n await client().addToolApprovalResponse(response)\n }\n\n // The \"active\" structured-output part is on the assistant message after\n // the latest user message. When no user message exists yet, return null\n // rather than scanning history — otherwise a stale `final` from\n // `initialMessages` would leak into the value on first render.\n const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {\n const list = messages()\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]!.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1) return null\n for (let i = list.length - 1; i > lastUserIndex; i--) {\n const m = list[i]!\n if (m.role !== 'assistant') continue\n const part = m.parts.find(\n (p): p is StructuredOutputPart => p.type === 'structured-output',\n )\n if (part) return part\n }\n return null\n })\n\n const partial = createMemo<Partial>(() => {\n const part = activeStructuredPart()\n if (!part) return {} as Partial\n const v = part.partial ?? part.data\n return (v ?? {}) as Partial\n })\n\n const final = createMemo<Final | null>(() => {\n const part = activeStructuredPart()\n if (!part || part.status !== 'complete') return null\n return part.data as Final\n })\n\n // partial / final are runtime-tracked unconditionally; the conditional\n // return type hides them when no `outputSchema` is supplied.\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 partial,\n final,\n } as unknown as UseChatReturn<TTools, TSchema>\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Solid hooks for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
"streaming"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai-client": "0.
|
|
34
|
+
"@tanstack/ai-client": "0.11.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"solid-js": ">=1.9.10",
|
|
38
|
-
"@tanstack/ai": "^0.
|
|
38
|
+
"@tanstack/ai": "^0.19.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@solidjs/testing-library": "^0.8.10",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"tsdown": "^0.17.0-beta.6",
|
|
48
48
|
"typescript": "5.9.3",
|
|
49
49
|
"vitest": "^4.0.14",
|
|
50
|
-
"@tanstack/ai": "0.
|
|
50
|
+
"@tanstack/ai": "0.19.0"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"clean": "premove ./build ./dist",
|
package/src/types.ts
CHANGED
|
@@ -78,26 +78,35 @@ export type UseChatOptions<
|
|
|
78
78
|
export type UseChatReturn<
|
|
79
79
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
80
80
|
TSchema extends SchemaInput | undefined = undefined,
|
|
81
|
-
> = BaseUseChatReturn<
|
|
81
|
+
> = BaseUseChatReturn<
|
|
82
|
+
TTools,
|
|
83
|
+
TSchema extends SchemaInput ? InferSchemaType<TSchema> : unknown
|
|
84
|
+
> &
|
|
82
85
|
(TSchema extends SchemaInput
|
|
83
86
|
? {
|
|
84
87
|
/**
|
|
85
|
-
* Live progressively-parsed structured output.
|
|
88
|
+
* Live progressively-parsed structured output. Derived from the
|
|
89
|
+
* latest assistant message's structured-output part.
|
|
86
90
|
*/
|
|
87
91
|
partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>
|
|
88
92
|
/**
|
|
89
|
-
* Final, schema-validated structured output. `null` until the
|
|
90
|
-
*
|
|
93
|
+
* Final, schema-validated structured output. `null` until the latest
|
|
94
|
+
* assistant turn's structured-output part transitions to `complete`.
|
|
91
95
|
*/
|
|
92
96
|
final: Accessor<InferSchemaType<TSchema> | null>
|
|
93
97
|
}
|
|
94
98
|
: Record<never, never>)
|
|
95
99
|
|
|
96
|
-
interface BaseUseChatReturn<
|
|
100
|
+
interface BaseUseChatReturn<
|
|
101
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
102
|
+
TData = unknown,
|
|
103
|
+
> {
|
|
97
104
|
/**
|
|
98
|
-
* Current messages in the conversation
|
|
105
|
+
* Current messages in the conversation. When `outputSchema` is supplied,
|
|
106
|
+
* `messages()[i].parts.find(p => p.type === 'structured-output')` is typed
|
|
107
|
+
* by the schema — `data: T`, `partial: DeepPartial<T>`.
|
|
99
108
|
*/
|
|
100
|
-
messages: Accessor<Array<UIMessage<TTools>>>
|
|
109
|
+
messages: Accessor<Array<UIMessage<TTools, TData>>>
|
|
101
110
|
|
|
102
111
|
/**
|
|
103
112
|
* Send a message and get a response.
|
|
@@ -108,7 +117,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
|
108
117
|
/**
|
|
109
118
|
* Append a message to the conversation
|
|
110
119
|
*/
|
|
111
|
-
append: (message: ModelMessage | UIMessage<TTools>) => Promise<void>
|
|
120
|
+
append: (message: ModelMessage | UIMessage<TTools, TData>) => Promise<void>
|
|
112
121
|
|
|
113
122
|
/**
|
|
114
123
|
* Add the result of a client-side tool execution
|
|
@@ -152,7 +161,7 @@ interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
|
152
161
|
/**
|
|
153
162
|
* Set messages manually
|
|
154
163
|
*/
|
|
155
|
-
setMessages: (messages: Array<UIMessage<TTools>>) => void
|
|
164
|
+
setMessages: (messages: Array<UIMessage<TTools, TData>>) => void
|
|
156
165
|
|
|
157
166
|
/**
|
|
158
167
|
* Clear all messages
|
package/src/use-chat.ts
CHANGED
|
@@ -7,8 +7,11 @@ import {
|
|
|
7
7
|
} from 'solid-js'
|
|
8
8
|
|
|
9
9
|
import { ChatClient } from '@tanstack/ai-client'
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import type {
|
|
11
|
+
ChatClientState,
|
|
12
|
+
ConnectionStatus,
|
|
13
|
+
StructuredOutputPart,
|
|
14
|
+
} from '@tanstack/ai-client'
|
|
12
15
|
import type {
|
|
13
16
|
AnyClientTool,
|
|
14
17
|
InferSchemaType,
|
|
@@ -47,14 +50,12 @@ export function useChat<
|
|
|
47
50
|
createSignal<ConnectionStatus>('disconnected')
|
|
48
51
|
const [sessionGenerating, setSessionGenerating] = createSignal(false)
|
|
49
52
|
|
|
50
|
-
// Structured-output
|
|
51
|
-
//
|
|
53
|
+
// Structured-output `partial` / `final` are derived from `messages` —
|
|
54
|
+
// specifically from the structured-output part on the latest assistant
|
|
55
|
+
// message (the one after the most recent user message). Per-turn parts
|
|
56
|
+
// keep history coherent without a separate reset signal.
|
|
52
57
|
type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
|
|
53
58
|
type Final = InferSchemaType<NonNullable<TSchema>>
|
|
54
|
-
const [partial, setPartial] = createSignal<Partial>({} as Partial)
|
|
55
|
-
const [final, setFinal] = createSignal<Final | null>(null)
|
|
56
|
-
// Raw JSON accumulator — kept outside the signal to avoid extra re-runs.
|
|
57
|
-
let rawJson = ''
|
|
58
59
|
|
|
59
60
|
// Create ChatClient instance with callbacks to sync state.
|
|
60
61
|
// Every user-provided callback is wrapped so the LATEST `options.xxx` value
|
|
@@ -71,25 +72,6 @@ export function useChat<
|
|
|
71
72
|
forwardedProps: options.forwardedProps,
|
|
72
73
|
onResponse: (response) => options.onResponse?.(response),
|
|
73
74
|
onChunk: (chunk: StreamChunk) => {
|
|
74
|
-
if (options.outputSchema !== undefined) {
|
|
75
|
-
if (chunk.type === 'RUN_STARTED') {
|
|
76
|
-
rawJson = ''
|
|
77
|
-
setPartial({} as Partial)
|
|
78
|
-
setFinal(null)
|
|
79
|
-
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
|
|
80
|
-
rawJson += chunk.delta
|
|
81
|
-
const progressive = parsePartialJSON(rawJson)
|
|
82
|
-
if (progressive && typeof progressive === 'object') {
|
|
83
|
-
setPartial(() => progressive as Partial)
|
|
84
|
-
}
|
|
85
|
-
} else if (
|
|
86
|
-
chunk.type === 'CUSTOM' &&
|
|
87
|
-
chunk.name === 'structured-output.complete'
|
|
88
|
-
) {
|
|
89
|
-
const value = chunk.value as { object: unknown }
|
|
90
|
-
setFinal(() => value.object as Final)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
75
|
options.onChunk?.(chunk)
|
|
94
76
|
},
|
|
95
77
|
onFinish: (message) => {
|
|
@@ -218,6 +200,44 @@ export function useChat<
|
|
|
218
200
|
await client().addToolApprovalResponse(response)
|
|
219
201
|
}
|
|
220
202
|
|
|
203
|
+
// The "active" structured-output part is on the assistant message after
|
|
204
|
+
// the latest user message. When no user message exists yet, return null
|
|
205
|
+
// rather than scanning history — otherwise a stale `final` from
|
|
206
|
+
// `initialMessages` would leak into the value on first render.
|
|
207
|
+
const activeStructuredPart = createMemo<StructuredOutputPart | null>(() => {
|
|
208
|
+
const list = messages()
|
|
209
|
+
let lastUserIndex = -1
|
|
210
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
211
|
+
if (list[i]!.role === 'user') {
|
|
212
|
+
lastUserIndex = i
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (lastUserIndex === -1) return null
|
|
217
|
+
for (let i = list.length - 1; i > lastUserIndex; i--) {
|
|
218
|
+
const m = list[i]!
|
|
219
|
+
if (m.role !== 'assistant') continue
|
|
220
|
+
const part = m.parts.find(
|
|
221
|
+
(p): p is StructuredOutputPart => p.type === 'structured-output',
|
|
222
|
+
)
|
|
223
|
+
if (part) return part
|
|
224
|
+
}
|
|
225
|
+
return null
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const partial = createMemo<Partial>(() => {
|
|
229
|
+
const part = activeStructuredPart()
|
|
230
|
+
if (!part) return {} as Partial
|
|
231
|
+
const v = part.partial ?? part.data
|
|
232
|
+
return (v ?? {}) as Partial
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const final = createMemo<Final | null>(() => {
|
|
236
|
+
const part = activeStructuredPart()
|
|
237
|
+
if (!part || part.status !== 'complete') return null
|
|
238
|
+
return part.data as Final
|
|
239
|
+
})
|
|
240
|
+
|
|
221
241
|
// partial / final are runtime-tracked unconditionally; the conditional
|
|
222
242
|
// return type hides them when no `outputSchema` is supplied.
|
|
223
243
|
return {
|