🎨 chore(web): apply ESLint and Prettier auto-fixes (baseline)

- Ran: bun run eslint:fix && bun run lint:fix
- Inserted AGPL license header via eslint-plugin-header
- Enforced no-multiple-empty-lines and other lint rules
- Formatted code using Prettier v3 (@so1ve/prettier-config)
- No functional changes; formatting-only baseline across JS/JSX files
This commit is contained in:
t0ng7u
2025-08-30 21:15:10 +08:00
parent 41cf516ec5
commit 0d57b1acd4
274 changed files with 11025 additions and 7659 deletions
+347 -281
View File
@@ -23,13 +23,13 @@ import { SSE } from 'sse.js';
import {
API_ENDPOINTS,
MESSAGE_STATUS,
DEBUG_TABS
DEBUG_TABS,
} from '../../constants/playground.constants';
import {
getUserIdFromLocalStorage,
handleApiError,
processThinkTags,
processIncompleteThinkTags
processIncompleteThinkTags,
} from '../../helpers';
export const useApiRequest = (
@@ -37,167 +37,233 @@ export const useApiRequest = (
setDebugData,
setActiveDebugTab,
sseSourceRef,
saveMessages
saveMessages,
) => {
const { t } = useTranslation();
// 处理消息自动关闭逻辑的公共函数
const applyAutoCollapseLogic = useCallback((message, isThinkingComplete = true) => {
const shouldAutoCollapse = isThinkingComplete && !message.hasAutoCollapsed;
return {
isThinkingComplete,
hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
isReasoningExpanded: shouldAutoCollapse ? false : message.isReasoningExpanded,
};
}, []);
const applyAutoCollapseLogic = useCallback(
(message, isThinkingComplete = true) => {
const shouldAutoCollapse =
isThinkingComplete && !message.hasAutoCollapsed;
return {
isThinkingComplete,
hasAutoCollapsed: shouldAutoCollapse || message.hasAutoCollapsed,
isReasoningExpanded: shouldAutoCollapse
? false
: message.isReasoningExpanded,
};
},
[],
);
// 流式消息更新
const streamMessageUpdate = useCallback((textChunk, type) => {
setMessage(prevMessage => {
const lastMessage = prevMessage[prevMessage.length - 1];
if (!lastMessage) return prevMessage;
if (lastMessage.role !== 'assistant') return prevMessage;
if (lastMessage.status === MESSAGE_STATUS.ERROR) {
return prevMessage;
}
const streamMessageUpdate = useCallback(
(textChunk, type) => {
setMessage((prevMessage) => {
const lastMessage = prevMessage[prevMessage.length - 1];
if (!lastMessage) return prevMessage;
if (lastMessage.role !== 'assistant') return prevMessage;
if (lastMessage.status === MESSAGE_STATUS.ERROR) {
return prevMessage;
}
if (lastMessage.status === MESSAGE_STATUS.LOADING ||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
if (
lastMessage.status === MESSAGE_STATUS.LOADING ||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE
) {
let newMessage = { ...lastMessage };
let newMessage = { ...lastMessage };
if (type === 'reasoning') {
newMessage = {
...newMessage,
reasoningContent:
(lastMessage.reasoningContent || '') + textChunk,
status: MESSAGE_STATUS.INCOMPLETE,
isThinkingComplete: false,
};
} else if (type === 'content') {
const shouldCollapseReasoning =
!lastMessage.content && lastMessage.reasoningContent;
const newContent = (lastMessage.content || '') + textChunk;
if (type === 'reasoning') {
newMessage = {
...newMessage,
reasoningContent: (lastMessage.reasoningContent || '') + textChunk,
status: MESSAGE_STATUS.INCOMPLETE,
isThinkingComplete: false,
};
} else if (type === 'content') {
const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent;
const newContent = (lastMessage.content || '') + textChunk;
let shouldCollapseFromThinkTag = false;
let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
let shouldCollapseFromThinkTag = false;
let thinkingCompleteFromTags = lastMessage.isThinkingComplete;
if (lastMessage.isReasoningExpanded && newContent.includes('</think>')) {
const thinkMatches = newContent.match(/<think>/g);
const thinkCloseMatches = newContent.match(/<\/think>/g);
if (thinkMatches && thinkCloseMatches &&
thinkCloseMatches.length >= thinkMatches.length) {
shouldCollapseFromThinkTag = true;
thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
if (
lastMessage.isReasoningExpanded &&
newContent.includes('</think>')
) {
const thinkMatches = newContent.match(/<think>/g);
const thinkCloseMatches = newContent.match(/<\/think>/g);
if (
thinkMatches &&
thinkCloseMatches &&
thinkCloseMatches.length >= thinkMatches.length
) {
shouldCollapseFromThinkTag = true;
thinkingCompleteFromTags = true; // think标签闭合也标记思考完成
}
}
// 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
const isThinkingComplete =
(lastMessage.reasoningContent &&
!lastMessage.isThinkingComplete) ||
thinkingCompleteFromTags;
const autoCollapseState = applyAutoCollapseLogic(
lastMessage,
isThinkingComplete,
);
newMessage = {
...newMessage,
content: newContent,
status: MESSAGE_STATUS.INCOMPLETE,
...autoCollapseState,
};
}
// 如果开始接收content内容,且之前有reasoning内容,或者think标签已闭合,则标记思考完成
const isThinkingComplete = (lastMessage.reasoningContent && !lastMessage.isThinkingComplete) ||
thinkingCompleteFromTags;
const autoCollapseState = applyAutoCollapseLogic(lastMessage, isThinkingComplete);
newMessage = {
...newMessage,
content: newContent,
status: MESSAGE_STATUS.INCOMPLETE,
...autoCollapseState,
};
return [...prevMessage.slice(0, -1), newMessage];
}
return [...prevMessage.slice(0, -1), newMessage];
}
return prevMessage;
});
}, [setMessage, applyAutoCollapseLogic]);
return prevMessage;
});
},
[setMessage, applyAutoCollapseLogic],
);
// 完成消息
const completeMessage = useCallback((status = MESSAGE_STATUS.COMPLETE) => {
setMessage(prevMessage => {
const lastMessage = prevMessage[prevMessage.length - 1];
if (lastMessage.status === MESSAGE_STATUS.COMPLETE ||
lastMessage.status === MESSAGE_STATUS.ERROR) {
return prevMessage;
}
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
const updatedMessages = [
...prevMessage.slice(0, -1),
{
...lastMessage,
status: status,
...autoCollapseState,
const completeMessage = useCallback(
(status = MESSAGE_STATUS.COMPLETE) => {
setMessage((prevMessage) => {
const lastMessage = prevMessage[prevMessage.length - 1];
if (
lastMessage.status === MESSAGE_STATUS.COMPLETE ||
lastMessage.status === MESSAGE_STATUS.ERROR
) {
return prevMessage;
}
];
// 在消息完成时保存,传入更新后的消息列表
if (status === MESSAGE_STATUS.COMPLETE || status === MESSAGE_STATUS.ERROR) {
setTimeout(() => saveMessages(updatedMessages), 0);
}
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
return updatedMessages;
});
}, [setMessage, applyAutoCollapseLogic, saveMessages]);
const updatedMessages = [
...prevMessage.slice(0, -1),
{
...lastMessage,
status: status,
...autoCollapseState,
},
];
// 在消息完成时保存,传入更新后的消息列表
if (
status === MESSAGE_STATUS.COMPLETE ||
status === MESSAGE_STATUS.ERROR
) {
setTimeout(() => saveMessages(updatedMessages), 0);
}
return updatedMessages;
});
},
[setMessage, applyAutoCollapseLogic, saveMessages],
);
// 非流式请求
const handleNonStreamRequest = useCallback(async (payload) => {
setDebugData(prev => ({
...prev,
request: payload,
timestamp: new Date().toISOString(),
response: null
}));
setActiveDebugTab(DEBUG_TABS.REQUEST);
const handleNonStreamRequest = useCallback(
async (payload) => {
setDebugData((prev) => ({
...prev,
request: payload,
timestamp: new Date().toISOString(),
response: null,
}));
setActiveDebugTab(DEBUG_TABS.REQUEST);
try {
const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'New-Api-User': getUserIdFromLocalStorage(),
},
body: JSON.stringify(payload),
});
try {
const response = await fetch(API_ENDPOINTS.CHAT_COMPLETIONS, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'New-Api-User': getUserIdFromLocalStorage(),
},
body: JSON.stringify(payload),
});
if (!response.ok) {
let errorBody = '';
try {
errorBody = await response.text();
} catch (e) {
errorBody = '无法读取错误响应体';
if (!response.ok) {
let errorBody = '';
try {
errorBody = await response.text();
} catch (e) {
errorBody = '无法读取错误响应体';
}
const errorInfo = handleApiError(
new Error(
`HTTP error! status: ${response.status}, body: ${errorBody}`,
),
response,
);
setDebugData((prev) => ({
...prev,
response: JSON.stringify(errorInfo, null, 2),
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
throw new Error(
`HTTP error! status: ${response.status}, body: ${errorBody}`,
);
}
const errorInfo = handleApiError(
new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`),
response
);
const data = await response.json();
setDebugData(prev => ({
setDebugData((prev) => ({
...prev,
response: JSON.stringify(errorInfo, null, 2)
response: JSON.stringify(data, null, 2),
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
}
if (data.choices?.[0]) {
const choice = data.choices[0];
let content = choice.message?.content || '';
let reasoningContent = choice.message?.reasoning_content || '';
const data = await response.json();
const processed = processThinkTags(content, reasoningContent);
setDebugData(prev => ({
...prev,
response: JSON.stringify(data, null, 2)
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
setMessage((prevMessage) => {
const newMessages = [...prevMessage];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
const autoCollapseState = applyAutoCollapseLogic(
lastMessage,
true,
);
if (data.choices?.[0]) {
const choice = data.choices[0];
let content = choice.message?.content || '';
let reasoningContent = choice.message?.reasoning_content || '';
newMessages[newMessages.length - 1] = {
...lastMessage,
content: processed.content,
reasoningContent: processed.reasoningContent,
status: MESSAGE_STATUS.COMPLETE,
...autoCollapseState,
};
}
return newMessages;
});
}
} catch (error) {
console.error('Non-stream request error:', error);
const processed = processThinkTags(content, reasoningContent);
const errorInfo = handleApiError(error);
setDebugData((prev) => ({
...prev,
response: JSON.stringify(errorInfo, null, 2),
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
setMessage(prevMessage => {
setMessage((prevMessage) => {
const newMessages = [...prevMessage];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
@@ -205,168 +271,164 @@ export const useApiRequest = (
newMessages[newMessages.length - 1] = {
...lastMessage,
content: processed.content,
reasoningContent: processed.reasoningContent,
status: MESSAGE_STATUS.COMPLETE,
content: t('请求发生错误: ') + error.message,
status: MESSAGE_STATUS.ERROR,
...autoCollapseState,
};
}
return newMessages;
});
}
} catch (error) {
console.error('Non-stream request error:', error);
const errorInfo = handleApiError(error);
setDebugData(prev => ({
...prev,
response: JSON.stringify(errorInfo, null, 2)
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
setMessage(prevMessage => {
const newMessages = [...prevMessage];
const lastMessage = newMessages[newMessages.length - 1];
if (lastMessage?.status === MESSAGE_STATUS.LOADING) {
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
newMessages[newMessages.length - 1] = {
...lastMessage,
content: t('请求发生错误: ') + error.message,
status: MESSAGE_STATUS.ERROR,
...autoCollapseState,
};
}
return newMessages;
});
}
}, [setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic]);
},
[setDebugData, setActiveDebugTab, setMessage, t, applyAutoCollapseLogic],
);
// SSE请求
const handleSSE = useCallback((payload) => {
setDebugData(prev => ({
...prev,
request: payload,
timestamp: new Date().toISOString(),
response: null
}));
setActiveDebugTab(DEBUG_TABS.REQUEST);
const handleSSE = useCallback(
(payload) => {
setDebugData((prev) => ({
...prev,
request: payload,
timestamp: new Date().toISOString(),
response: null,
}));
setActiveDebugTab(DEBUG_TABS.REQUEST);
const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
headers: {
'Content-Type': 'application/json',
'New-Api-User': getUserIdFromLocalStorage(),
},
method: 'POST',
payload: JSON.stringify(payload),
});
const source = new SSE(API_ENDPOINTS.CHAT_COMPLETIONS, {
headers: {
'Content-Type': 'application/json',
'New-Api-User': getUserIdFromLocalStorage(),
},
method: 'POST',
payload: JSON.stringify(payload),
});
sseSourceRef.current = source;
sseSourceRef.current = source;
let responseData = '';
let hasReceivedFirstResponse = false;
let isStreamComplete = false; // 添加标志位跟踪流是否正常完成
let responseData = '';
let hasReceivedFirstResponse = false;
let isStreamComplete = false; // 添加标志位跟踪流是否正常完成
source.addEventListener('message', (e) => {
if (e.data === '[DONE]') {
isStreamComplete = true; // 标记流正常完成
source.close();
sseSourceRef.current = null;
setDebugData(prev => ({ ...prev, response: responseData }));
completeMessage();
return;
}
source.addEventListener('message', (e) => {
if (e.data === '[DONE]') {
isStreamComplete = true; // 标记流正常完成
source.close();
sseSourceRef.current = null;
setDebugData((prev) => ({ ...prev, response: responseData }));
completeMessage();
return;
}
try {
const payload = JSON.parse(e.data);
responseData += e.data + '\n';
if (!hasReceivedFirstResponse) {
setActiveDebugTab(DEBUG_TABS.RESPONSE);
hasReceivedFirstResponse = true;
}
const delta = payload.choices?.[0]?.delta;
if (delta) {
if (delta.reasoning_content) {
streamMessageUpdate(delta.reasoning_content, 'reasoning');
}
if (delta.content) {
streamMessageUpdate(delta.content, 'content');
}
}
} catch (error) {
console.error('Failed to parse SSE message:', error);
const errorInfo = `解析错误: ${error.message}`;
setDebugData((prev) => ({
...prev,
response: responseData + `\n\nError: ${errorInfo}`,
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
completeMessage(MESSAGE_STATUS.ERROR);
}
});
source.addEventListener('error', (e) => {
// 只有在流没有正常完成且连接状态异常时才处理错误
if (!isStreamComplete && source.readyState !== 2) {
console.error('SSE Error:', e);
const errorMessage = e.data || t('请求发生错误');
const errorInfo = handleApiError(new Error(errorMessage));
errorInfo.readyState = source.readyState;
setDebugData((prev) => ({
...prev,
response:
responseData +
'\n\nSSE Error:\n' +
JSON.stringify(errorInfo, null, 2),
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
streamMessageUpdate(errorMessage, 'content');
completeMessage(MESSAGE_STATUS.ERROR);
sseSourceRef.current = null;
source.close();
}
});
source.addEventListener('readystatechange', (e) => {
// 检查 HTTP 状态错误,但避免与正常关闭重复处理
if (
e.readyState >= 2 &&
source.status !== undefined &&
source.status !== 200 &&
!isStreamComplete
) {
const errorInfo = handleApiError(new Error('HTTP状态错误'));
errorInfo.status = source.status;
errorInfo.readyState = source.readyState;
setDebugData((prev) => ({
...prev,
response:
responseData +
'\n\nHTTP Error:\n' +
JSON.stringify(errorInfo, null, 2),
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
source.close();
streamMessageUpdate(t('连接已断开'), 'content');
completeMessage(MESSAGE_STATUS.ERROR);
}
});
try {
const payload = JSON.parse(e.data);
responseData += e.data + '\n';
if (!hasReceivedFirstResponse) {
setActiveDebugTab(DEBUG_TABS.RESPONSE);
hasReceivedFirstResponse = true;
}
const delta = payload.choices?.[0]?.delta;
if (delta) {
if (delta.reasoning_content) {
streamMessageUpdate(delta.reasoning_content, 'reasoning');
}
if (delta.content) {
streamMessageUpdate(delta.content, 'content');
}
}
source.stream();
} catch (error) {
console.error('Failed to parse SSE message:', error);
const errorInfo = `解析错误: ${error.message}`;
console.error('Failed to start SSE stream:', error);
const errorInfo = handleApiError(error);
setDebugData(prev => ({
setDebugData((prev) => ({
...prev,
response: responseData + `\n\nError: ${errorInfo}`
response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2),
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
streamMessageUpdate(t('建立连接时发生错误'), 'content');
completeMessage(MESSAGE_STATUS.ERROR);
}
});
source.addEventListener('error', (e) => {
// 只有在流没有正常完成且连接状态异常时才处理错误
if (!isStreamComplete && source.readyState !== 2) {
console.error('SSE Error:', e);
const errorMessage = e.data || t('请求发生错误');
const errorInfo = handleApiError(new Error(errorMessage));
errorInfo.readyState = source.readyState;
setDebugData(prev => ({
...prev,
response: responseData + '\n\nSSE Error:\n' + JSON.stringify(errorInfo, null, 2)
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
streamMessageUpdate(errorMessage, 'content');
completeMessage(MESSAGE_STATUS.ERROR);
sseSourceRef.current = null;
source.close();
}
});
source.addEventListener('readystatechange', (e) => {
// 检查 HTTP 状态错误,但避免与正常关闭重复处理
if (e.readyState >= 2 && source.status !== undefined && source.status !== 200 && !isStreamComplete) {
const errorInfo = handleApiError(new Error('HTTP状态错误'));
errorInfo.status = source.status;
errorInfo.readyState = source.readyState;
setDebugData(prev => ({
...prev,
response: responseData + '\n\nHTTP Error:\n' + JSON.stringify(errorInfo, null, 2)
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
source.close();
streamMessageUpdate(t('连接已断开'), 'content');
completeMessage(MESSAGE_STATUS.ERROR);
}
});
try {
source.stream();
} catch (error) {
console.error('Failed to start SSE stream:', error);
const errorInfo = handleApiError(error);
setDebugData(prev => ({
...prev,
response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2)
}));
setActiveDebugTab(DEBUG_TABS.RESPONSE);
streamMessageUpdate(t('建立连接时发生错误'), 'content');
completeMessage(MESSAGE_STATUS.ERROR);
}
}, [setDebugData, setActiveDebugTab, streamMessageUpdate, completeMessage, t, applyAutoCollapseLogic]);
},
[
setDebugData,
setActiveDebugTab,
streamMessageUpdate,
completeMessage,
t,
applyAutoCollapseLogic,
],
);
// 停止生成
const onStopGenerator = useCallback(() => {
@@ -377,16 +439,17 @@ export const useApiRequest = (
}
// 无论是否存在 SSE 连接,都尝试处理最后一条正在生成的消息
setMessage(prevMessage => {
setMessage((prevMessage) => {
if (prevMessage.length === 0) return prevMessage;
const lastMessage = prevMessage[prevMessage.length - 1];
if (lastMessage.status === MESSAGE_STATUS.LOADING ||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE) {
if (
lastMessage.status === MESSAGE_STATUS.LOADING ||
lastMessage.status === MESSAGE_STATUS.INCOMPLETE
) {
const processed = processIncompleteThinkTags(
lastMessage.content || '',
lastMessage.reasoningContent || ''
lastMessage.reasoningContent || '',
);
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true);
@@ -399,7 +462,7 @@ export const useApiRequest = (
reasoningContent: processed.reasoningContent || null,
content: processed.content,
...autoCollapseState,
}
},
];
// 停止生成时也保存,传入更新后的消息列表
@@ -412,13 +475,16 @@ export const useApiRequest = (
}, [setMessage, applyAutoCollapseLogic, saveMessages]);
// 发送请求
const sendRequest = useCallback((payload, isStream) => {
if (isStream) {
handleSSE(payload);
} else {
handleNonStreamRequest(payload);
}
}, [handleSSE, handleNonStreamRequest]);
const sendRequest = useCallback(
(payload, isStream) => {
if (isStream) {
handleSSE(payload);
} else {
handleNonStreamRequest(payload);
}
},
[handleSSE, handleNonStreamRequest],
);
return {
sendRequest,
@@ -426,4 +492,4 @@ export const useApiRequest = (
streamMessageUpdate,
completeMessage,
};
};
};
+13 -6
View File
@@ -27,7 +27,7 @@ export const useDataLoader = (
inputs,
handleInputChange,
setModels,
setGroups
setGroups,
) => {
const { t } = useTranslation();
@@ -37,7 +37,10 @@ export const useDataLoader = (
const { success, message, data } = res.data;
if (success) {
const { modelOptions, selectedModel } = processModelsData(data, inputs.model);
const { modelOptions, selectedModel } = processModelsData(
data,
inputs.model,
);
setModels(modelOptions);
if (selectedModel !== inputs.model) {
@@ -57,11 +60,15 @@ export const useDataLoader = (
const { success, message, data } = res.data;
if (success) {
const userGroup = userState?.user?.group || JSON.parse(localStorage.getItem('user'))?.group;
const userGroup =
userState?.user?.group ||
JSON.parse(localStorage.getItem('user'))?.group;
const groupOptions = processGroupsData(data, userGroup);
setGroups(groupOptions);
const hasCurrentGroup = groupOptions.some(option => option.value === inputs.group);
const hasCurrentGroup = groupOptions.some(
(option) => option.value === inputs.group,
);
if (!hasCurrentGroup) {
handleInputChange('group', groupOptions[0]?.value || '');
}
@@ -83,6 +90,6 @@ export const useDataLoader = (
return {
loadModels,
loadGroups
loadGroups,
};
};
};
+210 -161
View File
@@ -23,43 +23,49 @@ import { useTranslation } from 'react-i18next';
import { getTextContent } from '../../helpers';
import { ERROR_MESSAGES } from '../../constants/playground.constants';
export const useMessageActions = (message, setMessage, onMessageSend, saveMessages) => {
export const useMessageActions = (
message,
setMessage,
onMessageSend,
saveMessages,
) => {
const { t } = useTranslation();
// 复制消息
const handleMessageCopy = useCallback((targetMessage) => {
const textToCopy = getTextContent(targetMessage);
const handleMessageCopy = useCallback(
(targetMessage) => {
const textToCopy = getTextContent(targetMessage);
if (!textToCopy) {
Toast.warning({
content: t(ERROR_MESSAGES.NO_TEXT_CONTENT),
duration: 2,
});
return;
}
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);
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);
}
} else {
fallbackCopy(text);
}
};
};
const fallbackCopy = (text) => {
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.cssText = `
const fallbackCopy = (text) => {
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.cssText = `
position: fixed;
top: -9999px;
left: -9999px;
@@ -67,171 +73,214 @@ export const useMessageActions = (message, setMessage, onMessageSend, saveMessag
pointer-events: none;
z-index: -1;
`;
textArea.setAttribute('readonly', '');
textArea.setAttribute('readonly', '');
document.body.appendChild(textArea);
textArea.select();
textArea.setSelectionRange(0, text.length);
document.body.appendChild(textArea);
textArea.select();
textArea.setSelectionRange(0, text.length);
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
Toast.success({
content: t('消息已复制到剪贴板'),
duration: 2,
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,
});
} 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]);
copyToClipboard(textToCopy);
},
[t],
);
// 重新生成消息
const handleMessageReset = useCallback((targetMessage) => {
setMessage(prevMessages => {
// 使用引用查找索引,防止重复 id 造成误匹配
let messageIndex = prevMessages.findIndex(msg => msg === targetMessage);
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--;
// 回退到 id 匹配(兼容不同引用场景)
if (messageIndex === -1) {
messageIndex = prevMessages.findIndex(
(msg) => msg.id === targetMessage.id,
);
}
if (userMessageIndex >= 0) {
const userMessage = prevMessages[userMessageIndex];
const newMessages = prevMessages.slice(0, userMessageIndex);
const contentToSend = getTextContent(userMessage);
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;
}
}
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);
} else if (
targetMessage.role === 'assistant' ||
targetMessage.role === 'system'
) {
let userMessageIndex = messageIndex - 1;
while (
userMessageIndex >= 0 &&
prevMessages[userMessageIndex].role !== 'user'
) {
userMessageIndex--;
}
if (messageIndex === -1) return prevMessages;
if (userMessageIndex >= 0) {
const userMessage = prevMessages[userMessageIndex];
const newMessages = prevMessages.slice(0, userMessageIndex);
const contentToSend = getTextContent(userMessage);
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
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);
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]);
// 删除消息后保存,传入更新后的消息列表
setTimeout(() => saveMessages(updatedMessages), 0);
return updatedMessages;
});
},
});
},
[setMessage, t, saveMessages],
);
// 切换角色
const handleRoleToggle = useCallback((targetMessage) => {
if (!(targetMessage.role === 'assistant' || targetMessage.role === 'system')) {
return;
}
const handleRoleToggle = useCallback(
(targetMessage) => {
if (
!(targetMessage.role === 'assistant' || targetMessage.role === 'system')
) {
return;
}
const newRole = targetMessage.role === 'assistant' ? 'system' : 'assistant';
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;
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;
});
// 切换角色后保存,传入更新后的消息列表
setTimeout(() => saveMessages(updatedMessages), 0);
return updatedMessages;
});
Toast.success({
content: t(`已切换为${newRole === 'system' ? 'System' : 'Assistant'}角色`),
duration: 2,
});
}, [setMessage, t, saveMessages]);
Toast.success({
content: t(
`已切换为${newRole === 'system' ? 'System' : 'Assistant'}角色`,
),
duration: 2,
});
},
[setMessage, t, saveMessages],
);
return {
handleMessageCopy,
@@ -239,4 +288,4 @@ export const useMessageActions = (message, setMessage, onMessageSend, saveMessag
handleMessageDelete,
handleRoleToggle,
};
};
};
+46 -17
View File
@@ -20,7 +20,11 @@ For commercial licensing, please contact support@quantumnous.com
import { useCallback, useState, useRef } from 'react';
import { Toast, Modal } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../../helpers';
import {
getTextContent,
buildApiPayload,
createLoadingAssistantMessage,
} from '../../helpers';
import { MESSAGE_ROLES } from '../../constants/playground.constants';
export const useMessageEdit = (
@@ -28,7 +32,7 @@ export const useMessageEdit = (
inputs,
parameterEnabled,
sendRequest,
saveMessages
saveMessages,
) => {
const { t } = useTranslation();
const [editingMessageId, setEditingMessageId] = useState(null);
@@ -45,31 +49,36 @@ export const useMessageEdit = (
const handleEditSave = useCallback(() => {
if (!editingMessageId || !editValue.trim()) return;
setMessage(prevMessages => {
let messageIndex = prevMessages.findIndex(msg => msg === editingMessageRef.current);
setMessage((prevMessages) => {
let messageIndex = prevMessages.findIndex(
(msg) => msg === editingMessageRef.current,
);
if (messageIndex === -1) {
messageIndex = prevMessages.findIndex(msg => msg.id === editingMessageId);
messageIndex = prevMessages.findIndex(
(msg) => msg.id === editingMessageId,
);
}
const targetMessage = prevMessages[messageIndex];
let newContent;
if (Array.isArray(targetMessage.content)) {
newContent = targetMessage.content.map(item =>
item.type === 'text' ? { ...item, text: editValue.trim() } : item
newContent = targetMessage.content.map((item) =>
item.type === 'text' ? { ...item, text: editValue.trim() } : item,
);
} else {
newContent = editValue.trim();
}
const updatedMessages = prevMessages.map(msg =>
msg.id === editingMessageId ? { ...msg, content: newContent } : msg
const updatedMessages = prevMessages.map((msg) =>
msg.id === editingMessageId ? { ...msg, content: newContent } : msg,
);
// 处理用户消息编辑后的重新生成
if (targetMessage.role === MESSAGE_ROLES.USER) {
const hasSubsequentAssistantReply = messageIndex < prevMessages.length - 1 &&
const hasSubsequentAssistantReply =
messageIndex < prevMessages.length - 1 &&
prevMessages[messageIndex + 1].role === MESSAGE_ROLES.ASSISTANT;
if (hasSubsequentAssistantReply) {
@@ -79,14 +88,25 @@ export const useMessageEdit = (
okText: t('重新生成'),
cancelText: t('仅保存'),
onOk: () => {
const messagesUntilUser = updatedMessages.slice(0, messageIndex + 1);
const messagesUntilUser = updatedMessages.slice(
0,
messageIndex + 1,
);
setMessage(messagesUntilUser);
// 编辑后保存(重新生成的情况),传入更新后的消息列表
setTimeout(() => saveMessages(messagesUntilUser), 0);
setTimeout(() => {
const payload = buildApiPayload(messagesUntilUser, null, inputs, parameterEnabled);
setMessage(prevMsg => [...prevMsg, createLoadingAssistantMessage()]);
const payload = buildApiPayload(
messagesUntilUser,
null,
inputs,
parameterEnabled,
);
setMessage((prevMsg) => [
...prevMsg,
createLoadingAssistantMessage(),
]);
sendRequest(payload, inputs.stream);
}, 100);
},
@@ -94,7 +114,7 @@ export const useMessageEdit = (
setMessage(updatedMessages);
// 编辑后保存(仅保存的情况),传入更新后的消息列表
setTimeout(() => saveMessages(updatedMessages), 0);
}
},
});
return prevMessages;
}
@@ -109,7 +129,16 @@ export const useMessageEdit = (
editingMessageRef.current = null;
setEditValue('');
Toast.success({ content: t('消息已更新'), duration: 2 });
}, [editingMessageId, editValue, t, inputs, parameterEnabled, sendRequest, setMessage, saveMessages]);
}, [
editingMessageId,
editValue,
t,
inputs,
parameterEnabled,
sendRequest,
setMessage,
saveMessages,
]);
const handleEditCancel = useCallback(() => {
setEditingMessageId(null);
@@ -123,6 +152,6 @@ export const useMessageEdit = (
setEditValue,
handleMessageEdit,
handleEditSave,
handleEditCancel
handleEditCancel,
};
};
};
+48 -21
View File
@@ -18,8 +18,18 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS, MESSAGE_STATUS } from '../../constants/playground.constants';
import { loadConfig, saveConfig, loadMessages, saveMessages } from '../../components/playground/configStorage';
import {
DEFAULT_MESSAGES,
DEFAULT_CONFIG,
DEBUG_TABS,
MESSAGE_STATUS,
} from '../../constants/playground.constants';
import {
loadConfig,
saveConfig,
loadMessages,
saveMessages,
} from '../../components/playground/configStorage';
import { processIncompleteThinkTags } from '../../helpers';
export const usePlaygroundState = () => {
@@ -28,18 +38,20 @@ export const usePlaygroundState = () => {
const [initialMessages] = useState(() => loadMessages() || DEFAULT_MESSAGES);
// 基础配置状态
const [inputs, setInputs] = useState(savedConfig.inputs || DEFAULT_CONFIG.inputs);
const [inputs, setInputs] = useState(
savedConfig.inputs || DEFAULT_CONFIG.inputs,
);
const [parameterEnabled, setParameterEnabled] = useState(
savedConfig.parameterEnabled || DEFAULT_CONFIG.parameterEnabled
savedConfig.parameterEnabled || DEFAULT_CONFIG.parameterEnabled,
);
const [showDebugPanel, setShowDebugPanel] = useState(
savedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel
savedConfig.showDebugPanel || DEFAULT_CONFIG.showDebugPanel,
);
const [customRequestMode, setCustomRequestMode] = useState(
savedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode
savedConfig.customRequestMode || DEFAULT_CONFIG.customRequestMode,
);
const [customRequestBody, setCustomRequestBody] = useState(
savedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody
savedConfig.customRequestBody || DEFAULT_CONFIG.customRequestBody,
);
// UI状态
@@ -57,7 +69,7 @@ export const usePlaygroundState = () => {
response: null,
timestamp: null,
previewRequest: null,
previewTimestamp: null
previewTimestamp: null,
});
const [activeDebugTab, setActiveDebugTab] = useState(DEBUG_TABS.PREVIEW);
const [previewPayload, setPreviewPayload] = useState(null);
@@ -74,21 +86,24 @@ export const usePlaygroundState = () => {
// 配置更新函数
const handleInputChange = useCallback((name, value) => {
setInputs(prev => ({ ...prev, [name]: value }));
setInputs((prev) => ({ ...prev, [name]: value }));
}, []);
const handleParameterToggle = useCallback((paramName) => {
setParameterEnabled(prev => ({
setParameterEnabled((prev) => ({
...prev,
[paramName]: !prev[paramName]
[paramName]: !prev[paramName],
}));
}, []);
// 消息保存函数 - 改为立即保存,可以接受参数
const saveMessagesImmediately = useCallback((messagesToSave) => {
// 如果提供了参数,使用参数;否则使用当前状态
saveMessages(messagesToSave || message);
}, [message]);
const saveMessagesImmediately = useCallback(
(messagesToSave) => {
// 如果提供了参数,使用参数;否则使用当前状态
saveMessages(messagesToSave || message);
},
[message],
);
// 配置保存
const debouncedSaveConfig = useCallback(() => {
@@ -106,15 +121,24 @@ export const usePlaygroundState = () => {
};
saveConfig(configToSave);
}, 1000);
}, [inputs, parameterEnabled, showDebugPanel, customRequestMode, customRequestBody]);
}, [
inputs,
parameterEnabled,
showDebugPanel,
customRequestMode,
customRequestBody,
]);
// 配置导入/重置
const handleConfigImport = useCallback((importedConfig) => {
if (importedConfig.inputs) {
setInputs(prev => ({ ...prev, ...importedConfig.inputs }));
setInputs((prev) => ({ ...prev, ...importedConfig.inputs }));
}
if (importedConfig.parameterEnabled) {
setParameterEnabled(prev => ({ ...prev, ...importedConfig.parameterEnabled }));
setParameterEnabled((prev) => ({
...prev,
...importedConfig.parameterEnabled,
}));
}
if (typeof importedConfig.showDebugPanel === 'boolean') {
setShowDebugPanel(importedConfig.showDebugPanel);
@@ -163,10 +187,13 @@ export const usePlaygroundState = () => {
if (!Array.isArray(message) || message.length === 0) return;
const lastMsg = message[message.length - 1];
if (lastMsg.status === MESSAGE_STATUS.LOADING || lastMsg.status === MESSAGE_STATUS.INCOMPLETE) {
if (
lastMsg.status === MESSAGE_STATUS.LOADING ||
lastMsg.status === MESSAGE_STATUS.INCOMPLETE
) {
const processed = processIncompleteThinkTags(
lastMsg.content || '',
lastMsg.reasoningContent || ''
lastMsg.reasoningContent || '',
);
const fixedLastMsg = {
@@ -241,4 +268,4 @@ export const usePlaygroundState = () => {
handleConfigImport,
handleConfigReset,
};
};
};
@@ -27,7 +27,7 @@ export const useSyncMessageAndCustomBody = (
inputs,
setCustomRequestBody,
setMessage,
debouncedSaveConfig
debouncedSaveConfig,
) => {
const isUpdatingFromMessage = useRef(false);
const isUpdatingFromCustomBody = useRef(false);
@@ -35,11 +35,13 @@ export const useSyncMessageAndCustomBody = (
const lastCustomBodyHash = useRef('');
const getMessageHash = useCallback((messages) => {
return JSON.stringify(messages.map(msg => ({
id: msg.id,
role: msg.role,
content: msg.content
})));
return JSON.stringify(
messages.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
})),
);
}, []);
const getCustomBodyHash = useCallback((customBody) => {
@@ -68,13 +70,13 @@ export const useSyncMessageAndCustomBody = (
model: inputs.model || 'gpt-4o',
messages: [],
temperature: inputs.temperature || 0.7,
stream: inputs.stream !== false
stream: inputs.stream !== false,
};
}
customPayload.messages = message.map(msg => ({
customPayload.messages = message.map((msg) => ({
role: msg.role,
content: msg.content
content: msg.content,
}));
const newCustomBody = JSON.stringify(customPayload, null, 2);
@@ -88,7 +90,18 @@ export const useSyncMessageAndCustomBody = (
} finally {
isUpdatingFromMessage.current = false;
}
}, [customRequestMode, customRequestBody, message, inputs.model, inputs.temperature, inputs.stream, getMessageHash, getCustomBodyHash, setCustomRequestBody, debouncedSaveConfig]);
}, [
customRequestMode,
customRequestBody,
message,
inputs.model,
inputs.temperature,
inputs.stream,
getMessageHash,
getCustomBodyHash,
setCustomRequestBody,
debouncedSaveConfig,
]);
const syncCustomBodyToMessage = useCallback(() => {
if (!customRequestMode || isUpdatingFromMessage.current) return;
@@ -108,8 +121,8 @@ export const useSyncMessageAndCustomBody = (
createAt: Date.now(),
...(msg.role === MESSAGE_ROLES.ASSISTANT && {
reasoningContent: msg.reasoningContent || '',
isReasoningExpanded: false
})
isReasoningExpanded: false,
}),
}));
setMessage(newMessages);
@@ -121,10 +134,16 @@ export const useSyncMessageAndCustomBody = (
} finally {
isUpdatingFromCustomBody.current = false;
}
}, [customRequestMode, customRequestBody, getCustomBodyHash, getMessageHash, setMessage]);
}, [
customRequestMode,
customRequestBody,
getCustomBodyHash,
getMessageHash,
setMessage,
]);
return {
syncMessageToCustomBody,
syncCustomBodyToMessage
syncCustomBodyToMessage,
};
};
};