| | import { X, Link2, PlusCircle } from 'lucide-react'; |
| | import { EModelEndpoint } from 'librechat-data-provider'; |
| | import React, { useState, useMemo, useCallback, useEffect } from 'react'; |
| | import { |
| | ControlCombobox, |
| | HoverCard, |
| | HoverCardPortal, |
| | HoverCardContent, |
| | HoverCardTrigger, |
| | CircleHelpIcon, |
| | } from '@librechat/client'; |
| | import type { ControllerRenderProps } from 'react-hook-form'; |
| | import type { TMessage } from 'librechat-data-provider'; |
| | import type { AgentForm, OptionWithIcon } from '~/common'; |
| | import MessageIcon from '~/components/Share/MessageIcon'; |
| | import { useAgentsMapContext } from '~/Providers'; |
| | import { useLocalize } from '~/hooks'; |
| | import { ESide } from '~/common'; |
| |
|
| | interface AgentChainProps { |
| | field: ControllerRenderProps<AgentForm, 'agent_ids'>; |
| | currentAgentId: string; |
| | } |
| |
|
| | |
| | const MAX_AGENTS = 10; |
| |
|
| | const AgentChain: React.FC<AgentChainProps> = ({ field, currentAgentId }) => { |
| | const localize = useLocalize(); |
| | const [newAgentId, setNewAgentId] = useState(''); |
| | const agentsMap = useAgentsMapContext() || {}; |
| | const agentIds = field.value || []; |
| |
|
| | const agents = useMemo(() => Object.values(agentsMap), [agentsMap]); |
| |
|
| | const selectableAgents = useMemo( |
| | () => |
| | agents |
| | .filter((agent) => agent?.id !== currentAgentId) |
| | .map( |
| | (agent) => |
| | ({ |
| | label: agent?.name || '', |
| | value: agent?.id, |
| | icon: ( |
| | <MessageIcon |
| | message={ |
| | { |
| | endpoint: EModelEndpoint.agents, |
| | isCreatedByUser: false, |
| | } as TMessage |
| | } |
| | agent={agent} |
| | /> |
| | ), |
| | }) as OptionWithIcon, |
| | ), |
| | [agents, currentAgentId], |
| | ); |
| |
|
| | const getAgentDetails = useCallback((id: string) => agentsMap[id], [agentsMap]); |
| |
|
| | useEffect(() => { |
| | if (newAgentId && agentIds.length < MAX_AGENTS) { |
| | field.onChange([...agentIds, newAgentId]); |
| | setNewAgentId(''); |
| | } |
| | }, [newAgentId, agentIds, field]); |
| |
|
| | const removeAgentAt = (index: number) => { |
| | field.onChange(agentIds.filter((_, i) => i !== index)); |
| | }; |
| |
|
| | const updateAgentAt = (index: number, id: string) => { |
| | const updated = [...agentIds]; |
| | updated[index] = id; |
| | field.onChange(updated); |
| | }; |
| |
|
| | return ( |
| | <HoverCard openDelay={50}> |
| | <div className="flex items-center justify-between gap-2"> |
| | <div className="flex items-center gap-2"> |
| | <label className="font-semibold text-text-primary"> |
| | {localize('com_ui_agent_chain')} |
| | </label> |
| | <HoverCardTrigger> |
| | <CircleHelpIcon className="h-4 w-4 text-text-tertiary" /> |
| | </HoverCardTrigger> |
| | </div> |
| | <div className="text-xs text-text-secondary"> |
| | {agentIds.length} / {MAX_AGENTS} |
| | </div> |
| | </div> |
| | <div className="space-y-1"> |
| | {/* Current fixed agent */} |
| | <div className="flex h-10 items-center justify-between rounded-md border border-border-medium bg-surface-primary-contrast px-3 py-2"> |
| | <div className="flex items-center gap-2"> |
| | <div className="flex h-6 w-6 items-center justify-center overflow-hidden rounded-full"> |
| | <MessageIcon |
| | message={ |
| | { |
| | endpoint: EModelEndpoint.agents, |
| | isCreatedByUser: false, |
| | } as TMessage |
| | } |
| | agent={currentAgentId ? agentsMap[currentAgentId] : undefined} |
| | /> |
| | </div> |
| | <div className="font-medium text-text-primary"> |
| | {getAgentDetails(currentAgentId)?.name} |
| | </div> |
| | </div> |
| | </div> |
| | {<Link2 className="mx-auto text-text-secondary" size={14} />} |
| | {agentIds.map((agentId, idx) => ( |
| | <React.Fragment key={agentId}> |
| | <div className="flex h-10 items-center gap-2 rounded-md border border-border-medium bg-surface-tertiary pr-2"> |
| | <ControlCombobox |
| | isCollapsed={false} |
| | ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_select') })} |
| | selectedValue={agentId} |
| | setValue={(id) => updateAgentAt(idx, id)} |
| | selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_select') })} |
| | searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })} |
| | items={selectableAgents} |
| | displayValue={getAgentDetails(agentId)?.name ?? ''} |
| | SelectIcon={ |
| | <MessageIcon |
| | message={ |
| | { |
| | endpoint: EModelEndpoint.agents, |
| | isCreatedByUser: false, |
| | } as TMessage |
| | } |
| | agent={agentId ? agentsMap[agentId] : undefined} |
| | /> |
| | } |
| | className="flex-1 border-border-heavy" |
| | containerClassName="px-0" |
| | /> |
| | {/* Future Settings button? */} |
| | {/* <button className="hover:bg-surface-hover p-1 rounded transition"> |
| | <Settings size={16} className="text-text-secondary" /> |
| | </button> */} |
| | <button |
| | className="rounded-xl p-1 transition hover:bg-surface-hover" |
| | onClick={() => removeAgentAt(idx)} |
| | aria-label={localize('com_ui_remove_agent_from_chain', { |
| | 0: getAgentDetails(agentId)?.name || localize('com_ui_agent'), |
| | })} |
| | > |
| | <X size={18} className="text-text-secondary" /> |
| | </button> |
| | </div> |
| | {idx < agentIds.length - 1 && ( |
| | <Link2 className="mx-auto text-text-secondary" size={14} /> |
| | )} |
| | </React.Fragment> |
| | ))} |
| | |
| | {agentIds.length < MAX_AGENTS && ( |
| | <> |
| | {agentIds.length > 0 && <Link2 className="mx-auto text-text-secondary" size={14} />} |
| | <ControlCombobox |
| | isCollapsed={false} |
| | ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_add') })} |
| | selectedValue="" |
| | setValue={setNewAgentId} |
| | selectPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_add') })} |
| | searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })} |
| | items={selectableAgents} |
| | className="h-10 w-full border-dashed border-border-heavy text-center text-text-secondary hover:text-text-primary" |
| | containerClassName="px-0" |
| | SelectIcon={<PlusCircle size={16} className="text-text-secondary" />} |
| | /> |
| | </> |
| | )} |
| | |
| | {agentIds.length >= MAX_AGENTS && ( |
| | <p className="pt-1 text-center text-xs italic text-text-tertiary"> |
| | {localize('com_ui_agent_chain_max', { 0: MAX_AGENTS })} |
| | </p> |
| | )} |
| | </div> |
| | <HoverCardPortal> |
| | <HoverCardContent side={ESide.Top} className="w-80"> |
| | <div className="space-y-2"> |
| | <p className="text-sm text-text-secondary">{localize('com_ui_agent_chain_info')}</p> |
| | </div> |
| | </HoverCardContent> |
| | </HoverCardPortal> |
| | </HoverCard> |
| | ); |
| | }; |
| |
|
| | export default AgentChain; |
| |
|