| import debounce from 'lodash/debounce'; |
| import { useState, useEffect } from 'react'; |
| import { useFormContext } from 'react-hook-form'; |
| import { Spinner, useToastContext } from '@librechat/client'; |
| import { |
| validateAndParseOpenAPISpec, |
| openapiToFunction, |
| AuthTypeEnum, |
| } from 'librechat-data-provider'; |
| import type { |
| Action, |
| FunctionTool, |
| ActionMetadata, |
| ValidationResult, |
| AssistantsEndpoint, |
| } from 'librechat-data-provider'; |
| import type { ActionAuthForm, ActionWithNullableMetadata } from '~/common'; |
| import type { Spec } from './ActionsTable'; |
| import ActionCallback from '~/components/SidePanel/Builder/ActionCallback'; |
| import { useAssistantsMapContext } from '~/Providers'; |
| import { ActionsTable, columns } from './ActionsTable'; |
| import { useUpdateAction } from '~/data-provider'; |
| import { useLocalize } from '~/hooks'; |
|
|
| const debouncedValidation = debounce( |
| (input: string, callback: (result: ValidationResult) => void) => { |
| const result = validateAndParseOpenAPISpec(input); |
| callback(result); |
| }, |
| 800, |
| ); |
|
|
| export default function ActionsInput({ |
| action, |
| assistant_id, |
| endpoint, |
| version, |
| setAction, |
| }: { |
| action?: ActionWithNullableMetadata; |
| assistant_id?: string; |
| endpoint: AssistantsEndpoint; |
| version: number | string; |
| setAction: React.Dispatch<React.SetStateAction<Action | undefined>>; |
| }) { |
| const handleResult = (result: ValidationResult) => { |
| if (!result.status) { |
| setData(null); |
| setFunctions(null); |
| } |
| setValidationResult(result); |
| }; |
|
|
| const localize = useLocalize(); |
| const { showToast } = useToastContext(); |
| const assistantMap = useAssistantsMapContext(); |
| const { handleSubmit, reset } = useFormContext<ActionAuthForm>(); |
| const [validationResult, setValidationResult] = useState<null | ValidationResult>(null); |
| const [inputValue, setInputValue] = useState(''); |
|
|
| const [data, setData] = useState<Spec[] | null>(null); |
| const [functions, setFunctions] = useState<FunctionTool[] | null>(null); |
|
|
| useEffect(() => { |
| const rawSpec = action?.metadata?.raw_spec ?? ''; |
| if (!rawSpec) { |
| return; |
| } |
| setInputValue(rawSpec); |
| debouncedValidation(rawSpec, handleResult); |
| }, [action?.metadata?.raw_spec]); |
|
|
| useEffect(() => { |
| if (!validationResult || !validationResult.status || !validationResult.spec) { |
| return; |
| } |
|
|
| const { functionSignatures, requestBuilders } = openapiToFunction(validationResult.spec); |
| const specs = Object.entries(requestBuilders).map(([name, props]) => { |
| return { |
| name, |
| method: props.method, |
| path: props.path, |
| domain: props.domain, |
| }; |
| }); |
|
|
| setData(specs); |
| setValidationResult(null); |
| setFunctions(functionSignatures.map((f) => f.toObjectTool())); |
| }, [validationResult]); |
|
|
| const updateAction = useUpdateAction({ |
| onSuccess(data) { |
| showToast({ |
| message: localize('com_assistants_update_actions_success'), |
| status: 'success', |
| }); |
| reset(); |
| setAction(data[2]); |
| }, |
| onError(error) { |
| showToast({ |
| message: |
| (error as Error | undefined)?.message ?? localize('com_assistants_update_actions_error'), |
| status: 'error', |
| }); |
| }, |
| }); |
|
|
| const saveAction = handleSubmit((authFormData) => { |
| console.log('authFormData', authFormData); |
| const currentAssistantId = assistant_id ?? ''; |
| if (!currentAssistantId) { |
| |
| return; |
| } |
|
|
| if (!functions) { |
| return; |
| } |
|
|
| if (!data) { |
| return; |
| } |
|
|
| let { metadata } = action ?? {}; |
| if (!metadata) { |
| metadata = {}; |
| } |
| const action_id = action?.action_id; |
| metadata.raw_spec = inputValue; |
| const parsedUrl = new URL(data[0].domain); |
| const domain = parsedUrl.hostname; |
| if (!domain) { |
| |
| return; |
| } |
| metadata.domain = domain; |
|
|
| const { type, saved_auth_fields } = authFormData; |
|
|
| const removeSensitiveFields = (obj: ActionMetadata) => { |
| delete obj.auth; |
| delete obj.api_key; |
| delete obj.oauth_client_id; |
| delete obj.oauth_client_secret; |
| }; |
|
|
| if (saved_auth_fields && type === AuthTypeEnum.ServiceHttp) { |
| metadata = { |
| ...metadata, |
| api_key: authFormData.api_key, |
| auth: { |
| type, |
| authorization_type: authFormData.authorization_type, |
| custom_auth_header: authFormData.custom_auth_header, |
| }, |
| }; |
| } else if (saved_auth_fields && type === AuthTypeEnum.OAuth) { |
| metadata = { |
| ...metadata, |
| auth: { |
| type, |
| authorization_url: authFormData.authorization_url, |
| client_url: authFormData.client_url, |
| scope: authFormData.scope, |
| token_exchange_method: authFormData.token_exchange_method, |
| }, |
| oauth_client_id: authFormData.oauth_client_id, |
| oauth_client_secret: authFormData.oauth_client_secret, |
| }; |
| } else if (saved_auth_fields) { |
| removeSensitiveFields(metadata); |
| metadata.auth = { |
| type, |
| }; |
| } else { |
| removeSensitiveFields(metadata); |
| } |
|
|
| updateAction.mutate({ |
| action_id, |
| metadata, |
| functions, |
| assistant_id: currentAssistantId, |
| endpoint, |
| version, |
| model: assistantMap?.[endpoint][currentAssistantId].model ?? '', |
| }); |
| }); |
|
|
| const handleInputChange: React.ChangeEventHandler<HTMLTextAreaElement> = (event) => { |
| const newValue = event.target.value; |
| setInputValue(newValue); |
| if (!newValue) { |
| setData(null); |
| setFunctions(null); |
| return setValidationResult(null); |
| } |
| debouncedValidation(newValue, handleResult); |
| }; |
|
|
| const submitContext = () => { |
| if (updateAction.isLoading) { |
| return <Spinner className="icon-md" />; |
| } else if (action?.action_id.length ?? 0) { |
| return localize('com_ui_update'); |
| } else { |
| return localize('com_ui_create'); |
| } |
| }; |
|
|
| return ( |
| <> |
| <div className=""> |
| <div className="mb-1 flex flex-wrap items-center justify-between gap-4"> |
| <label |
| htmlFor="example-schema" |
| className="text-token-text-primary whitespace-nowrap font-medium" |
| > |
| {localize('com_ui_schema')} |
| </label> |
| <div className="flex items-center gap-2"> |
| {/* <button className="btn btn-neutral border-token-border-light relative h-8 min-w-[100px] rounded-lg font-medium"> |
| <div className="flex w-full items-center justify-center text-xs">Import from URL</div> |
| </button> */} |
| <select |
| id="example-schema" |
| onChange={(e) => console.log(e.target.value)} |
| className="border-token-border-medium h-8 min-w-[100px] rounded-lg border bg-transparent px-2 py-0 text-sm" |
| > |
| <option value="label">{localize('com_ui_examples')}</option> |
| {/* TODO: make these appear and function correctly */} |
| <option value="0">Weather (JSON)</option> |
| <option value="1">Pet Store (YAML)</option> |
| <option value="2">Blank Template</option> |
| </select> |
| </div> |
| </div> |
| <div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover mb-4 w-full overflow-hidden rounded-lg border ring-0"> |
| <div className="relative"> |
| <textarea |
| id="schemaInput" |
| value={inputValue} |
| onChange={handleInputChange} |
| spellCheck="false" |
| placeholder={localize('com_ui_enter_openapi_schema')} |
| className="text-token-text-primary block h-96 w-full bg-transparent p-2 font-mono text-xs outline-none focus:ring-1 focus:ring-border-light" |
| /> |
| {/* TODO: format input button */} |
| </div> |
| {validationResult && validationResult.message !== 'OpenAPI spec is valid.' && ( |
| <div className="border-token-border-light border-t p-2 text-red-500"> |
| {validationResult.message.split('\n').map((line: string, i: number) => ( |
| <div key={i}>{line}</div> |
| ))} |
| </div> |
| )} |
| </div> |
| </div> |
| {!!data && ( |
| <div className="my-2"> |
| <div className="flex items-center"> |
| <label className="text-token-text-primary block font-medium"> |
| {localize('com_assistants_available_actions')} |
| </label> |
| </div> |
| <ActionsTable columns={columns} data={data} /> |
| </div> |
| )} |
| <div className="relative my-1"> |
| <ActionCallback action_id={action?.action_id} /> |
| <div className="mb-1.5 flex items-center"> |
| <label className="text-token-text-primary block font-medium"> |
| {localize('com_ui_privacy_policy_url')} |
| </label> |
| </div> |
| <div className="border-token-border-medium bg-token-surface-primary hover:border-token-border-hover flex h-9 w-full rounded-lg border"> |
| <input |
| type="text" |
| placeholder="https://api.example-weather-app.com/privacy" |
| className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light" |
| /> |
| </div> |
| </div> |
| <div className="flex items-center justify-end"> |
| <button |
| disabled={!functions || !functions.length} |
| onClick={saveAction} |
| className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400" |
| type="button" |
| > |
| {submitContext()} |
| </button> |
| </div> |
| </> |
| ); |
| } |
|
|