Feedback.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { apiInterceptors, cancelFeedback, feedbackAdd, getFeedbackReasons } from '@/client/api';
  2. import { CopyOutlined, DislikeOutlined, LikeOutlined } from '@ant-design/icons';
  3. import { useRequest } from 'ahooks';
  4. import { Button, Divider, Input, Popover, Tag, message } from 'antd';
  5. import classNames from 'classnames';
  6. import copy from 'copy-to-clipboard';
  7. import { useSearchParams } from 'next/navigation';
  8. import React, { useState } from 'react';
  9. import { useTranslation } from 'react-i18next';
  10. interface Tags {
  11. reason: string;
  12. reason_type: string;
  13. }
  14. const DislikeContent: React.FC<{
  15. list: Tags[];
  16. loading: boolean;
  17. feedback: (params: {
  18. feedback_type: string;
  19. reason_types?: string[] | undefined;
  20. remark?: string | undefined;
  21. }) => void;
  22. setFeedbackOpen: React.Dispatch<React.SetStateAction<boolean>>;
  23. }> = ({ list, loading, feedback, setFeedbackOpen }) => {
  24. const { t } = useTranslation();
  25. const [selectedTags, setSelectedTags] = useState<Tags[]>([]);
  26. const [remark, setRemark] = useState('');
  27. return (
  28. <div className='flex flex-col'>
  29. <div className='flex flex-1 flex-wrap w-72'>
  30. {list?.map(item => {
  31. const isSelect = selectedTags.findIndex(tag => tag.reason_type === item.reason_type) > -1;
  32. return (
  33. <Tag
  34. key={item.reason_type}
  35. className={`text-xs text-[#525964] mb-2 p-1 px-2 rounded-md cursor-pointer ${isSelect ? 'border-[#0c75fc] text-[#0c75fc]' : ''}`}
  36. onClick={() => {
  37. setSelectedTags((preArr: Tags[]) => {
  38. const index = preArr.findIndex(tag => tag.reason_type === item.reason_type);
  39. if (index > -1) {
  40. return [...preArr.slice(0, index), ...preArr.slice(index + 1)];
  41. }
  42. return [...preArr, item];
  43. });
  44. }}
  45. >
  46. {item.reason}
  47. </Tag>
  48. );
  49. })}
  50. </div>
  51. <Input.TextArea
  52. placeholder={t('feedback_tip')}
  53. className='w-64 h-20 resize-none mb-2'
  54. value={remark}
  55. onChange={e => setRemark(e.target.value.trim())}
  56. />
  57. <div className='flex gap-2 justify-end'>
  58. <Button
  59. className='w-16 h-8'
  60. onClick={() => {
  61. setFeedbackOpen(false);
  62. }}
  63. >
  64. 取消
  65. </Button>
  66. <Button
  67. type='primary'
  68. className='min-w-16 h-8'
  69. onClick={async () => {
  70. const reason_types = selectedTags.map(item => item.reason_type);
  71. await feedback?.({
  72. feedback_type: 'unlike',
  73. reason_types,
  74. remark,
  75. });
  76. }}
  77. loading={loading}
  78. >
  79. 确认
  80. </Button>
  81. </div>
  82. </div>
  83. );
  84. };
  85. const Feedback: React.FC<{ content: Record<string, any> }> = ({ content }) => {
  86. const { t } = useTranslation();
  87. const searchParams = useSearchParams();
  88. const chatId = searchParams?.get('id') ?? '';
  89. const [messageApi, contextHolder] = message.useMessage();
  90. const [feedbackOpen, setFeedbackOpen] = useState<boolean>(false);
  91. const [status, setStatus] = useState<'like' | 'unlike' | 'none'>(content?.feedback?.feedback_type);
  92. const [list, setList] = useState<Tags[]>();
  93. // 复制回答
  94. const onCopyContext = async (context: any) => {
  95. const pureStr = context?.replace(/\trelations:.*/g, '');
  96. const result = copy(pureStr);
  97. if (result) {
  98. if (pureStr) {
  99. messageApi.open({ type: 'success', content: t('copy_success') });
  100. } else {
  101. messageApi.open({ type: 'warning', content: t('copy_nothing') });
  102. }
  103. } else {
  104. messageApi.open({ type: 'error', content: t('copy_failed') });
  105. }
  106. };
  107. // 点赞/踩
  108. const { run: feedback, loading } = useRequest(
  109. async (params: { feedback_type: string; reason_types?: string[]; remark?: string }) =>
  110. await apiInterceptors(
  111. feedbackAdd({
  112. conv_uid: chatId,
  113. message_id: content.order + '',
  114. feedback_type: params.feedback_type,
  115. reason_types: params.reason_types,
  116. remark: params.remark,
  117. }),
  118. ),
  119. {
  120. manual: true,
  121. onSuccess: data => {
  122. const [, res] = data;
  123. setStatus(res?.feedback_type);
  124. message.success('反馈成功');
  125. setFeedbackOpen(false);
  126. },
  127. },
  128. );
  129. // 反馈原因类型
  130. const { run: getReasonList } = useRequest(async () => await apiInterceptors(getFeedbackReasons()), {
  131. manual: true,
  132. onSuccess: data => {
  133. const [, res] = data;
  134. setList(res || []);
  135. if (res) {
  136. setFeedbackOpen(true);
  137. }
  138. },
  139. });
  140. // 取消反馈
  141. const { run: cancel } = useRequest(
  142. async () => await apiInterceptors(cancelFeedback({ conv_uid: chatId, message_id: content?.order + '' })),
  143. {
  144. manual: true,
  145. onSuccess: data => {
  146. const [, res] = data;
  147. if (res) {
  148. setStatus('none');
  149. message.success('操作成功');
  150. }
  151. },
  152. },
  153. );
  154. return (
  155. <>
  156. {contextHolder}
  157. <div className='flex flex-1 items-center text-sm px-4'>
  158. <div className='flex gap-3'>
  159. <LikeOutlined
  160. className={classNames('cursor-pointer', { 'text-[#0C75FC]': status === 'like' })}
  161. onClick={async () => {
  162. if (status === 'like') {
  163. await cancel();
  164. return;
  165. }
  166. await feedback({ feedback_type: 'like' });
  167. }}
  168. />
  169. <Popover
  170. placement='bottom'
  171. autoAdjustOverflow
  172. destroyTooltipOnHide={true}
  173. content={
  174. <DislikeContent
  175. setFeedbackOpen={setFeedbackOpen}
  176. feedback={feedback}
  177. list={list || []}
  178. loading={loading}
  179. />
  180. }
  181. trigger='click'
  182. open={feedbackOpen}
  183. >
  184. <DislikeOutlined
  185. className={classNames('cursor-pointer', {
  186. 'text-[#0C75FC]': status === 'unlike',
  187. })}
  188. onClick={async () => {
  189. if (status === 'unlike') {
  190. await cancel();
  191. return;
  192. }
  193. await getReasonList();
  194. }}
  195. />
  196. </Popover>
  197. </div>
  198. <Divider type='vertical' />
  199. <CopyOutlined className='cursor-pointer' onClick={() => onCopyContext(content.context)} />
  200. </div>
  201. </>
  202. );
  203. };
  204. export default Feedback;