| import { useMemo, useCallback, useEffect, useRef } from 'react'; |
| import { Plus } from 'lucide-react'; |
| import { SelectDropDown } from '@librechat/client'; |
| import { |
| Tools, |
| FileSources, |
| Capabilities, |
| EModelEndpoint, |
| LocalStorageKeys, |
| isImageVisionTool, |
| defaultAssistantFormValues, |
| } from 'librechat-data-provider'; |
| import type { |
| TPlugin, |
| Assistant, |
| AssistantDocument, |
| AssistantsEndpoint, |
| AssistantCreateParams, |
| } from 'librechat-data-provider'; |
| import type { |
| Actions, |
| ExtendedFile, |
| AssistantForm, |
| TAssistantOption, |
| LastSelectedModels, |
| } from '~/common'; |
| import type { UseMutationResult } from '@tanstack/react-query'; |
| import type { UseFormReset } from 'react-hook-form'; |
| import { useListAssistantsQuery } from '~/data-provider'; |
| import { useLocalize, useLocalStorage } from '~/hooks'; |
| import { cn, createDropdownSetter } from '~/utils'; |
| import { useFileMapContext } from '~/Providers'; |
|
|
| const keys = new Set([ |
| 'name', |
| 'id', |
| 'description', |
| 'instructions', |
| 'conversation_starters', |
| 'model', |
| 'append_current_datetime', |
| ]); |
|
|
| export default function AssistantSelect({ |
| reset, |
| value, |
| endpoint, |
| documentsMap, |
| selectedAssistant, |
| setCurrentAssistantId, |
| createMutation, |
| allTools, |
| }: { |
| reset: UseFormReset<AssistantForm>; |
| value: TAssistantOption; |
| endpoint: AssistantsEndpoint; |
| selectedAssistant: string | null; |
| documentsMap: Map<string, AssistantDocument> | null; |
| setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>; |
| createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>; |
| allTools?: TPlugin[]; |
| }) { |
| const localize = useLocalize(); |
| const fileMap = useFileMapContext(); |
| const lastSelectedAssistant = useRef<string | null>(null); |
| const [lastSelectedModels] = useLocalStorage<LastSelectedModels | undefined>( |
| LocalStorageKeys.LAST_MODEL, |
| {} as LastSelectedModels, |
| ); |
|
|
| const toolkits = useMemo( |
| () => new Set(allTools?.filter((tool) => tool.toolkit === true).map((tool) => tool.pluginKey)), |
| [allTools], |
| ); |
|
|
| const query = useListAssistantsQuery(endpoint, undefined, { |
| select: (res) => |
| res.data.map((_assistant) => { |
| const source = |
| endpoint === EModelEndpoint.assistants ? FileSources.openai : FileSources.azure; |
| const assistant: TAssistantOption = { |
| ..._assistant, |
| label: _assistant.name ?? '', |
| value: _assistant.id, |
| files: _assistant.file_ids ? ([] as Array<[string, ExtendedFile]>) : undefined, |
| code_files: _assistant.tool_resources?.code_interpreter?.file_ids |
| ? ([] as Array<[string, ExtendedFile]>) |
| : undefined, |
| }; |
|
|
| const handleFile = (file_id: string, list?: Array<[string, ExtendedFile]>) => { |
| const file = fileMap?.[file_id]; |
| if (file) { |
| list?.push([ |
| file_id, |
| { |
| file_id: file.file_id, |
| type: file.type, |
| filepath: file.filepath, |
| filename: file.filename, |
| width: file.width, |
| height: file.height, |
| size: file.bytes, |
| preview: file.filepath, |
| progress: 1, |
| source, |
| }, |
| ]); |
| } else { |
| list?.push([ |
| file_id, |
| { |
| file_id, |
| type: '', |
| filename: '', |
| size: 1, |
| progress: 1, |
| filepath: endpoint, |
| source, |
| }, |
| ]); |
| } |
| }; |
|
|
| if (assistant.files && _assistant.file_ids) { |
| _assistant.file_ids.forEach((file_id) => handleFile(file_id, assistant.files)); |
| } |
|
|
| if (assistant.code_files && _assistant.tool_resources?.code_interpreter?.file_ids) { |
| _assistant.tool_resources.code_interpreter.file_ids.forEach((file_id) => |
| handleFile(file_id, assistant.code_files), |
| ); |
| } |
|
|
| const assistantDoc = documentsMap?.get(_assistant.id); |
| |
| if (assistantDoc) { |
| if (!assistant.conversation_starters) { |
| assistant.conversation_starters = assistantDoc.conversation_starters; |
| } |
| assistant.append_current_datetime = assistantDoc.append_current_datetime ?? false; |
| } |
|
|
| return assistant; |
| }), |
| }); |
|
|
| const onSelect = useCallback( |
| (value: string) => { |
| const assistant = query.data?.find((assistant) => assistant.id === value); |
|
|
| createMutation.reset(); |
| if (!assistant) { |
| setCurrentAssistantId(undefined); |
| return reset({ |
| ...defaultAssistantFormValues, |
| model: lastSelectedModels?.[endpoint] ?? '', |
| }); |
| } |
|
|
| const update = { |
| ...assistant, |
| label: assistant.name ?? '', |
| value: assistant.id || '', |
| }; |
|
|
| const actions: Actions = { |
| [Capabilities.code_interpreter]: false, |
| [Capabilities.image_vision]: false, |
| [Capabilities.retrieval]: false, |
| }; |
|
|
| (assistant.tools ?? []) |
| .filter((tool) => tool.type !== 'function' || isImageVisionTool(tool)) |
| .map((tool) => (tool.function?.name ?? '') || tool.type) |
| .forEach((tool) => { |
| if (tool === Tools.file_search) { |
| actions[Capabilities.retrieval] = true; |
| } |
| actions[tool] = true; |
| }); |
|
|
| const seenToolkits = new Set<string>(); |
| const functions = (assistant.tools ?? []) |
| .filter((tool) => tool.type === 'function' && !isImageVisionTool(tool)) |
| .map((tool) => tool.function?.name ?? '') |
| .filter((fnName) => { |
| const fnPrefix = fnName.split('_')[0]; |
| const seenToolkit = toolkits.has(fnPrefix); |
| if (seenToolkit) { |
| seenToolkits.add(fnPrefix); |
| } |
| return !seenToolkit; |
| }); |
|
|
| if (seenToolkits.size > 0) { |
| functions.push(...Array.from(seenToolkits)); |
| } |
|
|
| const formValues: Partial<AssistantForm & Actions> = { |
| functions, |
| ...actions, |
| assistant: update, |
| model: update.model, |
| }; |
|
|
| Object.entries(assistant).forEach(([name, value]) => { |
| if (!keys.has(name)) { |
| return; |
| } |
|
|
| if (name === 'append_current_datetime') { |
| formValues[name] = !!value; |
| return; |
| } |
|
|
| if ( |
| name === 'conversation_starters' && |
| Array.isArray(value) && |
| value.every((item) => typeof item === 'string') |
| ) { |
| formValues[name] = value; |
| return; |
| } |
|
|
| if (typeof value !== 'number' && typeof value !== 'object') { |
| formValues[name] = value; |
| } |
| }); |
|
|
| reset(formValues); |
| setCurrentAssistantId(assistant.id); |
| }, |
| [ |
| query.data, |
| reset, |
| setCurrentAssistantId, |
| createMutation, |
| endpoint, |
| lastSelectedModels, |
| toolkits, |
| ], |
| ); |
|
|
| useEffect(() => { |
| let timerId: NodeJS.Timeout | null = null; |
|
|
| if (selectedAssistant === lastSelectedAssistant.current) { |
| return; |
| } |
|
|
| if (selectedAssistant !== '' && selectedAssistant != null && query.data) { |
| timerId = setTimeout(() => { |
| lastSelectedAssistant.current = selectedAssistant; |
| onSelect(selectedAssistant); |
| }, 5); |
| } |
|
|
| return () => { |
| if (timerId) { |
| clearTimeout(timerId); |
| } |
| }; |
| }, [selectedAssistant, query.data, onSelect]); |
|
|
| const createAssistant = localize('com_ui_create') + ' ' + localize('com_ui_assistant'); |
| return ( |
| <SelectDropDown |
| value={!value ? createAssistant : value} |
| setValue={createDropdownSetter(onSelect)} |
| availableValues={ |
| query.data ?? [ |
| { |
| label: 'Loading...', |
| value: '', |
| }, |
| ] |
| } |
| iconSide="left" |
| showAbove={false} |
| showLabel={false} |
| emptyTitle={true} |
| containerClassName="flex-grow" |
| searchClassName="dark:from-gray-850" |
| searchPlaceholder={localize('com_assistants_search_name')} |
| optionsClass="hover:bg-gray-20/50 dark:border-gray-700" |
| optionsListClass="rounded-lg shadow-lg dark:bg-gray-850 dark:border-gray-700 dark:last:border" |
| currentValueClass={cn( |
| 'text-md font-semibold text-gray-900 dark:text-white', |
| value === '' ? 'text-gray-500' : '', |
| )} |
| className={cn( |
| 'mt-1 rounded-md dark:border-gray-700 dark:bg-gray-850', |
| 'z-50 flex h-[40px] w-full flex-none items-center justify-center px-4 hover:cursor-pointer hover:border-green-500 focus:border-gray-400', |
| )} |
| renderOption={() => ( |
| <span className="flex items-center gap-1.5 truncate"> |
| <span className="absolute inset-y-0 left-0 flex items-center pl-2 text-gray-800 dark:text-gray-100"> |
| <Plus className="w-[16px]" /> |
| </span> |
| <span className={cn('ml-4 flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100')}> |
| {createAssistant} |
| </span> |
| </span> |
| )} |
| /> |
| ); |
| } |
|
|