🎨 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:
@@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user