/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useCallback } from 'react'; import { Toast, Modal } from '@douyinfe/semi-ui'; import { useTranslation } from 'react-i18next'; import { getTextContent } from '../../helpers'; import { ERROR_MESSAGES } from '../../constants/playground.constants'; export const useMessageActions = ( message, setMessage, onMessageSend, saveMessages, ) => { const { t } = useTranslation(); // 复制消息 const handleMessageCopy = useCallback( (targetMessage) => { const textToCopy = getTextContent(targetMessage); if (!textToCopy) { Toast.warning({ content: t(ERROR_MESSAGES.NO_TEXT_CONTENT), duration: 2, }); return; } const copyToClipboard = async (text) => { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); Toast.success({ content: t('消息已复制到剪贴板'), duration: 2, }); } catch (err) { console.error('Clipboard API 复制失败:', err); fallbackCopy(text); } } else { fallbackCopy(text); } }; const fallbackCopy = (text) => { try { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.cssText = ` position: fixed; top: -9999px; left: -9999px; opacity: 0; pointer-events: none; z-index: -1; `; textArea.setAttribute('readonly', ''); document.body.appendChild(textArea); textArea.select(); textArea.setSelectionRange(0, text.length); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { Toast.success({ content: t('消息已复制到剪贴板'), duration: 2, }); } else { throw new Error('execCommand copy failed'); } } catch (err) { console.error('回退复制方案也失败:', err); let errorMessage = t(ERROR_MESSAGES.COPY_FAILED); if ( window.location.protocol === 'http:' && window.location.hostname !== 'localhost' ) { errorMessage = t(ERROR_MESSAGES.COPY_HTTPS_REQUIRED); } else if (!navigator.clipboard && !document.execCommand) { errorMessage = t(ERROR_MESSAGES.BROWSER_NOT_SUPPORTED); } Toast.error({ content: errorMessage, duration: 4, }); } }; copyToClipboard(textToCopy); }, [t], ); // 重新生成消息 const handleMessageReset = useCallback( (targetMessage) => { setMessage((prevMessages) => { // 使用引用查找索引,防止重复 id 造成误匹配 let messageIndex = prevMessages.findIndex( (msg) => msg === targetMessage, ); // 回退到 id 匹配(兼容不同引用场景) if (messageIndex === -1) { messageIndex = prevMessages.findIndex( (msg) => msg.id === targetMessage.id, ); } if (messageIndex === -1) return prevMessages; if (targetMessage.role === 'user') { const newMessages = prevMessages.slice(0, messageIndex); const contentToSend = getTextContent(targetMessage); setTimeout(() => { onMessageSend(contentToSend); }, 100); return newMessages; } else if ( targetMessage.role === 'assistant' || targetMessage.role === 'system' ) { let userMessageIndex = messageIndex - 1; while ( userMessageIndex >= 0 && prevMessages[userMessageIndex].role !== 'user' ) { userMessageIndex--; } if (userMessageIndex >= 0) { const userMessage = prevMessages[userMessageIndex]; const newMessages = prevMessages.slice(0, userMessageIndex); const contentToSend = getTextContent(userMessage); setTimeout(() => { onMessageSend(contentToSend); }, 100); return newMessages; } } return prevMessages; }); }, [setMessage, onMessageSend], ); // 删除消息 const handleMessageDelete = useCallback( (targetMessage) => { Modal.confirm({ title: t('确认删除'), content: t('确定要删除这条消息吗?'), okText: t('确定'), cancelText: t('取消'), okButtonProps: { type: 'danger', }, onOk: () => { setMessage((prevMessages) => { // 使用引用查找索引,防止重复 id 造成误匹配 let messageIndex = prevMessages.findIndex( (msg) => msg === targetMessage, ); // 回退到 id 匹配(兼容不同引用场景) if (messageIndex === -1) { messageIndex = prevMessages.findIndex( (msg) => msg.id === targetMessage.id, ); } if (messageIndex === -1) return prevMessages; let updatedMessages; if ( targetMessage.role === 'user' && messageIndex < prevMessages.length - 1 ) { const nextMessage = prevMessages[messageIndex + 1]; if (nextMessage.role === 'assistant') { Toast.success({ content: t('已删除消息及其回复'), duration: 2, }); updatedMessages = prevMessages.filter( (_, index) => index !== messageIndex && index !== messageIndex + 1, ); } else { Toast.success({ content: t('消息已删除'), duration: 2, }); updatedMessages = prevMessages.filter( (msg) => msg.id !== targetMessage.id, ); } } else { Toast.success({ content: t('消息已删除'), duration: 2, }); updatedMessages = prevMessages.filter( (msg) => msg.id !== targetMessage.id, ); } // 删除消息后保存,传入更新后的消息列表 setTimeout(() => saveMessages(updatedMessages), 0); return updatedMessages; }); }, }); }, [setMessage, t, saveMessages], ); // 切换角色 const handleRoleToggle = useCallback( (targetMessage) => { if ( !(targetMessage.role === 'assistant' || targetMessage.role === 'system') ) { return; } const newRole = targetMessage.role === 'assistant' ? 'system' : 'assistant'; setMessage((prevMessages) => { const updatedMessages = prevMessages.map((msg) => { if ( msg.id === targetMessage.id && (msg.role === 'assistant' || msg.role === 'system') ) { return { ...msg, role: newRole }; } return msg; }); // 切换角色后保存,传入更新后的消息列表 setTimeout(() => saveMessages(updatedMessages), 0); return updatedMessages; }); Toast.success({ content: t( `已切换为${newRole === 'system' ? 'System' : 'Assistant'}角色`, ), duration: 2, }); }, [setMessage, t, saveMessages], ); return { handleMessageCopy, handleMessageReset, handleMessageDelete, handleRoleToggle, }; };