import { useTranslate } from '@/hooks/common-hooks'; import { useDeleteDocument, useFetchDocumentInfosByIds, useRemoveNextDocument, useUploadAndParseDocument, } from '@/hooks/document-hooks'; import { getExtension } from '@/utils/document-util'; import { formatBytes } from '@/utils/file-util'; import { CloseCircleOutlined, InfoCircleOutlined, LoadingOutlined, } from '@ant-design/icons'; import type { GetProp, UploadFile } from 'antd'; import { Button, Card, Flex, Input, List, Space, Spin, Typography, Upload, UploadProps, } from 'antd'; import classNames from 'classnames'; import get from 'lodash/get'; import { Paperclip } from 'lucide-react'; import { ChangeEventHandler, memo, useCallback, useEffect, useRef, useState, } from 'react'; import FileIcon from '../file-icon'; import styles from './index.less'; type FileType = Parameters>[0]; const { Text } = Typography; const getFileId = (file: UploadFile) => get(file, 'response.data.0'); const getFileIds = (fileList: UploadFile[]) => { const ids = fileList.reduce((pre, cur) => { return pre.concat(get(cur, 'response.data', [])); }, []); return ids; }; const isUploadSuccess = (file: UploadFile) => { const code = get(file, 'response.code'); return typeof code === 'number' && code === 0; }; interface IProps { disabled: boolean; value: string; sendDisabled: boolean; sendLoading: boolean; onPressEnter(documentIds: string[]): void; onInputChange: ChangeEventHandler; conversationId: string; uploadMethod?: string; isShared?: boolean; showUploadIcon?: boolean; createConversationBeforeUploadDocument?(message: string): Promise; } const getBase64 = (file: FileType): Promise => new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file as any); reader.onload = () => resolve(reader.result as string); reader.onerror = (error) => reject(error); }); const MessageInput = ({ isShared = false, disabled, value, onPressEnter, sendDisabled, sendLoading, onInputChange, conversationId, showUploadIcon = true, createConversationBeforeUploadDocument, uploadMethod = 'upload_and_parse', }: IProps) => { const { t } = useTranslate('chat'); const { removeDocument } = useRemoveNextDocument(); const { deleteDocument } = useDeleteDocument(); const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds(); const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod); const conversationIdRef = useRef(conversationId); const [fileList, setFileList] = useState([]); const handlePreview = async (file: UploadFile) => { if (!file.url && !file.preview) { file.preview = await getBase64(file.originFileObj as FileType); } }; const handleChange: UploadProps['onChange'] = async ({ // fileList: newFileList, file, }) => { let nextConversationId: string = conversationId; if (createConversationBeforeUploadDocument) { const creatingRet = await createConversationBeforeUploadDocument( file.name, ); if (creatingRet?.code === 0) { nextConversationId = creatingRet.data.id; } } setFileList((list) => { list.push({ ...file, status: 'uploading', originFileObj: file as any, }); return [...list]; }); const ret = await uploadAndParseDocument({ conversationId: nextConversationId, fileList: [file], }); setFileList((list) => { const nextList = list.filter((x) => x.uid !== file.uid); nextList.push({ ...file, originFileObj: file as any, response: ret, percent: 100, status: ret?.code === 0 ? 'done' : 'error', }); return nextList; }); }; const isUploadingFile = fileList.some((x) => x.status === 'uploading'); const handlePressEnter = useCallback(async () => { if (isUploadingFile) return; const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x))); onPressEnter(ids); setFileList([]); }, [fileList, onPressEnter, isUploadingFile]); const handleRemove = useCallback( async (file: UploadFile) => { const ids = get(file, 'response.data', []); // Upload Successfully if (Array.isArray(ids) && ids.length) { if (isShared) { await deleteDocument(ids); } else { await removeDocument(ids[0]); } setFileList((preList) => { return preList.filter((x) => getFileId(x) !== ids[0]); }); } else { // Upload failed setFileList((preList) => { return preList.filter((x) => x.uid !== file.uid); }); } }, [removeDocument, deleteDocument, isShared], ); const getDocumentInfoById = useCallback( (id: string) => { return documentInfos.find((x) => x.id === id); }, [documentInfos], ); useEffect(() => { const ids = getFileIds(fileList); setDocumentIds(ids); }, [fileList, setDocumentIds]); useEffect(() => { if ( conversationIdRef.current && conversationId !== conversationIdRef.current ) { setFileList([]); } conversationIdRef.current = conversationId; }, [conversationId, setFileList]); return ( {showUploadIcon && ( { return false; }} > )} } onPressEnter={handlePressEnter} onChange={onInputChange} /> {fileList.length > 0 && ( { const id = getFileId(item); const documentInfo = getDocumentInfoById(id); const fileExtension = getExtension(documentInfo?.name ?? ''); const fileName = item.originFileObj?.name ?? ''; return ( {item.status === 'uploading' ? ( } /> ) : item.status === 'error' ? ( ) : ( )} {fileName} {item.status === 'error' ? ( t('uploadFailed') ) : ( <> {item.percent !== 100 ? ( t('uploading') ) : !item.response ? ( t('parsing') ) : ( {fileExtension?.toUpperCase()}, {formatBytes( getDocumentInfoById(id)?.size ?? 0, )} )} )} {item.status !== 'uploading' && ( handleRemove(item)} /> )} ); }} /> )} ); }; export default memo(MessageInput);