| import React, { useState, useMemo, useCallback } from 'react'; | |
| import { useToastContext } from '@librechat/client'; | |
| import { Controller, useWatch, useFormContext } from 'react-hook-form'; | |
| import { EModelEndpoint, getEndpointField } from 'librechat-data-provider'; | |
| import type { AgentForm, IconComponentTypes } from '~/common'; | |
| import { | |
| removeFocusOutlines, | |
| processAgentOption, | |
| defaultTextProps, | |
| validateEmail, | |
| getIconKey, | |
| cn, | |
| } from '~/utils'; | |
| import { ToolSelectDialog, MCPToolSelectDialog } from '~/components/Tools'; | |
| import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities'; | |
| import { useFileMapContext, useAgentPanelContext } from '~/Providers'; | |
| import AgentCategorySelector from './AgentCategorySelector'; | |
| import Action from '~/components/SidePanel/Builder/Action'; | |
| import { useLocalize, useVisibleTools } from '~/hooks'; | |
| import { Panel, isEphemeralAgent } from '~/common'; | |
| import { useGetAgentFiles } from '~/data-provider'; | |
| import { icons } from '~/hooks/Endpoint/Icons'; | |
| import Instructions from './Instructions'; | |
| import AgentAvatar from './AgentAvatar'; | |
| import FileContext from './FileContext'; | |
| import SearchForm from './Search/Form'; | |
| import FileSearch from './FileSearch'; | |
| import Artifacts from './Artifacts'; | |
| import AgentTool from './AgentTool'; | |
| import CodeForm from './Code/Form'; | |
| import MCPTools from './MCPTools'; | |
| const labelClass = 'mb-2 text-token-text-primary block font-medium'; | |
| const inputClass = cn( | |
| defaultTextProps, | |
| 'flex w-full px-3 py-2 border-border-light bg-surface-secondary focus-visible:ring-2 focus-visible:ring-ring-primary', | |
| removeFocusOutlines, | |
| ); | |
| export default function AgentConfig() { | |
| const localize = useLocalize(); | |
| const fileMap = useFileMapContext(); | |
| const { showToast } = useToastContext(); | |
| const methods = useFormContext<AgentForm>(); | |
| const [showToolDialog, setShowToolDialog] = useState(false); | |
| const [showMCPToolDialog, setShowMCPToolDialog] = useState(false); | |
| const { | |
| actions, | |
| setAction, | |
| regularTools, | |
| agentsConfig, | |
| startupConfig, | |
| mcpServersMap, | |
| setActivePanel, | |
| endpointsConfig, | |
| } = useAgentPanelContext(); | |
| const { | |
| control, | |
| formState: { errors }, | |
| } = methods; | |
| const provider = useWatch({ control, name: 'provider' }); | |
| const model = useWatch({ control, name: 'model' }); | |
| const agent = useWatch({ control, name: 'agent' }); | |
| const tools = useWatch({ control, name: 'tools' }); | |
| const agent_id = useWatch({ control, name: 'id' }); | |
| const { data: agentFiles = [] } = useGetAgentFiles(agent_id); | |
| const mergedFileMap = useMemo(() => { | |
| const newFileMap = { ...fileMap }; | |
| agentFiles.forEach((file) => { | |
| if (file.file_id) { | |
| newFileMap[file.file_id] = file; | |
| } | |
| }); | |
| return newFileMap; | |
| }, [fileMap, agentFiles]); | |
| const { | |
| codeEnabled, | |
| toolsEnabled, | |
| contextEnabled, | |
| actionsEnabled, | |
| artifactsEnabled, | |
| webSearchEnabled, | |
| fileSearchEnabled, | |
| } = useAgentCapabilities(agentsConfig?.capabilities); | |
| const context_files = useMemo(() => { | |
| if (typeof agent === 'string') { | |
| return []; | |
| } | |
| if (agent?.id !== agent_id) { | |
| return []; | |
| } | |
| if (agent.context_files) { | |
| return agent.context_files; | |
| } | |
| const _agent = processAgentOption({ | |
| agent, | |
| fileMap: mergedFileMap, | |
| }); | |
| return _agent.context_files ?? []; | |
| }, [agent, agent_id, mergedFileMap]); | |
| const knowledge_files = useMemo(() => { | |
| if (typeof agent === 'string') { | |
| return []; | |
| } | |
| if (agent?.id !== agent_id) { | |
| return []; | |
| } | |
| if (agent.knowledge_files) { | |
| return agent.knowledge_files; | |
| } | |
| const _agent = processAgentOption({ | |
| agent, | |
| fileMap: mergedFileMap, | |
| }); | |
| return _agent.knowledge_files ?? []; | |
| }, [agent, agent_id, mergedFileMap]); | |
| const code_files = useMemo(() => { | |
| if (typeof agent === 'string') { | |
| return []; | |
| } | |
| if (agent?.id !== agent_id) { | |
| return []; | |
| } | |
| if (agent.code_files) { | |
| return agent.code_files; | |
| } | |
| const _agent = processAgentOption({ | |
| agent, | |
| fileMap: mergedFileMap, | |
| }); | |
| return _agent.code_files ?? []; | |
| }, [agent, agent_id, mergedFileMap]); | |
| const handleAddActions = useCallback(() => { | |
| if (isEphemeralAgent(agent_id)) { | |
| showToast({ | |
| message: localize('com_assistants_actions_disabled'), | |
| status: 'warning', | |
| }); | |
| return; | |
| } | |
| setActivePanel(Panel.actions); | |
| }, [agent_id, setActivePanel, showToast, localize]); | |
| const providerValue = typeof provider === 'string' ? provider : provider?.value; | |
| let Icon: IconComponentTypes | null | undefined; | |
| let endpointType: EModelEndpoint | undefined; | |
| let endpointIconURL: string | undefined; | |
| let iconKey: string | undefined; | |
| if (providerValue !== undefined) { | |
| endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type'); | |
| endpointIconURL = getEndpointField(endpointsConfig, providerValue as string, 'iconURL'); | |
| iconKey = getIconKey({ | |
| endpoint: providerValue as string, | |
| endpointsConfig, | |
| endpointType, | |
| endpointIconURL, | |
| }); | |
| Icon = icons[iconKey]; | |
| } | |
| const { toolIds, mcpServerNames } = useVisibleTools(tools, regularTools, mcpServersMap); | |
| return ( | |
| <> | |
| <div className="h-auto bg-white px-4 pt-3 dark:bg-transparent"> | |
| {/* Avatar & Name */} | |
| <div className="mb-4"> | |
| <AgentAvatar avatar={agent?.['avatar'] ?? null} /> | |
| <label className={labelClass} htmlFor="name"> | |
| {localize('com_ui_name')} | |
| <span className="text-red-500">*</span> | |
| </label> | |
| <Controller | |
| name="name" | |
| rules={{ required: localize('com_ui_agent_name_is_required') }} | |
| control={control} | |
| render={({ field }) => ( | |
| <> | |
| <input | |
| {...field} | |
| value={field.value ?? ''} | |
| maxLength={256} | |
| className={inputClass} | |
| id="name" | |
| type="text" | |
| placeholder={localize('com_agents_name_placeholder')} | |
| aria-label="Agent name" | |
| /> | |
| <div | |
| className={cn( | |
| 'mt-1 w-56 text-sm text-red-500', | |
| errors.name ? 'visible h-auto' : 'invisible h-0', | |
| )} | |
| > | |
| {errors.name ? errors.name.message : ' '} | |
| </div> | |
| </> | |
| )} | |
| /> | |
| <Controller | |
| name="id" | |
| control={control} | |
| render={({ field }) => ( | |
| <p className="h-3 text-xs italic text-text-secondary" aria-live="polite"> | |
| {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 ?? ''} | |
| maxLength={512} | |
| className={inputClass} | |
| id="description" | |
| type="text" | |
| placeholder={localize('com_agents_description_placeholder')} | |
| aria-label="Agent description" | |
| /> | |
| )} | |
| /> | |
| </div> | |
| {/* Category */} | |
| <div className="mb-4"> | |
| <label className={labelClass} htmlFor="category-selector"> | |
| {localize('com_ui_category')} <span className="text-red-500">*</span> | |
| </label> | |
| <AgentCategorySelector className="w-full" /> | |
| </div> | |
| {/* Instructions */} | |
| <Instructions /> | |
| {/* Model and Provider */} | |
| <div className="mb-4"> | |
| <label className={labelClass} htmlFor="provider"> | |
| {localize('com_ui_model')} <span className="text-red-500">*</span> | |
| </label> | |
| <button | |
| type="button" | |
| onClick={() => setActivePanel(Panel.model)} | |
| className="btn btn-neutral border-token-border-light relative h-10 w-full rounded-lg font-medium" | |
| aria-haspopup="true" | |
| aria-expanded="false" | |
| > | |
| <div className="flex w-full items-center gap-2"> | |
| {Icon && ( | |
| <div className="shadow-stroke relative flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-white text-black dark:bg-white"> | |
| <Icon | |
| className="h-2/3 w-2/3" | |
| endpoint={providerValue as string} | |
| endpointType={endpointType} | |
| iconURL={endpointIconURL} | |
| /> | |
| </div> | |
| )} | |
| <span>{model != null && model ? model : localize('com_ui_select_model')}</span> | |
| </div> | |
| </button> | |
| </div> | |
| {(codeEnabled || | |
| fileSearchEnabled || | |
| artifactsEnabled || | |
| contextEnabled || | |
| webSearchEnabled) && ( | |
| <div className="mb-4 flex w-full flex-col items-start gap-3"> | |
| <label className="text-token-text-primary block font-medium"> | |
| {localize('com_assistants_capabilities')} | |
| </label> | |
| {/* Code Execution */} | |
| {codeEnabled && <CodeForm agent_id={agent_id} files={code_files} />} | |
| {/* Web Search */} | |
| {webSearchEnabled && <SearchForm />} | |
| {/* File Context */} | |
| {contextEnabled && <FileContext agent_id={agent_id} files={context_files} />} | |
| {/* Artifacts */} | |
| {artifactsEnabled && <Artifacts />} | |
| {/* File Search */} | |
| {fileSearchEnabled && <FileSearch agent_id={agent_id} files={knowledge_files} />} | |
| </div> | |
| )} | |
| {/* MCP Section */} | |
| {startupConfig?.mcpServers != null && ( | |
| <MCPTools | |
| agentId={agent_id} | |
| mcpServerNames={mcpServerNames} | |
| setShowMCPToolDialog={setShowMCPToolDialog} | |
| /> | |
| )} | |
| {/* Agent Tools & Actions */} | |
| <div className="mb-4"> | |
| <label className={labelClass}> | |
| {`${toolsEnabled === true ? localize('com_ui_tools') : ''} | |
| ${toolsEnabled === true && actionsEnabled === true ? ' + ' : ''} | |
| ${actionsEnabled === true ? localize('com_assistants_actions') : ''}`} | |
| </label> | |
| <div> | |
| <div className="mb-1"> | |
| {/* Render all visible IDs */} | |
| {toolIds.map((toolId, i) => { | |
| const tool = regularTools?.find((t) => t.pluginKey === toolId); | |
| if (!tool) return null; | |
| return ( | |
| <AgentTool | |
| key={`${toolId}-${i}-${agent_id}`} | |
| tool={toolId} | |
| regularTools={regularTools} | |
| agent_id={agent_id} | |
| /> | |
| ); | |
| })} | |
| </div> | |
| <div className="flex flex-col gap-1"> | |
| {(actions ?? []) | |
| .filter((action) => action.agent_id === agent_id) | |
| .map((action, i) => ( | |
| <Action | |
| key={i} | |
| action={action} | |
| onClick={() => { | |
| setAction(action); | |
| setActivePanel(Panel.actions); | |
| }} | |
| /> | |
| ))} | |
| </div> | |
| <div className="mt-2 flex space-x-2"> | |
| {(toolsEnabled ?? false) && ( | |
| <button | |
| type="button" | |
| onClick={() => setShowToolDialog(true)} | |
| className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium" | |
| aria-haspopup="dialog" | |
| > | |
| <div className="flex w-full items-center justify-center gap-2"> | |
| {localize('com_assistants_add_tools')} | |
| </div> | |
| </button> | |
| )} | |
| {(actionsEnabled ?? false) && ( | |
| <button | |
| type="button" | |
| disabled={isEphemeralAgent(agent_id)} | |
| onClick={handleAddActions} | |
| className="btn btn-neutral border-token-border-light relative h-9 w-full rounded-lg font-medium" | |
| aria-haspopup="dialog" | |
| > | |
| <div className="flex w-full items-center justify-center gap-2"> | |
| {localize('com_assistants_add_actions')} | |
| </div> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Support Contact (Optional) */} | |
| <div className="mb-4"> | |
| <div className="mb-1.5 flex items-center gap-2"> | |
| <span> | |
| <label className="text-token-text-primary block font-medium"> | |
| {localize('com_ui_support_contact')} | |
| </label> | |
| </span> | |
| </div> | |
| <div className="space-y-3"> | |
| {/* Support Contact Name */} | |
| <div className="flex flex-col"> | |
| <label | |
| className="mb-1 flex items-center justify-between" | |
| htmlFor="support-contact-name" | |
| > | |
| <span className="text-sm">{localize('com_ui_support_contact_name')}</span> | |
| </label> | |
| <Controller | |
| name="support_contact.name" | |
| control={control} | |
| rules={{ | |
| minLength: { | |
| value: 3, | |
| message: localize('com_ui_support_contact_name_min_length', { minLength: 3 }), | |
| }, | |
| }} | |
| render={({ field, fieldState: { error } }) => ( | |
| <> | |
| <input | |
| {...field} | |
| value={field.value ?? ''} | |
| className={cn(inputClass, error ? 'border-2 border-red-500' : '')} | |
| id="support-contact-name" | |
| type="text" | |
| placeholder={localize('com_ui_support_contact_name_placeholder')} | |
| aria-label="Support contact name" | |
| aria-invalid={error ? 'true' : 'false'} | |
| aria-describedby={error ? 'support-contact-name-error' : undefined} | |
| /> | |
| {error && ( | |
| <span | |
| id="support-contact-name-error" | |
| className="text-sm text-red-500 transition duration-300 ease-in-out" | |
| role="alert" | |
| aria-live="polite" | |
| > | |
| {error.message} | |
| </span> | |
| )} | |
| </> | |
| )} | |
| /> | |
| </div> | |
| {/* Support Contact Email */} | |
| <div className="flex flex-col"> | |
| <label | |
| className="mb-1 flex items-center justify-between" | |
| htmlFor="support-contact-email" | |
| > | |
| <span className="text-sm">{localize('com_ui_support_contact_email')}</span> | |
| </label> | |
| <Controller | |
| name="support_contact.email" | |
| control={control} | |
| rules={{ | |
| validate: (value) => | |
| validateEmail(value ?? '', localize('com_ui_support_contact_email_invalid')), | |
| }} | |
| render={({ field, fieldState: { error } }) => ( | |
| <> | |
| <input | |
| {...field} | |
| value={field.value ?? ''} | |
| className={cn(inputClass, error ? 'border-2 border-red-500' : '')} | |
| id="support-contact-email" | |
| type="email" | |
| placeholder={localize('com_ui_support_contact_email_placeholder')} | |
| aria-label="Support contact email" | |
| aria-invalid={error ? 'true' : 'false'} | |
| aria-describedby={error ? 'support-contact-email-error' : undefined} | |
| /> | |
| {error && ( | |
| <span | |
| id="support-contact-email-error" | |
| className="text-sm text-red-500 transition duration-300 ease-in-out" | |
| role="alert" | |
| aria-live="polite" | |
| > | |
| {error.message} | |
| </span> | |
| )} | |
| </> | |
| )} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <ToolSelectDialog | |
| isOpen={showToolDialog} | |
| setIsOpen={setShowToolDialog} | |
| endpoint={EModelEndpoint.agents} | |
| /> | |
| {startupConfig?.mcpServers != null && ( | |
| <MCPToolSelectDialog | |
| agentId={agent_id} | |
| isOpen={showMCPToolDialog} | |
| mcpServerNames={mcpServerNames} | |
| setIsOpen={setShowMCPToolDialog} | |
| endpoint={EModelEndpoint.agents} | |
| /> | |
| )} | |
| </> | |
| ); | |
| } | |