@tanstack/ai-solid 0.15.1 → 0.16.1

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.
Files changed (38) hide show
  1. package/README.md +17 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.js +2 -2
  4. package/dist/types.d.ts +29 -3
  5. package/dist/use-chat.js +81 -5
  6. package/dist/use-chat.js.map +1 -1
  7. package/dist/use-generate-audio.d.ts +9 -10
  8. package/dist/use-generate-audio.js +4 -11
  9. package/dist/use-generate-audio.js.map +1 -1
  10. package/dist/use-generate-image.d.ts +9 -10
  11. package/dist/use-generate-image.js +4 -11
  12. package/dist/use-generate-image.js.map +1 -1
  13. package/dist/use-generate-speech.d.ts +9 -10
  14. package/dist/use-generate-speech.js +4 -11
  15. package/dist/use-generate-speech.js.map +1 -1
  16. package/dist/use-generate-video.d.ts +50 -4
  17. package/dist/use-generate-video.js +54 -23
  18. package/dist/use-generate-video.js.map +1 -1
  19. package/dist/use-generation.d.ts +59 -6
  20. package/dist/use-generation.js +43 -19
  21. package/dist/use-generation.js.map +1 -1
  22. package/dist/use-summarize.d.ts +9 -10
  23. package/dist/use-summarize.js +4 -11
  24. package/dist/use-summarize.js.map +1 -1
  25. package/dist/use-transcription.d.ts +9 -10
  26. package/dist/use-transcription.js +4 -11
  27. package/dist/use-transcription.js.map +1 -1
  28. package/package.json +6 -6
  29. package/src/index.ts +10 -0
  30. package/src/types.ts +44 -0
  31. package/src/use-chat.ts +123 -6
  32. package/src/use-generate-audio.ts +33 -23
  33. package/src/use-generate-image.ts +33 -23
  34. package/src/use-generate-speech.ts +32 -23
  35. package/src/use-generate-video.ts +120 -25
  36. package/src/use-generation.ts +129 -25
  37. package/src/use-summarize.ts +34 -23
  38. package/src/use-transcription.ts +33 -24
@@ -1,5 +1,6 @@
1
+ import { UseGenerationOptions, UseGenerationReturn } from "./use-generation.js";
1
2
  import { Accessor } from "solid-js";
2
- import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, InferGenerationOutputFromReturn, SummarizeGenerateInput } from "@tanstack/ai-client";
3
+ import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, GenerationPersistenceOptions, InferGenerationOutputFromReturn, SummarizeGenerateInput } from "@tanstack/ai-client";
3
4
  import { StreamChunk, SummarizationResult } from "@tanstack/ai";
4
5
 
5
6
  //#region src/use-summarize.d.ts
@@ -9,12 +10,14 @@ import { StreamChunk, SummarizationResult } from "@tanstack/ai";
9
10
  *
10
11
  * @template TOutput - The transformed output type (defaults to SummarizationResult)
11
12
  */
