| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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, |
| sseMessages: null, |
| isStreaming: false, |
| })); |
| 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, |
| sseMessages: [], |
| isStreaming: true, |
| })); |
| 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, |
| sseMessages: [...(prev.sseMessages || []), '[DONE]'], |
| isStreaming: false, |
| })); |
| completeMessage(); |
| return; |
| } |
|
|
| try { |
| const payload = JSON.parse(e.data); |
| responseData += e.data + '\n'; |
|
|
| if (!hasReceivedFirstResponse) { |
| setActiveDebugTab(DEBUG_TABS.RESPONSE); |
| hasReceivedFirstResponse = true; |
| } |
|
|
| |
| setDebugData((prev) => ({ |
| ...prev, |
| sseMessages: [...(prev.sseMessages || []), e.data], |
| })); |
|
|
| 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}`, |
| sseMessages: [...(prev.sseMessages || []), e.data], |
| isStreaming: false, |
| })); |
| 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, |
| }; |
| }; |
|
|