### What problem does this PR solve? feat: Delete the file from the upload control of the message input box #1880 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| return ( | return ( | ||||
| <Modal | <Modal | ||||
| title={t('chunk.graph')} | |||||
| title={t('chunk.mind')} | |||||
| open={visible} | open={visible} | ||||
| onCancel={hideModal} | onCancel={hideModal} | ||||
| width={'90vw'} | width={'90vw'} |
| import { Authorization } from '@/constants/authorization'; | import { Authorization } from '@/constants/authorization'; | ||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { useRemoveNextDocument } from '@/hooks/document-hooks'; | |||||
| import { getAuthorization } from '@/utils/authorization-util'; | import { getAuthorization } from '@/utils/authorization-util'; | ||||
| import { PlusOutlined } from '@ant-design/icons'; | import { PlusOutlined } from '@ant-design/icons'; | ||||
| import type { GetProp, UploadFile } from 'antd'; | import type { GetProp, UploadFile } from 'antd'; | ||||
| value: string; | value: string; | ||||
| sendDisabled: boolean; | sendDisabled: boolean; | ||||
| sendLoading: boolean; | sendLoading: boolean; | ||||
| onPressEnter(documentIds: string[]): Promise<any>; | |||||
| onPressEnter(documentIds: string[]): void; | |||||
| onInputChange: ChangeEventHandler<HTMLInputElement>; | onInputChange: ChangeEventHandler<HTMLInputElement>; | ||||
| conversationId: string; | conversationId: string; | ||||
| } | } | ||||
| conversationId, | conversationId, | ||||
| }: IProps) => { | }: IProps) => { | ||||
| const { t } = useTranslate('chat'); | const { t } = useTranslate('chat'); | ||||
| const { removeDocument } = useRemoveNextDocument(); | |||||
| const [fileList, setFileList] = useState<UploadFile[]>([ | |||||
| // { | |||||
| // uid: '-1', | |||||
| // name: 'image.png', | |||||
| // status: 'done', | |||||
| // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', | |||||
| // }, | |||||
| // { | |||||
| // uid: '-xxx', | |||||
| // percent: 50, | |||||
| // name: 'image.png', | |||||
| // status: 'uploading', | |||||
| // url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', | |||||
| // }, | |||||
| // { | |||||
| // uid: '-5', | |||||
| // name: 'image.png', | |||||
| // status: 'error', | |||||
| // }, | |||||
| ]); | |||||
| const [fileList, setFileList] = useState<UploadFile[]>([]); | |||||
| const handlePreview = async (file: UploadFile) => { | const handlePreview = async (file: UploadFile) => { | ||||
| if (!file.url && !file.preview) { | if (!file.url && !file.preview) { | ||||
| file.preview = await getBase64(file.originFileObj as FileType); | file.preview = await getBase64(file.originFileObj as FileType); | ||||
| } | } | ||||
| // setPreviewImage(file.url || (file.preview as string)); | |||||
| // setPreviewOpen(true); | |||||
| }; | }; | ||||
| const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { | const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { | ||||
| console.log('🚀 ~ newFileList:', newFileList); | console.log('🚀 ~ newFileList:', newFileList); | ||||
| setFileList(newFileList); | setFileList(newFileList); | ||||
| }; | }; | ||||
| const isUploadingFile = fileList.some((x) => x.status === 'uploading'); | |||||
| const handlePressEnter = useCallback(async () => { | const handlePressEnter = useCallback(async () => { | ||||
| if (isUploadingFile) return; | |||||
| const ids = fileList.reduce((pre, cur) => { | const ids = fileList.reduce((pre, cur) => { | ||||
| return pre.concat(get(cur, 'response.data', [])); | return pre.concat(get(cur, 'response.data', [])); | ||||
| }, []); | }, []); | ||||
| await onPressEnter(ids); | |||||
| onPressEnter(ids); | |||||
| setFileList([]); | setFileList([]); | ||||
| }, [fileList, onPressEnter]); | |||||
| }, [fileList, onPressEnter, isUploadingFile]); | |||||
| const handleRemove = useCallback( | |||||
| (file: UploadFile) => { | |||||
| const ids = get(file, 'response.data', []); | |||||
| if (ids.length) { | |||||
| removeDocument(ids[0]); | |||||
| } | |||||
| }, | |||||
| [removeDocument], | |||||
| ); | |||||
| const uploadButton = ( | const uploadButton = ( | ||||
| <button style={{ border: 0, background: 'none' }} type="button"> | <button style={{ border: 0, background: 'none' }} type="button"> | ||||
| type="primary" | type="primary" | ||||
| onClick={handlePressEnter} | onClick={handlePressEnter} | ||||
| loading={sendLoading} | loading={sendLoading} | ||||
| disabled={sendDisabled} | |||||
| disabled={sendDisabled || isUploadingFile} | |||||
| > | > | ||||
| {t('send')} | {t('send')} | ||||
| </Button> | </Button> | ||||
| headers={{ [Authorization]: getAuthorization() }} | headers={{ [Authorization]: getAuthorization() }} | ||||
| data={{ conversation_id: conversationId }} | data={{ conversation_id: conversationId }} | ||||
| method="post" | method="post" | ||||
| onRemove={handleRemove} | |||||
| > | > | ||||
| {fileList.length >= 8 ? null : uploadButton} | {fileList.length >= 8 ? null : uploadButton} | ||||
| </Upload> | </Upload> |
| width: 80%; | width: 80%; | ||||
| } | } | ||||
| .messageItemSectionRight { | .messageItemSectionRight { | ||||
| width: 80%; | |||||
| // width: 80%; | |||||
| // max-width: 50vw; | |||||
| } | } | ||||
| .messageItemContent { | .messageItemContent { | ||||
| display: inline-flex; | display: inline-flex; |
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { memo, useCallback, useEffect, useMemo, useState } from 'react'; | import { memo, useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import { useFetchDocumentInfosByIds } from '@/hooks/document-hooks'; | |||||
| import { | |||||
| useFetchDocumentInfosByIds, | |||||
| useFetchDocumentThumbnailsByIds, | |||||
| } from '@/hooks/document-hooks'; | |||||
| import MarkdownContent from '@/pages/chat/markdown-content'; | import MarkdownContent from '@/pages/chat/markdown-content'; | ||||
| import { getExtension, isImage } from '@/utils/document-util'; | import { getExtension, isImage } from '@/utils/document-util'; | ||||
| import { Avatar, Button, Flex, List } from 'antd'; | |||||
| import { Avatar, Button, Flex, List, Typography } from 'antd'; | |||||
| import IndentedTreeModal from '../indented-tree/modal'; | import IndentedTreeModal from '../indented-tree/modal'; | ||||
| import NewDocumentLink from '../new-document-link'; | import NewDocumentLink from '../new-document-link'; | ||||
| import SvgIcon from '../svg-icon'; | import SvgIcon from '../svg-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { Text } = Typography; | |||||
| interface IProps { | interface IProps { | ||||
| item: Message; | item: Message; | ||||
| reference: IReference; | reference: IReference; | ||||
| const { t } = useTranslate('chat'); | const { t } = useTranslate('chat'); | ||||
| const fileThumbnails = useSelectFileThumbnails(); | const fileThumbnails = useSelectFileThumbnails(); | ||||
| const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds(); | const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds(); | ||||
| console.log('🚀 ~ documentList:', documentList); | |||||
| const { data: documentThumbnails, setDocumentIds: setIds } = | |||||
| useFetchDocumentThumbnailsByIds(); | |||||
| const { visible, hideModal, showModal } = useSetModalState(); | const { visible, hideModal, showModal } = useSetModalState(); | ||||
| const [clickedDocumentId, setClickedDocumentId] = useState(''); | const [clickedDocumentId, setClickedDocumentId] = useState(''); | ||||
| const ids = item?.doc_ids ?? []; | const ids = item?.doc_ids ?? []; | ||||
| if (ids.length) { | if (ids.length) { | ||||
| setDocumentIds(ids); | setDocumentIds(ids); | ||||
| const documentIds = ids.filter((x) => !(x in fileThumbnails)); | |||||
| if (documentIds.length) { | |||||
| setIds(documentIds); | |||||
| } | |||||
| } | } | ||||
| }, [item.doc_ids, setDocumentIds]); | |||||
| }, [item.doc_ids, setDocumentIds, setIds, fileThumbnails]); | |||||
| return ( | return ( | ||||
| <div | <div | ||||
| dataSource={referenceDocumentList} | dataSource={referenceDocumentList} | ||||
| renderItem={(item) => { | renderItem={(item) => { | ||||
| const fileThumbnail = fileThumbnails[item.doc_id]; | const fileThumbnail = fileThumbnails[item.doc_id]; | ||||
| const fileExtension = getExtension(item.doc_name); | const fileExtension = getExtension(item.doc_name); | ||||
| return ( | return ( | ||||
| <List.Item> | <List.Item> | ||||
| bordered | bordered | ||||
| dataSource={documentList} | dataSource={documentList} | ||||
| renderItem={(item) => { | renderItem={(item) => { | ||||
| const fileThumbnail = fileThumbnails[item.id]; | |||||
| const fileThumbnail = | |||||
| documentThumbnails[item.id] || fileThumbnails[item.id]; | |||||
| const fileExtension = getExtension(item.name); | const fileExtension = getExtension(item.name); | ||||
| return ( | return ( | ||||
| <List.Item> | <List.Item> | ||||
| type={'text'} | type={'text'} | ||||
| onClick={handleUserDocumentClick(item.id)} | onClick={handleUserDocumentClick(item.id)} | ||||
| > | > | ||||
| {item.name} | |||||
| <Text | |||||
| style={{ maxWidth: '40vw' }} | |||||
| ellipsis={{ tooltip: item.name }} | |||||
| > | |||||
| {item.name} | |||||
| </Text> | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| </Flex> | </Flex> |
| import kbService from '@/services/knowledge-service'; | import kbService from '@/services/knowledge-service'; | ||||
| import { api_host } from '@/utils/api'; | import { api_host } from '@/utils/api'; | ||||
| import { buildChunkHighlights } from '@/utils/document-util'; | import { buildChunkHighlights } from '@/utils/document-util'; | ||||
| import { useQuery } from '@tanstack/react-query'; | |||||
| import { useMutation, useQuery } from '@tanstack/react-query'; | |||||
| import { UploadFile } from 'antd'; | import { UploadFile } from 'antd'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | import { useCallback, useMemo, useState } from 'react'; | ||||
| import { IHighlight } from 'react-pdf-highlighter'; | import { IHighlight } from 'react-pdf-highlighter'; | ||||
| export const useFetchDocumentThumbnailsByIds = () => { | export const useFetchDocumentThumbnailsByIds = () => { | ||||
| const [ids, setDocumentIds] = useState<string[]>([]); | const [ids, setDocumentIds] = useState<string[]>([]); | ||||
| const { data } = useQuery({ | |||||
| const { data } = useQuery<Record<string, string>>({ | |||||
| queryKey: ['fetchDocumentThumbnails', ids], | queryKey: ['fetchDocumentThumbnails', ids], | ||||
| initialData: [], | |||||
| enabled: ids.length > 0, | |||||
| initialData: {}, | |||||
| queryFn: async () => { | queryFn: async () => { | ||||
| const { data } = await kbService.document_thumbnails({ doc_ids: ids }); | const { data } = await kbService.document_thumbnails({ doc_ids: ids }); | ||||
| if (data.retcode === 0) { | |||||
| return data.data; | |||||
| } | |||||
| return {}; | |||||
| }, | |||||
| }); | |||||
| return { data, setDocumentIds }; | |||||
| }; | |||||
| export const useRemoveNextDocument = () => { | |||||
| // const queryClient = useQueryClient(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: ['removeDocument'], | |||||
| mutationFn: async (documentId: string) => { | |||||
| const data = await kbService.document_rm({ doc_id: documentId }); | |||||
| // if (data.retcode === 0) { | |||||
| // queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] }); | |||||
| // } | |||||
| return data; | return data; | ||||
| }, | }, | ||||
| }); | }); | ||||
| return { data, setDocumentIds }; | |||||
| return { data, loading, removeDocument: mutateAsync }; | |||||
| }; | }; |
| onPressEnter={handlePressEnter} | onPressEnter={handlePressEnter} | ||||
| conversationId={conversation.id} | conversationId={conversation.id} | ||||
| ></MessageInput> | ></MessageInput> | ||||
| {/* <Input | |||||
| size="large" | |||||
| placeholder={t('sendPlaceholder')} | |||||
| value={value} | |||||
| disabled={disabled} | |||||
| suffix={ | |||||
| <Button | |||||
| type="primary" | |||||
| onClick={handlePressEnter} | |||||
| loading={sendLoading} | |||||
| disabled={sendDisabled} | |||||
| > | |||||
| {t('send')} | |||||
| </Button> | |||||
| } | |||||
| onPressEnter={handlePressEnter} | |||||
| onChange={handleInputChange} | |||||
| /> */} | |||||
| </Flex> | </Flex> | ||||
| <Drawer | <Drawer | ||||
| title="Document Previewer" | title="Document Previewer" |
| } from '@/hooks/common-hooks'; | } from '@/hooks/common-hooks'; | ||||
| import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | ||||
| import { useOneNamespaceEffectsLoading } from '@/hooks/store-hooks'; | import { useOneNamespaceEffectsLoading } from '@/hooks/store-hooks'; | ||||
| import { IAnswer, IConversation, IDialog } from '@/interfaces/database/chat'; | |||||
| import { | |||||
| IAnswer, | |||||
| IConversation, | |||||
| IDialog, | |||||
| Message, | |||||
| } from '@/interfaces/database/chat'; | |||||
| import { IChunk } from '@/interfaces/database/knowledge'; | import { IChunk } from '@/interfaces/database/knowledge'; | ||||
| import { getFileExtension } from '@/utils'; | import { getFileExtension } from '@/utils'; | ||||
| import omit from 'lodash/omit'; | import omit from 'lodash/omit'; | ||||
| const { conversationId, dialogId } = useGetChatSearchParams(); | const { conversationId, dialogId } = useGetChatSearchParams(); | ||||
| const addNewestConversation = useCallback( | const addNewestConversation = useCallback( | ||||
| (message: string, answer: string = '') => { | |||||
| (message: Partial<Message>, answer: string = '') => { | |||||
| setCurrentConversation((pre) => { | setCurrentConversation((pre) => { | ||||
| return { | return { | ||||
| ...pre, | ...pre, | ||||
| ...pre.message, | ...pre.message, | ||||
| { | { | ||||
| role: MessageType.User, | role: MessageType.User, | ||||
| content: message, | |||||
| content: message.content, | |||||
| doc_ids: message.doc_ids, | |||||
| id: uuid(), | id: uuid(), | ||||
| } as IMessage, | } as IMessage, | ||||
| { | { | ||||
| export const useSendMessage = ( | export const useSendMessage = ( | ||||
| conversation: IClientConversation, | conversation: IClientConversation, | ||||
| addNewestConversation: (message: string, answer?: string) => void, | |||||
| addNewestConversation: (message: Partial<Message>, answer?: string) => void, | |||||
| removeLatestMessage: () => void, | removeLatestMessage: () => void, | ||||
| addNewestAnswer: (answer: IAnswer) => void, | addNewestAnswer: (answer: IAnswer) => void, | ||||
| ) => { | ) => { | ||||
| const handleSendMessage = useCallback( | const handleSendMessage = useCallback( | ||||
| async (message: string, documentIds: string[]) => { | async (message: string, documentIds: string[]) => { | ||||
| if (conversationId !== '') { | if (conversationId !== '') { | ||||
| return sendMessage(message, documentIds); | |||||
| sendMessage(message, documentIds); | |||||
| } else { | } else { | ||||
| const data = await setConversation(message); | const data = await setConversation(message); | ||||
| if (data.retcode === 0) { | if (data.retcode === 0) { | ||||
| const id = data.data.id; | const id = data.data.id; | ||||
| return sendMessage(message, documentIds, id); | |||||
| sendMessage(message, documentIds, id); | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| }, [setDone, conversationId]); | }, [setDone, conversationId]); | ||||
| const handlePressEnter = useCallback( | const handlePressEnter = useCallback( | ||||
| async (documentIds: string[]) => { | |||||
| (documentIds: string[]) => { | |||||
| if (trim(value) === '') return; | if (trim(value) === '') return; | ||||
| let ret; | |||||
| addNewestConversation({ content: value, doc_ids: documentIds }); | |||||
| if (done) { | if (done) { | ||||
| setValue(''); | setValue(''); | ||||
| ret = await handleSendMessage(value.trim(), documentIds); | |||||
| handleSendMessage(value.trim(), documentIds); | |||||
| } | } | ||||
| addNewestConversation(value); | |||||
| return ret; | |||||
| }, | }, | ||||
| [addNewestConversation, handleSendMessage, done, setValue, value], | [addNewestConversation, handleSendMessage, done, setValue, value], | ||||
| ); | ); |