UNPKG

@ai-sdk/react

Version:

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

270 lines (226 loc) 6.81 kB
import { FetchFunction, FlexibleSchema, InferSchema, isAbortError, Resolvable, resolve, normalizeHeaders, safeValidateTypes, } from '@ai-sdk/provider-utils'; import { asSchema, DeepPartial, isDeepEqualData, parsePartialJson } from 'ai'; import { useCallback, useId, useRef, useState } from 'react'; import useSWR from 'swr'; // use function to allow for mocking in tests: const getOriginalFetch = () => fetch; export type Experimental_UseObjectOptions< SCHEMA extends FlexibleSchema, RESULT, > = { /** * The API endpoint. It should stream JSON that matches the schema as chunked text. */ api: string; /** * A schema that defines the shape of the complete object. */ schema: SCHEMA; /** * An unique identifier. If not provided, a random one will be * generated. When provided, the `useObject` hook with the same `id` will * have shared states across components. */ id?: string; /** * An optional value for the initial object. */ initialValue?: DeepPartial<RESULT>; /** * Custom fetch implementation. You can use it as a middleware to intercept requests, * or to provide a custom fetch implementation for e.g. testing. */ fetch?: FetchFunction; /** * Callback that is called when the stream has finished. */ onFinish?: (event: { /** * The generated object (typed according to the schema). * Can be undefined if the final object does not match the schema. */ object: RESULT | undefined; /** * Optional error object. This is e.g. a TypeValidationError when the final object does not match the schema. */ error: Error | undefined; }) => Promise<void> | void; /** * Callback function to be called when an error is encountered. */ onError?: (error: Error) => void; /** * Additional HTTP headers to be included in the request. * Can be a static object, a function that returns headers, or an async function * for dynamic auth tokens. */ headers?: Resolvable<Record<string, string> | Headers>; /** * The credentials mode to be used for the fetch request. * Possible values are: 'omit', 'same-origin', 'include'. * Defaults to 'same-origin'. */ credentials?: RequestCredentials; }; export type Experimental_UseObjectHelpers<RESULT, INPUT> = { /** * Calls the API with the provided input as JSON body. */ submit: (input: INPUT) => void; /** * The current value for the generated object. Updated as the API streams JSON chunks. */ object: DeepPartial<RESULT> | undefined; /** * The error object of the API request if any. */ error: Error | undefined; /** * Flag that indicates whether an API request is in progress. */ isLoading: boolean; /** * Abort the current request immediately, keep the current partial object if any. */ stop: () => void; /** * Clear the object state. */ clear: () => void; }; function useObject< SCHEMA extends FlexibleSchema, RESULT = InferSchema<SCHEMA>, INPUT = any, >({ api, id, schema, // required, in the future we will use it for validation initialValue, fetch, onError, onFinish, headers, credentials, }: Experimental_UseObjectOptions< SCHEMA, RESULT >): Experimental_UseObjectHelpers<RESULT, INPUT> { // Generate an unique id if not provided. const hookId = useId(); const completionId = id ?? hookId; // Store the completion state in SWR, using the completionId as the key to share states. const { data, mutate } = useSWR<DeepPartial<RESULT>>( [api, completionId], null, { fallbackData: initialValue }, ); const [error, setError] = useState<undefined | Error>(undefined); const [isLoading, setIsLoading] = useState(false); // Abort controller to cancel the current API call. const abortControllerRef = useRef<AbortController | null>(null); const stop = useCallback(() => { try { abortControllerRef.current?.abort(); } catch (ignored) { } finally { setIsLoading(false); abortControllerRef.current = null; } }, []); const submit = async (input: INPUT) => { try { clearObject(); setIsLoading(true); const abortController = new AbortController(); abortControllerRef.current = abortController; // Resolve headers at request time (supports async functions for dynamic auth tokens) const resolvedHeaders = await resolve(headers); const actualFetch = fetch ?? getOriginalFetch(); const response = await actualFetch(api, { method: 'POST', headers: { 'Content-Type': 'application/json', ...normalizeHeaders(resolvedHeaders), }, 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: DeepPartial<RESULT> | undefined = undefined; await response.body.pipeThrough(new TextDecoderStream()).pipeTo( new WritableStream<string>({ async write(chunk) { accumulatedText += chunk; const { value } = await parsePartialJson(accumulatedText); const currentObject = value as DeepPartial<RESULT>; if (!isDeepEqualData(latestObject, currentObject)) { latestObject = currentObject; mutate(currentObject); } }, async close() { setIsLoading(false); abortControllerRef.current = null; if (onFinish != null) { const validationResult = await safeValidateTypes({ value: latestObject, schema: asSchema(schema), }); onFinish( validationResult.success ? { object: validationResult.value, error: undefined } : { object: undefined, error: validationResult.error }, ); } }, }), ); } catch (error) { if (isAbortError(error)) { return; } if (onError && error instanceof Error) { onError(error); } setIsLoading(false); setError(error instanceof Error ? error : new Error(String(error))); } }; const clear = () => { stop(); clearObject(); }; const clearObject = () => { setError(undefined); setIsLoading(false); mutate(undefined); }; return { submit, object: data, error, isLoading, stop, clear, }; } export const experimental_useObject = useObject;