### 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
| @@ -1,11 +1,10 @@ | |||
| import { Authorization } from '@/constants/authorization'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { | |||
| useDeleteDocument, | |||
| useFetchDocumentInfosByIds, | |||
| useRemoveNextDocument, | |||
| useUploadAndParseDocument, | |||
| } from '@/hooks/document-hooks'; | |||
| import { getAuthorization } from '@/utils/authorization-util'; | |||
| import { getExtension } from '@/utils/document-util'; | |||
| import { formatBytes } from '@/utils/file-util'; | |||
| import { | |||
| @@ -28,7 +27,14 @@ import { | |||
| } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| 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 SvgIcon from '../svg-icon'; | |||
| import styles from './index.less'; | |||
| @@ -64,9 +70,10 @@ interface IProps { | |||
| onPressEnter(documentIds: string[]): void; | |||
| onInputChange: ChangeEventHandler<HTMLInputElement>; | |||
| conversationId: string; | |||
| uploadUrl?: string; | |||
| uploadMethod?: string; | |||
| isShared?: boolean; | |||
| showUploadIcon?: boolean; | |||
| createConversationBeforeUploadDocument?(message: string): Promise<any>; | |||
| } | |||
| const getBase64 = (file: FileType): Promise<string> => | |||
| @@ -87,12 +94,15 @@ const MessageInput = ({ | |||
| onInputChange, | |||
| conversationId, | |||
| showUploadIcon = true, | |||
| uploadUrl = '/v1/document/upload_and_parse', | |||
| 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[]>([]); | |||
| @@ -102,9 +112,44 @@ const MessageInput = ({ | |||
| } | |||
| }; | |||
| 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 handlePressEnter = useCallback(async () => { | |||
| @@ -150,6 +195,16 @@ const MessageInput = ({ | |||
| 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 | |||
| @@ -160,18 +215,22 @@ const MessageInput = ({ | |||
| className={classNames({ [styles.inputWrapper]: fileList.length === 0 })} | |||
| suffix={ | |||
| <Space> | |||
| {conversationId && showUploadIcon && ( | |||
| {showUploadIcon && ( | |||
| <Upload | |||
| action={uploadUrl} | |||
| fileList={fileList} | |||
| // action={uploadUrl} | |||
| // fileList={fileList} | |||
| onPreview={handlePreview} | |||
| 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} | |||
| showUploadList={false} | |||
| beforeUpload={(file, fileList) => { | |||
| console.log('🚀 ~ beforeUpload:', fileList); | |||
| return false; | |||
| }} | |||
| > | |||
| <Button | |||
| type={'text'} | |||
| @@ -209,8 +268,10 @@ const MessageInput = ({ | |||
| dataSource={fileList} | |||
| className={styles.listWrapper} | |||
| renderItem={(item) => { | |||
| const fileExtension = getExtension(item.name); | |||
| const id = getFileId(item); | |||
| const documentInfo = getDocumentInfoById(id); | |||
| const fileExtension = getExtension(documentInfo?.name ?? ''); | |||
| const fileName = item.originFileObj?.name ?? ''; | |||
| return ( | |||
| <List.Item> | |||
| @@ -228,14 +289,14 @@ const MessageInput = ({ | |||
| // width={30} | |||
| ></InfoCircleOutlined> | |||
| ) : ( | |||
| <FileIcon id={id} name={item.name}></FileIcon> | |||
| <FileIcon id={id} name={fileName}></FileIcon> | |||
| )} | |||
| <Flex vertical style={{ width: '90%' }}> | |||
| <Text | |||
| ellipsis={{ tooltip: item.name }} | |||
| ellipsis={{ tooltip: fileName }} | |||
| className={styles.nameText} | |||
| > | |||
| <b> {item.name}</b> | |||
| <b> {fileName}</b> | |||
| </Text> | |||
| {isUploadError(item) ? ( | |||
| t('uploadFailed') | |||
| @@ -275,4 +336,4 @@ const MessageInput = ({ | |||
| ); | |||
| }; | |||
| export default MessageInput; | |||
| export default memo(MessageInput); | |||
| @@ -125,6 +125,23 @@ export const useUpdateConversation = () => { | |||
| 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 = () => { | |||
| const dispatch = useDispatch(); | |||
| @@ -1,6 +1,7 @@ | |||
| import { IDocumentInfo } from '@/interfaces/database/document'; | |||
| import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; | |||
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | |||
| import chatService from '@/services/chat-service'; | |||
| import kbService from '@/services/knowledge-service'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { buildChunkHighlights } from '@/utils/document-util'; | |||
| @@ -333,3 +334,34 @@ export const useDeleteDocument = () => { | |||
| 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 }; | |||
| }; | |||
| @@ -4,6 +4,7 @@ import { MessageType } from '@/constants/chat'; | |||
| import { Drawer, Flex, Spin } from 'antd'; | |||
| import { | |||
| useClickDrawer, | |||
| useCreateConversationBeforeUploadDocument, | |||
| useFetchConversationOnMount, | |||
| useGetFileIcon, | |||
| useGetSendButtonDisabled, | |||
| @@ -24,6 +25,7 @@ const ChatContainer = () => { | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| conversationId, | |||
| } = useFetchConversationOnMount(); | |||
| const { | |||
| handleInputChange, | |||
| @@ -43,6 +45,8 @@ const ChatContainer = () => { | |||
| useGetFileIcon(); | |||
| const loading = useSelectConversationLoading(); | |||
| const { data: userInfo } = useFetchUserInfo(); | |||
| const { createConversationBeforeUploadDocument } = | |||
| useCreateConversationBeforeUploadDocument(); | |||
| return ( | |||
| <> | |||
| @@ -78,7 +82,10 @@ const ChatContainer = () => { | |||
| value={value} | |||
| onInputChange={handleInputChange} | |||
| onPressEnter={handlePressEnter} | |||
| conversationId={conversation.id} | |||
| conversationId={conversationId} | |||
| createConversationBeforeUploadDocument={ | |||
| createConversationBeforeUploadDocument | |||
| } | |||
| ></MessageInput> | |||
| </Flex> | |||
| <Drawer | |||
| @@ -520,6 +520,7 @@ export const useFetchConversationOnMount = () => { | |||
| ref, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| conversationId, | |||
| }; | |||
| }; | |||
| @@ -769,4 +770,28 @@ export const useGetSendButtonDisabled = () => { | |||
| export const useSendButtonDisabled = (value: string) => { | |||
| 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 | |||
| @@ -75,7 +75,7 @@ const ChatContainer = () => { | |||
| onInputChange={handleInputChange} | |||
| onPressEnter={handlePressEnter} | |||
| sendLoading={sendLoading} | |||
| uploadUrl="/v1/api/document/upload_and_parse" | |||
| uploadMethod="external_upload_and_parse" | |||
| showUploadIcon={from === SharedFrom.Chat} | |||
| ></MessageInput> | |||
| </Flex> | |||
| @@ -19,6 +19,7 @@ const { | |||
| createExternalConversation, | |||
| getExternalConversation, | |||
| completeExternalConversation, | |||
| uploadAndParseExternal, | |||
| } = api; | |||
| const methods = { | |||
| @@ -86,6 +87,10 @@ const methods = { | |||
| url: completeExternalConversation, | |||
| method: 'post', | |||
| }, | |||
| uploadAndParseExternal: { | |||
| url: uploadAndParseExternal, | |||
| method: 'post', | |||
| }, | |||
| } as const; | |||
| const chatService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -30,6 +30,7 @@ const { | |||
| web_crawl, | |||
| knowledge_graph, | |||
| document_infos, | |||
| upload_and_parse, | |||
| } = api; | |||
| const methods = { | |||
| @@ -136,6 +137,10 @@ const methods = { | |||
| url: document_delete, | |||
| method: 'delete', | |||
| }, | |||
| upload_and_parse: { | |||
| url: upload_and_parse, | |||
| method: 'post', | |||
| }, | |||
| }; | |||
| const kbService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -51,6 +51,7 @@ export default { | |||
| document_upload: `${api_host}/document/upload`, | |||
| web_crawl: `${api_host}/document/web_crawl`, | |||
| document_infos: `${api_host}/document/infos`, | |||
| upload_and_parse: `${api_host}/document/upload_and_parse`, | |||
| // chat | |||
| setDialog: `${api_host}/dialog/set`, | |||
| @@ -70,6 +71,7 @@ export default { | |||
| createExternalConversation: `${api_host}/api/new_conversation`, | |||
| getExternalConversation: `${api_host}/api/conversation`, | |||
| completeExternalConversation: `${api_host}/api/completion`, | |||
| uploadAndParseExternal: `${api_host}/api/document/upload_and_parse`, | |||
| // file manager | |||
| listFile: `${api_host}/file/list`, | |||