|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { useCallback } from 'react'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import { SSE } from 'sse.js'; |
|
|
import { |
|
|
API_ENDPOINTS, |
|
|
MESSAGE_STATUS, |
|
|
DEBUG_TABS, |
|
|
} from '../../constants/playground.constants'; |
|
|
import { |
|
|
getUserIdFromLocalStorage, |
|
|
handleApiError, |
|
|
processThinkTags, |
|
|
processIncompleteThinkTags, |
|
|
} from '../../helpers'; |
|
|
|
|
|
export const useApiRequest = ( |
|
|
setMessage, |
|
|
setDebugData, |
|
|
setActiveDebugTab, |
|
|
sseSourceRef, |
|
|
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 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 |
|
|
) { |
|
|
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; |
|
|
|
|
|
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; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
}, |
|
|
[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, |
|
|
}, |
|
|
]; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
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 = '无法读取错误响应体'; |
|
|
} |
|
|
|
|
|
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 data = await response.json(); |
|
|
|
|
|
setDebugData((prev) => ({ |
|
|
...prev, |
|
|
response: JSON.stringify(data, null, 2), |
|
|
})); |
|
|
setActiveDebugTab(DEBUG_TABS.RESPONSE); |
|
|
|
|
|
if (data.choices?.[0]) { |
|
|
const choice = data.choices[0]; |
|
|
let content = choice.message?.content || ''; |
|
|
let reasoningContent = choice.message?.reasoning_content || choice.message?.reasoning || ''; |
|
|
|
|
|
const processed = processThinkTags(content, reasoningContent); |
|
|
|
|
|
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: processed.content, |
|
|
reasoningContent: processed.reasoningContent, |
|
|
status: MESSAGE_STATUS.COMPLETE, |
|
|
...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], |
|
|
); |
|
|
|
|
|
|
|
|
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), |
|
|
}); |
|
|
|
|
|
sseSourceRef.current = source; |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
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.reasoning) { |
|
|
streamMessageUpdate(delta.reasoning, '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) => { |
|
|
|
|
|
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, |
|
|
], |
|
|
); |
|
|
|
|
|
|
|
|
const onStopGenerator = useCallback(() => { |
|
|
|
|
|
if (sseSourceRef.current) { |
|
|
sseSourceRef.current.close(); |
|
|
sseSourceRef.current = null; |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
) { |
|
|
const processed = processIncompleteThinkTags( |
|
|
lastMessage.content || '', |
|
|
lastMessage.reasoningContent || '', |
|
|
); |
|
|
|
|
|
const autoCollapseState = applyAutoCollapseLogic(lastMessage, true); |
|
|
|
|
|
const updatedMessages = [ |
|
|
...prevMessage.slice(0, -1), |
|
|
{ |
|
|
...lastMessage, |
|
|
status: MESSAGE_STATUS.COMPLETE, |
|
|
reasoningContent: processed.reasoningContent || null, |
|
|
content: processed.content, |
|
|
...autoCollapseState, |
|
|
}, |
|
|
]; |
|
|
|
|
|
|
|
|
setTimeout(() => saveMessages(updatedMessages), 0); |
|
|
|
|
|
return updatedMessages; |
|
|
} |
|
|
return prevMessage; |
|
|
}); |
|
|
}, [setMessage, applyAutoCollapseLogic, saveMessages]); |
|
|
|
|
|
|
|
|
const sendRequest = useCallback( |
|
|
(payload, isStream) => { |
|
|
if (isStream) { |
|
|
handleSSE(payload); |
|
|
} else { |
|
|
handleNonStreamRequest(payload); |
|
|
} |
|
|
}, |
|
|
[handleSSE, handleNonStreamRequest], |
|
|
); |
|
|
|
|
|
return { |
|
|
sendRequest, |
|
|
onStopGenerator, |
|
|
streamMessageUpdate, |
|
|
completeMessage, |
|
|
}; |
|
|
}; |
|
|
|