@tanstack/ai-solid 0.7.2 → 0.9.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/index.d.ts +2 -2
- package/dist/types.d.ts +35 -4
- package/dist/use-chat.d.ts +2 -2
- package/dist/use-chat.js +29 -4
- package/dist/use-chat.js.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +1 -0
- package/src/types.ts +66 -17
- package/src/use-chat.ts +62 -10
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/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatRequestBody, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
|
|
1
|
+
import { ChatRequestBody, DeepPartial, UIMessage, UseChatOptions, UseChatReturn } from "./types.js";
|
|
2
2
|
import { useChat } from "./use-chat.js";
|
|
3
3
|
import { UseGenerationOptions, UseGenerationReturn, useGeneration } from "./use-generation.js";
|
|
4
4
|
import { UseGenerateImageOptions, UseGenerateImageReturn, useGenerateImage } from "./use-generate-image.js";
|
|
@@ -8,4 +8,4 @@ import { UseTranscriptionOptions, UseTranscriptionReturn, useTranscription } fro
|
|
|
8
8
|
import { UseSummarizeOptions, UseSummarizeReturn, useSummarize } from "./use-summarize.js";
|
|
9
9
|
import { UseGenerateVideoOptions, UseGenerateVideoReturn, useGenerateVideo } from "./use-generate-video.js";
|
|
10
10
|
import { AudioGenerateInput, ConnectionAdapter, FetchConnectionOptions, GenerationClientState, ImageGenerateInput, InferChatMessages, SpeechGenerateInput, SummarizeGenerateInput, TranscriptionGenerateInput, VideoGenerateInput, VideoGenerateResult, VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream } from "@tanstack/ai-client";
|
|
11
|
-
export { type AudioGenerateInput, type ChatRequestBody, type ConnectionAdapter, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type InferChatMessages, type SpeechGenerateInput, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription };
|
|
11
|
+
export { type AudioGenerateInput, type ChatRequestBody, type ConnectionAdapter, type DeepPartial, type FetchConnectionOptions, type GenerationClientState, type ImageGenerateInput, type InferChatMessages, type SpeechGenerateInput, type SummarizeGenerateInput, type TranscriptionGenerateInput, type UIMessage, type UseChatOptions, type UseChatReturn, type UseGenerateAudioOptions, type UseGenerateAudioReturn, type UseGenerateImageOptions, type UseGenerateImageReturn, type UseGenerateSpeechOptions, type UseGenerateSpeechReturn, type UseGenerateVideoOptions, type UseGenerateVideoReturn, type UseGenerationOptions, type UseGenerationReturn, type UseSummarizeOptions, type UseSummarizeReturn, type UseTranscriptionOptions, type UseTranscriptionReturn, type VideoGenerateInput, type VideoGenerateResult, type VideoStatusInfo, createChatClientOptions, fetchHttpStream, fetchServerSentEvents, stream, useChat, useGenerateAudio, useGenerateImage, useGenerateSpeech, useGenerateVideo, useGeneration, useSummarize, useTranscription };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Accessor } from "solid-js";
|
|
2
2
|
import { ChatClientOptions, ChatClientState, ChatRequestBody, ConnectionStatus, MultimodalContent, UIMessage } from "@tanstack/ai-client";
|
|
3
|
-
import { AnyClientTool, ModelMessage } from "@tanstack/ai";
|
|
3
|
+
import { AnyClientTool, InferSchemaType, ModelMessage, SchemaInput } from "@tanstack/ai";
|
|
4
4
|
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Recursive partial — every property and every nested array element is optional.
|
|
9
|
+
* Used to type the in-flight `partial` accessor while a structured-output
|
|
10
|
+
* stream is still arriving.
|
|
11
|
+
*/
|
|
12
|
+
type DeepPartial<T> = T extends ReadonlyArray<infer U> ? Array<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
|
|
7
13
|
/**
|
|
8
14
|
* Options for the useChat hook.
|
|
9
15
|
*
|
|
@@ -17,13 +23,38 @@ import { AnyClientTool, ModelMessage } from "@tanstack/ai";
|
|
|
17
23
|
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
18
24
|
* passed through to the underlying ChatClient and can be used for side effects.
|
|
19
25
|
*
|
|
26
|
+
* When `outputSchema` is supplied, the hook returns typed `partial` and `final`
|
|
27
|
+
* accessors. The schema is used purely for type inference; server-side
|
|
28
|
+
* validation still runs against the schema passed to `chat({ outputSchema })`
|
|
29
|
+
* on the server route.
|
|
30
|
+
*
|
|
20
31
|
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
21
32
|
* To update these options, remount the component or use a key prop.
|
|
22
33
|
*/
|
|
23
|
-
type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
|
|
34
|
+
type UseChatOptions<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = Omit<ChatClientOptions<TTools>, 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' | 'onSubscriptionChange' | 'onConnectionStatusChange' | 'onSessionGeneratingChange'> & {
|
|
24
35
|
live?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Standard-schema-compatible schema (Zod, Valibot, ArkType, or plain JSON
|
|
38
|
+
* Schema). Used to infer the shape of `partial` and `final`.
|
|
39
|
+
*/
|
|
40
|
+
outputSchema?: TSchema;
|
|
25
41
|
};
|
|
26
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Discriminated return shape: when `outputSchema` is supplied, the hook adds
|
|
44
|
+
* typed `partial` / `final` accessors; otherwise the return is unchanged.
|
|
45
|
+
*/
|
|
46
|
+
type UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined> = BaseUseChatReturn<TTools> & (TSchema extends SchemaInput ? {
|
|
47
|
+
/**
|
|
48
|
+
* Live progressively-parsed structured output. Resets on every new run.
|
|
49
|
+
*/
|
|
50
|
+
partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>;
|
|
51
|
+
/**
|
|
52
|
+
* Final, schema-validated structured output. `null` until the terminal
|
|
53
|
+
* `structured-output.complete` event arrives. Resets on every new run.
|
|
54
|
+
*/
|
|
55
|
+
final: Accessor<InferSchemaType<TSchema> | null>;
|
|
56
|
+
} : Record<never, never>);
|
|
57
|
+
interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
27
58
|
/**
|
|
28
59
|
* Current messages in the conversation
|
|
29
60
|
*/
|
|
@@ -99,5 +130,5 @@ interface UseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
|
99
130
|
sessionGenerating: Accessor<boolean>;
|
|
100
131
|
}
|
|
101
132
|
//#endregion
|
|
102
|
-
export { type ChatRequestBody, type UIMessage, UseChatOptions, UseChatReturn };
|
|
133
|
+
export { type ChatRequestBody, DeepPartial, type UIMessage, UseChatOptions, UseChatReturn };
|
|
103
134
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/use-chat.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { UseChatOptions, UseChatReturn } from "./types.js";
|
|
2
|
-
import { AnyClientTool } from "@tanstack/ai";
|
|
2
|
+
import { AnyClientTool, SchemaInput } from "@tanstack/ai";
|
|
3
3
|
|
|
4
4
|
//#region src/use-chat.d.ts
|
|
5
|
-
declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(options?: UseChatOptions<TTools>): UseChatReturn<TTools>;
|
|
5
|
+
declare function useChat<TTools extends ReadonlyArray<AnyClientTool> = any, TSchema extends SchemaInput | undefined = undefined>(options?: UseChatOptions<TTools, TSchema>): UseChatReturn<TTools, TSchema>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { useChat };
|
|
8
8
|
//# sourceMappingURL=use-chat.d.ts.map
|
package/dist/use-chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
3
4
|
|
|
4
5
|
//#region src/use-chat.ts
|
|
5
6
|
function useChat(options = {}) {
|
|
@@ -12,14 +13,34 @@ function useChat(options = {}) {
|
|
|
12
13
|
const [isSubscribed, setIsSubscribed] = createSignal(false);
|
|
13
14
|
const [connectionStatus, setConnectionStatus] = createSignal("disconnected");
|
|
14
15
|
const [sessionGenerating, setSessionGenerating] = createSignal(false);
|
|
16
|
+
const [partial, setPartial] = createSignal({});
|
|
17
|
+
const [final, setFinal] = createSignal(null);
|
|
18
|
+
let rawJson = "";
|
|
15
19
|
const client = createMemo(() => {
|
|
16
20
|
return new ChatClient({
|
|
17
21
|
connection: options.connection,
|
|
18
22
|
id: clientId,
|
|
19
23
|
initialMessages: options.initialMessages,
|
|
20
24
|
body: options.body,
|
|
25
|
+
forwardedProps: options.forwardedProps,
|
|
21
26
|
onResponse: (response) => options.onResponse?.(response),
|
|
22
|
-
onChunk: (chunk) =>
|
|
27
|
+
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
|
+
options.onChunk?.(chunk);
|
|
43
|
+
},
|
|
23
44
|
onFinish: (message) => {
|
|
24
45
|
options.onFinish?.(message);
|
|
25
46
|
},
|
|
@@ -53,8 +74,10 @@ function useChat(options = {}) {
|
|
|
53
74
|
});
|
|
54
75
|
}, [clientId]);
|
|
55
76
|
createEffect(() => {
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
client().updateOptions({
|
|
78
|
+
body: options.body,
|
|
79
|
+
forwardedProps: options.forwardedProps
|
|
80
|
+
});
|
|
58
81
|
});
|
|
59
82
|
createEffect(() => {
|
|
60
83
|
if (options.initialMessages && options.initialMessages.length > 0) {
|
|
@@ -110,7 +133,9 @@ function useChat(options = {}) {
|
|
|
110
133
|
setMessages: setMessagesManually,
|
|
111
134
|
clear,
|
|
112
135
|
addToolResult,
|
|
113
|
-
addToolApprovalResponse
|
|
136
|
+
addToolApprovalResponse,
|
|
137
|
+
partial,
|
|
138
|
+
final
|
|
114
139
|
};
|
|
115
140
|
}
|
|
116
141
|
|
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 type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'\nimport type { AnyClientTool, ModelMessage } from '@tanstack/ai'\nimport type {\n MultimodalContent,\n UIMessage,\n UseChatOptions,\n UseChatReturn,\n} from './types'\n\nexport function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(\n options: UseChatOptions<TTools> = {} as UseChatOptions<TTools>,\n): UseChatReturn<TTools> {\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 // 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) => options.onChunk?.(chunk),\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 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"],"mappings":";;;;AAkBA,SAAgB,QACd,UAAkC,EAAE,EACb;CACvB,MAAM,SAAS,gBAAgB;CAC/B,MAAM,WAAW,QAAQ,MAAM;CAE/B,MAAM,CAAC,UAAU,eAAe,aAC9B,QAAQ,mBAAmB,EAAE,CAC9B;CACD,MAAM,CAAC,WAAW,gBAAgB,aAAa,MAAM;CACrD,MAAM,CAAC,OAAO,YAAY,aAAgC,OAAU;CACpE,MAAM,CAAC,QAAQ,aAAa,aAA8B,QAAQ;CAClE,MAAM,CAAC,cAAc,mBAAmB,aAAa,MAAM;CAC3D,MAAM,CAAC,kBAAkB,uBACvB,aAA+B,eAAe;CAChD,MAAM,CAAC,mBAAmB,wBAAwB,aAAa,MAAM;CAQrE,MAAM,SAAS,iBAAiB;AAC9B,SAAO,IAAI,WAAW;GACpB,YAAY,QAAQ;GACpB,IAAI;GACJ,iBAAiB,QAAQ;GACzB,MAAM,QAAQ;GACd,aAAa,aAAa,QAAQ,aAAa,SAAS;GACxD,UAAU,UAAU,QAAQ,UAAU,MAAM;GAC5C,WAAW,YAAY;AACrB,YAAQ,WAAW,QAAQ;;GAE7B,UAAU,QAAQ;AAChB,YAAQ,UAAU,IAAI;;GAExB,OAAO,QAAQ;GACf,gBAAgB,WAAW,MAAM,YAC/B,QAAQ,gBAAgB,WAAW,MAAM,QAAQ;GACnD,iBAAiB,QAAQ;GACzB,mBAAmB,gBAA0C;AAC3D,gBAAY,YAAY;;GAE1B,kBAAkB,iBAA0B;AAC1C,iBAAa,aAAa;;GAE5B,iBAAiB,cAA+B;AAC9C,cAAU,UAAU;;GAEtB,gBAAgB,aAAgC;AAC9C,aAAS,SAAS;;GAEpB,uBAAuB,qBAA8B;AACnD,oBAAgB,iBAAiB;;GAEnC,2BAA2B,eAAiC;AAC1D,wBAAoB,WAAW;;GAEjC,4BAA4B,iBAA0B;AACpD,yBAAqB,aAAa;;GAErC,CAAC;IAGD,CAAC,SAAS,CAAC;AAId,oBAAmB;EACjB,MAAM,cAAc,QAAQ;AAC5B,UAAQ,CAAC,cAAc,EAAE,MAAM,aAAa,CAAC;GAC7C;AAKF,oBAAmB;AACjB,MAAI,QAAQ,mBAAmB,QAAQ,gBAAgB,SAAS,GAE9D;OAAI,UAAU,CAAC,WAAW,EACxB,SAAQ,CAAC,oBAAoB,QAAQ,gBAAgB;;GAGzD;AAGF,KAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;KAEpB,SAAQ,CAAC,aAAa;AAGxB,oBAAmB;AACjB,MAAI,QAAQ,KACV,SAAQ,CAAC,WAAW;MAEpB,SAAQ,CAAC,aAAa;GAExB;AAGF,iBAAgB;AACd,MAAI,QAAQ,KACV,SAAQ,CAAC,aAAa;MAEtB,SAAQ,CAAC,MAAM;GAEjB;CAKF,MAAM,cAAc,OAAO,YAAwC;AACjE,QAAM,QAAQ,CAAC,YAAY,QAAQ;;CAGrC,MAAM,SAAS,OAAO,YAA8C;AAClE,QAAM,QAAQ,CAAC,OAAO,QAAQ;;CAGhC,MAAM,SAAS,YAAY;AACzB,QAAM,QAAQ,CAAC,QAAQ;;CAGzB,MAAM,aAAa;AACjB,UAAQ,CAAC,MAAM;;CAGjB,MAAM,cAAc;AAClB,UAAQ,CAAC,OAAO;;CAGlB,MAAM,uBAAuB,gBAA0C;AACrE,UAAQ,CAAC,oBAAoB,YAAY;;CAG3C,MAAM,gBAAgB,OAAO,WAMvB;AACJ,QAAM,QAAQ,CAAC,cAAc,OAAO;;CAGtC,MAAM,0BAA0B,OAAO,aAGjC;AACJ,QAAM,QAAQ,CAAC,wBAAwB,SAAS;;AAGlD,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAAa;EACb;EACA;EACA;EACD"}
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-solid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Solid hooks for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,14 +31,15 @@
|
|
|
31
31
|
"streaming"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai-client": "0.
|
|
34
|
+
"@tanstack/ai-client": "0.10.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"solid-js": ">=1.9.10",
|
|
38
|
-
"@tanstack/ai": "^0.
|
|
38
|
+
"@tanstack/ai": "^0.18.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@solidjs/testing-library": "^0.8.10",
|
|
42
|
+
"@standard-schema/spec": "^1.1.0",
|
|
42
43
|
"@types/node": "^24.10.1",
|
|
43
44
|
"@vitest/coverage-v8": "4.0.14",
|
|
44
45
|
"jsdom": "^27.2.0",
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"tsdown": "^0.17.0-beta.6",
|
|
47
48
|
"typescript": "5.9.3",
|
|
48
49
|
"vitest": "^4.0.14",
|
|
49
|
-
"@tanstack/ai": "0.
|
|
50
|
+
"@tanstack/ai": "0.18.0"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"clean": "premove ./build ./dist",
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AnyClientTool,
|
|
3
|
+
InferSchemaType,
|
|
4
|
+
ModelMessage,
|
|
5
|
+
SchemaInput,
|
|
6
|
+
} from '@tanstack/ai'
|
|
2
7
|
import type {
|
|
3
8
|
ChatClientOptions,
|
|
4
9
|
ChatClientState,
|
|
@@ -12,6 +17,18 @@ import type { Accessor } from 'solid-js'
|
|
|
12
17
|
// Re-export types from ai-client
|
|
13
18
|
export type { ChatRequestBody, MultimodalContent, UIMessage }
|
|
14
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Recursive partial — every property and every nested array element is optional.
|
|
22
|
+
* Used to type the in-flight `partial` accessor while a structured-output
|
|
23
|
+
* stream is still arriving.
|
|
24
|
+
*/
|
|
25
|
+
export type DeepPartial<T> =
|
|
26
|
+
T extends ReadonlyArray<infer U>
|
|
27
|
+
? Array<DeepPartial<U>>
|
|
28
|
+
: T extends object
|
|
29
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
30
|
+
: T
|
|
31
|
+
|
|
15
32
|
/**
|
|
16
33
|
* Options for the useChat hook.
|
|
17
34
|
*
|
|
@@ -25,26 +42,58 @@ export type { ChatRequestBody, MultimodalContent, UIMessage }
|
|
|
25
42
|
* All other callbacks (onResponse, onChunk, onFinish, onError) are
|
|
26
43
|
* passed through to the underlying ChatClient and can be used for side effects.
|
|
27
44
|
*
|
|
45
|
+
* When `outputSchema` is supplied, the hook returns typed `partial` and `final`
|
|
46
|
+
* accessors. The schema is used purely for type inference; server-side
|
|
47
|
+
* validation still runs against the schema passed to `chat({ outputSchema })`
|
|
48
|
+
* on the server route.
|
|
49
|
+
*
|
|
28
50
|
* Note: Connection and body changes will recreate the ChatClient instance.
|
|
29
51
|
* To update these options, remount the component or use a key prop.
|
|
30
52
|
*/
|
|
31
|
-
export type UseChatOptions<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
export type UseChatOptions<
|
|
54
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
55
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
56
|
+
> = Omit<
|
|
57
|
+
ChatClientOptions<TTools>,
|
|
58
|
+
| 'onMessagesChange'
|
|
59
|
+
| 'onLoadingChange'
|
|
60
|
+
| 'onErrorChange'
|
|
61
|
+
| 'onStatusChange'
|
|
62
|
+
| 'onSubscriptionChange'
|
|
63
|
+
| 'onConnectionStatusChange'
|
|
64
|
+
| 'onSessionGeneratingChange'
|
|
65
|
+
> & {
|
|
66
|
+
live?: boolean
|
|
67
|
+
/**
|
|
68
|
+
* Standard-schema-compatible schema (Zod, Valibot, ArkType, or plain JSON
|
|
69
|
+
* Schema). Used to infer the shape of `partial` and `final`.
|
|
70
|
+
*/
|
|
71
|
+
outputSchema?: TSchema
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Discriminated return shape: when `outputSchema` is supplied, the hook adds
|
|
76
|
+
* typed `partial` / `final` accessors; otherwise the return is unchanged.
|
|
77
|
+
*/
|
|
78
|
+
export type UseChatReturn<
|
|
46
79
|
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
47
|
-
|
|
80
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
81
|
+
> = BaseUseChatReturn<TTools> &
|
|
82
|
+
(TSchema extends SchemaInput
|
|
83
|
+
? {
|
|
84
|
+
/**
|
|
85
|
+
* Live progressively-parsed structured output. Resets on every new run.
|
|
86
|
+
*/
|
|
87
|
+
partial: Accessor<DeepPartial<InferSchemaType<TSchema>>>
|
|
88
|
+
/**
|
|
89
|
+
* Final, schema-validated structured output. `null` until the terminal
|
|
90
|
+
* `structured-output.complete` event arrives. Resets on every new run.
|
|
91
|
+
*/
|
|
92
|
+
final: Accessor<InferSchemaType<TSchema> | null>
|
|
93
|
+
}
|
|
94
|
+
: Record<never, never>)
|
|
95
|
+
|
|
96
|
+
interface BaseUseChatReturn<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
48
97
|
/**
|
|
49
98
|
* Current messages in the conversation
|
|
50
99
|
*/
|
package/src/use-chat.ts
CHANGED
|
@@ -7,18 +7,32 @@ import {
|
|
|
7
7
|
} from 'solid-js'
|
|
8
8
|
|
|
9
9
|
import { ChatClient } from '@tanstack/ai-client'
|
|
10
|
+
import { parsePartialJSON } from '@tanstack/ai'
|
|
10
11
|
import type { ChatClientState, ConnectionStatus } from '@tanstack/ai-client'
|
|
11
|
-
import type { AnyClientTool, ModelMessage } from '@tanstack/ai'
|
|
12
12
|
import type {
|
|
13
|
+
AnyClientTool,
|
|
14
|
+
InferSchemaType,
|
|
15
|
+
ModelMessage,
|
|
16
|
+
SchemaInput,
|
|
17
|
+
StreamChunk,
|
|
18
|
+
} from '@tanstack/ai'
|
|
19
|
+
import type {
|
|
20
|
+
DeepPartial,
|
|
13
21
|
MultimodalContent,
|
|
14
22
|
UIMessage,
|
|
15
23
|
UseChatOptions,
|
|
16
24
|
UseChatReturn,
|
|
17
25
|
} from './types'
|
|
18
26
|
|
|
19
|
-
export function useChat<
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
export function useChat<
|
|
28
|
+
TTools extends ReadonlyArray<AnyClientTool> = any,
|
|
29
|
+
TSchema extends SchemaInput | undefined = undefined,
|
|
30
|
+
>(
|
|
31
|
+
options: UseChatOptions<TTools, TSchema> = {} as UseChatOptions<
|
|
32
|
+
TTools,
|
|
33
|
+
TSchema
|
|
34
|
+
>,
|
|
35
|
+
): UseChatReturn<TTools, TSchema> {
|
|
22
36
|
const hookId = createUniqueId()
|
|
23
37
|
const clientId = options.id || hookId
|
|
24
38
|
|
|
@@ -33,6 +47,15 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
33
47
|
createSignal<ConnectionStatus>('disconnected')
|
|
34
48
|
const [sessionGenerating, setSessionGenerating] = createSignal(false)
|
|
35
49
|
|
|
50
|
+
// Structured-output state. Runtime always tracks them — the conditional
|
|
51
|
+
// return type hides them from callers that didn't supply `outputSchema`.
|
|
52
|
+
type Partial = DeepPartial<InferSchemaType<NonNullable<TSchema>>>
|
|
53
|
+
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
|
+
|
|
36
59
|
// Create ChatClient instance with callbacks to sync state.
|
|
37
60
|
// Every user-provided callback is wrapped so the LATEST `options.xxx` value
|
|
38
61
|
// is read at call time. Direct assignment would freeze the callback to the
|
|
@@ -45,8 +68,30 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
45
68
|
id: clientId,
|
|
46
69
|
initialMessages: options.initialMessages,
|
|
47
70
|
body: options.body,
|
|
71
|
+
forwardedProps: options.forwardedProps,
|
|
48
72
|
onResponse: (response) => options.onResponse?.(response),
|
|
49
|
-
onChunk: (chunk) =>
|
|
73
|
+
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
|
+
options.onChunk?.(chunk)
|
|
94
|
+
},
|
|
50
95
|
onFinish: (message) => {
|
|
51
96
|
options.onFinish?.(message)
|
|
52
97
|
},
|
|
@@ -83,11 +128,14 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
83
128
|
// Connection and other options are captured at creation time
|
|
84
129
|
}, [clientId])
|
|
85
130
|
|
|
86
|
-
// Sync body changes to the client
|
|
87
|
-
//
|
|
131
|
+
// Sync body / forwardedProps changes to the client.
|
|
132
|
+
// Both populate the same wire payload; `forwardedProps` is preferred
|
|
133
|
+
// and `body` is deprecated but still supported.
|
|
88
134
|
createEffect(() => {
|
|
89
|
-
|
|
90
|
-
|
|
135
|
+
client().updateOptions({
|
|
136
|
+
body: options.body,
|
|
137
|
+
forwardedProps: options.forwardedProps,
|
|
138
|
+
})
|
|
91
139
|
})
|
|
92
140
|
|
|
93
141
|
// Sync initial messages on mount only
|
|
@@ -170,6 +218,8 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
170
218
|
await client().addToolApprovalResponse(response)
|
|
171
219
|
}
|
|
172
220
|
|
|
221
|
+
// partial / final are runtime-tracked unconditionally; the conditional
|
|
222
|
+
// return type hides them when no `outputSchema` is supplied.
|
|
173
223
|
return {
|
|
174
224
|
messages,
|
|
175
225
|
sendMessage,
|
|
@@ -186,5 +236,7 @@ export function useChat<TTools extends ReadonlyArray<AnyClientTool> = any>(
|
|
|
186
236
|
clear,
|
|
187
237
|
addToolResult,
|
|
188
238
|
addToolApprovalResponse,
|
|
189
|
-
|
|
239
|
+
partial,
|
|
240
|
+
final,
|
|
241
|
+
} as unknown as UseChatReturn<TTools, TSchema>
|
|
190
242
|
}
|