|
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<GetProp<UploadProps, 'beforeUpload'>>[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<HTMLInputElement>; |
|
conversationId: string; |
|
uploadMethod?: string; |
|
isShared?: boolean; |
|
showUploadIcon?: boolean; |
|
createConversationBeforeUploadDocument?(message: string): Promise<any>; |
|
} |
|
|
|
const getBase64 = (file: FileType): Promise<string> => |
|
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<UploadFile[]>([]); |
|
|
|
const handlePreview = async (file: UploadFile) => { |
|
if (!file.url && !file.preview) { |
|
file.preview = await getBase64(file.originFileObj as FileType); |
|
} |
|
}; |
|
|
|
const handleChange: UploadProps['onChange'] = async ({ |
|
|
|
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', []); |
|
|
|
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 { |
|
|
|
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 ( |
|
<Flex gap={20} vertical className={styles.messageInputWrapper}> |
|
<Input |
|
size="large" |
|
placeholder={t('sendPlaceholder')} |
|
value={value} |
|
disabled={disabled} |
|
className={classNames({ [styles.inputWrapper]: fileList.length === 0 })} |
|
suffix={ |
|
<Space> |
|
{showUploadIcon && ( |
|
<Upload |
|
onPreview={handlePreview} |
|
onChange={handleChange} |
|
multiple={false} |
|
onRemove={handleRemove} |
|
showUploadList={false} |
|
beforeUpload={() => { |
|
return false; |
|
}} |
|
> |
|
<Button |
|
type={'text'} |
|
disabled={disabled} |
|
icon={<Paperclip></Paperclip>} |
|
></Button> |
|
</Upload> |
|
)} |
|
<Button |
|
type="primary" |
|
onClick={handlePressEnter} |
|
loading={sendLoading} |
|
disabled={sendDisabled || isUploadingFile} |
|
> |
|
{t('send')} |
|
</Button> |
|
</Space> |
|
} |
|
onPressEnter={handlePressEnter} |
|
onChange={onInputChange} |
|
/> |
|
|
|
{fileList.length > 0 && ( |
|
<List |
|
grid={{ |
|
gutter: 16, |
|
xs: 1, |
|
sm: 1, |
|
md: 1, |
|
lg: 1, |
|
xl: 2, |
|
xxl: 4, |
|
}} |
|
dataSource={fileList} |
|
className={styles.listWrapper} |
|
renderItem={(item) => { |
|
const id = getFileId(item); |
|
const documentInfo = getDocumentInfoById(id); |
|
const fileExtension = getExtension(documentInfo?.name ?? ''); |
|
const fileName = item.originFileObj?.name ?? ''; |
|
|
|
return ( |
|
<List.Item> |
|
<Card className={styles.documentCard}> |
|
<Flex gap={10} align="center"> |
|
{item.status === 'uploading' ? ( |
|
<Spin |
|
indicator={ |
|
<LoadingOutlined style={{ fontSize: 24 }} spin /> |
|
} |
|
/> |
|
) : item.status === 'error' ? ( |
|
<InfoCircleOutlined size={30}></InfoCircleOutlined> |
|
) : ( |
|
<FileIcon id={id} name={fileName}></FileIcon> |
|
)} |
|
<Flex vertical style={{ width: '90%' }}> |
|
<Text |
|
ellipsis={{ tooltip: fileName }} |
|
className={styles.nameText} |
|
> |
|
<b> {fileName}</b> |
|
</Text> |
|
{item.status === 'error' ? ( |
|
t('uploadFailed') |
|
) : ( |
|
<> |
|
{item.percent !== 100 ? ( |
|
t('uploading') |
|
) : !item.response ? ( |
|
t('parsing') |
|
) : ( |
|
<Space> |
|
<span>{fileExtension?.toUpperCase()},</span> |
|
<span> |
|
{formatBytes( |
|
getDocumentInfoById(id)?.size ?? 0, |
|
)} |
|
</span> |
|
</Space> |
|
)} |
|
</> |
|
)} |
|
</Flex> |
|
</Flex> |
|
|
|
{item.status !== 'uploading' && ( |
|
<span className={styles.deleteIcon}> |
|
<CloseCircleOutlined onClick={() => handleRemove(item)} /> |
|
</span> |
|
)} |
|
</Card> |
|
</List.Item> |
|
); |
|
}} |
|
/> |
|
)} |
|
</Flex> |
|
); |
|
}; |
|
|
|
export default memo(MessageInput); |
|
|