feat: Submit Feedback #2088 (#2134)

### What problem does this PR solve?

feat: Submit Feedback #2088

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-08-28 16:39:21 +08:00 committed by GitHub
parent f843dd05e5
commit 54f7c6ea8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 185 additions and 26 deletions

View File

@ -1,20 +1,34 @@
import { Form, Input, Modal } from 'antd'; import { Form, Input, Modal } from 'antd';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { useCallback } from 'react';
type FieldType = { type FieldType = {
username?: string; feedback?: string;
}; };
const FeedbackModal = ({ visible, hideModal }: IModalProps<any>) => { const FeedbackModal = ({
visible,
hideModal,
onOk,
loading,
}: IModalProps<IFeedbackRequestBody>) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const handleOk = async () => { const handleOk = useCallback(async () => {
const ret = await form.validateFields(); const ret = await form.validateFields();
}; return onOk?.({ thumbup: false, feedback: ret.feedback });
}, [onOk, form]);
return ( return (
<Modal title="Feedback" open={visible} onOk={handleOk} onCancel={hideModal}> <Modal
title="Feedback"
open={visible}
onOk={handleOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Form <Form
name="basic" name="basic"
labelCol={{ span: 0 }} labelCol={{ span: 0 }}
@ -24,10 +38,10 @@ const FeedbackModal = ({ visible, hideModal }: IModalProps<any>) => {
form={form} form={form}
> >
<Form.Item<FieldType> <Form.Item<FieldType>
name="username" name="feedback"
rules={[{ required: true, message: 'Please input your username!' }]} rules={[{ required: true, message: 'Please input your feedback!' }]}
> >
<Input.TextArea rows={8} placeholder="Please input your username!" /> <Input.TextArea rows={8} placeholder="Please input your feedback!" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

View File

@ -1,5 +1,4 @@
import CopyToClipboard from '@/components/copy-to-clipboard'; import CopyToClipboard from '@/components/copy-to-clipboard';
import { useSetModalState } from '@/hooks/common-hooks';
import { import {
DeleteOutlined, DeleteOutlined,
DislikeOutlined, DislikeOutlined,
@ -8,21 +7,33 @@ import {
SyncOutlined, SyncOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Radio } from 'antd'; import { Radio } from 'antd';
import { useCallback } from 'react';
import FeedbackModal from './feedback-modal'; import FeedbackModal from './feedback-modal';
import { useSendFeedback } from './hooks';
export const AssistantGroupButton = () => { interface IProps {
const { visible, hideModal, showModal } = useSetModalState(); messageId: string;
content: string;
}
export const AssistantGroupButton = ({ messageId, content }: IProps) => {
const { visible, hideModal, showModal, onFeedbackOk, loading } =
useSendFeedback(messageId);
const handleLike = useCallback(() => {
onFeedbackOk({ thumbup: true });
}, [onFeedbackOk]);
return ( return (
<> <>
<Radio.Group size="small"> <Radio.Group size="small">
<Radio.Button value="a"> <Radio.Button value="a">
<CopyToClipboard text="xxx"></CopyToClipboard> <CopyToClipboard text={content}></CopyToClipboard>
</Radio.Button> </Radio.Button>
<Radio.Button value="b"> <Radio.Button value="b">
<SoundOutlined /> <SoundOutlined />
</Radio.Button> </Radio.Button>
<Radio.Button value="c"> <Radio.Button value="c" onClick={handleLike}>
<LikeOutlined /> <LikeOutlined />
</Radio.Button> </Radio.Button>
<Radio.Button value="d" onClick={showModal}> <Radio.Button value="d" onClick={showModal}>
@ -30,7 +41,12 @@ export const AssistantGroupButton = () => {
</Radio.Button> </Radio.Button>
</Radio.Group> </Radio.Group>
{visible && ( {visible && (
<FeedbackModal visible={visible} hideModal={hideModal}></FeedbackModal> <FeedbackModal
visible={visible}
hideModal={hideModal}
onOk={onFeedbackOk}
loading={loading}
></FeedbackModal>
)} )}
</> </>
); );

View File

@ -0,0 +1,32 @@
import { useFeedback } from '@/hooks/chat-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { getMessagePureId } from '@/utils/chat';
import { useCallback } from 'react';
export const useSendFeedback = (messageId: string) => {
const { visible, hideModal, showModal } = useSetModalState();
const { feedback, loading } = useFeedback();
const onFeedbackOk = useCallback(
async (params: IFeedbackRequestBody) => {
const ret = await feedback({
...params,
messageId: getMessagePureId(messageId),
});
if (ret === 0) {
hideModal();
}
},
[feedback, hideModal, messageId],
);
return {
loading,
onFeedbackOk,
visible,
hideModal,
showModal,
};
};

View File

@ -2,7 +2,7 @@ import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat'; import { MessageType } from '@/constants/chat';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks'; import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useSelectFileThumbnails } from '@/hooks/knowledge-hooks'; import { useSelectFileThumbnails } from '@/hooks/knowledge-hooks';
import { IReference, Message } from '@/interfaces/database/chat'; import { IReference } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge'; import { IChunk } from '@/interfaces/database/knowledge';
import classNames from 'classnames'; import classNames from 'classnames';
import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
@ -11,19 +11,20 @@ import {
useFetchDocumentInfosByIds, useFetchDocumentInfosByIds,
useFetchDocumentThumbnailsByIds, useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks'; } from '@/hooks/document-hooks';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content'; import MarkdownContent from '@/pages/chat/markdown-content';
import { getExtension, isImage } from '@/utils/document-util'; import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd'; import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
import FileIcon from '../file-icon'; import FileIcon from '../file-icon';
import IndentedTreeModal from '../indented-tree/modal'; import IndentedTreeModal from '../indented-tree/modal';
import NewDocumentLink from '../new-document-link'; import NewDocumentLink from '../new-document-link';
// import { AssistantGroupButton, UserGroupButton } from './group-button'; import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less'; import styles from './index.less';
const { Text } = Typography; const { Text } = Typography;
interface IProps { interface IProps {
item: Message; item: IMessage;
reference: IReference; reference: IReference;
loading?: boolean; loading?: boolean;
nickname?: string; nickname?: string;
@ -36,7 +37,6 @@ const MessageItem = ({
reference, reference,
loading = false, loading = false,
avatar = '', avatar = '',
nickname = '',
clickDocumentButton, clickDocumentButton,
}: IProps) => { }: IProps) => {
const isAssistant = item.role === MessageType.Assistant; const isAssistant = item.role === MessageType.Assistant;
@ -111,13 +111,16 @@ const MessageItem = ({
)} )}
<Flex vertical gap={8} flex={1}> <Flex vertical gap={8} flex={1}>
<Space> <Space>
{/* {isAssistant ? ( {isAssistant ? (
<AssistantGroupButton></AssistantGroupButton> <AssistantGroupButton
messageId={item.id}
content={item.content}
></AssistantGroupButton>
) : ( ) : (
<UserGroupButton></UserGroupButton> <UserGroupButton></UserGroupButton>
)} */} )}
<b>{isAssistant ? '' : nickname}</b> {/* <b>{isAssistant ? '' : nickname}</b> */}
</Space> </Space>
<div <div
className={ className={

View File

@ -6,16 +6,16 @@ import {
IToken, IToken,
Message, Message,
} from '@/interfaces/database/chat'; } from '@/interfaces/database/chat';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
import { IClientConversation, IMessage } from '@/pages/chat/interface'; import { IClientConversation, IMessage } from '@/pages/chat/interface';
import chatService from '@/services/chat-service'; import chatService from '@/services/chat-service';
import { isConversationIdExist } from '@/utils/chat'; import { buildMessageUuid, isConversationIdExist } from '@/utils/chat';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd'; import { message } from 'antd';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';
//#region logic //#region logic
@ -218,7 +218,7 @@ export const useFetchNextConversation = () => {
const messageList = const messageList =
conversation?.message?.map((x: Message | IMessage) => ({ conversation?.message?.map((x: Message | IMessage) => ({
...x, ...x,
id: 'id' in x && x.id ? x.id : uuid(), id: buildMessageUuid(x),
})) ?? []; })) ?? [];
return { ...conversation, message: messageList }; return { ...conversation, message: messageList };
@ -292,6 +292,56 @@ export const useRemoveNextConversation = () => {
return { data, loading, removeConversation: mutateAsync }; return { data, loading, removeConversation: mutateAsync };
}; };
export const useDeleteMessage = () => {
// const queryClient = useQueryClient();
const { conversationId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteMessage'],
mutationFn: async (messageId: string) => {
const { data } = await chatService.deleteMessage({
messageId,
conversationId,
});
if (data.retcode === 0) {
// queryClient.invalidateQueries({ queryKey: ['fetchConversationList'] });
}
return data.retcode;
},
});
return { data, loading, deleteMessage: mutateAsync };
};
export const useFeedback = () => {
const { conversationId } = useGetChatSearchParams();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['feedback'],
mutationFn: async (params: IFeedbackRequestBody) => {
const { data } = await chatService.thumbup({
...params,
conversationId,
});
if (data.retcode === 0) {
message.success(i18n.t(`message.operated`));
}
return data.retcode;
},
});
return { data, loading, feedback: mutateAsync };
};
//#endregion //#endregion
// #region API provided for external calls // #region API provided for external calls

View File

@ -0,0 +1,5 @@
export interface IFeedbackRequestBody {
messageId?: string;
thumbup?: boolean;
feedback?: string;
}

View File

@ -421,6 +421,7 @@ export const useSendMessage = (
messages: [ messages: [
...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')), ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
{ {
id: uuid(),
role: MessageType.User, role: MessageType.User,
content: message, content: message,
doc_ids: documentIds, doc_ids: documentIds,

View File

@ -20,6 +20,9 @@ const {
getExternalConversation, getExternalConversation,
completeExternalConversation, completeExternalConversation,
uploadAndParseExternal, uploadAndParseExternal,
deleteMessage,
thumbup,
tts,
} = api; } = api;
const methods = { const methods = {
@ -91,6 +94,18 @@ const methods = {
url: uploadAndParseExternal, url: uploadAndParseExternal,
method: 'post', method: 'post',
}, },
deleteMessage: {
url: deleteMessage,
method: 'post',
},
thumbup: {
url: thumbup,
method: 'post',
},
tts: {
url: tts,
method: 'post',
},
} as const; } as const;
const chatService = registerServer<keyof typeof methods>(methods, request); const chatService = registerServer<keyof typeof methods>(methods, request);

View File

@ -63,6 +63,9 @@ export default {
listConversation: `${api_host}/conversation/list`, listConversation: `${api_host}/conversation/list`,
removeConversation: `${api_host}/conversation/rm`, removeConversation: `${api_host}/conversation/rm`,
completeConversation: `${api_host}/conversation/completion`, completeConversation: `${api_host}/conversation/completion`,
deleteMessage: `${api_host}/conversation/delete_msg`,
thumbup: `${api_host}/conversation/thumbup`,
tts: `${api_host}/conversation/tts`,
// chat for external // chat for external
createToken: `${api_host}/api/new_token`, createToken: `${api_host}/api/new_token`,
listToken: `${api_host}/api/token_list`, listToken: `${api_host}/api/token_list`,

View File

@ -1,5 +1,25 @@
import { EmptyConversationId } from '@/constants/chat'; import { EmptyConversationId, MessageType } from '@/constants/chat';
import { Message } from '@/interfaces/database/chat';
import { IMessage } from '@/pages/chat/interface';
import { v4 as uuid } from 'uuid';
export const isConversationIdExist = (conversationId: string) => { export const isConversationIdExist = (conversationId: string) => {
return conversationId !== EmptyConversationId && conversationId !== ''; return conversationId !== EmptyConversationId && conversationId !== '';
}; };
export const buildMessageUuid = (message: Message | IMessage) => {
if ('id' in message && message.id) {
return message.role === MessageType.User
? `${MessageType.User}_${message.id}`
: `${MessageType.Assistant}_${message.id}`;
}
return uuid();
};
export const getMessagePureId = (id: string) => {
const strings = id.split('_');
if (strings.length > 0) {
return strings.at(-1);
}
return id;
};