### What problem does this PR solve? feat: Delete the files uploaded in the external dialog box #1880 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| } | } | ||||
| .listWrapper { | .listWrapper { | ||||
| padding: 0 10px; | padding: 0 10px; | ||||
| overflow: auto; | |||||
| max-height: 170px; | |||||
| } | } | ||||
| .inputWrapper { | .inputWrapper { | ||||
| border-radius: 8px; | border-radius: 8px; |
| import { Authorization } from '@/constants/authorization'; | import { Authorization } from '@/constants/authorization'; | ||||
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { | import { | ||||
| useDeleteDocument, | |||||
| useFetchDocumentInfosByIds, | useFetchDocumentInfosByIds, | ||||
| useRemoveNextDocument, | useRemoveNextDocument, | ||||
| } from '@/hooks/document-hooks'; | } from '@/hooks/document-hooks'; | ||||
| import { getAuthorization } from '@/utils/authorization-util'; | 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 { CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'; | |||||
| import { | |||||
| CloseCircleOutlined, | |||||
| InfoCircleOutlined, | |||||
| LoadingOutlined, | |||||
| } from '@ant-design/icons'; | |||||
| import type { GetProp, UploadFile } from 'antd'; | import type { GetProp, UploadFile } from 'antd'; | ||||
| import { | import { | ||||
| Button, | Button, | ||||
| return ids; | return ids; | ||||
| }; | }; | ||||
| const isUploadError = (file: UploadFile) => { | |||||
| const retcode = get(file, 'response.retcode'); | |||||
| return typeof retcode === 'number' && retcode !== 0; | |||||
| }; | |||||
| const isUploadSuccess = (file: UploadFile) => { | |||||
| const retcode = get(file, 'response.retcode'); | |||||
| return typeof retcode === 'number' && retcode === 0; | |||||
| }; | |||||
| interface IProps { | interface IProps { | ||||
| disabled: boolean; | disabled: boolean; | ||||
| value: string; | value: string; | ||||
| onInputChange: ChangeEventHandler<HTMLInputElement>; | onInputChange: ChangeEventHandler<HTMLInputElement>; | ||||
| conversationId: string; | conversationId: string; | ||||
| uploadUrl?: string; | uploadUrl?: string; | ||||
| isShared?: boolean; | |||||
| } | } | ||||
| const getBase64 = (file: FileType): Promise<string> => | const getBase64 = (file: FileType): Promise<string> => | ||||
| }); | }); | ||||
| const MessageInput = ({ | const MessageInput = ({ | ||||
| isShared = false, | |||||
| disabled, | disabled, | ||||
| value, | value, | ||||
| onPressEnter, | onPressEnter, | ||||
| }: IProps) => { | }: IProps) => { | ||||
| const { t } = useTranslate('chat'); | const { t } = useTranslate('chat'); | ||||
| const { removeDocument } = useRemoveNextDocument(); | const { removeDocument } = useRemoveNextDocument(); | ||||
| const { deleteDocument } = useDeleteDocument(); | |||||
| const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds(); | const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds(); | ||||
| const [fileList, setFileList] = useState<UploadFile[]>([]); | const [fileList, setFileList] = useState<UploadFile[]>([]); | ||||
| const handlePressEnter = useCallback(async () => { | const handlePressEnter = useCallback(async () => { | ||||
| if (isUploadingFile) return; | if (isUploadingFile) return; | ||||
| const ids = getFileIds(fileList); | |||||
| const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x))); | |||||
| onPressEnter(ids); | onPressEnter(ids); | ||||
| setFileList([]); | setFileList([]); | ||||
| const handleRemove = useCallback( | const handleRemove = useCallback( | ||||
| async (file: UploadFile) => { | async (file: UploadFile) => { | ||||
| const ids = get(file, 'response.data', []); | const ids = get(file, 'response.data', []); | ||||
| if (ids.length) { | |||||
| await removeDocument(ids[0]); | |||||
| // Upload Successfully | |||||
| if (Array.isArray(ids) && ids.length) { | |||||
| if (isShared) { | |||||
| await deleteDocument(ids); | |||||
| } else { | |||||
| await removeDocument(ids[0]); | |||||
| } | |||||
| setFileList((preList) => { | setFileList((preList) => { | ||||
| return preList.filter((x) => getFileId(x) !== ids[0]); | return preList.filter((x) => getFileId(x) !== ids[0]); | ||||
| }); | }); | ||||
| } else { | |||||
| // Upload failed | |||||
| setFileList((preList) => { | |||||
| return preList.filter((x) => x.uid !== file.uid); | |||||
| }); | |||||
| } | } | ||||
| }, | }, | ||||
| [removeDocument], | |||||
| [removeDocument, deleteDocument, isShared], | |||||
| ); | ); | ||||
| const getDocumentInfoById = useCallback( | const getDocumentInfoById = useCallback( | ||||
| <LoadingOutlined style={{ fontSize: 24 }} spin /> | <LoadingOutlined style={{ fontSize: 24 }} spin /> | ||||
| } | } | ||||
| /> | /> | ||||
| ) : !getFileId(item) ? ( | |||||
| <InfoCircleOutlined | |||||
| size={30} | |||||
| // width={30} | |||||
| ></InfoCircleOutlined> | |||||
| ) : ( | ) : ( | ||||
| <FileIcon id={id} name={item.name}></FileIcon> | <FileIcon id={id} name={item.name}></FileIcon> | ||||
| )} | )} | ||||
| > | > | ||||
| <b> {item.name}</b> | <b> {item.name}</b> | ||||
| </Text> | </Text> | ||||
| {item.percent !== 100 ? ( | |||||
| t('uploading') | |||||
| ) : !item.response ? ( | |||||
| t('parsing') | |||||
| {isUploadError(item) ? ( | |||||
| t('uploadFailed') | |||||
| ) : ( | ) : ( | ||||
| <Space> | |||||
| <span>{fileExtension?.toUpperCase()},</span> | |||||
| <span> | |||||
| {formatBytes(getDocumentInfoById(id)?.size ?? 0)} | |||||
| </span> | |||||
| </Space> | |||||
| <> | |||||
| {item.percent !== 100 ? ( | |||||
| t('uploading') | |||||
| ) : !item.response ? ( | |||||
| t('parsing') | |||||
| ) : ( | |||||
| <Space> | |||||
| <span>{fileExtension?.toUpperCase()},</span> | |||||
| <span> | |||||
| {formatBytes( | |||||
| getDocumentInfoById(id)?.size ?? 0, | |||||
| )} | |||||
| </span> | |||||
| </Space> | |||||
| )} | |||||
| </> | |||||
| )} | )} | ||||
| </Flex> | </Flex> | ||||
| </Flex> | </Flex> | ||||
| {item.status !== 'uploading' && ( | {item.status !== 'uploading' && ( | ||||
| <CloseCircleOutlined | |||||
| className={styles.deleteIcon} | |||||
| onClick={() => handleRemove(item)} | |||||
| /> | |||||
| <span className={styles.deleteIcon}> | |||||
| <CloseCircleOutlined onClick={() => handleRemove(item)} /> | |||||
| </span> | |||||
| )} | )} | ||||
| </Card> | </Card> | ||||
| </List.Item> | </List.Item> |
| return { data, loading, removeDocument: mutateAsync }; | return { data, loading, removeDocument: mutateAsync }; | ||||
| }; | }; | ||||
| export const useDeleteDocument = () => { | |||||
| // const queryClient = useQueryClient(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: ['deleteDocument'], | |||||
| mutationFn: async (documentIds: string[]) => { | |||||
| const data = await kbService.document_delete({ doc_ids: documentIds }); | |||||
| // if (data.retcode === 0) { | |||||
| // queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] }); | |||||
| // } | |||||
| return data; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, deleteDocument: mutateAsync }; | |||||
| }; |
| searching: 'searching...', | searching: 'searching...', | ||||
| parsing: 'Parsing', | parsing: 'Parsing', | ||||
| uploading: 'Uploading', | uploading: 'Uploading', | ||||
| uploadFailed: 'Upload failed', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Profile', | profile: 'Profile', |
| searching: '搜索中', | searching: '搜索中', | ||||
| parsing: '解析中', | parsing: '解析中', | ||||
| uploading: '上傳中', | uploading: '上傳中', | ||||
| uploadFailed: '上傳失敗', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概述', | profile: '概述', |
| searching: '搜索中', | searching: '搜索中', | ||||
| parsing: '解析中', | parsing: '解析中', | ||||
| uploading: '上传中', | uploading: '上传中', | ||||
| uploadFailed: '上传失败', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概要', | profile: '概要', |
| </Flex> | </Flex> | ||||
| <MessageInput | <MessageInput | ||||
| isShared | |||||
| value={value} | value={value} | ||||
| disabled={false} | disabled={false} | ||||
| sendDisabled={sendDisabled} | sendDisabled={sendDisabled} |
| } from '@/hooks/chat-hooks'; | } from '@/hooks/chat-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 } from '@/interfaces/database/chat'; | |||||
| import { IAnswer, Message } from '@/interfaces/database/chat'; | |||||
| import api from '@/utils/api'; | import api from '@/utils/api'; | ||||
| import omit from 'lodash/omit'; | import omit from 'lodash/omit'; | ||||
| import trim from 'lodash/trim'; | import trim from 'lodash/trim'; | ||||
| const ref = useScrollToBottom(currentConversation); | const ref = useScrollToBottom(currentConversation); | ||||
| const addNewestConversation = useCallback((message: string) => { | |||||
| const addNewestConversation = useCallback((message: Partial<Message>) => { | |||||
| 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, | ||||
| { | { | ||||
| role: MessageType.Assistant, | role: MessageType.Assistant, | ||||
| content: '', | content: '', | ||||
| id: uuid(), | id: uuid(), | ||||
| reference: [], | |||||
| reference: {}, | |||||
| } as IMessage, | } as IMessage, | ||||
| ], | ], | ||||
| }; | }; | ||||
| export const useSendSharedMessage = ( | export const useSendSharedMessage = ( | ||||
| conversation: IClientConversation, | conversation: IClientConversation, | ||||
| addNewestConversation: (message: string) => void, | |||||
| addNewestConversation: (message: Partial<Message>, answer?: string) => void, | |||||
| removeLatestMessage: () => void, | removeLatestMessage: () => void, | ||||
| setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>, | setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>, | ||||
| addNewestAnswer: (answer: IAnswer) => void, | addNewestAnswer: (answer: IAnswer) => void, | ||||
| } | } | ||||
| }, [answer, addNewestAnswer]); | }, [answer, addNewestAnswer]); | ||||
| const handlePressEnter = useCallback(() => { | |||||
| if (trim(value) === '') return; | |||||
| if (done) { | |||||
| setValue(''); | |||||
| addNewestConversation(value); | |||||
| handleSendMessage(value.trim()); | |||||
| } | |||||
| }, [addNewestConversation, done, handleSendMessage, setValue, value]); | |||||
| const handlePressEnter = useCallback( | |||||
| (documentIds: string[]) => { | |||||
| if (trim(value) === '') return; | |||||
| if (done) { | |||||
| setValue(''); | |||||
| addNewestConversation({ content: value, doc_ids: documentIds }); | |||||
| handleSendMessage(value.trim()); | |||||
| } | |||||
| }, | |||||
| [addNewestConversation, done, handleSendMessage, setValue, value], | |||||
| ); | |||||
| return { | return { | ||||
| handlePressEnter, | handlePressEnter, |
| get_document_list, | get_document_list, | ||||
| document_change_status, | document_change_status, | ||||
| document_rm, | document_rm, | ||||
| document_delete, | |||||
| document_create, | document_create, | ||||
| document_change_parser, | document_change_parser, | ||||
| document_thumbnails, | document_thumbnails, | ||||
| url: knowledge_graph, | url: knowledge_graph, | ||||
| method: 'get', | method: 'get', | ||||
| }, | }, | ||||
| document_delete: { | |||||
| url: document_delete, | |||||
| method: 'delete', | |||||
| }, | |||||
| }; | }; | ||||
| const kbService = registerServer<keyof typeof methods>(methods, request); | const kbService = registerServer<keyof typeof methods>(methods, request); |
| get_document_list: `${api_host}/document/list`, | get_document_list: `${api_host}/document/list`, | ||||
| document_change_status: `${api_host}/document/change_status`, | document_change_status: `${api_host}/document/change_status`, | ||||
| document_rm: `${api_host}/document/rm`, | document_rm: `${api_host}/document/rm`, | ||||
| document_delete: `${api_host}/api/document`, | |||||
| document_rename: `${api_host}/document/rename`, | document_rename: `${api_host}/document/rename`, | ||||
| document_create: `${api_host}/document/create`, | document_create: `${api_host}/document/create`, | ||||
| document_run: `${api_host}/document/run`, | document_run: `${api_host}/document/run`, |
| (params?: any, urlAppendix?: string) => any | (params?: any, urlAppendix?: string) => any | ||||
| >; | >; | ||||
| const Methods = ['post', 'delete', 'put']; | |||||
| const registerServer = <T extends string>( | const registerServer = <T extends string>( | ||||
| opt: Record<T, { url: string; method: string }>, | opt: Record<T, { url: string; method: string }>, | ||||
| request: RequestMethod, | request: RequestMethod, | ||||
| if (urlAppendix) { | if (urlAppendix) { | ||||
| url = url + '/' + urlAppendix; | url = url + '/' + urlAppendix; | ||||
| } | } | ||||
| if (opt[key].method === 'post' || opt[key].method === 'POST') { | |||||
| if (Methods.some((x) => x === opt[key].method.toLowerCase())) { | |||||
| return request(url, { | return request(url, { | ||||
| method: opt[key].method, | method: opt[key].method, | ||||
| data: params, | data: params, |