import { useMemo, useState, useEffect, useRef, useLayoutEffect } from 'react'; import { Button } from '@librechat/client'; import { TriangleAlert } from 'lucide-react'; import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider'; import type { TAttachment } from 'librechat-data-provider'; import { useLocalize, useProgress } from '~/hooks'; import { AttachmentGroup } from './Parts'; import ToolCallInfo from './ToolCallInfo'; import ProgressText from './ProgressText'; import { logger, cn } from '~/utils'; export default function ToolCall({ initialProgress = 0.1, isLast = false, isSubmitting, name, args: _args = '', output, attachments, auth, }: { initialProgress: number; isLast?: boolean; isSubmitting: boolean; name: string; args: string | Record; output?: string | null; attachments?: TAttachment[]; auth?: string; expires_at?: number; }) { const localize = useLocalize(); const [showInfo, setShowInfo] = useState(false); const contentRef = useRef(null); const [contentHeight, setContentHeight] = useState(0); const [isAnimating, setIsAnimating] = useState(false); const prevShowInfoRef = useRef(showInfo); const { function_name, domain, isMCPToolCall } = useMemo(() => { if (typeof name !== 'string') { return { function_name: '', domain: null, isMCPToolCall: false }; } if (name.includes(Constants.mcp_delimiter)) { const [func, server] = name.split(Constants.mcp_delimiter); return { function_name: func || '', domain: server && (server.replaceAll(actionDomainSeparator, '.') || null), isMCPToolCall: true, }; } const [func, _domain] = name.includes(actionDelimiter) ? name.split(actionDelimiter) : [name, '']; return { function_name: func || '', domain: _domain && (_domain.replaceAll(actionDomainSeparator, '.') || null), isMCPToolCall: false, }; }, [name]); const error = typeof output === 'string' && output.toLowerCase().includes('error processing tool'); const args = useMemo(() => { if (typeof _args === 'string') { return _args; } try { return JSON.stringify(_args, null, 2); } catch (e) { logger.error( 'client/src/components/Chat/Messages/Content/ToolCall.tsx - Failed to stringify args', e, ); return ''; } }, [_args]) as string | undefined; const hasInfo = useMemo( () => (args?.length ?? 0) > 0 || (output?.length ?? 0) > 0, [args, output], ); const authDomain = useMemo(() => { const authURL = auth ?? ''; if (!authURL) { return ''; } try { const url = new URL(authURL); return url.hostname; } catch (e) { logger.error( 'client/src/components/Chat/Messages/Content/ToolCall.tsx - Failed to parse auth URL', e, ); return ''; } }, [auth]); const progress = useProgress(initialProgress); const cancelled = (!isSubmitting && progress < 1) || error === true; const getFinishedText = () => { if (cancelled) { return localize('com_ui_cancelled'); } if (isMCPToolCall === true) { return localize('com_assistants_completed_function', { 0: function_name }); } if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) { return localize('com_assistants_completed_action', { 0: domain }); } return localize('com_assistants_completed_function', { 0: function_name }); }; useLayoutEffect(() => { if (showInfo !== prevShowInfoRef.current) { prevShowInfoRef.current = showInfo; setIsAnimating(true); if (showInfo && contentRef.current) { requestAnimationFrame(() => { if (contentRef.current) { const height = contentRef.current.scrollHeight; setContentHeight(height + 4); } }); } else { setContentHeight(0); } const timer = setTimeout(() => { setIsAnimating(false); }, 400); return () => clearTimeout(timer); } }, [showInfo]); useEffect(() => { if (!contentRef.current) { return; } const resizeObserver = new ResizeObserver((entries) => { if (showInfo && !isAnimating) { for (const entry of entries) { if (entry.target === contentRef.current) { setContentHeight(entry.contentRect.height + 4); } } } }); resizeObserver.observe(contentRef.current); return () => { resizeObserver.disconnect(); }; }, [showInfo, isAnimating]); if (!isLast && (!function_name || function_name.length === 0) && !output) { return null; } return ( <>
setShowInfo((prev) => !prev)} inProgressText={ function_name ? localize('com_assistants_running_var', { 0: function_name }) : localize('com_assistants_running_action') } authText={ !cancelled && authDomain.length > 0 ? localize('com_ui_requires_auth') : undefined } finishedText={getFinishedText()} hasInput={hasInfo} isExpanded={showInfo} error={cancelled} />
{showInfo && hasInfo && ( 0 && !cancelled && progress < 1} attachments={attachments} /> )}
{auth != null && auth && progress < 1 && !cancelled && (

{localize('com_assistants_allow_sites_you_trust')}

)} {attachments && attachments.length > 0 && } ); }