* feat: fetch file thumbnails * feat: preview reference image * feat: delete conversation * feat: rename conversationtags/v0.1.0
| @@ -0,0 +1,78 @@ | |||
| import { Form, Input, Modal } from 'antd'; | |||
| import { useEffect } from 'react'; | |||
| import { IModalManagerChildrenProps } from '../modal-manager'; | |||
| interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> { | |||
| loading: boolean; | |||
| initialName: string; | |||
| onOk: (name: string) => void; | |||
| showModal?(): void; | |||
| } | |||
| const RenameModal = ({ | |||
| visible, | |||
| hideModal, | |||
| loading, | |||
| initialName, | |||
| onOk, | |||
| }: IProps) => { | |||
| const [form] = Form.useForm(); | |||
| type FieldType = { | |||
| name?: string; | |||
| }; | |||
| const handleOk = async () => { | |||
| const ret = await form.validateFields(); | |||
| return onOk(ret.name); | |||
| }; | |||
| const handleCancel = () => { | |||
| hideModal(); | |||
| }; | |||
| const onFinish = (values: any) => { | |||
| console.log('Success:', values); | |||
| }; | |||
| const onFinishFailed = (errorInfo: any) => { | |||
| console.log('Failed:', errorInfo); | |||
| }; | |||
| useEffect(() => { | |||
| form.setFieldValue('name', initialName); | |||
| }, [initialName, form]); | |||
| return ( | |||
| <Modal | |||
| title="Rename" | |||
| open={visible} | |||
| onOk={handleOk} | |||
| onCancel={handleCancel} | |||
| okButtonProps={{ loading }} | |||
| confirmLoading={loading} | |||
| > | |||
| <Form | |||
| name="basic" | |||
| labelCol={{ span: 4 }} | |||
| wrapperCol={{ span: 20 }} | |||
| style={{ maxWidth: 600 }} | |||
| onFinish={onFinish} | |||
| onFinishFailed={onFinishFailed} | |||
| autoComplete="off" | |||
| form={form} | |||
| > | |||
| <Form.Item<FieldType> | |||
| label="Name" | |||
| name="name" | |||
| rules={[{ required: true, message: 'Please input name!' }]} | |||
| > | |||
| <Input /> | |||
| </Form.Item> | |||
| </Form> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| export default RenameModal; | |||
| @@ -150,3 +150,34 @@ export const useFetchKnowledgeList = ( | |||
| return list; | |||
| }; | |||
| export const useSelectFileThumbnails = () => { | |||
| const fileThumbnails: Record<string, string> = useSelector( | |||
| (state: any) => state.kFModel.fileThumbnails, | |||
| ); | |||
| return fileThumbnails; | |||
| }; | |||
| export const useFetchFileThumbnails = (docIds?: Array<string>) => { | |||
| const dispatch = useDispatch(); | |||
| const fileThumbnails = useSelectFileThumbnails(); | |||
| const fetchFileThumbnails = useCallback( | |||
| (docIds: Array<string>) => { | |||
| dispatch({ | |||
| type: 'kFModel/fetch_document_thumbnails', | |||
| payload: { doc_ids: docIds.join(',') }, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| useEffect(() => { | |||
| if (docIds) { | |||
| fetchFileThumbnails(docIds); | |||
| } | |||
| }, [docIds, fetchFileThumbnails]); | |||
| return { fileThumbnails, fetchFileThumbnails }; | |||
| }; | |||
| @@ -20,9 +20,11 @@ | |||
| } | |||
| .img { | |||
| height: 16px; | |||
| width: 16px; | |||
| margin-right: 6px; | |||
| height: 24px; | |||
| width: 24px; | |||
| margin-right: 10px; | |||
| display: inline-block; | |||
| vertical-align: middle; | |||
| } | |||
| .column { | |||
| @@ -22,7 +22,7 @@ import { | |||
| } from 'antd'; | |||
| import type { ColumnsType } from 'antd/es/table'; | |||
| import { PaginationProps } from 'antd/lib'; | |||
| import React, { useEffect, useMemo, useState } from 'react'; | |||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import { Link, useDispatch, useNavigate, useSelector } from 'umi'; | |||
| import CreateEPModal from './createEFileModal'; | |||
| import styles from './index.less'; | |||
| @@ -46,7 +46,7 @@ const KnowledgeFile = () => { | |||
| const [parser_id, setParserId] = useState('0'); | |||
| let navigate = useNavigate(); | |||
| const getKfList = () => { | |||
| const getKfList = useCallback(() => { | |||
| const payload = { | |||
| kb_id: knowledgeBaseId, | |||
| }; | |||
| @@ -55,7 +55,7 @@ const KnowledgeFile = () => { | |||
| type: 'kFModel/getKfList', | |||
| payload, | |||
| }); | |||
| }; | |||
| }, [dispatch, knowledgeBaseId]); | |||
| const throttledGetDocumentList = () => { | |||
| dispatch({ | |||
| @@ -64,23 +64,29 @@ const KnowledgeFile = () => { | |||
| }); | |||
| }; | |||
| const setPagination = (pageNumber = 1, pageSize?: number) => { | |||
| const pagination: Pagination = { | |||
| current: pageNumber, | |||
| } as Pagination; | |||
| if (pageSize) { | |||
| pagination.pageSize = pageSize; | |||
| } | |||
| dispatch({ | |||
| type: 'kFModel/setPagination', | |||
| payload: pagination, | |||
| }); | |||
| }; | |||
| const setPagination = useCallback( | |||
| (pageNumber = 1, pageSize?: number) => { | |||
| const pagination: Pagination = { | |||
| current: pageNumber, | |||
| } as Pagination; | |||
| if (pageSize) { | |||
| pagination.pageSize = pageSize; | |||
| } | |||
| dispatch({ | |||
| type: 'kFModel/setPagination', | |||
| payload: pagination, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| const onPageChange: PaginationProps['onChange'] = (pageNumber, pageSize) => { | |||
| setPagination(pageNumber, pageSize); | |||
| getKfList(); | |||
| }; | |||
| const onPageChange: PaginationProps['onChange'] = useCallback( | |||
| (pageNumber: number, pageSize: number) => { | |||
| setPagination(pageNumber, pageSize); | |||
| getKfList(); | |||
| }, | |||
| [getKfList, setPagination], | |||
| ); | |||
| const pagination: PaginationProps = useMemo(() => { | |||
| return { | |||
| @@ -92,7 +98,7 @@ const KnowledgeFile = () => { | |||
| pageSizeOptions: [1, 2, 10, 20, 50, 100], | |||
| onChange: onPageChange, | |||
| }; | |||
| }, [total, kFModel.pagination]); | |||
| }, [total, kFModel.pagination, onPageChange]); | |||
| useEffect(() => { | |||
| if (knowledgeBaseId) { | |||
| @@ -107,7 +113,7 @@ const KnowledgeFile = () => { | |||
| type: 'kFModel/pollGetDocumentList-stop', | |||
| }); | |||
| }; | |||
| }, [knowledgeBaseId]); | |||
| }, [knowledgeBaseId, dispatch, getKfList]); | |||
| const handleInputChange = ( | |||
| e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, | |||
| @@ -129,14 +135,14 @@ const KnowledgeFile = () => { | |||
| }); | |||
| }; | |||
| const showCEFModal = () => { | |||
| const showCEFModal = useCallback(() => { | |||
| dispatch({ | |||
| type: 'kFModel/updateState', | |||
| payload: { | |||
| isShowCEFwModal: true, | |||
| }, | |||
| }); | |||
| }; | |||
| }, [dispatch]); | |||
| const actionItems: MenuProps['items'] = useMemo(() => { | |||
| return [ | |||
| @@ -169,7 +175,7 @@ const KnowledgeFile = () => { | |||
| // disabled: true, | |||
| }, | |||
| ]; | |||
| }, []); | |||
| }, [knowledgeBaseId, showCEFModal]); | |||
| const toChunk = (id: string) => { | |||
| navigate( | |||
| @@ -187,13 +193,9 @@ const KnowledgeFile = () => { | |||
| title: 'Name', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render: (text: any, { id }) => ( | |||
| render: (text: any, { id, thumbnail }) => ( | |||
| <div className={styles.tochunks} onClick={() => toChunk(id)}> | |||
| <img | |||
| className={styles.img} | |||
| src="https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg" | |||
| alt="" | |||
| /> | |||
| <img className={styles.img} src={thumbnail} alt="" /> | |||
| {text} | |||
| </div> | |||
| ), | |||
| @@ -16,6 +16,7 @@ export interface KFModelState extends BaseState { | |||
| data: IKnowledgeFile[]; | |||
| total: number; | |||
| currentRecord: Nullable<IKnowledgeFile>; | |||
| fileThumbnails: Record<string, string>; | |||
| } | |||
| const model: DvaModel<KFModelState> = { | |||
| @@ -34,6 +35,7 @@ const model: DvaModel<KFModelState> = { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| fileThumbnails: {} as Record<string, string>, | |||
| }, | |||
| reducers: { | |||
| updateState(state, { payload }) { | |||
| @@ -54,6 +56,9 @@ const model: DvaModel<KFModelState> = { | |||
| setPagination(state, { payload }) { | |||
| return { ...state, pagination: { ...state.pagination, ...payload } }; | |||
| }, | |||
| setFileThumbnails(state, { payload }) { | |||
| return { ...state, fileThumbnails: payload }; | |||
| }, | |||
| }, | |||
| effects: { | |||
| *createKf({ payload = {} }, { call }) { | |||
| @@ -201,6 +206,12 @@ const model: DvaModel<KFModelState> = { | |||
| } | |||
| return retcode; | |||
| }, | |||
| *fetch_document_thumbnails({ payload = {} }, { call, put }) { | |||
| const { data } = yield call(kbService.document_thumbnails, payload); | |||
| if (data.retcode === 0) { | |||
| yield put({ type: 'setFileThumbnails', payload: data.data }); | |||
| } | |||
| }, | |||
| }, | |||
| }; | |||
| export default model; | |||
| @@ -47,3 +47,7 @@ | |||
| .referenceChunkImage { | |||
| width: 10vw; | |||
| } | |||
| .referenceImagePreview { | |||
| width: 600px; | |||
| } | |||
| @@ -3,21 +3,12 @@ import { MessageType } from '@/constants/chat'; | |||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | |||
| import { useSelectUserInfo } from '@/hooks/userSettingHook'; | |||
| import { IReference, Message } from '@/interfaces/database/chat'; | |||
| import { | |||
| Avatar, | |||
| Button, | |||
| Flex, | |||
| Input, | |||
| List, | |||
| Popover, | |||
| Space, | |||
| Typography, | |||
| } from 'antd'; | |||
| import { Avatar, Button, Flex, Input, List, Popover, Space } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; | |||
| import reactStringReplace from 'react-string-replace'; | |||
| import { | |||
| useFetchConversation, | |||
| useFetchConversationOnMount, | |||
| useGetFileIcon, | |||
| useScrollToBottom, | |||
| useSendMessage, | |||
| @@ -26,6 +17,7 @@ import { IClientConversation } from '../interface'; | |||
| import Image from '@/components/image'; | |||
| import NewDocumentLink from '@/components/new-document-link'; | |||
| import { useSelectFileThumbnails } from '@/hooks/knowledgeHook'; | |||
| import { InfoCircleOutlined } from '@ant-design/icons'; | |||
| import Markdown from 'react-markdown'; | |||
| import { visitParents } from 'unist-util-visit-parents'; | |||
| @@ -56,11 +48,10 @@ const MessageItem = ({ | |||
| reference: IReference; | |||
| }) => { | |||
| const userInfo = useSelectUserInfo(); | |||
| const fileThumbnails = useSelectFileThumbnails(); | |||
| const isAssistant = item.role === MessageType.Assistant; | |||
| const getFileIcon = useGetFileIcon(); | |||
| const getPopoverContent = useCallback( | |||
| (chunkIndex: number) => { | |||
| const chunks = reference?.chunks ?? []; | |||
| @@ -75,22 +66,35 @@ const MessageItem = ({ | |||
| gap={10} | |||
| className={styles.referencePopoverWrapper} | |||
| > | |||
| <Image | |||
| id={chunkItem?.img_id} | |||
| className={styles.referenceChunkImage} | |||
| ></Image> | |||
| <Popover | |||
| placement="topRight" | |||
| content={ | |||
| <Image | |||
| id={chunkItem?.img_id} | |||
| className={styles.referenceImagePreview} | |||
| ></Image> | |||
| } | |||
| > | |||
| <Image | |||
| id={chunkItem?.img_id} | |||
| className={styles.referenceChunkImage} | |||
| ></Image> | |||
| </Popover> | |||
| <Space direction={'vertical'}> | |||
| <div>{chunkItem?.content_with_weight}</div> | |||
| {documentId && ( | |||
| <NewDocumentLink documentId={documentId}> | |||
| {document?.doc_name} | |||
| </NewDocumentLink> | |||
| <Flex gap={'middle'}> | |||
| <img src={fileThumbnails[documentId]} alt="" /> | |||
| <NewDocumentLink documentId={documentId}> | |||
| {document?.doc_name} | |||
| </NewDocumentLink> | |||
| </Flex> | |||
| )} | |||
| </Space> | |||
| </Flex> | |||
| ); | |||
| }, | |||
| [reference], | |||
| [reference, fileThumbnails], | |||
| ); | |||
| const renderReference = useCallback( | |||
| @@ -163,12 +167,13 @@ const MessageItem = ({ | |||
| dataSource={referenceDocumentList} | |||
| renderItem={(item) => ( | |||
| <List.Item> | |||
| <Typography.Text mark> | |||
| {/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */} | |||
| </Typography.Text> | |||
| <NewDocumentLink documentId={item.doc_id}> | |||
| {item.doc_name} | |||
| </NewDocumentLink> | |||
| {/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */} | |||
| <Flex gap={'middle'}> | |||
| <img src={fileThumbnails[item.doc_id]}></img> | |||
| <NewDocumentLink documentId={item.doc_id}> | |||
| {item.doc_name} | |||
| </NewDocumentLink> | |||
| </Flex> | |||
| </List.Item> | |||
| )} | |||
| /> | |||
| @@ -182,11 +187,10 @@ const MessageItem = ({ | |||
| const ChatContainer = () => { | |||
| const [value, setValue] = useState(''); | |||
| const conversation: IClientConversation = useFetchConversation(); | |||
| const conversation: IClientConversation = useFetchConversationOnMount(); | |||
| const { sendMessage } = useSendMessage(); | |||
| const loading = useOneNamespaceEffectsLoading('chatModel', [ | |||
| 'completeConversation', | |||
| 'getConversation', | |||
| ]); | |||
| const ref = useScrollToBottom(); | |||
| useGetFileIcon(); | |||
| @@ -1,6 +1,8 @@ | |||
| import showDeleteConfirm from '@/components/deleting-confirm'; | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { fileIconMap } from '@/constants/common'; | |||
| import { useSetModalState } from '@/hooks/commonHooks'; | |||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | |||
| import { IConversation, IDialog } from '@/interfaces/database/chat'; | |||
| import { getFileExtension } from '@/utils'; | |||
| import omit from 'lodash/omit'; | |||
| @@ -14,7 +16,7 @@ import { | |||
| VariableTableDataType, | |||
| } from './interface'; | |||
| import { ChatModelState } from './model'; | |||
| import { isConversationIdNotExist } from './utils'; | |||
| import { isConversationIdExist } from './utils'; | |||
| export const useFetchDialogList = () => { | |||
| const dispatch = useDispatch(); | |||
| @@ -204,6 +206,24 @@ export const useSelectFirstDialogOnMount = () => { | |||
| return dialogList; | |||
| }; | |||
| export const useHandleItemHover = () => { | |||
| const [activated, setActivated] = useState<string>(''); | |||
| const handleItemEnter = (id: string) => { | |||
| setActivated(id); | |||
| }; | |||
| const handleItemLeave = () => { | |||
| setActivated(''); | |||
| }; | |||
| return { | |||
| activated, | |||
| handleItemEnter, | |||
| handleItemLeave, | |||
| }; | |||
| }; | |||
| //#region conversation | |||
| export const useCreateTemporaryConversation = () => { | |||
| @@ -374,30 +394,50 @@ export const useSetConversation = () => { | |||
| return { setConversation }; | |||
| }; | |||
| export const useFetchConversation = () => { | |||
| const dispatch = useDispatch(); | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| const conversation = useSelector( | |||
| export const useSelectCurrentConversation = () => { | |||
| const conversation: IClientConversation = useSelector( | |||
| (state: any) => state.chatModel.currentConversation, | |||
| ); | |||
| const setCurrentConversation = useSetCurrentConversation(); | |||
| const fetchConversation = useCallback(() => { | |||
| if (isConversationIdNotExist(conversationId)) { | |||
| dispatch<any>({ | |||
| return conversation; | |||
| }; | |||
| export const useFetchConversation = () => { | |||
| const dispatch = useDispatch(); | |||
| const fetchConversation = useCallback( | |||
| (conversationId: string, needToBeSaved = true) => { | |||
| return dispatch<any>({ | |||
| type: 'chatModel/getConversation', | |||
| payload: { | |||
| needToBeSaved, | |||
| conversation_id: conversationId, | |||
| }, | |||
| }); | |||
| }, | |||
| [dispatch], | |||
| ); | |||
| return fetchConversation; | |||
| }; | |||
| export const useFetchConversationOnMount = () => { | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| const conversation = useSelectCurrentConversation(); | |||
| const setCurrentConversation = useSetCurrentConversation(); | |||
| const fetchConversation = useFetchConversation(); | |||
| const fetchConversationOnMount = useCallback(() => { | |||
| if (isConversationIdExist(conversationId)) { | |||
| fetchConversation(conversationId); | |||
| } else { | |||
| setCurrentConversation({} as IClientConversation); | |||
| } | |||
| }, [dispatch, conversationId, setCurrentConversation]); | |||
| }, [fetchConversation, setCurrentConversation, conversationId]); | |||
| useEffect(() => { | |||
| fetchConversation(); | |||
| }, [fetchConversation]); | |||
| fetchConversationOnMount(); | |||
| }, [fetchConversationOnMount]); | |||
| return conversation; | |||
| }; | |||
| @@ -477,4 +517,83 @@ export const useGetFileIcon = () => { | |||
| return getFileIcon; | |||
| }; | |||
| export const useRemoveConversation = () => { | |||
| const dispatch = useDispatch(); | |||
| const { dialogId } = useGetChatSearchParams(); | |||
| const { handleClickConversation } = useClickConversationCard(); | |||
| const removeConversation = (conversationIds: Array<string>) => async () => { | |||
| const ret = await dispatch<any>({ | |||
| type: 'chatModel/removeConversation', | |||
| payload: { | |||
| dialog_id: dialogId, | |||
| conversation_ids: conversationIds, | |||
| }, | |||
| }); | |||
| if (ret === 0) { | |||
| handleClickConversation(''); | |||
| } | |||
| return ret; | |||
| }; | |||
| const onRemoveConversation = (conversationIds: Array<string>) => { | |||
| showDeleteConfirm({ onOk: removeConversation(conversationIds) }); | |||
| }; | |||
| return { onRemoveConversation }; | |||
| }; | |||
| export const useRenameConversation = () => { | |||
| const dispatch = useDispatch(); | |||
| const [conversation, setConversation] = useState<IClientConversation>( | |||
| {} as IClientConversation, | |||
| ); | |||
| const fetchConversation = useFetchConversation(); | |||
| const { | |||
| visible: conversationRenameVisible, | |||
| hideModal: hideConversationRenameModal, | |||
| showModal: showConversationRenameModal, | |||
| } = useSetModalState(); | |||
| const onConversationRenameOk = useCallback( | |||
| async (name: string) => { | |||
| const ret = await dispatch<any>({ | |||
| type: 'chatModel/setConversation', | |||
| payload: { ...conversation, conversation_id: conversation.id, name }, | |||
| }); | |||
| if (ret.retcode === 0) { | |||
| hideConversationRenameModal(); | |||
| } | |||
| }, | |||
| [dispatch, conversation, hideConversationRenameModal], | |||
| ); | |||
| const loading = useOneNamespaceEffectsLoading('chatModel', [ | |||
| 'setConversation', | |||
| ]); | |||
| const handleShowConversationRenameModal = useCallback( | |||
| async (conversationId: string) => { | |||
| const ret = await fetchConversation(conversationId, false); | |||
| if (ret.retcode === 0) { | |||
| setConversation(ret.data); | |||
| } | |||
| showConversationRenameModal(); | |||
| }, | |||
| [showConversationRenameModal, fetchConversation], | |||
| ); | |||
| return { | |||
| conversationRenameLoading: loading, | |||
| initialConversationName: conversation.name, | |||
| onConversationRenameOk, | |||
| conversationRenameVisible, | |||
| hideConversationRenameModal, | |||
| showConversationRenameModal: handleShowConversationRenameModal, | |||
| }; | |||
| }; | |||
| //#endregion | |||
| @@ -11,8 +11,9 @@ import { | |||
| Space, | |||
| Tag, | |||
| } from 'antd'; | |||
| import { MenuItemProps } from 'antd/lib/menu/MenuItem'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { useCallback } from 'react'; | |||
| import ChatConfigurationModal from './chat-configuration-modal'; | |||
| import ChatContainer from './chat-container'; | |||
| import { | |||
| @@ -21,42 +22,88 @@ import { | |||
| useFetchConversationList, | |||
| useFetchDialog, | |||
| useGetChatSearchParams, | |||
| useHandleItemHover, | |||
| useRemoveConversation, | |||
| useRemoveDialog, | |||
| useRenameConversation, | |||
| useSelectConversationList, | |||
| useSelectFirstDialogOnMount, | |||
| useSetCurrentDialog, | |||
| } from './hooks'; | |||
| import RenameModal from '@/components/rename-modal'; | |||
| import styles from './index.less'; | |||
| const Chat = () => { | |||
| const dialogList = useSelectFirstDialogOnMount(); | |||
| const [activated, setActivated] = useState<string>(''); | |||
| const { visible, hideModal, showModal } = useSetModalState(); | |||
| const { setCurrentDialog, currentDialog } = useSetCurrentDialog(); | |||
| const { onRemoveDialog } = useRemoveDialog(); | |||
| const { onRemoveConversation } = useRemoveConversation(); | |||
| const { handleClickDialog } = useClickDialogCard(); | |||
| const { handleClickConversation } = useClickConversationCard(); | |||
| const { dialogId, conversationId } = useGetChatSearchParams(); | |||
| const { list: conversationList, addTemporaryConversation } = | |||
| useSelectConversationList(); | |||
| const { activated, handleItemEnter, handleItemLeave } = useHandleItemHover(); | |||
| const { | |||
| activated: conversationActivated, | |||
| handleItemEnter: handleConversationItemEnter, | |||
| handleItemLeave: handleConversationItemLeave, | |||
| } = useHandleItemHover(); | |||
| const { | |||
| conversationRenameLoading, | |||
| initialConversationName, | |||
| onConversationRenameOk, | |||
| conversationRenameVisible, | |||
| hideConversationRenameModal, | |||
| showConversationRenameModal, | |||
| } = useRenameConversation(); | |||
| useFetchDialog(dialogId, true); | |||
| const handleAppCardEnter = (id: string) => () => { | |||
| setActivated(id); | |||
| handleItemEnter(id); | |||
| }; | |||
| const handleAppCardLeave = () => { | |||
| setActivated(''); | |||
| const handleConversationCardEnter = (id: string) => () => { | |||
| handleConversationItemEnter(id); | |||
| }; | |||
| const handleShowChatConfigurationModal = (dialogId?: string) => () => { | |||
| if (dialogId) { | |||
| setCurrentDialog(dialogId); | |||
| } | |||
| showModal(); | |||
| }; | |||
| const handleShowChatConfigurationModal = | |||
| (dialogId?: string): any => | |||
| (info: any) => { | |||
| info?.domEvent?.preventDefault(); | |||
| info?.domEvent?.stopPropagation(); | |||
| if (dialogId) { | |||
| setCurrentDialog(dialogId); | |||
| } | |||
| showModal(); | |||
| }; | |||
| const handleRemoveDialog = | |||
| (dialogId: string): MenuItemProps['onClick'] => | |||
| ({ domEvent }) => { | |||
| domEvent.preventDefault(); | |||
| domEvent.stopPropagation(); | |||
| onRemoveDialog([dialogId]); | |||
| }; | |||
| const handleRemoveConversation = | |||
| (conversationId: string): MenuItemProps['onClick'] => | |||
| ({ domEvent }) => { | |||
| domEvent.preventDefault(); | |||
| domEvent.stopPropagation(); | |||
| onRemoveConversation([conversationId]); | |||
| }; | |||
| const handleShowConversationRenameModal = | |||
| (conversationId: string): MenuItemProps['onClick'] => | |||
| ({ domEvent }) => { | |||
| domEvent.preventDefault(); | |||
| domEvent.stopPropagation(); | |||
| showConversationRenameModal(conversationId); | |||
| }; | |||
| const handleDialogCardClick = (dialogId: string) => () => { | |||
| handleClickDialog(dialogId); | |||
| @@ -97,7 +144,35 @@ const Chat = () => { | |||
| { type: 'divider' }, | |||
| { | |||
| key: '2', | |||
| onClick: () => onRemoveDialog([dialogId]), | |||
| onClick: handleRemoveDialog(dialogId), | |||
| label: ( | |||
| <Space> | |||
| <DeleteOutlined /> | |||
| Delete chat | |||
| </Space> | |||
| ), | |||
| }, | |||
| ]; | |||
| return appItems; | |||
| }; | |||
| const buildConversationItems = (conversationId: string) => { | |||
| const appItems: MenuProps['items'] = [ | |||
| { | |||
| key: '1', | |||
| onClick: handleShowConversationRenameModal(conversationId), | |||
| label: ( | |||
| <Space> | |||
| <EditOutlined /> | |||
| Edit | |||
| </Space> | |||
| ), | |||
| }, | |||
| { type: 'divider' }, | |||
| { | |||
| key: '2', | |||
| onClick: handleRemoveConversation(conversationId), | |||
| label: ( | |||
| <Space> | |||
| <DeleteOutlined /> | |||
| @@ -129,7 +204,7 @@ const Chat = () => { | |||
| [styles.chatAppCardSelected]: dialogId === x.id, | |||
| })} | |||
| onMouseEnter={handleAppCardEnter(x.id)} | |||
| onMouseLeave={handleAppCardLeave} | |||
| onMouseLeave={handleItemLeave} | |||
| onClick={handleDialogCardClick(x.id)} | |||
| > | |||
| <Flex justify="space-between" align="center"> | |||
| @@ -176,11 +251,22 @@ const Chat = () => { | |||
| key={x.id} | |||
| hoverable | |||
| onClick={handleConversationCardClick(x.id)} | |||
| onMouseEnter={handleConversationCardEnter(x.id)} | |||
| onMouseLeave={handleConversationItemLeave} | |||
| className={classNames(styles.chatTitleCard, { | |||
| [styles.chatTitleCardSelected]: x.id === conversationId, | |||
| })} | |||
| > | |||
| <div>{x.name}</div> | |||
| <Flex justify="space-between" align="center"> | |||
| <div>{x.name}</div> | |||
| {conversationActivated === x.id && x.id !== '' && ( | |||
| <section> | |||
| <Dropdown menu={{ items: buildConversationItems(x.id) }}> | |||
| <ChatAppCube className={styles.cubeIcon}></ChatAppCube> | |||
| </Dropdown> | |||
| </section> | |||
| )} | |||
| </Flex> | |||
| </Card> | |||
| ))} | |||
| </Flex> | |||
| @@ -194,6 +280,13 @@ const Chat = () => { | |||
| hideModal={hideModal} | |||
| id={currentDialog.id} | |||
| ></ChatConfigurationModal> | |||
| <RenameModal | |||
| visible={conversationRenameVisible} | |||
| hideModal={hideConversationRenameModal} | |||
| onOk={onConversationRenameOk} | |||
| initialName={initialConversationName} | |||
| loading={conversationRenameLoading} | |||
| ></RenameModal> | |||
| </Flex> | |||
| ); | |||
| }; | |||
| @@ -4,6 +4,7 @@ import { message } from 'antd'; | |||
| import { DvaModel } from 'umi'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| import { IClientConversation, IMessage } from './interface'; | |||
| import { getDocumentIdsFromConversionReference } from './utils'; | |||
| export interface ChatModelState { | |||
| name: string; | |||
| @@ -109,11 +110,19 @@ const model: DvaModel<ChatModelState> = { | |||
| return data.retcode; | |||
| }, | |||
| *getConversation({ payload }, { call, put }) { | |||
| const { data } = yield call(chatService.getConversation, payload); | |||
| if (data.retcode === 0) { | |||
| const { data } = yield call(chatService.getConversation, { | |||
| conversation_id: payload.conversation_id, | |||
| }); | |||
| if (data.retcode === 0 && payload.needToBeSaved) { | |||
| yield put({ | |||
| type: 'kFModel/fetch_document_thumbnails', | |||
| payload: { | |||
| doc_ids: getDocumentIdsFromConversionReference(data.data), | |||
| }, | |||
| }); | |||
| yield put({ type: 'setCurrentConversation', payload: data.data }); | |||
| } | |||
| return data.retcode; | |||
| return data; | |||
| }, | |||
| *setConversation({ payload }, { call, put }) { | |||
| const { data } = yield call(chatService.setConversation, payload); | |||
| @@ -138,6 +147,19 @@ const model: DvaModel<ChatModelState> = { | |||
| }); | |||
| } | |||
| }, | |||
| *removeConversation({ payload }, { call, put }) { | |||
| const { data } = yield call(chatService.removeConversation, { | |||
| conversation_ids: payload.conversation_ids, | |||
| }); | |||
| if (data.retcode === 0) { | |||
| yield put({ | |||
| type: 'listConversation', | |||
| payload: { dialog_id: payload.dialog_id }, | |||
| }); | |||
| message.success('Deleted successfully !'); | |||
| } | |||
| return data.retcode; | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -1,3 +1,4 @@ | |||
| import { IConversation, IReference } from '@/interfaces/database/chat'; | |||
| import { EmptyConversationId, variableEnabledFieldMap } from './constants'; | |||
| export const excludeUnEnabledVariables = (values: any) => { | |||
| @@ -11,6 +12,23 @@ export const excludeUnEnabledVariables = (values: any) => { | |||
| ); | |||
| }; | |||
| export const isConversationIdNotExist = (conversationId: string) => { | |||
| export const isConversationIdExist = (conversationId: string) => { | |||
| return conversationId !== EmptyConversationId && conversationId !== ''; | |||
| }; | |||
| export const getDocumentIdsFromConversionReference = (data: IConversation) => { | |||
| const documentIds = data.reference.reduce( | |||
| (pre: Array<string>, cur: IReference) => { | |||
| cur.doc_aggs | |||
| .map((x) => x.doc_id) | |||
| .forEach((x) => { | |||
| if (pre.every((y) => y !== x)) { | |||
| pre.push(x); | |||
| } | |||
| }); | |||
| return pre; | |||
| }, | |||
| [], | |||
| ); | |||
| return documentIds.join(','); | |||
| }; | |||
| @@ -11,6 +11,7 @@ const { | |||
| setConversation, | |||
| completeConversation, | |||
| listConversation, | |||
| removeConversation, | |||
| } = api; | |||
| const methods = { | |||
| @@ -46,6 +47,10 @@ const methods = { | |||
| url: completeConversation, | |||
| method: 'post', | |||
| }, | |||
| removeConversation: { | |||
| url: removeConversation, | |||
| method: 'post', | |||
| }, | |||
| } as const; | |||
| const chatService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -13,6 +13,7 @@ const { | |||
| document_rm, | |||
| document_create, | |||
| document_change_parser, | |||
| document_thumbnails, | |||
| chunk_list, | |||
| create_chunk, | |||
| set_chunk, | |||
| @@ -75,6 +76,10 @@ const methods = { | |||
| url: document_change_parser, | |||
| method: 'post', | |||
| }, | |||
| document_thumbnails: { | |||
| url: document_thumbnails, | |||
| method: 'get', | |||
| }, | |||
| // chunk管理 | |||
| chunk_list: { | |||
| url: chunk_list, | |||
| @@ -42,6 +42,7 @@ export default { | |||
| document_create: `${api_host}/document/create`, | |||
| document_run: `${api_host}/document/run`, | |||
| document_change_parser: `${api_host}/document/change_parser`, | |||
| document_thumbnails: `${api_host}/document/thumbnails`, | |||
| setDialog: `${api_host}/dialog/set`, | |||
| getDialog: `${api_host}/dialog/get`, | |||