### What problem does this PR solve? feat: Create a conversation before uploading files in it #1880 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| import { Authorization } from '@/constants/authorization'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { | import { | ||||
| useDeleteDocument, | useDeleteDocument, | ||||
| useFetchDocumentInfosByIds, | useFetchDocumentInfosByIds, | ||||
| useRemoveNextDocument, | useRemoveNextDocument, | ||||
| useUploadAndParseDocument, | |||||
| } from '@/hooks/document-hooks'; | } from '@/hooks/document-hooks'; | ||||
| import { getAuthorization } from '@/utils/authorization-util'; | |||||
| import { getExtension } from '@/utils/document-util'; | import { getExtension } from '@/utils/document-util'; | ||||
| import { formatBytes } from '@/utils/file-util'; | import { formatBytes } from '@/utils/file-util'; | ||||
| import { | import { | ||||
| } from 'antd'; | } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import get from 'lodash/get'; | import get from 'lodash/get'; | ||||
| import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; | |||||
| import { | |||||
| ChangeEventHandler, | |||||
| memo, | |||||
| useCallback, | |||||
| useEffect, | |||||
| useRef, | |||||
| useState, | |||||
| } from 'react'; | |||||
| import FileIcon from '../file-icon'; | import FileIcon from '../file-icon'; | ||||
| import SvgIcon from '../svg-icon'; | import SvgIcon from '../svg-icon'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| onPressEnter(documentIds: string[]): void; | onPressEnter(documentIds: string[]): void; | ||||
| onInputChange: ChangeEventHandler<HTMLInputElement>; | onInputChange: ChangeEventHandler<HTMLInputElement>; | ||||
| conversationId: string; | conversationId: string; | ||||
| uploadUrl?: string; | |||||
| uploadMethod?: string; | |||||
| isShared?: boolean; | isShared?: boolean; | ||||
| showUploadIcon?: boolean; | showUploadIcon?: boolean; | ||||
| createConversationBeforeUploadDocument?(message: string): Promise<any>; | |||||
| } | } | ||||
| const getBase64 = (file: FileType): Promise<string> => | const getBase64 = (file: FileType): Promise<string> => | ||||
| onInputChange, | onInputChange, | ||||
| conversationId, | conversationId, | ||||
| showUploadIcon = true, | showUploadIcon = true, | ||||
| uploadUrl = '/v1/document/upload_and_parse', | |||||
| createConversationBeforeUploadDocument, | |||||
| uploadMethod = 'upload_and_parse', | |||||
| }: IProps) => { | }: IProps) => { | ||||
| const { t } = useTranslate('chat'); | const { t } = useTranslate('chat'); | ||||
| const { removeDocument } = useRemoveNextDocument(); | const { removeDocument } = useRemoveNextDocument(); | ||||
| const { deleteDocument } = useDeleteDocument(); | const { deleteDocument } = useDeleteDocument(); | ||||
| const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds(); | const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds(); | ||||
| const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod); | |||||
| const conversationIdRef = useRef(conversationId); | |||||
| const [fileList, setFileList] = useState<UploadFile[]>([]); | const [fileList, setFileList] = useState<UploadFile[]>([]); | ||||
| } | } | ||||
| }; | }; | ||||
| const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { | |||||
| setFileList(newFileList); | |||||
| const handleChange: UploadProps['onChange'] = async ({ | |||||
| // fileList: newFileList, | |||||
| file, | |||||
| }) => { | |||||
| let nextConversationId: string = conversationId; | |||||
| if (createConversationBeforeUploadDocument && !conversationId) { | |||||
| const creatingRet = await createConversationBeforeUploadDocument( | |||||
| file.name, | |||||
| ); | |||||
| if (creatingRet.retcode === 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?.retcode === 0 ? 'done' : 'error', | |||||
| }); | |||||
| return nextList; | |||||
| }); | |||||
| }; | }; | ||||
| const isUploadingFile = fileList.some((x) => x.status === 'uploading'); | const isUploadingFile = fileList.some((x) => x.status === 'uploading'); | ||||
| const handlePressEnter = useCallback(async () => { | const handlePressEnter = useCallback(async () => { | ||||
| setDocumentIds(ids); | setDocumentIds(ids); | ||||
| }, [fileList, setDocumentIds]); | }, [fileList, setDocumentIds]); | ||||
| useEffect(() => { | |||||
| if ( | |||||
| conversationIdRef.current && | |||||
| conversationId !== conversationIdRef.current | |||||
| ) { | |||||
| setFileList([]); | |||||
| } | |||||
| conversationIdRef.current = conversationId; | |||||
| }, [conversationId, setFileList]); | |||||
| return ( | return ( | ||||
| <Flex gap={20} vertical className={styles.messageInputWrapper}> | <Flex gap={20} vertical className={styles.messageInputWrapper}> | ||||
| <Input | <Input | ||||
| className={classNames({ [styles.inputWrapper]: fileList.length === 0 })} | className={classNames({ [styles.inputWrapper]: fileList.length === 0 })} | ||||
| suffix={ | suffix={ | ||||
| <Space> | <Space> | ||||
| {conversationId && showUploadIcon && ( | |||||
| {showUploadIcon && ( | |||||
| <Upload | <Upload | ||||
| action={uploadUrl} | |||||
| fileList={fileList} | |||||
| // action={uploadUrl} | |||||
| // fileList={fileList} | |||||
| onPreview={handlePreview} | onPreview={handlePreview} | ||||
| onChange={handleChange} | onChange={handleChange} | ||||
| multiple | |||||
| headers={{ [Authorization]: getAuthorization() }} | |||||
| data={{ conversation_id: conversationId }} | |||||
| method="post" | |||||
| multiple={false} | |||||
| // headers={{ [Authorization]: getAuthorization() }} | |||||
| // data={{ conversation_id: conversationId }} | |||||
| // method="post" | |||||
| onRemove={handleRemove} | onRemove={handleRemove} | ||||
| showUploadList={false} | showUploadList={false} | ||||
| beforeUpload={(file, fileList) => { | |||||
| console.log('🚀 ~ beforeUpload:', fileList); | |||||
| return false; | |||||
| }} | |||||
| > | > | ||||
| <Button | <Button | ||||
| type={'text'} | type={'text'} | ||||
| dataSource={fileList} | dataSource={fileList} | ||||
| className={styles.listWrapper} | className={styles.listWrapper} | ||||
| renderItem={(item) => { | renderItem={(item) => { | ||||
| const fileExtension = getExtension(item.name); | |||||
| const id = getFileId(item); | const id = getFileId(item); | ||||
| const documentInfo = getDocumentInfoById(id); | |||||
| const fileExtension = getExtension(documentInfo?.name ?? ''); | |||||
| const fileName = item.originFileObj?.name ?? ''; | |||||
| return ( | return ( | ||||
| <List.Item> | <List.Item> | ||||
| // width={30} | // width={30} | ||||
| ></InfoCircleOutlined> | ></InfoCircleOutlined> | ||||
| ) : ( | ) : ( | ||||
| <FileIcon id={id} name={item.name}></FileIcon> | |||||
| <FileIcon id={id} name={fileName}></FileIcon> | |||||
| )} | )} | ||||
| <Flex vertical style={{ width: '90%' }}> | <Flex vertical style={{ width: '90%' }}> | ||||
| <Text | <Text | ||||
| ellipsis={{ tooltip: item.name }} | |||||
| ellipsis={{ tooltip: fileName }} | |||||
| className={styles.nameText} | className={styles.nameText} | ||||
| > | > | ||||
| <b> {item.name}</b> | |||||
| <b> {fileName}</b> | |||||
| </Text> | </Text> | ||||
| {isUploadError(item) ? ( | {isUploadError(item) ? ( | ||||
| t('uploadFailed') | t('uploadFailed') | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default MessageInput; | |||||
| export default memo(MessageInput); |
| return updateConversation; | return updateConversation; | ||||
| }; | }; | ||||
| export const useUpdateNextConversation = () => { | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: ['updateConversation'], | |||||
| mutationFn: async (params: Record<string, any>) => { | |||||
| const { data } = await chatService.setConversation(params); | |||||
| return data; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, updateConversation: mutateAsync }; | |||||
| }; | |||||
| export const useSetDialog = () => { | export const useSetDialog = () => { | ||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| import { IDocumentInfo } from '@/interfaces/database/document'; | import { IDocumentInfo } from '@/interfaces/database/document'; | ||||
| import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; | import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; | ||||
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | ||||
| import chatService from '@/services/chat-service'; | |||||
| 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'; | ||||
| return { data, loading, deleteDocument: mutateAsync }; | return { data, loading, deleteDocument: mutateAsync }; | ||||
| }; | }; | ||||
| export const useUploadAndParseDocument = (uploadMethod: string) => { | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: ['uploadAndParseDocument'], | |||||
| mutationFn: async ({ | |||||
| conversationId, | |||||
| fileList, | |||||
| }: { | |||||
| conversationId: string; | |||||
| fileList: UploadFile[]; | |||||
| }) => { | |||||
| const formData = new FormData(); | |||||
| formData.append('conversation_id', conversationId); | |||||
| fileList.forEach((file: UploadFile) => { | |||||
| formData.append('file', file as any); | |||||
| }); | |||||
| if (uploadMethod === 'upload_and_parse') { | |||||
| const data = await kbService.upload_and_parse(formData); | |||||
| return data?.data; | |||||
| } | |||||
| const data = await chatService.uploadAndParseExternal(formData); | |||||
| return data?.data; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, uploadAndParseDocument: mutateAsync }; | |||||
| }; |
| import { Drawer, Flex, Spin } from 'antd'; | import { Drawer, Flex, Spin } from 'antd'; | ||||
| import { | import { | ||||
| useClickDrawer, | useClickDrawer, | ||||
| useCreateConversationBeforeUploadDocument, | |||||
| useFetchConversationOnMount, | useFetchConversationOnMount, | ||||
| useGetFileIcon, | useGetFileIcon, | ||||
| useGetSendButtonDisabled, | useGetSendButtonDisabled, | ||||
| addNewestConversation, | addNewestConversation, | ||||
| removeLatestMessage, | removeLatestMessage, | ||||
| addNewestAnswer, | addNewestAnswer, | ||||
| conversationId, | |||||
| } = useFetchConversationOnMount(); | } = useFetchConversationOnMount(); | ||||
| const { | const { | ||||
| handleInputChange, | handleInputChange, | ||||
| useGetFileIcon(); | useGetFileIcon(); | ||||
| const loading = useSelectConversationLoading(); | const loading = useSelectConversationLoading(); | ||||
| const { data: userInfo } = useFetchUserInfo(); | const { data: userInfo } = useFetchUserInfo(); | ||||
| const { createConversationBeforeUploadDocument } = | |||||
| useCreateConversationBeforeUploadDocument(); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| value={value} | value={value} | ||||
| onInputChange={handleInputChange} | onInputChange={handleInputChange} | ||||
| onPressEnter={handlePressEnter} | onPressEnter={handlePressEnter} | ||||
| conversationId={conversation.id} | |||||
| conversationId={conversationId} | |||||
| createConversationBeforeUploadDocument={ | |||||
| createConversationBeforeUploadDocument | |||||
| } | |||||
| ></MessageInput> | ></MessageInput> | ||||
| </Flex> | </Flex> | ||||
| <Drawer | <Drawer |
| ref, | ref, | ||||
| removeLatestMessage, | removeLatestMessage, | ||||
| addNewestAnswer, | addNewestAnswer, | ||||
| conversationId, | |||||
| }; | }; | ||||
| }; | }; | ||||
| export const useSendButtonDisabled = (value: string) => { | export const useSendButtonDisabled = (value: string) => { | ||||
| return trim(value) === ''; | return trim(value) === ''; | ||||
| }; | }; | ||||
| export const useCreateConversationBeforeUploadDocument = () => { | |||||
| const { setConversation } = useSetConversation(); | |||||
| const { dialogId } = useGetChatSearchParams(); | |||||
| const { handleClickConversation } = useClickConversationCard(); | |||||
| const createConversationBeforeUploadDocument = useCallback( | |||||
| async (message: string) => { | |||||
| const data = await setConversation(message); | |||||
| if (data.retcode === 0) { | |||||
| const id = data.data.id; | |||||
| handleClickConversation(id); | |||||
| } | |||||
| return data; | |||||
| }, | |||||
| [setConversation, handleClickConversation], | |||||
| ); | |||||
| return { | |||||
| createConversationBeforeUploadDocument, | |||||
| dialogId, | |||||
| }; | |||||
| }; | |||||
| //#endregion | //#endregion |
| onInputChange={handleInputChange} | onInputChange={handleInputChange} | ||||
| onPressEnter={handlePressEnter} | onPressEnter={handlePressEnter} | ||||
| sendLoading={sendLoading} | sendLoading={sendLoading} | ||||
| uploadUrl="/v1/api/document/upload_and_parse" | |||||
| uploadMethod="external_upload_and_parse" | |||||
| showUploadIcon={from === SharedFrom.Chat} | showUploadIcon={from === SharedFrom.Chat} | ||||
| ></MessageInput> | ></MessageInput> | ||||
| </Flex> | </Flex> |
| createExternalConversation, | createExternalConversation, | ||||
| getExternalConversation, | getExternalConversation, | ||||
| completeExternalConversation, | completeExternalConversation, | ||||
| uploadAndParseExternal, | |||||
| } = api; | } = api; | ||||
| const methods = { | const methods = { | ||||
| url: completeExternalConversation, | url: completeExternalConversation, | ||||
| method: 'post', | method: 'post', | ||||
| }, | }, | ||||
| uploadAndParseExternal: { | |||||
| url: uploadAndParseExternal, | |||||
| method: 'post', | |||||
| }, | |||||
| } as const; | } as const; | ||||
| const chatService = registerServer<keyof typeof methods>(methods, request); | const chatService = registerServer<keyof typeof methods>(methods, request); |
| web_crawl, | web_crawl, | ||||
| knowledge_graph, | knowledge_graph, | ||||
| document_infos, | document_infos, | ||||
| upload_and_parse, | |||||
| } = api; | } = api; | ||||
| const methods = { | const methods = { | ||||
| url: document_delete, | url: document_delete, | ||||
| method: 'delete', | method: 'delete', | ||||
| }, | }, | ||||
| upload_and_parse: { | |||||
| url: upload_and_parse, | |||||
| method: 'post', | |||||
| }, | |||||
| }; | }; | ||||
| const kbService = registerServer<keyof typeof methods>(methods, request); | const kbService = registerServer<keyof typeof methods>(methods, request); |
| document_upload: `${api_host}/document/upload`, | document_upload: `${api_host}/document/upload`, | ||||
| web_crawl: `${api_host}/document/web_crawl`, | web_crawl: `${api_host}/document/web_crawl`, | ||||
| document_infos: `${api_host}/document/infos`, | document_infos: `${api_host}/document/infos`, | ||||
| upload_and_parse: `${api_host}/document/upload_and_parse`, | |||||
| // chat | // chat | ||||
| setDialog: `${api_host}/dialog/set`, | setDialog: `${api_host}/dialog/set`, | ||||
| createExternalConversation: `${api_host}/api/new_conversation`, | createExternalConversation: `${api_host}/api/new_conversation`, | ||||
| getExternalConversation: `${api_host}/api/conversation`, | getExternalConversation: `${api_host}/api/conversation`, | ||||
| completeExternalConversation: `${api_host}/api/completion`, | completeExternalConversation: `${api_host}/api/completion`, | ||||
| uploadAndParseExternal: `${api_host}/api/document/upload_and_parse`, | |||||
| // file manager | // file manager | ||||
| listFile: `${api_host}/file/list`, | listFile: `${api_host}/file/list`, |