Преглед на файлове

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)
tags/v0.11.0
balibabu преди 1 година
родител
ревизия
54f7c6ea8e
No account linked to committer's email address

+ 22
- 8
web/src/components/message-item/feedback-modal.tsx Целия файл

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 }}
form={form} form={form}
> >
<Form.Item<FieldType> <Form.Item<FieldType>
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
name="feedback"
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>

+ 22
- 6
web/src/components/message-item/group-button.tsx Целия файл

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,
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 = () => {
const { visible, hideModal, showModal } = useSetModalState();
interface IProps {
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}>
</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>
)} )}
</> </>
); );

+ 32
- 0
web/src/components/message-item/hooks.ts Целия файл

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,
};
};

+ 11
- 8
web/src/components/message-item/index.tsx Целия файл

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';
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;
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;
)} )}
<Flex vertical gap={8} flex={1}> <Flex vertical gap={8} flex={1}>
<Space> <Space>
{/* {isAssistant ? (
<AssistantGroupButton></AssistantGroupButton>
{isAssistant ? (
<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={

+ 53
- 3
web/src/hooks/chat-hooks.ts Целия файл

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


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 };


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

+ 5
- 0
web/src/interfaces/request/chat.ts Целия файл

export interface IFeedbackRequestBody {
messageId?: string;
thumbup?: boolean;
feedback?: string;
}

+ 1
- 0
web/src/pages/chat/hooks.ts Целия файл

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,

+ 15
- 0
web/src/services/chat-service.ts Целия файл

getExternalConversation, getExternalConversation,
completeExternalConversation, completeExternalConversation,
uploadAndParseExternal, uploadAndParseExternal,
deleteMessage,
thumbup,
tts,
} = api; } = api;


const methods = { 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);

+ 3
- 0
web/src/utils/api.ts Целия файл

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`,

+ 21
- 1
web/src/utils/chat.ts Целия файл

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;
};

Loading…
Отказ
Запис