UNPKG

@ai-sdk/svelte

Version:

[Svelte](https://svelte.dev/) UI components for the [AI SDK](https://ai-sdk.dev/docs):

135 lines (134 loc) 4.9 kB
import { generateId, isAbortError, safeValidateTypes, } from '@ai-sdk/provider-utils'; import { asSchema, isDeepEqualData, parsePartialJson, } from 'ai'; import { getStructuredObjectContext, hasStructuredObjectContext, KeyedStructuredObjectStore, } from './structured-object-context.svelte.js'; export class StructuredObject { #options = {}; #id = $derived(this.#options.id ?? generateId()); #keyedStore = $state(); #store = $derived(this.#keyedStore.get(this.#id)); #abortController; /** * The current value for the generated object. Updated as the API streams JSON chunks. */ get object() { return this.#store.object; } set #object(value) { this.#store.object = value; } /** The error object of the API request */ get error() { return this.#store.error; } /** * Flag that indicates whether an API request is in progress. */ get loading() { return this.#store.loading; } constructor(options) { if (hasStructuredObjectContext()) { this.#keyedStore = getStructuredObjectContext(); } else { this.#keyedStore = new KeyedStructuredObjectStore(); } this.#options = options; this.#object = options.initialValue; } /** * Abort the current request immediately, keep the current partial object if any. */ stop = () => { try { this.#abortController?.abort(); } catch { // ignore } finally { this.#store.loading = false; this.#abortController = undefined; } }; /** * Calls the API with the provided input as JSON body. */ submit = async (input) => { try { this.#clearObject(); this.#store.loading = true; const abortController = new AbortController(); this.#abortController = abortController; const actualFetch = this.#options.fetch ?? fetch; const response = await actualFetch(this.#options.api, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.#options.headers, }, credentials: this.#options.credentials, signal: abortController.signal, body: JSON.stringify(input), }); if (!response.ok) { throw new Error((await response.text()) ?? 'Failed to fetch the response.'); } if (response.body == null) { throw new Error('The response body is empty.'); } let accumulatedText = ''; let latestObject = undefined; await response.body.pipeThrough(new TextDecoderStream()).pipeTo(new WritableStream({ write: async (chunk) => { if (abortController?.signal.aborted) { throw new DOMException('Stream aborted', 'AbortError'); } accumulatedText += chunk; const { value } = await parsePartialJson(accumulatedText); const currentObject = value; if (!isDeepEqualData(latestObject, currentObject)) { latestObject = currentObject; this.#store.object = currentObject; } }, close: async () => { this.#store.loading = false; this.#abortController = undefined; if (this.#options.onFinish != null) { const validationResult = await safeValidateTypes({ value: latestObject, schema: asSchema(this.#options.schema), }); this.#options.onFinish(validationResult.success ? { object: validationResult.value, error: undefined } : { object: undefined, error: validationResult.error }); } }, })); } catch (error) { if (isAbortError(error)) { return; } const coalescedError = error instanceof Error ? error : new Error(String(error)); if (this.#options.onError) { this.#options.onError(coalescedError); } this.#store.loading = false; this.#store.error = coalescedError; } }; /** * Clears the object state. */ clear = () => { this.stop(); this.#clearObject(); }; #clearObject = () => { this.#store.object = undefined; this.#store.error = undefined; this.#store.loading = false; }; }