| | import { useState, useMemo } from 'react'; |
| | import { useGetModelsQuery } from 'librechat-data-provider/react-query'; |
| | import { Spinner, useToastContext, SelectDropDown } from '@librechat/client'; |
| | import { useForm, FormProvider, Controller, useWatch } from 'react-hook-form'; |
| | import { |
| | Tools, |
| | Capabilities, |
| | actionDelimiter, |
| | ImageVisionTool, |
| | defaultAssistantFormValues, |
| | } from 'librechat-data-provider'; |
| | import type { FunctionTool, TConfig } from 'librechat-data-provider'; |
| | import type { AssistantForm, AssistantPanelProps } from '~/common'; |
| | import { |
| | useCreateAssistantMutation, |
| | useUpdateAssistantMutation, |
| | useAvailableAgentToolsQuery, |
| | } from '~/data-provider'; |
| | import { cn, cardStyle, defaultTextProps, removeFocusOutlines } from '~/utils'; |
| | import AssistantConversationStarters from './AssistantConversationStarters'; |
| | import AssistantToolsDialog from '~/components/Tools/AssistantToolsDialog'; |
| | import { useSelectAssistant, useLocalize } from '~/hooks'; |
| | import { useAssistantsMapContext } from '~/Providers'; |
| | import AppendDateCheckbox from './AppendDateCheckbox'; |
| | import CapabilitiesForm from './CapabilitiesForm'; |
| | import AssistantAvatar from './AssistantAvatar'; |
| | import AssistantSelect from './AssistantSelect'; |
| | import ContextButton from './ContextButton'; |
| | import AssistantTool from './AssistantTool'; |
| | import Knowledge from './Knowledge'; |
| | import { Panel } from '~/common'; |
| | import Action from './Action'; |
| |
|
| | const labelClass = 'mb-2 text-token-text-primary block font-medium'; |
| | const inputClass = cn( |
| | defaultTextProps, |
| | 'flex w-full px-3 py-2 dark:border-gray-800 dark:bg-gray-800 rounded-xl mb-2', |
| | removeFocusOutlines, |
| | ); |
| |
|
| | export default function AssistantPanel({ |
| | // index = 0, |
| | setAction, |
| | endpoint, |
| | actions = [], |
| | setActivePanel, |
| | documentsMap, |
| | assistant_id: current_assistant_id, |
| | setCurrentAssistantId, |
| | assistantsConfig, |
| | version, |
| | }: AssistantPanelProps & { assistantsConfig?: TConfig | null }) { |
| | const modelsQuery = useGetModelsQuery(); |
| | const assistantMap = useAssistantsMapContext(); |
| |
|
| | const { data: allTools = [] } = useAvailableAgentToolsQuery(); |
| | const { onSelect: onSelectAssistant } = useSelectAssistant(endpoint); |
| | const { showToast } = useToastContext(); |
| | const localize = useLocalize(); |
| |
|
| | const methods = useForm<AssistantForm>({ |
| | defaultValues: defaultAssistantFormValues, |
| | }); |
| |
|
| | const [showToolDialog, setShowToolDialog] = useState(false); |
| |
|
| | const { control, handleSubmit, reset, setValue, getValues } = methods; |
| | const assistant = useWatch({ control, name: 'assistant' }); |
| | const functions = useWatch({ control, name: 'functions' }); |
| | const assistant_id = useWatch({ control, name: 'id' }); |
| |
|
| | const activeModel = useMemo(() => { |
| | return assistantMap?.[endpoint]?.[assistant_id]?.model; |
| | }, [assistantMap, endpoint, assistant_id]); |
| |
|
| | const toolsEnabled = useMemo( |
| | () => assistantsConfig?.capabilities?.includes(Capabilities.tools), |
| | [assistantsConfig], |
| | ); |
| | const actionsEnabled = useMemo( |
| | () => assistantsConfig?.capabilities?.includes(Capabilities.actions), |
| | [assistantsConfig], |
| | ); |
| | const retrievalEnabled = useMemo( |
| | () => assistantsConfig?.capabilities?.includes(Capabilities.retrieval), |
| | [assistantsConfig], |
| | ); |
| | const codeEnabled = useMemo( |
| | () => assistantsConfig?.capabilities?.includes(Capabilities.code_interpreter), |
| | [assistantsConfig], |
| | ); |
| |
|
| | |
| | const update = useUpdateAssistantMutation({ |
| | onSuccess: (data) => { |
| | showToast({ |
| | message: `${localize('com_assistants_update_success')} ${ |
| | data.name ?? localize('com_ui_assistant') |
| | }`, |
| | }); |
| | }, |
| | onError: (err) => { |
| | const error = err as Error; |
| | showToast({ |
| | message: `${localize('com_assistants_update_error')}${ |
| | error.message ? ` ${localize('com_ui_error')}: ${error.message}` : '' |
| | }`, |
| | status: 'error', |
| | }); |
| | }, |
| | }); |
| |
|
| | const create = useCreateAssistantMutation({ |
| | onSuccess: (data) => { |
| | setCurrentAssistantId(data.id); |
| | showToast({ |
| | message: `${localize('com_assistants_create_success')} ${ |
| | data.name ?? localize('com_ui_assistant') |
| | }`, |
| | }); |
| | }, |
| | onError: (err) => { |
| | const error = err as Error; |
| | showToast({ |
| | message: `${localize('com_assistants_create_error')}${ |
| | error.message ? ` ${localize('com_ui_error')}: ${error.message}` : '' |
| | }`, |
| | status: 'error', |
| | }); |
| | }, |
| | }); |
| |
|
| | const files = useMemo(() => { |
| | if (typeof assistant === 'string') { |
| | return []; |
| | } |
| | return assistant.files; |
| | }, [assistant]); |
| |
|
| | const onSubmit = (data: AssistantForm) => { |
| | const tools: Array<FunctionTool | string> = [...functions].map((functionName) => { |
| | if (!functionName.includes(actionDelimiter)) { |
| | return functionName; |
| | } else { |
| | const assistant = assistantMap?.[endpoint]?.[assistant_id]; |
| | const tool = assistant?.tools?.find((tool) => tool.function?.name === functionName); |
| | if (assistant && tool) { |
| | return tool; |
| | } |
| | } |
| |
|
| | return functionName; |
| | }); |
| |
|
| | if (data.code_interpreter) { |
| | tools.push({ type: Tools.code_interpreter }); |
| | } |
| | if (data.retrieval) { |
| | tools.push({ type: version == 2 ? Tools.file_search : Tools.retrieval }); |
| | } |
| | if (data.image_vision) { |
| | tools.push(ImageVisionTool); |
| | } |
| |
|
| | const { |
| | name, |
| | description, |
| | instructions, |
| | conversation_starters: starters, |
| | model, |
| | append_current_datetime, |
| | } = data; |
| |
|
| | if (assistant_id) { |
| | update.mutate({ |
| | assistant_id, |
| | data: { |
| | name, |
| | description, |
| | instructions, |
| | conversation_starters: starters.filter((starter) => starter.trim() !== ''), |
| | model, |
| | tools, |
| | endpoint, |
| | append_current_datetime, |
| | }, |
| | }); |
| | return; |
| | } |
| |
|
| | create.mutate({ |
| | name, |
| | description, |
| | instructions, |
| | conversation_starters: starters.filter((starter) => starter.trim() !== ''), |
| | model, |
| | tools, |
| | endpoint, |
| | version, |
| | append_current_datetime, |
| | }); |
| | }; |
| |
|
| | let submitContext: string | JSX.Element; |
| |
|
| | if (create.isLoading || update.isLoading) { |
| | submitContext = <Spinner className="icon-md" />; |
| | } else if (assistant_id) { |
| | submitContext = localize('com_ui_save'); |
| | } else { |
| | submitContext = localize('com_ui_create'); |
| | } |
| |
|
| | return ( |
| | <FormProvider {...methods}> |
| | <form |
| | onSubmit={handleSubmit(onSubmit)} |
| | className="h-auto w-full flex-shrink-0 overflow-x-hidden" |
| | > |
| | <div className="flex w-full flex-wrap"> |
| | <Controller |
| | name="assistant" |
| | control={control} |
| | render={({ field }) => ( |
| | <AssistantSelect |
| | reset={reset} |
| | value={field.value} |
| | endpoint={endpoint} |
| | documentsMap={documentsMap} |
| | allTools={allTools} |
| | setCurrentAssistantId={setCurrentAssistantId} |
| | selectedAssistant={current_assistant_id ?? null} |
| | createMutation={create} |
| | /> |
| | )} |
| | /> |
| | {/* Select Button */} |
| | {assistant_id && ( |
| | <button |
| | className="btn btn-primary focus:shadow-outline mx-2 mt-1 h-[40px] 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" |
| | type="button" |
| | disabled={!assistant_id} |
| | onClick={(e) => { |
| | e.preventDefault(); |
| | onSelectAssistant(assistant_id); |
| | }} |
| | > |
| | {localize('com_ui_select')} |
| | </button> |
| | )} |
| | </div> |
| | <div className="bg-surface-50 h-auto px-4 pb-8 pt-3 dark:bg-transparent"> |
| | {/* Avatar & Name */} |
| | <div className="mb-4"> |
| | <AssistantAvatar |
| | createMutation={create} |
| | assistant_id={assistant_id} |
| | metadata={assistant['metadata'] ?? null} |
| | endpoint={endpoint} |
| | version={version} |
| | /> |
| | <label className={labelClass} htmlFor="name"> |
| | {localize('com_ui_name')} |
| | </label> |
| | <Controller |
| | name="name" |
| | control={control} |
| | render={({ field }) => ( |
| | <input |
| | {...field} |
| | value={field.value ?? ''} |
| | {...{ max: 256 }} |
| | className={inputClass} |
| | id="name" |
| | type="text" |
| | placeholder={localize('com_assistants_name_placeholder')} |
| | /> |
| | )} |
| | /> |
| | <Controller |
| | name="id" |
| | control={control} |
| | render={({ field }) => ( |
| | <p className="h-3 text-xs italic text-text-secondary">{field.value}</p> |
| | )} |
| | /> |
| | </div> |
| | {/* Description */} |
| | <div className="mb-4"> |
| | <label className={labelClass} htmlFor="description"> |
| | {localize('com_ui_description')} |
| | </label> |
| | <Controller |
| | name="description" |
| | control={control} |
| | render={({ field }) => ( |
| | <input |
| | {...field} |
| | value={field.value ?? ''} |
| | {...{ max: 512 }} |
| | className={inputClass} |
| | id="description" |
| | type="text" |
| | placeholder={localize('com_assistants_description_placeholder')} |
| | /> |
| | )} |
| | /> |
| | </div> |
| | |
| | {/* Instructions */} |
| | <div className="mb-6"> |
| | <label className={labelClass} htmlFor="instructions"> |
| | {localize('com_ui_instructions')} |
| | </label> |
| | <Controller |
| | name="instructions" |
| | control={control} |
| | render={({ field }) => ( |
| | <textarea |
| | {...field} |
| | value={field.value ?? ''} |
| | {...{ max: 32768 }} |
| | className={cn(inputClass, 'min-h-[100px] resize-y')} |
| | id="instructions" |
| | placeholder={localize('com_assistants_instructions_placeholder')} |
| | rows={3} |
| | /> |
| | )} |
| | /> |
| | </div> |
| | |
| | {/* Append Today's Date */} |
| | <AppendDateCheckbox control={control} setValue={setValue} getValues={getValues} /> |
| | |
| | {/* Conversation Starters */} |
| | <div className="relative mb-6"> |
| | {/* the label of conversation starters is in the component */} |
| | <Controller |
| | name="conversation_starters" |
| | control={control} |
| | defaultValue={[]} |
| | render={({ field }) => ( |
| | <AssistantConversationStarters |
| | field={field} |
| | inputClass={inputClass} |
| | labelClass={labelClass} |
| | /> |
| | )} |
| | /> |
| | </div> |
| | {/* Model */} |
| | <div className="mb-6"> |
| | <label className={labelClass} htmlFor="model"> |
| | {localize('com_ui_model')} |
| | </label> |
| | <Controller |
| | name="model" |
| | control={control} |
| | rules={{ required: true, minLength: 1 }} |
| | render={({ field, fieldState: { error } }) => ( |
| | <> |
| | <SelectDropDown |
| | emptyTitle={true} |
| | value={field.value} |
| | setValue={field.onChange} |
| | availableValues={modelsQuery.data?.[endpoint] ?? []} |
| | showAbove={false} |
| | showLabel={false} |
| | className={cn( |
| | cardStyle, |
| | 'flex h-[40px] w-full flex-none items-center justify-center px-4 hover:cursor-pointer', |
| | )} |
| | containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')} |
| | /> |
| | {error && ( |
| | <span className="text-sm text-red-500 transition duration-300 ease-in-out"> |
| | {localize('com_ui_field_required')} |
| | </span> |
| | )} |
| | </> |
| | )} |
| | /> |
| | </div> |
| | {/* Knowledge */} |
| | {(codeEnabled === true || retrievalEnabled === true) && version == 1 && ( |
| | <Knowledge assistant_id={assistant_id} files={files} endpoint={endpoint} /> |
| | )} |
| | {/* Capabilities */} |
| | <CapabilitiesForm |
| | version={version} |
| | endpoint={endpoint} |
| | codeEnabled={codeEnabled} |
| | assistantsConfig={assistantsConfig} |
| | retrievalEnabled={retrievalEnabled} |
| | /> |
| | {/* Tools */} |
| | <div className="mb-6"> |
| | <label className={labelClass}> |
| | {`${toolsEnabled === true ? localize('com_ui_tools') : ''} |
| | ${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''} |
| | ${actionsEnabled === true ? localize('com_assistants_actions') : ''}`} |
| | </label> |
| | <div className="space-y-2"> |
| | {functions.map((func, i) => ( |
| | <AssistantTool |
| | key={`${func}-${i}-${assistant_id}`} |
| | tool={func} |
| | allTools={allTools} |
| | assistant_id={assistant_id} |
| | /> |
| | ))} |
| | {actions |
| | .filter((action) => action.assistant_id === assistant_id) |
| | .map((action, i) => { |
| | return <Action key={i} action={action} onClick={() => setAction(action)} />; |
| | })} |
| | <div className="flex space-x-2"> |
| | {toolsEnabled === true && ( |
| | <button |
| | type="button" |
| | onClick={() => setShowToolDialog(true)} |
| | className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium" |
| | > |
| | <div className="flex w-full items-center justify-center gap-2"> |
| | {localize('com_assistants_add_tools')} |
| | </div> |
| | </button> |
| | )} |
| | {actionsEnabled === true && ( |
| | <button |
| | type="button" |
| | disabled={!assistant_id} |
| | onClick={() => { |
| | if (!assistant_id) { |
| | return showToast({ |
| | message: localize('com_assistants_actions_disabled'), |
| | status: 'warning', |
| | }); |
| | } |
| | setActivePanel(Panel.actions); |
| | }} |
| | className="btn btn-neutral border-token-border-light relative h-8 w-full rounded-lg font-medium" |
| | > |
| | <div className="flex w-full items-center justify-center gap-2"> |
| | {localize('com_assistants_add_actions')} |
| | </div> |
| | </button> |
| | )} |
| | </div> |
| | </div> |
| | </div> |
| | <div className="flex items-center justify-end gap-2"> |
| | {/* Context Button */} |
| | <ContextButton |
| | assistant_id={assistant_id} |
| | activeModel={activeModel} |
| | setCurrentAssistantId={setCurrentAssistantId} |
| | createMutation={create} |
| | endpoint={endpoint} |
| | /> |
| | {/* Submit Button */} |
| | <button |
| | className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500" |
| | type="submit" |
| | > |
| | {submitContext} |
| | </button> |
| | </div> |
| | </div> |
| | <AssistantToolsDialog |
| | endpoint={endpoint} |
| | isOpen={showToolDialog} |
| | setIsOpen={setShowToolDialog} |
| | /> |
| | </form> |
| | </FormProvider> |
| | ); |
| | } |
| |
|