import { useCallback, useEffect, useId, useRef, useState } from 'react'; import useSWR from 'swr'; import { callCompletionApi } from '../shared/call-completion-api'; import { JSONValue, RequestOptions, UseCompletionOptions, } from '../shared/types'; export type { UseCompletionOptions }; export type UseCompletionHelpers = { /** The current completion result */ completion: string; /** * Send a new prompt to the API endpoint and update the completion state. */ complete: ( prompt: string, options?: RequestOptions, ) => Promise; /** The error object of the API request */ error: undefined | Error; /** * Abort the current API request but keep the generated tokens. */ stop: () => void; /** * Update the `completion` state locally. */ setCompletion: (completion: string) => void; /** The current value of the input */ input: string; /** setState-powered method to update the input value */ setInput: React.Dispatch>; /** * An input/textarea-ready onChange handler to control the value of the input * @example * ```jsx * * ``` */ handleInputChange: ( e: | React.ChangeEvent | React.ChangeEvent, ) => void; /** * Form submission handler to automatically reset input and append a user message * @example * ```jsx *
* *
* ``` */ handleSubmit: (e: React.FormEvent) => void; /** Whether the API request is in progress */ isLoading: boolean; /** Additional data added on the server via StreamData */ data?: JSONValue[]; }; export function useCompletion({ api = '/api/completion', id, initialCompletion = '', initialInput = '', credentials, headers, body, onResponse, onFinish, onError, }: UseCompletionOptions = {}): UseCompletionHelpers { // Generate an unique id for the completion 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([api, completionId], null, { fallbackData: initialCompletion, }); const { data: isLoading = false, mutate: mutateLoading } = useSWR( [completionId, 'loading'], null, ); const { data: streamData, mutate: mutateStreamData } = useSWR< JSONValue[] | undefined >([completionId, 'streamData'], null); const [error, setError] = useState(undefined); const completion = data!; // Abort controller to cancel the current API call. const [abortController, setAbortController] = useState(null); const extraMetadataRef = useRef({ credentials, headers, body, }); useEffect(() => { extraMetadataRef.current = { credentials, headers, body, }; }, [credentials, headers, body]); const triggerRequest = useCallback( async (prompt: string, options?: RequestOptions) => callCompletionApi({ api, prompt, credentials: extraMetadataRef.current.credentials, headers: { ...extraMetadataRef.current.headers, ...options?.headers }, body: { ...extraMetadataRef.current.body, ...options?.body, }, setCompletion: completion => mutate(completion, false), setLoading: mutateLoading, setError, setAbortController, onResponse, onFinish, onError, onData: data => { mutateStreamData([...(streamData || []), ...(data || [])], false); }, }), [ mutate, mutateLoading, api, extraMetadataRef, setAbortController, onResponse, onFinish, onError, setError, streamData, mutateStreamData, ], ); const stop = useCallback(() => { if (abortController) { abortController.abort(); setAbortController(null); } }, [abortController]); const setCompletion = useCallback( (completion: string) => { mutate(completion, false); }, [mutate], ); const complete = useCallback( async (prompt, options) => { return triggerRequest(prompt, options); }, [triggerRequest], ); const [input, setInput] = useState(initialInput); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); if (!input) return; return complete(input); }, [input, complete], ); const handleInputChange = (e: any) => { setInput(e.target.value); }; return { completion, complete, error, setCompletion, stop, input, setInput, handleInputChange, handleSubmit, isLoading, data: streamData, }; }