feat: Create a conversation before uploading files in it #1880 (#2057)

### 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)
This commit is contained in:
balibabu 2024-08-22 18:01:48 +08:00 committed by GitHub
parent 35e880c432
commit c739b68b29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 175 additions and 21 deletions

View File

@ -1,11 +1,10 @@
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 {
@ -28,7 +27,14 @@ 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';
@ -64,9 +70,10 @@ interface IProps {
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> =>
@ -87,12 +94,15 @@ const MessageInput = ({
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[]>([]);
@ -102,9 +112,44 @@ const MessageInput = ({
} }
}; };
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { const handleChange: UploadProps['onChange'] = async ({
setFileList(newFileList); // 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 () => {
@ -150,6 +195,16 @@ const MessageInput = ({
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
@ -160,18 +215,22 @@ const MessageInput = ({
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} // action={uploadUrl}
fileList={fileList} // fileList={fileList}
onPreview={handlePreview} onPreview={handlePreview}
onChange={handleChange} onChange={handleChange}
multiple multiple={false}
headers={{ [Authorization]: getAuthorization() }} // headers={{ [Authorization]: getAuthorization() }}
data={{ conversation_id: conversationId }} // data={{ conversation_id: conversationId }}
method="post" // 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'}
@ -209,8 +268,10 @@ const MessageInput = ({
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>
@ -228,14 +289,14 @@ const MessageInput = ({
// 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')
@ -275,4 +336,4 @@ const MessageInput = ({
); );
}; };
export default MessageInput; export default memo(MessageInput);

View File

@ -125,6 +125,23 @@ export const useUpdateConversation = () => {
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();

View File

@ -1,6 +1,7 @@
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';
@ -333,3 +334,34 @@ export const useDeleteDocument = () => {
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 };
};

View File

@ -4,6 +4,7 @@ import { MessageType } from '@/constants/chat';
import { Drawer, Flex, Spin } from 'antd'; import { Drawer, Flex, Spin } from 'antd';
import { import {
useClickDrawer, useClickDrawer,
useCreateConversationBeforeUploadDocument,
useFetchConversationOnMount, useFetchConversationOnMount,
useGetFileIcon, useGetFileIcon,
useGetSendButtonDisabled, useGetSendButtonDisabled,
@ -24,6 +25,7 @@ const ChatContainer = () => {
addNewestConversation, addNewestConversation,
removeLatestMessage, removeLatestMessage,
addNewestAnswer, addNewestAnswer,
conversationId,
} = useFetchConversationOnMount(); } = useFetchConversationOnMount();
const { const {
handleInputChange, handleInputChange,
@ -43,6 +45,8 @@ const ChatContainer = () => {
useGetFileIcon(); useGetFileIcon();
const loading = useSelectConversationLoading(); const loading = useSelectConversationLoading();
const { data: userInfo } = useFetchUserInfo(); const { data: userInfo } = useFetchUserInfo();
const { createConversationBeforeUploadDocument } =
useCreateConversationBeforeUploadDocument();
return ( return (
<> <>
@ -78,7 +82,10 @@ const ChatContainer = () => {
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

View File

@ -520,6 +520,7 @@ export const useFetchConversationOnMount = () => {
ref, ref,
removeLatestMessage, removeLatestMessage,
addNewestAnswer, addNewestAnswer,
conversationId,
}; };
}; };
@ -769,4 +770,28 @@ export const useGetSendButtonDisabled = () => {
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

View File

@ -75,7 +75,7 @@ const ChatContainer = () => {
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>

View File

@ -19,6 +19,7 @@ const {
createExternalConversation, createExternalConversation,
getExternalConversation, getExternalConversation,
completeExternalConversation, completeExternalConversation,
uploadAndParseExternal,
} = api; } = api;
const methods = { const methods = {
@ -86,6 +87,10 @@ 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);

View File

@ -30,6 +30,7 @@ const {
web_crawl, web_crawl,
knowledge_graph, knowledge_graph,
document_infos, document_infos,
upload_and_parse,
} = api; } = api;
const methods = { const methods = {
@ -136,6 +137,10 @@ 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);

View File

@ -51,6 +51,7 @@ export default {
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`,
@ -70,6 +71,7 @@ export default {
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`,