12
- interface UseSummarizeOptions<TOutput = SummarizationResult> {
13
+ interface UseSummarizeOptions<TOutput = SummarizationResult> extends Pick<UseGenerationOptions<SummarizeGenerateInput, SummarizationResult, TOutput>, 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'> {
13
14
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
14
15
  connection?: ConnectConnectionAdapter;
15
16
  /** Direct async function for summarization */
16
17
  fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>;
17
- /** Unique identifier for this generation instance */
18
+ /**
19
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
20
+ */
18
21
  id?: string;
19
22
  /** Additional body parameters to send with connect-based adapter requests */
20
23
  body?: Record<string, any>;
@@ -40,7 +43,7 @@ interface UseSummarizeOptions<TOutput = SummarizationResult> {
40
43
  *
41
44
  * @template TOutput - The transformed output type (defaults to SummarizationResult)
42
45
  */
43
- interface UseSummarizeReturn<TOutput = SummarizationResult> {
46
+ interface UseSummarizeReturn<TOutput = SummarizationResult> extends Omit<UseGenerationReturn<TOutput>, 'generate'> {
44
47
  /** Trigger summarization */
45
48
  generate: (input: SummarizeGenerateInput) => Promise<void>;
46
49
  /** The summarization result, or null */
@@ -51,10 +54,6 @@ interface UseSummarizeReturn<TOutput = SummarizationResult> {
51
54
  error: Accessor<Error | undefined>;
52
55
  /** Current state of the generation */
53
56
  status: Accessor<GenerationClientState>;
54
- /** Abort the current summarization */
55
- stop: () => void;
56
- /** Clear result, error, and return to idle */
57
- reset: () => void;
58
57
  }
59
58
  /**
60
59
  * Solid hook for summarizing text using AI models.
@@ -85,9 +84,9 @@ interface UseSummarizeReturn<TOutput = SummarizationResult> {
85
84
  * }
86
85
  * ```
87
86
  */
88
- declare function useSummarize<TTransformed = void>(options: Omit<UseSummarizeOptions, 'onResult'> & {
87
+ declare function useSummarize<TTransformed = void>(options: Omit<UseSummarizeOptions, 'onResult' | 'persistence' | 'threadId' | 'id'> & {
89
88
  onResult?: (result: SummarizationResult) => TTransformed;
90
- }): UseSummarizeReturn<InferGenerationOutputFromReturn<SummarizationResult, TTransformed>>;
89
+ } & GenerationPersistenceOptions): UseSummarizeReturn<InferGenerationOutputFromReturn<SummarizationResult, TTransformed>>;
91
90
  //#endregion
92
91
  export { UseSummarizeOptions, UseSummarizeReturn, useSummarize };
93
92
  //# sourceMappingURL=use-summarize.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { useGeneration } from "./use-generation.js";
2
+ import { reconstructSummarizeResult } from "@tanstack/ai-client";
2
3
 
3
4
  //#region src/use-summarize.ts
4
5
  /**
@@ -37,19 +38,11 @@ function useSummarize(options) {
37
38
  hookName: "useSummarize",
38
39
  outputKind: "text"
39
40
  };
40
- const { generate, result, isLoading, error, status, stop, reset } = useGeneration({
41
+ return useGeneration({
41
42
  ...options,
42
- devtools
43
+ devtools,
44
+ reconstructResult: reconstructSummarizeResult
43
45
  });
44
- return {
45
- generate,
46
- result,
47
- isLoading,
48
- error,
49
- status,
50
- stop,
51
- reset
52
- };
53
46
  }
54
47
 
55
48
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-summarize.js","names":[],"sources":["../src/use-summarize.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport type { StreamChunk, SummarizationResult } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutputFromReturn,\n SummarizeGenerateInput,\n} from '@tanstack/ai-client'\nimport type { Accessor } from 'solid-js'\n\n/**\n * Options for the useSummarize hook.\n *\n * @template TOutput - The transformed output type (defaults to SummarizationResult)\n */\nexport interface UseSummarizeOptions<TOutput = SummarizationResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for summarization */\n fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>\n /** Unique identifier for this generation instance */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * Callback when summarization is complete. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: SummarizationResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useSummarize hook.\n *\n * @template TOutput - The transformed output type (defaults to SummarizationResult)\n */\nexport interface UseSummarizeReturn<TOutput = SummarizationResult> {\n /** Trigger summarization */\n generate: (input: SummarizeGenerateInput) => Promise<void>\n /** The summarization result, or null */\n result: Accessor<TOutput | null>\n /** Whether summarization is in progress */\n isLoading: Accessor<boolean>\n /** Current error, if any */\n error: Accessor<Error | undefined>\n /** Current state of the generation */\n status: Accessor<GenerationClientState>\n /** Abort the current summarization */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n}\n\n/**\n * Solid hook for summarizing text using AI models.\n *\n * @example\n * ```tsx\n * import { useSummarize } from '@tanstack/ai-solid'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function Summarizer() {\n * const { generate, result, isLoading } = useSummarize({\n * connection: fetchServerSentEvents('/api/summarize'),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({\n * text: 'Long article text...',\n * style: 'bullet-points',\n * maxLength: 200,\n * })}>\n * Summarize\n * </button>\n * {isLoading() && <p>Summarizing...</p>}\n * {result() && <p>{result()!.summary}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useSummarize<TTransformed = void>(\n options: Omit<UseSummarizeOptions, 'onResult'> & {\n onResult?: (result: SummarizationResult) => TTransformed\n },\n): UseSummarizeReturn<\n InferGenerationOutputFromReturn<SummarizationResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useSummarize',\n outputKind: 'text' as const,\n }\n const { generate, result, isLoading, error, status, stop, reset } =\n useGeneration<SummarizeGenerateInput, SummarizationResult, TTransformed>({\n ...options,\n devtools,\n })\n\n return {\n generate: generate as (input: SummarizeGenerateInput) => Promise<void>,\n result,\n isLoading,\n error,\n status,\n stop,\n reset,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+FA,SAAgB,aACd,SAKA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;EACb;CACD,MAAM,EAAE,UAAU,QAAQ,WAAW,OAAO,QAAQ,MAAM,UACxD,cAAyE;EACvE,GAAG;EACH;EACD,CAAC;AAEJ,QAAO;EACK;EACV;EACA;EACA;EACA;EACA;EACA;EACD"}
1
+ {"version":3,"file":"use-summarize.js","names":[],"sources":["../src/use-summarize.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport { reconstructSummarizeResult } from '@tanstack/ai-client'\nimport type {\n UseGenerationOptions,\n UseGenerationReturn,\n} from './use-generation'\nimport type { StreamChunk, SummarizationResult } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n InferGenerationOutputFromReturn,\n SummarizeGenerateInput,\n} from '@tanstack/ai-client'\nimport type { Accessor } from 'solid-js'\n\n/**\n * Options for the useSummarize hook.\n *\n * @template TOutput - The transformed output type (defaults to SummarizationResult)\n */\nexport interface UseSummarizeOptions<\n TOutput = SummarizationResult,\n> extends Pick<\n UseGenerationOptions<SummarizeGenerateInput, SummarizationResult, TOutput>,\n 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'\n> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for summarization */\n fetcher?: GenerationFetcher<SummarizeGenerateInput, SummarizationResult>\n /**\n * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).\n */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * Callback when summarization is complete. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: SummarizationResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useSummarize hook.\n *\n * @template TOutput - The transformed output type (defaults to SummarizationResult)\n */\nexport interface UseSummarizeReturn<TOutput = SummarizationResult> extends Omit<\n UseGenerationReturn<TOutput>,\n 'generate'\n> {\n /** Trigger summarization */\n generate: (input: SummarizeGenerateInput) => Promise<void>\n /** The summarization result, or null */\n result: Accessor<TOutput | null>\n /** Whether summarization is in progress */\n isLoading: Accessor<boolean>\n /** Current error, if any */\n error: Accessor<Error | undefined>\n /** Current state of the generation */\n status: Accessor<GenerationClientState>\n}\n\n/**\n * Solid hook for summarizing text using AI models.\n *\n * @example\n * ```tsx\n * import { useSummarize } from '@tanstack/ai-solid'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function Summarizer() {\n * const { generate, result, isLoading } = useSummarize({\n * connection: fetchServerSentEvents('/api/summarize'),\n * })\n *\n * return (\n * <div>\n * <button onClick={() => generate({\n * text: 'Long article text...',\n * style: 'bullet-points',\n * maxLength: 200,\n * })}>\n * Summarize\n * </button>\n * {isLoading() && <p>Summarizing...</p>}\n * {result() && <p>{result()!.summary}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useSummarize<TTransformed = void>(\n options: Omit<\n UseSummarizeOptions,\n 'onResult' | 'persistence' | 'threadId' | 'id'\n > & {\n onResult?: (result: SummarizationResult) => TTransformed\n } & GenerationPersistenceOptions,\n): UseSummarizeReturn<\n InferGenerationOutputFromReturn<SummarizationResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useSummarize',\n outputKind: 'text' as const,\n }\n const generation = useGeneration<\n SummarizeGenerateInput,\n SummarizationResult,\n TTransformed\n >({\n ...options,\n devtools,\n reconstructResult: reconstructSummarizeResult,\n })\n\n return generation\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2GA,SAAgB,aACd,SAQA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;EACb;AAWD,QAVmB,cAIjB;EACA,GAAG;EACH;EACA,mBAAmB;EACpB,CAAC"}
@@ -1,5 +1,6 @@
1
+ import { UseGenerationOptions, UseGenerationReturn } from "./use-generation.js";
1
2
  import { Accessor } from "solid-js";
2
- import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, InferGenerationOutputFromReturn, TranscriptionGenerateInput } from "@tanstack/ai-client";
3
+ import { AIDevtoolsDisplayOptions, ConnectConnectionAdapter, GenerationClientState, GenerationFetcher, GenerationPersistenceOptions, InferGenerationOutputFromReturn, TranscriptionGenerateInput } from "@tanstack/ai-client";
3
4
  import { StreamChunk, TranscriptionResult } from "@tanstack/ai";
4
5
 
5
6
  //#region src/use-transcription.d.ts
@@ -9,12 +10,14 @@ import { StreamChunk, TranscriptionResult } from "@tanstack/ai";
9
10
  *
10
11
  * @template TOutput - The transformed output type (defaults to TranscriptionResult)
11
12
  */
12
- interface UseTranscriptionOptions<TOutput = TranscriptionResult> {
13
+ interface UseTranscriptionOptions<TOutput = TranscriptionResult> extends Pick<UseGenerationOptions<TranscriptionGenerateInput, TranscriptionResult, TOutput>, 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'> {
13
14
  /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */
14
15
  connection?: ConnectConnectionAdapter;
15
16
  /** Direct async function for transcription */
16
17
  fetcher?: GenerationFetcher<TranscriptionGenerateInput, TranscriptionResult>;
17
- /** Unique identifier for this generation instance */
18
+ /**
19
+ * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).
20
+ */
18
21
  id?: string;
19
22
  /** Additional body parameters to send with connect-based adapter requests */
20
23
  body?: Record<string, any>;
@@ -40,7 +43,7 @@ interface UseTranscriptionOptions<TOutput = TranscriptionResult> {
40
43
  *
41
44
  * @template TOutput - The transformed output type (defaults to TranscriptionResult)
42
45
  */
43
- interface UseTranscriptionReturn<TOutput = TranscriptionResult> {
46
+ interface UseTranscriptionReturn<TOutput = TranscriptionResult> extends Omit<UseGenerationReturn<TOutput>, 'generate'> {
44
47
  /** Trigger transcription */
45
48
  generate: (input: TranscriptionGenerateInput) => Promise<void>;
46
49
  /** The transcription result, or null */
@@ -51,10 +54,6 @@ interface UseTranscriptionReturn<TOutput = TranscriptionResult> {
51
54
  error: Accessor<Error | undefined>;
52
55
  /** Current state of the generation */
53
56
  status: Accessor<GenerationClientState>;
54
- /** Abort the current transcription */
55
- stop: () => void;
56
- /** Clear result, error, and return to idle */
57
- reset: () => void;
58
57
  }
59
58
  /**
60
59
  * Solid hook for transcribing audio to text using AI models.
@@ -91,9 +90,9 @@ interface UseTranscriptionReturn<TOutput = TranscriptionResult> {
91
90
  * }
92
91
  * ```
93
92
  */
94
- declare function useTranscription<TTransformed = void>(options: Omit<UseTranscriptionOptions, 'onResult'> & {
93
+ declare function useTranscription<TTransformed = void>(options: Omit<UseTranscriptionOptions, 'onResult' | 'persistence' | 'threadId' | 'id'> & {
95
94
  onResult?: (result: TranscriptionResult) => TTransformed;
96
- }): UseTranscriptionReturn<InferGenerationOutputFromReturn<TranscriptionResult, TTransformed>>;
95
+ } & GenerationPersistenceOptions): UseTranscriptionReturn<InferGenerationOutputFromReturn<TranscriptionResult, TTransformed>>;
97
96
  //#endregion
98
97
  export { UseTranscriptionOptions, UseTranscriptionReturn, useTranscription };
99
98
  //# sourceMappingURL=use-transcription.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { useGeneration } from "./use-generation.js";
2
+ import { reconstructTranscriptionResult } from "@tanstack/ai-client";
2
3
 
3
4
  //#region src/use-transcription.ts
4
5
  /**
@@ -43,19 +44,11 @@ function useTranscription(options) {
43
44
  hookName: "useTranscription",
44
45
  outputKind: "text"
45
46
  };
46
- const { generate, result, isLoading, error, status, stop, reset } = useGeneration({
47
+ return useGeneration({
47
48
  ...options,
48
- devtools
49
+ devtools,
50
+ reconstructResult: reconstructTranscriptionResult
49
51
  });
50
- return {
51
- generate,
52
- result,
53
- isLoading,
54
- error,
55
- status,
56
- stop,
57
- reset
58
- };
59
52
  }
60
53
 
61
54
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-transcription.js","names":[],"sources":["../src/use-transcription.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport type { StreamChunk, TranscriptionResult } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n InferGenerationOutputFromReturn,\n TranscriptionGenerateInput,\n} from '@tanstack/ai-client'\nimport type { Accessor } from 'solid-js'\n\n/**\n * Options for the useTranscription hook.\n *\n * @template TOutput - The transformed output type (defaults to TranscriptionResult)\n */\nexport interface UseTranscriptionOptions<TOutput = TranscriptionResult> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for transcription */\n fetcher?: GenerationFetcher<TranscriptionGenerateInput, TranscriptionResult>\n /** Unique identifier for this generation instance */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * Callback when transcription is complete. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TranscriptionResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useTranscription hook.\n *\n * @template TOutput - The transformed output type (defaults to TranscriptionResult)\n */\nexport interface UseTranscriptionReturn<TOutput = TranscriptionResult> {\n /** Trigger transcription */\n generate: (input: TranscriptionGenerateInput) => Promise<void>\n /** The transcription result, or null */\n result: Accessor<TOutput | null>\n /** Whether transcription is in progress */\n isLoading: Accessor<boolean>\n /** Current error, if any */\n error: Accessor<Error | undefined>\n /** Current state of the generation */\n status: Accessor<GenerationClientState>\n /** Abort the current transcription */\n stop: () => void\n /** Clear result, error, and return to idle */\n reset: () => void\n}\n\n/**\n * Solid hook for transcribing audio to text using AI models.\n *\n * @example\n * ```tsx\n * import { useTranscription } from '@tanstack/ai-solid'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function Transcriber() {\n * const { generate, result, isLoading } = useTranscription({\n * connection: fetchServerSentEvents('/api/transcribe'),\n * })\n *\n * const handleFile = (e: Event) => {\n * const input = e.target as HTMLInputElement\n * const file = input.files?.[0]\n * if (file) {\n * const reader = new FileReader()\n * reader.onload = () => {\n * generate({ audio: reader.result as string, language: 'en' })\n * }\n * reader.readAsDataURL(file)\n * }\n * }\n *\n * return (\n * <div>\n * <input type=\"file\" accept=\"audio/*\" onChange={handleFile} />\n * {isLoading() && <p>Transcribing...</p>}\n * {result() && <p>{result()!.text}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useTranscription<TTransformed = void>(\n options: Omit<UseTranscriptionOptions, 'onResult'> & {\n onResult?: (result: TranscriptionResult) => TTransformed\n },\n): UseTranscriptionReturn<\n InferGenerationOutputFromReturn<TranscriptionResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useTranscription',\n outputKind: 'text' as const,\n }\n const { generate, result, isLoading, error, status, stop, reset } =\n useGeneration<\n TranscriptionGenerateInput,\n TranscriptionResult,\n TTransformed\n >({ ...options, devtools })\n\n return {\n generate: generate as (input: TranscriptionGenerateInput) => Promise<void>,\n result,\n isLoading,\n error,\n status,\n stop,\n reset,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqGA,SAAgB,iBACd,SAKA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;EACb;CACD,MAAM,EAAE,UAAU,QAAQ,WAAW,OAAO,QAAQ,MAAM,UACxD,cAIE;EAAE,GAAG;EAAS;EAAU,CAAC;AAE7B,QAAO;EACK;EACV;EACA;EACA;EACA;EACA;EACA;EACD"}
1
+ {"version":3,"file":"use-transcription.js","names":[],"sources":["../src/use-transcription.ts"],"sourcesContent":["import { useGeneration } from './use-generation'\nimport { reconstructTranscriptionResult } from '@tanstack/ai-client'\nimport type {\n UseGenerationOptions,\n UseGenerationReturn,\n} from './use-generation'\nimport type { StreamChunk, TranscriptionResult } from '@tanstack/ai'\nimport type {\n AIDevtoolsDisplayOptions,\n ConnectConnectionAdapter,\n GenerationClientState,\n GenerationFetcher,\n GenerationPersistenceOptions,\n InferGenerationOutputFromReturn,\n TranscriptionGenerateInput,\n} from '@tanstack/ai-client'\nimport type { Accessor } from 'solid-js'\n\n/**\n * Options for the useTranscription hook.\n *\n * @template TOutput - The transformed output type (defaults to TranscriptionResult)\n */\nexport interface UseTranscriptionOptions<\n TOutput = TranscriptionResult,\n> extends Pick<\n UseGenerationOptions<\n TranscriptionGenerateInput,\n TranscriptionResult,\n TOutput\n >,\n 'persistence' | 'threadId' | 'hydrateGeneration' | 'joinRun'\n> {\n /** Connect-based adapter for streaming transport (SSE, HTTP stream, custom) */\n connection?: ConnectConnectionAdapter\n /** Direct async function for transcription */\n fetcher?: GenerationFetcher<TranscriptionGenerateInput, TranscriptionResult>\n /**\n * @deprecated Prefer `threadId`. Only allowed when `threadId` is omitted (see `GenerationPersistenceOptions`).\n */\n id?: string\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n /** Display options for TanStack AI Devtools. */\n devtools?: AIDevtoolsDisplayOptions\n /**\n * Callback when transcription is complete. Can optionally return a transformed value.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TranscriptionResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n}\n\n/**\n * Return type for the useTranscription hook.\n *\n * @template TOutput - The transformed output type (defaults to TranscriptionResult)\n */\nexport interface UseTranscriptionReturn<\n TOutput = TranscriptionResult,\n> extends Omit<UseGenerationReturn<TOutput>, 'generate'> {\n /** Trigger transcription */\n generate: (input: TranscriptionGenerateInput) => Promise<void>\n /** The transcription result, or null */\n result: Accessor<TOutput | null>\n /** Whether transcription is in progress */\n isLoading: Accessor<boolean>\n /** Current error, if any */\n error: Accessor<Error | undefined>\n /** Current state of the generation */\n status: Accessor<GenerationClientState>\n}\n\n/**\n * Solid hook for transcribing audio to text using AI models.\n *\n * @example\n * ```tsx\n * import { useTranscription } from '@tanstack/ai-solid'\n * import { fetchServerSentEvents } from '@tanstack/ai-client'\n *\n * function Transcriber() {\n * const { generate, result, isLoading } = useTranscription({\n * connection: fetchServerSentEvents('/api/transcribe'),\n * })\n *\n * const handleFile = (e: Event) => {\n * const input = e.target as HTMLInputElement\n * const file = input.files?.[0]\n * if (file) {\n * const reader = new FileReader()\n * reader.onload = () => {\n * generate({ audio: reader.result as string, language: 'en' })\n * }\n * reader.readAsDataURL(file)\n * }\n * }\n *\n * return (\n * <div>\n * <input type=\"file\" accept=\"audio/*\" onChange={handleFile} />\n * {isLoading() && <p>Transcribing...</p>}\n * {result() && <p>{result()!.text}</p>}\n * </div>\n * )\n * }\n * ```\n */\nexport function useTranscription<TTransformed = void>(\n options: Omit<\n UseTranscriptionOptions,\n 'onResult' | 'persistence' | 'threadId' | 'id'\n > & {\n onResult?: (result: TranscriptionResult) => TTransformed\n } & GenerationPersistenceOptions,\n): UseTranscriptionReturn<\n InferGenerationOutputFromReturn<TranscriptionResult, TTransformed>\n> {\n const devtools = {\n ...options.devtools,\n framework: 'solid',\n hookName: 'useTranscription',\n outputKind: 'text' as const,\n }\n const generation = useGeneration<\n TranscriptionGenerateInput,\n TranscriptionResult,\n TTransformed\n >({ ...options, devtools, reconstructResult: reconstructTranscriptionResult })\n\n return generation\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoHA,SAAgB,iBACd,SAQA;CACA,MAAM,WAAW;EACf,GAAG,QAAQ;EACX,WAAW;EACX,UAAU;EACV,YAAY;EACb;AAOD,QANmB,cAIjB;EAAE,GAAG;EAAS;EAAU,mBAAmB;EAAgC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-solid",
3
- "version": "0.15.1",
3
+ "version": "0.16.1",
4
4
  "description": "Solid hooks for TanStack AI streaming chat, structured outputs, and media generation.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -45,11 +45,11 @@
45
45
  "media-generation"
46
46
  ],
47
47
  "dependencies": {
48
- "@tanstack/ai-client": "0.22.1"
48
+ "@tanstack/ai-client": "0.23.1"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "solid-js": ">=1.9.10",
52
- "@tanstack/ai": "^0.42.0"
52
+ "@tanstack/ai": "^0.43.1"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@solidjs/testing-library": "^0.8.10",
@@ -60,12 +60,12 @@
60
60
  "solid-js": "^1.9.10",
61
61
  "tsdown": "^0.17.0-beta.6",
62
62
  "typescript": "5.9.3",
63
- "vitest": "^4.0.14",
64
- "@tanstack/ai": "0.42.0"
63
+ "vitest": "^4.1.10",
64
+ "@tanstack/ai": "0.43.1"
65
65
  },
66
66
  "scripts": {
67
67
  "clean": "premove ./build ./dist",
68
- "test:eslint": "eslint ./src",
68
+ "test:oxlint": "oxlint src --type-aware",
69
69
  "test:lib": "vitest run",
70
70
  "test:lib:dev": "pnpm test:lib --watch",
71
71
  "test:types": "tsc",
package/src/index.ts CHANGED
@@ -62,6 +62,16 @@ export type {
62
62
  // Re-export from ai-client for convenience
63
63
  export {
64
64
  fetchServerSentEvents,
65
+ localStoragePersistence,
66
+ sessionStoragePersistence,
67
+ indexedDBPersistence,
68
+ StorageUnavailableError,
69
+ type ChatClientPersistence,
70
+ type ChatPersistedState,
71
+ type ChatPersistenceOption,
72
+ type ChatStorageAdapter,
73
+ type WebStoragePersistenceOptions,
74
+ type IndexedDBPersistenceOptions,
65
75
  fetchHttpStream,
66
76
  xhrServerSentEvents,
67
77
  xhrHttpStream,
package/src/types.ts CHANGED
@@ -2,13 +2,18 @@ import type {
2
2
  AnyClientTool,
3
3
  InferSchemaType,
4
4
  ModelMessage,
5
+ RunAgentResumeItem,
5
6
  SchemaInput,
6
7
  } from '@tanstack/ai'
7
8
  import type {
8
9
  AIDevtoolsDisplayOptions,
10
+ BoundInterrupts,
9
11
  ChatClientOptions,
10
12
  ChatClientState,
13
+ ChatInterrupt,
14
+ ChatInterruptState,
11
15
  ChatRequestBody,
16
+ ChatResumeState,
12
17
  ClientContextOptionFromTools,
13
18
  ConnectionStatus,
14
19
  DistributedOmit,
@@ -84,8 +89,14 @@ export type UseChatOptions<
84
89
  | 'onConnectionStatusChange'
85
90
  | 'onSessionGeneratingChange'
86
91
  | 'onQueueChange'
92
+ | 'onResumeStateChange'
93
+ | 'onRunIdChange'
87
94
  | 'context'
88
95
  | 'devtools'
96
+ // `id` is not a hook option: the hook's identity is its `threadId`, which is
97
+ // also the persistence key. Persist across reloads by passing a stable
98
+ // `threadId`; there is no separate id to set.
99
+ | 'id'
89
100
  > & {
90
101
  /** Display options for TanStack AI Devtools. */
91
102
  devtools?: AIDevtoolsDisplayOptions
@@ -181,6 +192,39 @@ interface BaseUseChatReturn<
181
192
  approved: boolean
182
193
  }) => Promise<void>
183
194
 
195
+ /**
196
+ * The id of the run this client has in flight — one it started or rejoined —
197
+ * or `null` when there is none (including while a run sits paused on an
198
+ * interrupt, waiting on approval).
199
+ *
200
+ * A run is one turn of the conversation, so this changes from turn to turn. A
201
+ * whole tool loop stays inside one run, while resuming after an interrupt
202
+ * continues the turn under a new id — so one user message can produce several
203
+ * run ids. Use it to talk to your own server about that run (cancel it, poll
204
+ * it, correlate a log line).
205
+ */
206
+ runId: Accessor<string | null>
207
+ interrupts: Accessor<BoundInterrupts<TTools>>
208
+ /** @deprecated Use `interrupts`. */
209
+ pendingInterrupts: Accessor<BoundInterrupts<TTools>>
210
+ interruptErrors: Accessor<ChatInterruptState<TTools>['interruptErrors']>
211
+ resuming: Accessor<boolean>
212
+ resolveInterrupts: {
213
+ (approved: boolean): void
214
+ (resolver: (interrupt: ChatInterrupt<TTools>) => undefined): void
215
+ }
216
+ cancelInterrupts: () => void
217
+ retryInterrupts: () => void
218
+ resumeInterruptsUnsafe: (
219
+ resume: Array<RunAgentResumeItem>,
220
+ state?: ChatResumeState,
221
+ ) => Promise<boolean>
222
+ /** @deprecated Use bound interrupt methods or `resumeInterruptsUnsafe`. */
223
+ resumeInterrupts: (
224
+ resume: Array<RunAgentResumeItem>,
225
+ state?: ChatResumeState,
226
+ ) => Promise<boolean>
227
+
184
228
  /**
185
229
  * Reload the last assistant message
186
230
  */
package/src/use-chat.ts CHANGED
@@ -11,6 +11,9 @@ import { ChatClient } from '@tanstack/ai-client'
11
11
  import { createChatDevtoolsBridge } from '@tanstack/ai-client/devtools'
12
12
  import type {
13
13
  ChatClientState,
14
+ ChatInterrupt,
15
+ ChatInterruptState,
16
+ ChatResumeState,
14
17
  ConnectionStatus,
15
18
  InferredClientContext,
16
19
  QueuedMessage,
@@ -21,6 +24,7 @@ import type {
21
24
  AnyClientTool,
22
25
  InferSchemaType,
23
26
  ModelMessage,
27
+ RunAgentResumeItem,
24
28
  SchemaInput,
25
29
  StreamChunk,
26
30
  } from '@tanstack/ai'
@@ -32,6 +36,9 @@ import type {
32
36
  UseChatReturn,
33
37
  } from './types'
34
38
 
39
+ const EMPTY_INTERRUPTS = Object.freeze([])
40
+ const EMPTY_INTERRUPT_ERRORS = Object.freeze([])
41
+
35
42
  export function useChat<
36
43
  const TTools extends ReadonlyArray<AnyClientTool> = any,
37
44
  TSchema extends SchemaInput | undefined = undefined,
@@ -43,8 +50,12 @@ export function useChat<
43
50
  TContext
44
51
  >,
45
52
  ): UseChatReturn<TTools, TSchema> {
53
+ // The hook's identity is its `threadId` — also the persistence key, so a
54
+ // reload with the same `threadId` restores the same conversation. `hookId` is
55
+ // only a stable fallback for client-recreation keying when no `threadId` is
56
+ // given (an ephemeral chat), never a persistence key.
46
57
  const hookId = createUniqueId()
47
- const clientId = options.id || hookId
58
+ const clientId = options.threadId ?? hookId
48
59
 
49
60
  const [messages, setMessages] = createSignal<Array<UIMessage<TTools>>>(
50
61
  options.initialMessages || [],
@@ -57,6 +68,20 @@ export function useChat<
57
68
  createSignal<ConnectionStatus>('disconnected')
58
69
  const [sessionGenerating, setSessionGenerating] = createSignal(false)
59
70
  const [queue, setQueue] = createSignal<Array<QueuedMessage>>([])
71
+ const [runId, setRunId] = createSignal<string | null>(null)
72
+ const [interruptState, setInterruptState] = createSignal<
73
+ ChatInterruptState<TTools>
74
+ >({
75
+ interrupts: EMPTY_INTERRUPTS,
76
+ pendingInterrupts: EMPTY_INTERRUPTS,
77
+ interruptErrors: EMPTY_INTERRUPT_ERRORS,
78
+ resuming: false,
79
+ })
80
+
81
+ const syncResumeState = () => {
82
+ setRunId(client().getCurrentRunId())
83
+ setInterruptState(client().getInterruptState())
84
+ }
60
85
 
61
86
  // Structured-output `partial` / `final` are derived from `messages` —
62
87
  // specifically from the structured-output part on the latest assistant
@@ -82,13 +107,15 @@ export function useChat<
82
107
  return new ChatClient<TTools, TContext>({
83
108
  devtoolsBridgeFactory: createChatDevtoolsBridge,
84
109
  ...transport,
85
- id: clientId,
86
110
  ...(options.initialMessages !== undefined && {
87
111
  initialMessages: options.initialMessages,
88
112
  }),
89
113
  ...(options.persistence !== undefined && {
90
114
  persistence: options.persistence,
91
115
  }),
116
+ ...(options.initialResumeSnapshot !== undefined && {
117
+ initialResumeSnapshot: options.initialResumeSnapshot,
118
+ }),
92
119
  body: options.body,
93
120
  ...(options.threadId !== undefined && { threadId: options.threadId }),
94
121
  ...(options.forwardedProps !== undefined && {
@@ -122,6 +149,7 @@ export function useChat<
122
149
  },
123
150
  onLoadingChange: (newIsLoading: boolean) => {
124
151
  setIsLoading(newIsLoading)
152
+ syncResumeState()
125
153
  },
126
154
  onStatusChange: (newStatus: ChatClientState) => {
127
155
  setStatus(newStatus)
@@ -142,12 +170,27 @@ export function useChat<
142
170
  onQueueChange: (nextQueue: Array<QueuedMessage>) => {
143
171
  setQueue(nextQueue)
144
172
  },
173
+ onRunIdChange: (nextRunId) => {
174
+ setRunId(nextRunId)
175
+ },
176
+ onResumeStateChange: (_nextResumeState, nextPendingInterrupts) => {
177
+ setInterruptState((current) => ({
178
+ ...current,
179
+ interrupts: nextPendingInterrupts,
180
+ pendingInterrupts: nextPendingInterrupts,
181
+ }))
182
+ },
183
+ onInterruptStateChange: (nextInterruptState) => {
184
+ setInterruptState(nextInterruptState)
185
+ options.onInterruptStateChange?.(nextInterruptState)
186
+ },
145
187
  })
146
188
  // Only recreate when clientId changes
147
189
  // Connection and other options are captured at creation time
148
190
  }, [clientId])
149
191
 
150
192
  setMessages(client().getMessages())
193
+ syncResumeState()
151
194
 
152
195
  // Sync body / forwardedProps changes to the client.
153
196
  // Both populate the same wire payload; `forwardedProps` is preferred
@@ -181,11 +224,24 @@ export function useChat<
181
224
  })
182
225
 
183
226
  onMount(() => {
227
+ // START TAILING HERE, not in the constructor. A client is idle until a view
228
+ // attaches it, so a client that gets built and thrown away never opens a
229
+ // connection — an unreachable stream would hold one of the browser's ~6
230
+ // connections per origin until the page reloaded.
231
+ client().attach()
184
232
  client().mountDevtools()
233
+ // Delivery-durability resume is transparent: the resumable SSE connection
234
+ // adapter reattaches via the browser's native Last-Event-ID on reconnect.
235
+ // We only seed interrupt (state) resume from the client here.
236
+ syncResumeState()
185
237
  })
186
238
 
187
239
  // Cleanup on unmount: stop any in-flight requests.
188
240
  onCleanup(() => {
241
+ // Release the connection first: `detach` is the counterpart of the `attach`
242
+ // above, and it keeps the transcript and resume pointer so a later mount can
243
+ // pick the run back up from the durable log.
244
+ client().detach()
189
245
  if (options.live) {
190
246
  client().unsubscribe()
191
247
  } else {
@@ -201,15 +257,27 @@ export function useChat<
201
257
  content: string | MultimodalContent,
202
258
  sendOptions?: SendMessageOptions,
203
259
  ) => {
204
- await client().sendMessage(content, undefined, sendOptions)
260
+ try {
261
+ await client().sendMessage(content, undefined, sendOptions)
262
+ } finally {
263
+ syncResumeState()
264
+ }
205
265
  }
206
266
 
207
267
  const append = async (message: ModelMessage | UIMessage<TTools>) => {
208
- await client().append(message)
268
+ try {
269
+ await client().append(message)
270
+ } finally {
271
+ syncResumeState()
272
+ }
209
273
  }
210
274
 
211
275
  const reload = async () => {
212
- await client().reload()
276
+ try {
277
+ await client().reload()
278
+ } finally {
279
+ syncResumeState()
280
+ }
213
281
  }
214
282
 
215
283
  const stop = () => {
@@ -218,6 +286,7 @@ export function useChat<
218
286
 
219
287
  const clear = () => {
220
288
  client().clear()
289
+ syncResumeState()
221
290
  }
222
291
 
223
292
  const setMessagesManually = (newMessages: Array<UIMessage<TTools>>) => {
@@ -239,10 +308,48 @@ export function useChat<
239
308
  approved: boolean
240
309
  }) => {
241
310
  await client().addToolApprovalResponse(response)
311
+ syncResumeState()
242
312
  }
243
313
 
244
314
  const cancelQueued = (id: string) => client().cancelQueued(id)
245
315
 
316
+ const resumeInterrupts = async (
317
+ resumeItems: Array<RunAgentResumeItem>,
318
+ state?: ChatResumeState,
319
+ ) => {
320
+ const result = await client().resumeInterrupts(resumeItems, state)
321
+ syncResumeState()
322
+ return result
323
+ }
324
+
325
+ const resolveInterrupts = (
326
+ resolution: boolean | ((interrupt: ChatInterrupt<TTools>) => undefined),
327
+ ) => {
328
+ if (typeof resolution === 'boolean') {
329
+ client().resolveInterrupts(resolution)
330
+ } else {
331
+ client().resolveInterrupts(resolution)
332
+ }
333
+ }
334
+
335
+ const cancelInterrupts = () => {
336
+ client().cancelInterrupts()
337
+ }
338
+
339
+ const retryInterrupts = () => {
340
+ client().retryInterrupts()
341
+ }
342
+
343
+ const resumeInterruptsUnsafe = (
344
+ resumeItems: Array<RunAgentResumeItem>,
345
+ state?: ChatResumeState,
346
+ ) => client().resumeInterruptsUnsafe(resumeItems, state)
347
+
348
+ const interrupts = () => interruptState().interrupts
349
+ const pendingInterrupts = () => interruptState().pendingInterrupts
350
+ const interruptErrors = () => interruptState().interruptErrors
351
+ const resuming = () => interruptState().resuming
352
+
246
353
  // The "active" structured-output part is on the assistant message after
247
354
  // the latest user message. When no user message exists yet, return null
248
355
  // rather than scanning history — otherwise a stale `final` from
@@ -283,7 +390,7 @@ export function useChat<
283
390
 
284
391
  // partial / final are runtime-tracked unconditionally; the conditional
285
392
  // return type hides them when no `outputSchema` is supplied.
286
- // eslint-disable-next-line no-restricted-syntax -- primitive return shape diverges from generic UseChatReturn<TTools, TSchema>; TS can't structurally narrow the conditional partial/final fields
393
+ // oxlint-disable-next-line eslint-js/no-restricted-syntax -- primitive return shape diverges from generic UseChatReturn<TTools, TSchema>; TS can't structurally narrow the conditional partial/final fields
287
394
  return {
288
395
  messages,
289
396
  queue,
@@ -302,6 +409,16 @@ export function useChat<
302
409
  addToolResult,
303
410
  addToolApprovalResponse,
304
411
  cancelQueued,
412
+ runId,
413
+ interrupts,
414
+ pendingInterrupts,
415
+ interruptErrors,
416
+ resuming,
417
+ resolveInterrupts,
418
+ cancelInterrupts,
419
+ retryInterrupts,
420
+ resumeInterruptsUnsafe,
421
+ resumeInterrupts,
305
422
  partial,
306
423
  final,
307
424
  } as unknown as UseChatReturn<TTools, TSchema>