From 2dcd6fa2b9fe6df3c04367c48414c6d656f6df94 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Thu, 29 May 2025 04:21:33 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A0=20fix(Playground):=20Enhance=20=20tag=20processing=20and=20remove=20redundant=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit significantly refactors the handling of tags within the Playground component to improve robustness, prevent Markdown rendering errors, and ensure accurate separation of reasoning and displayable content. Key changes include: - Modified `handleNonStreamRequest`, `onStopGenerator`, and `renderCustomChatContent` for more resilient tag parsing. - Implemented logic to correctly extract content from fully paired `...` tags. - Added handling for unclosed `` tags, particularly relevant during streaming responses, to capture ongoing thought processes. - Ensured `reasoningContent` is accurately populated from all sources (API, paired tags, and unclosed streaming tags). - Thoroughly sanitized the final `currentDisplayableFinalContent` to remove all residual `` and `` string instances, preventing issues with the Markdown renderer. - Corrected the usage of `thinkTagRegex.lastIndex = 0;` to ensure proper regex state resetting before each use in loops. Additionally, numerous explanatory comments detailing the "how" of the code (rather than the "why") have been removed to improve code readability and reduce noise. --- web/src/pages/Playground/Playground.js | 239 +++++++++++-------------- 1 file changed, 109 insertions(+), 130 deletions(-) diff --git a/web/src/pages/Playground/Playground.js b/web/src/pages/Playground/Playground.js index 0ee5fab7..ec10931c 100644 --- a/web/src/pages/Playground/Playground.js +++ b/web/src/pages/Playground/Playground.js @@ -190,7 +190,6 @@ const Playground = () => { value: model, })); setModels(localModelOptions); - // if default model is not in the list, set the first one as default const hasDefault = localModelOptions.some(option => option.value === defaultModel); if (!hasDefault && localModelOptions.length > 0) { setInputs((inputs) => ({ ...inputs, model: localModelOptions[0].value })); @@ -258,7 +257,6 @@ const Playground = () => { }; let handleNonStreamRequest = async (payload) => { - // 记录请求数据并自动切换到请求体标签 setDebugData(prev => ({ ...prev, request: payload, @@ -278,7 +276,6 @@ const Playground = () => { }); if (!response.ok) { - // 尝试读取错误响应体 let errorBody = ''; try { errorBody = await response.text(); @@ -294,7 +291,6 @@ const Playground = () => { timestamp: new Date().toISOString() }; - // 记录HTTP错误到调试数据 setDebugData(prev => ({ ...prev, response: JSON.stringify(errorInfo, null, 2) @@ -306,20 +302,17 @@ const Playground = () => { const data = await response.json(); - // 记录响应数据并自动切换到响应标签 setDebugData(prev => ({ ...prev, response: JSON.stringify(data, null, 2) })); setActiveDebugTab('response'); - // 处理响应数据 if (data.choices && data.choices[0]) { const choice = data.choices[0]; let content = choice.message?.content || ''; let reasoningContent = choice.message?.reasoning_content || ''; - // 处理 标签格式的思维链 if (content.includes('')) { const thinkTagRegex = /([\s\S]*?)<\/think>/g; let thoughts = []; @@ -327,6 +320,7 @@ const Playground = () => { let lastIndex = 0; let match; + thinkTagRegex.lastIndex = 0; while ((match = thinkTagRegex.exec(content)) !== null) { replyParts.push(content.substring(lastIndex, match.index)); thoughts.push(match[1]); @@ -334,13 +328,18 @@ const Playground = () => { } replyParts.push(content.substring(lastIndex)); - content = replyParts.join('').trim(); + content = replyParts.join(''); if (thoughts.length > 0) { - reasoningContent = thoughts.join('\n\n---\n\n'); + if (reasoningContent) { + reasoningContent += '\n\n---\n\n' + thoughts.join('\n\n---\n\n'); + } else { + reasoningContent = thoughts.join('\n\n---\n\n'); + } } } - // 更新消息 + content = content.replace(/<\/?think>/g, '').trim(); + setMessage((prevMessage) => { const newMessages = [...prevMessage]; const lastMessage = newMessages[newMessages.length - 1]; @@ -359,7 +358,6 @@ const Playground = () => { } catch (error) { console.error('Non-stream request error:', error); - // 构建详细的错误信息 const errorInfo = { error: '非流式请求错误', message: error.message, @@ -367,21 +365,18 @@ const Playground = () => { stack: error.stack }; - // 如果是 fetch 错误,尝试获取更多信息 if (error.message.includes('HTTP error')) { errorInfo.details = '服务器返回了错误状态码'; } else if (error.message.includes('Failed to fetch')) { errorInfo.details = '网络连接失败或服务器无响应'; } - // 记录详细的错误响应并切换到响应标签 setDebugData(prev => ({ ...prev, response: JSON.stringify(errorInfo, null, 2) })); setActiveDebugTab('response'); - // 更新消息为错误状态 setMessage((prevMessage) => { const newMessages = [...prevMessage]; const lastMessage = newMessages[newMessages.length - 1]; @@ -399,7 +394,6 @@ const Playground = () => { }; let handleSSE = (payload) => { - // 记录请求数据并自动切换到请求体标签 setDebugData(prev => ({ ...prev, request: payload, @@ -417,7 +411,6 @@ const Playground = () => { payload: JSON.stringify(payload), }); - // 保存 source 引用以便后续停止生成 sseSourceRef.current = source; let responseData = ''; @@ -427,7 +420,6 @@ const Playground = () => { if (e.data === '[DONE]') { source.close(); sseSourceRef.current = null; - // 记录完整响应 setDebugData(prev => ({ ...prev, response: responseData @@ -440,7 +432,6 @@ const Playground = () => { let payload = JSON.parse(e.data); responseData += e.data + '\n'; - // 收到第一个响应时自动切换到响应标签 if (!hasReceivedFirstResponse) { setActiveDebugTab('response'); hasReceivedFirstResponse = true; @@ -459,7 +450,6 @@ const Playground = () => { console.error('Failed to parse SSE message:', error); const errorInfo = `解析错误: ${error.message}`; - // 记录错误到调试数据 setDebugData(prev => ({ ...prev, response: responseData + `\n\nError: ${errorInfo}` @@ -475,7 +465,6 @@ const Playground = () => { console.error('SSE Error:', e); const errorMessage = e.data || t('请求发生错误'); - // 记录错误信息到调试数据 const errorInfo = { error: 'SSE连接错误', message: errorMessage, @@ -506,7 +495,6 @@ const Playground = () => { timestamp: new Date().toISOString() }; - // 记录状态错误到调试数据 setDebugData(prev => ({ ...prev, response: responseData + '\n\nHTTP Error:\n' + JSON.stringify(errorInfo, null, 2) @@ -530,7 +518,6 @@ const Playground = () => { timestamp: new Date().toISOString() }; - // 记录启动错误到调试数据 setDebugData(prev => ({ ...prev, response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2) @@ -574,7 +561,6 @@ const Playground = () => { group: inputs.group, }; - // 只添加启用的参数 if (parameterEnabled.max_tokens && inputs.max_tokens > 0) { payload.max_tokens = parseInt(inputs.max_tokens); } @@ -635,7 +621,6 @@ const Playground = () => { const lastMessage = prevMessage[prevMessage.length - 1]; let newMessage = { ...lastMessage }; - // 如果消息已经是错误状态,保持错误状态 if (lastMessage.status === 'error') { return prevMessage; } @@ -648,15 +633,12 @@ const Playground = () => { status: 'incomplete', }; } else if (type === 'content') { - // 当开始接收 content 时,说明思考部分已经完成,应该折叠思考面板 const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent && lastMessage.isReasoningExpanded; const newContent = (lastMessage.content || '') + textChunk; - // 检测 标签的完成 let shouldCollapseFromThinkTag = false; if (lastMessage.isReasoningExpanded && newContent.includes('')) { - // 检查是否有完整的 ... 对 const thinkMatches = newContent.match(//g); const thinkCloseMatches = newContent.match(/<\/think>/g); if (thinkMatches && thinkCloseMatches && thinkCloseMatches.length >= thinkMatches.length) { @@ -676,11 +658,9 @@ const Playground = () => { }); }, [setMessage]); - // 处理消息复制 const handleMessageCopy = useCallback((message) => { if (!message.content) return; - // 现代浏览器的 Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(message.content).then(() => { Toast.success({ @@ -689,28 +669,22 @@ const Playground = () => { }); }).catch(err => { console.error('Clipboard API 复制失败:', err); - // 如果 Clipboard API 失败,尝试回退方案 fallbackCopyToClipboard(message.content); }); } else { - // 回退方案:使用传统的 document.execCommand fallbackCopyToClipboard(message.content); } }, [t]); - // 回退复制方案 const fallbackCopyToClipboard = useCallback((text) => { try { - // 检查是否支持 execCommand if (!document.execCommand) { throw new Error('execCommand not supported'); } - // 创建一个临时的 textarea 元素 const textArea = document.createElement('textarea'); textArea.value = text; - // 设置样式使其不可见但可选中 textArea.style.position = 'fixed'; textArea.style.top = '-9999px'; textArea.style.left = '-9999px'; @@ -721,7 +695,6 @@ const Playground = () => { document.body.appendChild(textArea); - // 选中文本 if (textArea.select) { textArea.select(); } @@ -729,7 +702,6 @@ const Playground = () => { textArea.setSelectionRange(0, text.length); } - // 使用 execCommand 复制 const successful = document.execCommand('copy'); document.body.removeChild(textArea); @@ -744,7 +716,6 @@ const Playground = () => { } catch (err) { console.error('回退复制方案也失败:', err); - // 提供更详细的错误信息 let errorMessage = t('复制失败,请手动选择文本复制'); if (window.location.protocol === 'http:' && window.location.hostname !== 'localhost') { @@ -760,33 +731,25 @@ const Playground = () => { } }, [t]); - // 处理消息重试 const handleMessageReset = useCallback((targetMessage) => { setMessage(prevMessages => { - // 找到要重试的消息的索引 const messageIndex = prevMessages.findIndex(msg => msg.id === targetMessage.id); if (messageIndex === -1) return prevMessages; - // 如果是用户消息,重新发送 if (targetMessage.role === 'user') { - // 删除该消息及其后面的所有消息 const newMessages = prevMessages.slice(0, messageIndex); - // 重新发送消息 setTimeout(() => { onMessageSend(targetMessage.content); }, 100); return newMessages; } else if (targetMessage.role === 'assistant') { - // 如果是助手消息,找到它前面最近的用户消息并重试 let userMessageIndex = messageIndex - 1; while (userMessageIndex >= 0 && prevMessages[userMessageIndex].role !== 'user') { userMessageIndex--; } if (userMessageIndex >= 0) { const userMessage = prevMessages[userMessageIndex]; - // 删除用户消息之后的所有消息 const newMessages = prevMessages.slice(0, userMessageIndex); - // 重新发送用户消息 setTimeout(() => { onMessageSend(userMessage.content); }, 100); @@ -797,7 +760,6 @@ const Playground = () => { }); }, [onMessageSend]); - // 处理消息删除 const handleMessageDelete = useCallback((targetMessage) => { Modal.confirm({ title: t('确认删除'), @@ -809,15 +771,12 @@ const Playground = () => { }, onOk: () => { setMessage(prevMessages => { - // 找到要删除的消息索引 const messageIndex = prevMessages.findIndex(msg => msg.id === targetMessage.id); if (messageIndex === -1) return prevMessages; - // 如果是用户消息,同时删除后面紧跟的助手回复 if (targetMessage.role === 'user' && messageIndex < prevMessages.length - 1) { const nextMessage = prevMessages[messageIndex + 1]; if (nextMessage.role === 'assistant') { - // 删除用户消息和助手回复 Toast.success({ content: t('已删除消息及其回复'), duration: 2, @@ -826,7 +785,6 @@ const Playground = () => { } } - // 否则只删除当前消息 Toast.success({ content: t('消息已删除'), duration: 2, @@ -844,37 +802,61 @@ const Playground = () => { setMessage((prevMessage) => { const lastMessage = prevMessage[prevMessage.length - 1]; if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') { - let content = lastMessage.content || ''; - let reasoningContent = lastMessage.reasoningContent || ''; + let currentContent = lastMessage.content || ''; + let currentReasoningContent = lastMessage.reasoningContent || ''; - // 处理 标签格式的思维链 - if (content.includes('')) { - const thinkTagRegex = /([\s\S]*?)(?:<\/think>|$)/g; - let thoughts = []; - let replyParts = []; - let lastIndex = 0; - let match; + let finalDisplayableContent = currentContent; + let extractedThinking = currentReasoningContent; - while ((match = thinkTagRegex.exec(content)) !== null) { - replyParts.push(content.substring(lastIndex, match.index)); - thoughts.push(match[1]); - lastIndex = match.index + match[0].length; - } - replyParts.push(content.substring(lastIndex)); + const thinkTagRegex = /([\s\S]*?)<\/think>/g; + let match; + let thoughtsFromPairedTags = []; + let contentParts = []; + let lastIndex = 0; - // 更新内容和思维链 - content = replyParts.join('').trim(); - if (thoughts.length > 0) { - reasoningContent = thoughts.join('\n\n---\n\n'); + thinkTagRegex.lastIndex = 0; + let tempContentForPairExtraction = finalDisplayableContent; + while ((match = thinkTagRegex.exec(tempContentForPairExtraction)) !== null) { + contentParts.push(tempContentForPairExtraction.substring(lastIndex, match.index)); + thoughtsFromPairedTags.push(match[1]); + lastIndex = match.index + match[0].length; + } + contentParts.push(tempContentForPairExtraction.substring(lastIndex)); + + if (thoughtsFromPairedTags.length > 0) { + const pairedThoughtsStr = thoughtsFromPairedTags.join('\n\n---\n\n'); + if (extractedThinking) { + extractedThinking += '\n\n---\n\n' + pairedThoughtsStr; + } else { + extractedThinking = pairedThoughtsStr; } } + finalDisplayableContent = contentParts.join(''); + + const lastOpenThinkIndex = finalDisplayableContent.lastIndexOf(''); + if (lastOpenThinkIndex !== -1) { + const fragmentAfterLastOpen = finalDisplayableContent.substring(lastOpenThinkIndex); + if (!fragmentAfterLastOpen.includes('')) { + const unclosedThought = fragmentAfterLastOpen.substring(''.length); + if (unclosedThought.trim()) { + if (extractedThinking) { + extractedThinking += '\n\n---\n\n' + unclosedThought; + } else { + extractedThinking = unclosedThought; + } + } + finalDisplayableContent = finalDisplayableContent.substring(0, lastOpenThinkIndex); + } + } + + finalDisplayableContent = finalDisplayableContent.replace(/<\/?think>/g, '').trim(); return [...prevMessage.slice(0, -1), { ...lastMessage, status: 'complete', - reasoningContent: reasoningContent, - content: content, - isReasoningExpanded: false // 停止时折叠思维链面板 + reasoningContent: extractedThinking || null, + content: finalDisplayableContent, + isReasoningExpanded: false }]; } return prevMessage; @@ -953,16 +935,13 @@ const Playground = () => { return ; }, []); - // 自定义操作按钮渲染 const renderChatBoxAction = useCallback((props) => { const { message } = props; - // 对于正在加载或未完成的消息,只显示部分按钮 const isLoading = message.status === 'loading' || message.status === 'incomplete'; return (
- {/* 重试按钮 - 只在消息完成或出错时显示 */} {!isLoading && (