useMessageActions.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { useCallback } from 'react';
  2. import { Toast, Modal } from '@douyinfe/semi-ui';
  3. import { useTranslation } from 'react-i18next';
  4. import { getTextContent } from '../helpers';
  5. import { ERROR_MESSAGES } from '../constants/playground.constants';
  6. export const useMessageActions = (message, setMessage, onMessageSend, saveMessages) => {
  7. const { t } = useTranslation();
  8. // 复制消息
  9. const handleMessageCopy = useCallback((targetMessage) => {
  10. const textToCopy = getTextContent(targetMessage);
  11. if (!textToCopy) {
  12. Toast.warning({
  13. content: t(ERROR_MESSAGES.NO_TEXT_CONTENT),
  14. duration: 2,
  15. });
  16. return;
  17. }
  18. const copyToClipboard = async (text) => {
  19. if (navigator.clipboard?.writeText) {
  20. try {
  21. await navigator.clipboard.writeText(text);
  22. Toast.success({
  23. content: t('消息已复制到剪贴板'),
  24. duration: 2,
  25. });
  26. } catch (err) {
  27. console.error('Clipboard API 复制失败:', err);
  28. fallbackCopy(text);
  29. }
  30. } else {
  31. fallbackCopy(text);
  32. }
  33. };
  34. const fallbackCopy = (text) => {
  35. try {
  36. const textArea = document.createElement('textarea');
  37. textArea.value = text;
  38. textArea.style.cssText = `
  39. position: fixed;
  40. top: -9999px;
  41. left: -9999px;
  42. opacity: 0;
  43. pointer-events: none;
  44. z-index: -1;
  45. `;
  46. textArea.setAttribute('readonly', '');
  47. document.body.appendChild(textArea);
  48. textArea.select();
  49. textArea.setSelectionRange(0, text.length);
  50. const successful = document.execCommand('copy');
  51. document.body.removeChild(textArea);
  52. if (successful) {
  53. Toast.success({
  54. content: t('消息已复制到剪贴板'),
  55. duration: 2,
  56. });
  57. } else {
  58. throw new Error('execCommand copy failed');
  59. }
  60. } catch (err) {
  61. console.error('回退复制方案也失败:', err);
  62. let errorMessage = t(ERROR_MESSAGES.COPY_FAILED);
  63. if (window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
  64. errorMessage = t(ERROR_MESSAGES.COPY_HTTPS_REQUIRED);
  65. } else if (!navigator.clipboard && !document.execCommand) {
  66. errorMessage = t(ERROR_MESSAGES.BROWSER_NOT_SUPPORTED);
  67. }
  68. Toast.error({
  69. content: errorMessage,
  70. duration: 4,
  71. });
  72. }
  73. };
  74. copyToClipboard(textToCopy);
  75. }, [t]);
  76. // 重新生成消息
  77. const handleMessageReset = useCallback((targetMessage) => {
  78. setMessage(prevMessages => {
  79. // 使用引用查找索引,防止重复 id 造成误匹配
  80. let messageIndex = prevMessages.findIndex(msg => msg === targetMessage);
  81. // 回退到 id 匹配(兼容不同引用场景)
  82. if (messageIndex === -1) {
  83. messageIndex = prevMessages.findIndex(msg => msg.id === targetMessage.id);
  84. }
  85. if (messageIndex === -1) return prevMessages;
  86. if (targetMessage.role === 'user') {
  87. const newMessages = prevMessages.slice(0, messageIndex);
  88. const contentToSend = getTextContent(targetMessage);
  89. setTimeout(() => {
  90. onMessageSend(contentToSend);
  91. }, 100);
  92. return newMessages;
  93. } else if (targetMessage.role === 'assistant' || targetMessage.role === 'system') {
  94. let userMessageIndex = messageIndex - 1;
  95. while (userMessageIndex >= 0 && prevMessages[userMessageIndex].role !== 'user') {
  96. userMessageIndex--;
  97. }
  98. if (userMessageIndex >= 0) {
  99. const userMessage = prevMessages[userMessageIndex];
  100. const newMessages = prevMessages.slice(0, userMessageIndex);
  101. const contentToSend = getTextContent(userMessage);
  102. setTimeout(() => {
  103. onMessageSend(contentToSend);
  104. }, 100);
  105. return newMessages;
  106. }
  107. }
  108. return prevMessages;
  109. });
  110. }, [setMessage, onMessageSend]);
  111. // 删除消息
  112. const handleMessageDelete = useCallback((targetMessage) => {
  113. Modal.confirm({
  114. title: t('确认删除'),
  115. content: t('确定要删除这条消息吗?'),
  116. okText: t('确定'),
  117. cancelText: t('取消'),
  118. okButtonProps: {
  119. type: 'danger',
  120. },
  121. onOk: () => {
  122. setMessage(prevMessages => {
  123. // 使用引用查找索引,防止重复 id 造成误匹配
  124. let messageIndex = prevMessages.findIndex(msg => msg === targetMessage);
  125. // 回退到 id 匹配(兼容不同引用场景)
  126. if (messageIndex === -1) {
  127. messageIndex = prevMessages.findIndex(msg => msg.id === targetMessage.id);
  128. }
  129. if (messageIndex === -1) return prevMessages;
  130. let updatedMessages;
  131. if (targetMessage.role === 'user' && messageIndex < prevMessages.length - 1) {
  132. const nextMessage = prevMessages[messageIndex + 1];
  133. if (nextMessage.role === 'assistant') {
  134. Toast.success({
  135. content: t('已删除消息及其回复'),
  136. duration: 2,
  137. });
  138. updatedMessages = prevMessages.filter((_, index) =>
  139. index !== messageIndex && index !== messageIndex + 1
  140. );
  141. } else {
  142. Toast.success({
  143. content: t('消息已删除'),
  144. duration: 2,
  145. });
  146. updatedMessages = prevMessages.filter(msg => msg.id !== targetMessage.id);
  147. }
  148. } else {
  149. Toast.success({
  150. content: t('消息已删除'),
  151. duration: 2,
  152. });
  153. updatedMessages = prevMessages.filter(msg => msg.id !== targetMessage.id);
  154. }
  155. // 删除消息后保存,传入更新后的消息列表
  156. setTimeout(() => saveMessages(updatedMessages), 0);
  157. return updatedMessages;
  158. });
  159. },
  160. });
  161. }, [setMessage, t, saveMessages]);
  162. // 切换角色
  163. const handleRoleToggle = useCallback((targetMessage) => {
  164. if (!(targetMessage.role === 'assistant' || targetMessage.role === 'system')) {
  165. return;
  166. }
  167. const newRole = targetMessage.role === 'assistant' ? 'system' : 'assistant';
  168. setMessage(prevMessages => {
  169. const updatedMessages = prevMessages.map(msg => {
  170. if (msg.id === targetMessage.id &&
  171. (msg.role === 'assistant' || msg.role === 'system')) {
  172. return { ...msg, role: newRole };
  173. }
  174. return msg;
  175. });
  176. // 切换角色后保存,传入更新后的消息列表
  177. setTimeout(() => saveMessages(updatedMessages), 0);
  178. return updatedMessages;
  179. });
  180. Toast.success({
  181. content: t(`已切换为${newRole === 'system' ? 'System' : 'Assistant'}角色`),
  182. duration: 2,
  183. });
  184. }, [setMessage, t, saveMessages]);
  185. return {
  186. handleMessageCopy,
  187. handleMessageReset,
  188. handleMessageDelete,
  189. handleRoleToggle,
  190. };
  191. };