### 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
| 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> |
| 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> | |||||
| )} | )} | ||||
| </> | </> | ||||
| ); | ); |
| 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, | |||||
| }; | |||||
| }; |
| 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={ |
| 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 |
| export interface IFeedbackRequestBody { | |||||
| messageId?: string; | |||||
| thumbup?: boolean; | |||||
| feedback?: string; | |||||
| } |
| 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, |
| 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); |
| 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`, |
| 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; | |||||
| }; |