| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- import { useTranslate } from '@/hooks/common-hooks';
- import {
- useDeleteDocument,
- useFetchDocumentInfosByIds,
- useRemoveNextDocument,
- useUploadAndParseDocument,
- } from '@/hooks/document-hooks';
- import { cn } from '@/lib/utils';
- 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,
- Divider,
- Flex,
- Input,
- List,
- Space,
- Spin,
- Typography,
- Upload,
- UploadProps,
- } from 'antd';
- import get from 'lodash/get';
- import { CircleStop, Paperclip, SendHorizontal } 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 { TextArea } = Input;
-
- 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<HTMLTextAreaElement>;
- conversationId: string;
- uploadMethod?: string;
- isShared?: boolean;
- showUploadIcon?: boolean;
- createConversationBeforeUploadDocument?(message: string): Promise<any>;
- stopOutputMessage?(): void;
- }
-
- 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',
- stopOutputMessage,
- }: 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 ({
- // 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 handleKeyDown = useCallback(
- async (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
- // check if it was shift + enter
- if (event.key === 'Enter' && event.shiftKey) return;
- if (event.key !== 'Enter') return;
- if (sendDisabled || isUploadingFile || sendLoading) return;
-
- event.preventDefault();
- handlePressEnter();
- },
- [sendDisabled, isUploadingFile, sendLoading, handlePressEnter],
- );
-
- 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 handleStopOutputMessage = useCallback(() => {
- stopOutputMessage?.();
- }, [stopOutputMessage]);
-
- 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={1}
- vertical
- className={cn(styles.messageInputWrapper, 'dark:bg-black')}
- >
- <TextArea
- size="large"
- placeholder={t('sendPlaceholder')}
- value={value}
- allowClear
- disabled={disabled}
- style={{
- border: 'none',
- boxShadow: 'none',
- padding: '0px 10px',
- marginTop: 10,
- }}
- autoSize={{ minRows: 2, maxRows: 10 }}
- onKeyDown={handleKeyDown}
- onChange={onInputChange}
- />
- <Divider style={{ margin: '5px 30px 10px 0px' }} />
- <Flex justify="space-between" align="center">
- {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
- gap={5}
- align="center"
- justify="flex-end"
- style={{
- paddingRight: 10,
- paddingBottom: 10,
- width: fileList.length > 0 ? '50%' : '100%',
- }}
- >
- {showUploadIcon && (
- <Upload
- onPreview={handlePreview}
- onChange={handleChange}
- multiple={false}
- onRemove={handleRemove}
- showUploadList={false}
- beforeUpload={() => {
- return false;
- }}
- >
- <Button type={'primary'} disabled={disabled}>
- <Paperclip className="size-4" />
- </Button>
- </Upload>
- )}
- {sendLoading ? (
- <Button onClick={handleStopOutputMessage}>
- <CircleStop className="size-5" />
- </Button>
- ) : (
- <Button
- type="primary"
- onClick={handlePressEnter}
- loading={sendLoading}
- disabled={sendDisabled || isUploadingFile || sendLoading}
- >
- <SendHorizontal className="size-5" />
- </Button>
- )}
- </Flex>
- </Flex>
- </Flex>
- );
- };
-
- export default memo(MessageInput);
|