/* * @Author: ZhaoYing * @Date: 2026-03-17 14:22:25 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-03-23 17:42:38 */ // Toolbar component for chat input area, supporting file upload, audio recording, and variable configuration import { useRef, forwardRef, useImperativeHandle, type ReactNode, useEffect } from 'react' import { Flex, Dropdown, Divider, App, Form, type MenuProps, Tooltip } from 'antd' import { useTranslation } from 'react-i18next' import clsx from 'clsx' import AudioRecorder from '@/components/AudioRecorder' import UploadFiles from '@/views/Conversation/components/FileUpload' import UploadFileListModal from '@/views/Conversation/components/UploadFileListModal' import VariableConfigModal from '@/views/Workflow/components/Chat/VariableConfigModal' import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types' import type { UploadFileListModalRef } from '@/views/Conversation/types' import type { VariableConfigModalRef } from '@/views/Workflow/types' import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types' import { getFileInfoByUrl } from '@/api/fileStorage'; // Exposed methods via ref for parent components to access/set form state export interface ChatToolbarRef { getFiles: () => any[] getVariables: () => Variable[] setFiles: (files: any[]) => void setVariables: (variables: Variable[]) => void } // Props for configuring toolbar features, upload settings, and event callbacks export interface ChatToolbarProps { features: FeaturesConfigForm leftExtra?: ReactNode; rightExtra?: ReactNode uploadAction?: string uploadRequestConfig?: { data?: Record headers?: Record } onFilesChange?: (files: any[]) => void onVariablesChange?: (variables: Variable[]) => void onRecordingComplete?: (file: any) => void; defaultValue?: { memory: boolean } } interface FormValues { files: any[] variables: Variable[]; memory?: boolean; } const ChatToolbar = forwardRef(({ features, leftExtra, rightExtra, uploadAction, uploadRequestConfig, onFilesChange, onVariablesChange, onRecordingComplete, defaultValue, }, ref) => { const { t } = useTranslation() const { message: messageApi } = App.useApp() const uploadFileListModalRef = useRef(null) const variableConfigModalRef = useRef(null) const [form] = Form.useForm() const queryValues = Form.useWatch([], form) useEffect(() => { if (!defaultValue) return form.setFieldsValue(defaultValue) }, [defaultValue]) useImperativeHandle(ref, () => ({ getFiles: () => form.getFieldValue('files') || [], getVariables: () => form.getFieldValue('variables') || [], setFiles: (files) => form.setFieldValue('files', files), setVariables: (variables) => { console.log('variables', variables) form.setFieldValue('variables', variables) }, })) const { file_upload } = features || {} // Append newly uploaded file to the file list when upload is complete const fileChange = (file?: any) => { if (file?.status !== 'done') return const files = [...(queryValues?.files || []), file] form.setFieldValue('files', files) onFilesChange?.(files) } // Append recorded audio file to the file list and notify parent const handleRecordingComplete = (file: any) => { const files = [...(queryValues?.files || []), file] form.setFieldValue('files', files) onFilesChange?.(files) onRecordingComplete?.(file) } // Merge a batch of files (e.g. from remote URL modal) into the file list const addFileList = (list?: any[]) => { if (!list?.length) return const uploadingList = list.map(f => ({ ...f, status: 'uploading' })) const files = [...(queryValues?.files || []), ...uploadingList] form.setFieldValue('files', files) onFilesChange?.(files) uploadingList.forEach(file => { getFileInfoByUrl(file.url) .then((res) => { const { file_name, file_size, content_type } = res as { file_name: string; file_size: number; content_type: string; } const current: any[] = form.getFieldValue('files') || [] const updated = current.map(f => f.uid === file.uid ? { ...f, status: 'done', name: file_name, size: file_size, type: content_type, } : f) form.setFieldValue('files', updated) onFilesChange?.(updated) }) .catch(() => { const current: any[] = form.getFieldValue('files') || [] const updated = current.map(f => f.uid === file.uid ? { ...f, status: 'error' } : f) form.setFieldValue('files', updated) onFilesChange?.(updated) }) }) } // Persist variable values from the config modal and notify parent const handleVariablesSave = (values: Variable[]) => { form.setFieldValue('variables', values) onVariablesChange?.(values) } // True when any required variable is missing a value, used to highlight the config button const isNeedVariableConfig = queryValues?.variables?.some( vo => vo.required && (vo.value === null || vo.value === undefined || vo.value === '') ) // Build dropdown menu items based on allowed transfer methods const fileMenus: MenuProps['items'] = [] const enabledTypes = ['image', 'document', 'video', 'audio'].filter( type => file_upload?.[`${type}_enabled` as keyof FeaturesConfigForm['file_upload']] ) if (file_upload?.allowed_transfer_methods?.includes('remote_url') && enabledTypes.length > 0) { fileMenus.push({ key: 'url', label: t('memoryConversation.addRemoteFile'), onClick: () => { if ((queryValues?.files?.length || 0) >= file_upload.max_file_count) { messageApi.warning(t('common.fileNumTip', { num: file_upload.max_file_count })) return } uploadFileListModalRef.current?.handleOpen() } }) } if (file_upload?.allowed_transfer_methods?.includes('local_file') && enabledTypes.length > 0) { fileMenus.push({ key: 'upload', label: ( = file_upload.max_file_count} /> ) }) } return (