@tanstack/ai-solid 0.8.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/README.md +5 -2
- package/dist/types.d.ts +12 -9
- package/dist/use-chat.js +31 -22
- package/dist/use-chat.js.map +1 -1
- package/package.json +4 -4
- package/src/types.ts +18 -9
- package/src/use-chat.ts +55 -31
package/README.md
CHANGED
|
@@ -71,7 +71,10 @@ interface UseChatOptions {
|
|
|
71
71
|
// Configuration
|
|
72
72
|
initialMessages?: UIMessage[] // Starting messages
|
|
73
73
|
id?: string // Unique chat ID
|
|
74
|
-
|
|
74
|
+
threadId?: string // AG-UI thread ID (auto-generated if omitted)
|
|
75
|
+
forwardedProps?: Record<string, any> // Forwarded to AG-UI RunAgentInput.forwardedProps
|
|
76
|
+
/** @deprecated Use `forwardedProps` instead. */
|
|
77
|
+
body?: Record<string, any>
|
|
75
78
|
|
|
76
79
|
// Callbacks
|
|
77
80
|
onResponse?: (response?: Response) => void
|
|
@@ -256,7 +259,7 @@ const chat = useChat({
|
|
|
256
259
|
'X-Custom-Header': 'value',
|
|
257
260
|
},
|
|
258
261
|
}),
|
|
259
|
-
|
|
262
|
+
forwardedProps: {
|
|
260
263
|
userId: '123',
|
|
261
264
|
sessionId: 'abc',
|
|
262
265
|
},
|
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,31 +12,15 @@ 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,
|
|
22
18
|
id: clientId,
|
|
23
19
|
initialMessages: options.initialMessages,
|
|
24
20
|
body: options.body,
|
|
21
|
+
forwardedProps: options.forwardedProps,
|
|
25
22
|
onResponse: (response) => options.onResponse?.(response),
|
|
26
23
|
onChunk: (chunk) => {
|
|
27
|
-
if (options.outputSchema !== void 0) {
|
|
28
|
-
if (chunk.type === "RUN_STARTED") {
|
|
29
|
-
rawJson = "";
|
|
30
|
-
setPartial({});
|
|
31
|
-
setFinal(null);
|
|
32
|
-
} else if (chunk.type === "TEXT_MESSAGE_CONTENT" && chunk.delta) {
|
|
33
|
-
rawJson += chunk.delta;
|
|
34
|
-
const progressive = parsePartialJSON(rawJson);
|
|
35
|
-
if (progressive && typeof progressive === "object") setPartial(() => progressive);
|
|
36
|
-
} else if (chunk.type === "CUSTOM" && chunk.name === "structured-output.complete") {
|
|
37
|
-
const value = chunk.value;
|
|
38
|
-
setFinal(() => value.object);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
24
|
options.onChunk?.(chunk);
|
|
42
25
|
},
|
|
43
26
|
onFinish: (message) => {
|
|
@@ -73,8 +56,10 @@ function useChat(options = {}) {
|
|
|
73
56
|
});
|
|
74
57
|
}, [clientId]);
|
|
75
58
|
createEffect(() => {
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
client().updateOptions({
|
|
60
|
+
body: options.body,
|
|
61
|
+
forwardedProps: options.forwardedProps
|
|
62
|
+
});
|
|
78
63
|
});
|
|
79
64
|
createEffect(() => {
|
|
80
65
|
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
@@ -115,6 +100,22 @@ function useChat(options = {}) {
|
|
|
115
100
|
const addToolApprovalResponse = async (response) => {
|
|
116
101
|
await client().addToolApprovalResponse(response);
|
|
117
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
|
+
});
|
|
118
119
|
return {
|
|
119
120
|
messages,
|
|
120
121
|
sendMessage,
|
|
@@ -131,8 +132,16 @@ function useChat(options = {}) {
|
|
|
131
132
|
clear,
|
|
132
133
|
addToolResult,
|
|
133
134
|
addToolApprovalResponse,
|
|
134
|
-
partial
|
|
135
|
-
|
|
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
|
+
})
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
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 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 changes to the client\n // This allows dynamic body values (like model selection) to be updated without recreating the client\n createEffect(() => {\n const currentBody = options.body\n client().updateOptions({ body: currentBody })\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
|
|
@@ -68,27 +69,9 @@ export function useChat<
|
|
|
68
69
|
id: clientId,
|
|
69
70
|
initialMessages: options.initialMessages,
|
|
70
71
|
body: options.body,
|
|
72
|
+
forwardedProps: options.forwardedProps,
|
|
71
73
|
onResponse: (response) => options.onResponse?.(response),
|
|
72
74
|
onChunk: (chunk: StreamChunk) => {
|
|
73
|
-
if (options.outputSchema !== undefined) {
|
|
74
|
-
if (chunk.type === 'RUN_STARTED') {
|
|
75
|
-
rawJson = ''
|
|
76
|
-
setPartial({} as Partial)
|
|
77
|
-
setFinal(null)
|
|
78
|
-
} else if (chunk.type === 'TEXT_MESSAGE_CONTENT' && chunk.delta) {
|
|
79
|
-
rawJson += chunk.delta
|
|
80
|
-
const progressive = parsePartialJSON(rawJson)
|
|
81
|
-
if (progressive && typeof progressive === 'object') {
|
|
82
|
-
setPartial(() => progressive as Partial)
|
|
83
|
-
}
|
|
84
|
-
} else if (
|
|
85
|
-
chunk.type === 'CUSTOM' &&
|
|
86
|
-
chunk.name === 'structured-output.complete'
|
|
87
|
-
) {
|
|
88
|
-
const value = chunk.value as { object: unknown }
|
|
89
|
-
setFinal(() => value.object as Final)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
75
|
options.onChunk?.(chunk)
|
|
93
76
|
},
|
|
94
77
|
onFinish: (message) => {
|
|
@@ -127,11 +110,14 @@ export function useChat<
|
|
|
127
110
|
// Connection and other options are captured at creation time
|
|
128
111
|
}, [clientId])
|
|
129
112
|
|
|
130
|
-
// Sync body changes to the client
|
|
131
|
-
//
|
|
113
|
+
// Sync body / forwardedProps changes to the client.
|
|
114
|
+
// Both populate the same wire payload; `forwardedProps` is preferred
|
|
115
|
+
// and `body` is deprecated but still supported.
|
|
132
116
|
createEffect(() => {
|
|
133
|
-
|
|
134
|
-
|
|
117
|
+
client().updateOptions({
|
|
118
|
+
body: options.body,
|
|
119
|
+
forwardedProps: options.forwardedProps,
|
|
120
|
+
})
|
|
135
121
|
})
|
|
136
122
|
|
|
137
123
|
// Sync initial messages on mount only
|
|
@@ -214,6 +200,44 @@ export function useChat<
|
|
|
214
200
|
await client().addToolApprovalResponse(response)
|
|
215
201
|
}
|
|
216
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
|
+
|
|
217
241
|
// partial / final are runtime-tracked unconditionally; the conditional
|
|
218
242
|
// return type hides them when no `outputSchema` is supplied.
|
|
219
243
|
return {
|