hts/packages/isdk/react/use-completion.ts

201 lines
4.9 KiB
TypeScript

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<string | null | undefined>;
/** 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<React.SetStateAction<string>>;
/**
* An input/textarea-ready onChange handler to control the value of the input
* @example
* ```jsx
* <input onChange={handleInputChange} value={input} />
* ```
*/
handleInputChange: (
e:
| React.ChangeEvent<HTMLInputElement>
| React.ChangeEvent<HTMLTextAreaElement>,
) => void;
/**
* Form submission handler to automatically reset input and append a user message
* @example
* ```jsx
* <form onSubmit={handleSubmit}>
* <input onChange={handleInputChange} value={input} />
* </form>
* ```
*/
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => 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<string>([api, completionId], null, {
fallbackData: initialCompletion,
});
const { data: isLoading = false, mutate: mutateLoading } = useSWR<boolean>(
[completionId, 'loading'],
null,
);
const { data: streamData, mutate: mutateStreamData } = useSWR<
JSONValue[] | undefined
>([completionId, 'streamData'], null);
const [error, setError] = useState<undefined | Error>(undefined);
const completion = data!;
// Abort controller to cancel the current API call.
const [abortController, setAbortController] =
useState<AbortController | null>(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<UseCompletionHelpers['complete']>(
async (prompt, options) => {
return triggerRequest(prompt, options);
},
[triggerRequest],
);
const [input, setInput] = useState(initialInput);
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
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,
};
}