feat: rename conversation and delete conversation and preview reference image and fetch file thumbnails (#79)
* feat: fetch file thumbnails * feat: preview reference image * feat: delete conversation * feat: rename conversation
This commit is contained in:
@@ -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(',');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user