new-api / web /src /hooks /playground /useMessageActions.jsx
liuzhao521
Deploy New API v0.9.25+ (commit b47cf4ef) to HuggingFace Spaces
4674012
/*
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 <https://www.gnu.org/licenses/>.
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,
};
};