diff --git a/web/src/index.css b/web/src/index.css index 7600f7cf..4badd4fe 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -84,17 +84,6 @@ body { display: revert; } -.semi-chat { - padding-top: 0 !important; - padding-bottom: 0 !important; - height: 100%; -} - -.semi-chat-chatBox-content { - min-width: auto; - word-break: break-word; -} - .tableHiddle { display: none !important; } @@ -144,4 +133,114 @@ code { .semi-tabs-content { padding: 0 !important; +} + +/* 聊天 */ +.semi-chat { + padding-top: 0 !important; + padding-bottom: 0 !important; + height: 100%; + max-width: 100% !important; + width: 100% !important; + overflow: hidden !important; +} + +.semi-chat-chatBox { + max-width: 100% !important; + overflow: hidden !important; +} + +.semi-chat-chatBox-wrap { + max-width: 100% !important; + overflow: hidden !important; +} + +.semi-chat-chatBox-content { + min-width: auto; + word-break: break-word; + max-width: 100% !important; + overflow-wrap: break-word !important; +} + +.semi-chat-content { + max-width: 100% !important; + overflow: hidden !important; +} + +.semi-chat-list { + max-width: 100% !important; + overflow-x: hidden !important; +} + +.semi-chat-container { + overflow-x: hidden !important; +} + +.semi-chat-container::-webkit-scrollbar { + width: 4px; +} + +.semi-chat-container::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.1); + border-radius: 2px; +} + +.semi-chat-container::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.15); +} + +.semi-chat-container::-webkit-scrollbar-track { + background: transparent; +} + +/* 隐藏模型设置区域的滚动条 */ +.model-settings-scroll::-webkit-scrollbar { + display: none; +} + +.model-settings-scroll { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* 调试面板代码样式 */ +.debug-code { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 11px; + line-height: 1.4; +} + +/* 调试面板标签样式 */ +.semi-tabs-content { + height: calc(100% - 40px) !important; + flex: 1 !important; +} + +.semi-tabs-content .semi-tabs-pane { + height: 100% !important; + overflow: hidden !important; +} + +.semi-tabs-content .semi-tabs-pane>div { + height: 100% !important; +} + +/* 调试面板特定样式 */ +.debug-panel .semi-tabs { + height: 100% !important; + display: flex !important; + flex-direction: column !important; +} + +.debug-panel .semi-tabs-bar { + flex-shrink: 0 !important; +} + +.debug-panel .semi-tabs-content { + flex: 1 !important; + overflow: hidden !important; +} + +.semi-chat-chatBox-action { + column-gap: 0 !important; } \ No newline at end of file diff --git a/web/src/pages/Playground/Playground.js b/web/src/pages/Playground/Playground.js index 3021e106..0ee5fab7 100644 --- a/web/src/pages/Playground/Playground.js +++ b/web/src/pages/Playground/Playground.js @@ -19,12 +19,47 @@ import { Button, MarkdownRender, Tag, + Tabs, + TabPane, + Toast, + Tooltip, + Modal, } from '@douyinfe/semi-ui'; import { SSE } from 'sse'; -import { IconSetting, IconSpin, IconChevronRight, IconChevronUp } from '@douyinfe/semi-icons'; +import { + Settings, + Sparkles, + ChevronRight, + ChevronUp, + Brain, + Zap, + MessageSquare, + SlidersHorizontal, + Hash, + Thermometer, + Type, + Users, + Loader2, + Target, + Repeat, + Ban, + Shuffle, + ToggleLeft, + Code, + Eye, + EyeOff, + FileText, + Clock, + Check, + X, + Copy, + RefreshCw, + Trash2, +} from 'lucide-react'; import { StyleContext } from '../../context/Style/index.js'; import { useTranslation } from 'react-i18next'; import { renderGroupOption, truncateText, stringToColor } from '../../helpers/render.js'; +import { IconSend } from '@douyinfe/semi-icons'; let id = 4; function getId() { @@ -89,6 +124,19 @@ const Playground = () => { group: '', max_tokens: 0, temperature: 0, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + seed: null, + stream: true, + }); + const [parameterEnabled, setParameterEnabled] = useState({ + max_tokens: true, + temperature: true, + top_p: false, + frequency_penalty: false, + presence_penalty: false, + seed: false, }); const [searchParams, setSearchParams] = useSearchParams(); const [status, setStatus] = useState({}); @@ -99,6 +147,13 @@ const Playground = () => { const [models, setModels] = useState([]); const [groups, setGroups] = useState([]); const [showSettings, setShowSettings] = useState(true); + const [showDebugPanel, setShowDebugPanel] = useState(true); + const [debugData, setDebugData] = useState({ + request: null, + response: null, + timestamp: null + }); + const [activeDebugTab, setActiveDebugTab] = useState('request'); const [styleState, styleDispatch] = useContext(StyleContext); const sseSourceRef = useRef(null); @@ -106,6 +161,13 @@ const Playground = () => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; + const handleParameterToggle = (paramName) => { + setParameterEnabled(prev => ({ + ...prev, + [paramName]: !prev[paramName] + })); + }; + useEffect(() => { if (searchParams.get('expired')) { showError(t('未登录或登录已过期,请重新登录!')); @@ -184,12 +246,6 @@ const Playground = () => { } }; - const commonOuterStyle = { - border: '1px solid var(--semi-color-border)', - borderRadius: '16px', - margin: '0px 8px', - }; - const getSystemMessage = () => { if (systemPrompt !== '') { return { @@ -201,7 +257,157 @@ const Playground = () => { } }; + let handleNonStreamRequest = async (payload) => { + // 记录请求数据并自动切换到请求体标签 + setDebugData(prev => ({ + ...prev, + request: payload, + timestamp: new Date().toISOString(), + response: null + })); + setActiveDebugTab('request'); + + try { + const response = await fetch('/pg/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 = { + error: 'HTTP错误', + status: response.status, + statusText: response.statusText, + body: errorBody, + timestamp: new Date().toISOString() + }; + + // 记录HTTP错误到调试数据 + setDebugData(prev => ({ + ...prev, + response: JSON.stringify(errorInfo, null, 2) + })); + setActiveDebugTab('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('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 = []; + let replyParts = []; + let lastIndex = 0; + let match; + + 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)); + + content = replyParts.join('').trim(); + if (thoughts.length > 0) { + reasoningContent = thoughts.join('\n\n---\n\n'); + } + } + + // 更新消息 + setMessage((prevMessage) => { + const newMessages = [...prevMessage]; + const lastMessage = newMessages[newMessages.length - 1]; + if (lastMessage && lastMessage.status === 'loading') { + newMessages[newMessages.length - 1] = { + ...lastMessage, + content: content, + reasoningContent: reasoningContent, + status: 'complete', + isReasoningExpanded: false + }; + } + return newMessages; + }); + } + } catch (error) { + console.error('Non-stream request error:', error); + + // 构建详细的错误信息 + const errorInfo = { + error: '非流式请求错误', + message: error.message, + timestamp: new Date().toISOString(), + 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]; + if (lastMessage && lastMessage.status === 'loading') { + newMessages[newMessages.length - 1] = { + ...lastMessage, + content: t('请求发生错误: ') + error.message, + status: 'error', + isReasoningExpanded: false + }; + } + return newMessages; + }); + } + }; + let handleSSE = (payload) => { + // 记录请求数据并自动切换到请求体标签 + setDebugData(prev => ({ + ...prev, + request: payload, + timestamp: new Date().toISOString(), + response: null + })); + setActiveDebugTab('request'); + let source = new SSE('/pg/chat/completions', { headers: { 'Content-Type': 'application/json', @@ -214,16 +420,32 @@ const Playground = () => { // 保存 source 引用以便后续停止生成 sseSourceRef.current = source; + let responseData = ''; + let hasReceivedFirstResponse = false; + source.addEventListener('message', (e) => { if (e.data === '[DONE]') { source.close(); sseSourceRef.current = null; + // 记录完整响应 + setDebugData(prev => ({ + ...prev, + response: responseData + })); completeMessage(); return; } try { let payload = JSON.parse(e.data); + responseData += e.data + '\n'; + + // 收到第一个响应时自动切换到响应标签 + if (!hasReceivedFirstResponse) { + setActiveDebugTab('response'); + hasReceivedFirstResponse = true; + } + const delta = payload.choices?.[0]?.delta; if (delta) { if (delta.reasoning_content) { @@ -235,6 +457,15 @@ const Playground = () => { } } catch (error) { console.error('Failed to parse SSE message:', error); + const errorInfo = `解析错误: ${error.message}`; + + // 记录错误到调试数据 + setDebugData(prev => ({ + ...prev, + response: responseData + `\n\nError: ${errorInfo}` + })); + setActiveDebugTab('response'); + streamMessageUpdate(t('解析响应数据时发生错误'), 'content'); completeMessage('error'); } @@ -243,6 +474,22 @@ const Playground = () => { source.addEventListener('error', (e) => { console.error('SSE Error:', e); const errorMessage = e.data || t('请求发生错误'); + + // 记录错误信息到调试数据 + const errorInfo = { + error: 'SSE连接错误', + message: errorMessage, + status: source.status, + readyState: source.readyState, + timestamp: new Date().toISOString() + }; + + setDebugData(prev => ({ + ...prev, + response: responseData + '\n\nSSE Error:\n' + JSON.stringify(errorInfo, null, 2) + })); + setActiveDebugTab('response'); + streamMessageUpdate(errorMessage, 'content'); completeMessage('error'); sseSourceRef.current = null; @@ -252,6 +499,20 @@ const Playground = () => { source.addEventListener('readystatechange', (e) => { if (e.readyState >= 2) { if (source.status !== undefined && source.status !== 200) { + const errorInfo = { + error: 'HTTP状态错误', + status: source.status, + readyState: source.readyState, + timestamp: new Date().toISOString() + }; + + // 记录状态错误到调试数据 + setDebugData(prev => ({ + ...prev, + response: responseData + '\n\nHTTP Error:\n' + JSON.stringify(errorInfo, null, 2) + })); + setActiveDebugTab('response'); + source.close(); streamMessageUpdate(t('连接已断开'), 'content'); completeMessage('error'); @@ -263,6 +524,19 @@ const Playground = () => { source.stream(); } catch (error) { console.error('Failed to start SSE stream:', error); + const errorInfo = { + error: '启动SSE流失败', + message: error.message, + timestamp: new Date().toISOString() + }; + + // 记录启动错误到调试数据 + setDebugData(prev => ({ + ...prev, + response: 'Stream启动失败:\n' + JSON.stringify(errorInfo, null, 2) + })); + setActiveDebugTab('response'); + streamMessageUpdate(t('建立连接时发生错误'), 'content'); completeMessage('error'); } @@ -293,17 +567,44 @@ const Playground = () => { if (systemMessage) { messages.unshift(systemMessage); } - return { + const payload = { messages: messages, - stream: true, + stream: inputs.stream, model: inputs.model, group: inputs.group, - max_tokens: parseInt(inputs.max_tokens), - temperature: inputs.temperature, }; + + // 只添加启用的参数 + if (parameterEnabled.max_tokens && inputs.max_tokens > 0) { + payload.max_tokens = parseInt(inputs.max_tokens); + } + if (parameterEnabled.temperature) { + payload.temperature = inputs.temperature; + } + if (parameterEnabled.top_p) { + payload.top_p = inputs.top_p; + } + if (parameterEnabled.frequency_penalty) { + payload.frequency_penalty = inputs.frequency_penalty; + } + if (parameterEnabled.presence_penalty) { + payload.presence_penalty = inputs.presence_penalty; + } + if (parameterEnabled.seed && inputs.seed !== null && inputs.seed !== '') { + payload.seed = parseInt(inputs.seed); + } + + return payload; }; - handleSSE(getPayload()); + const payload = getPayload(); + + if (inputs.stream) { + handleSSE(payload); + } else { + handleNonStreamRequest(payload); + } + newMessage.push({ role: 'assistant', content: '', @@ -347,10 +648,27 @@ 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) { + shouldCollapseFromThinkTag = true; + } + } + newMessage = { ...newMessage, - content: (lastMessage.content || '') + textChunk, + content: newContent, status: 'incomplete', + isReasoningExpanded: (shouldCollapseReasoning || shouldCollapseFromThinkTag) ? false : lastMessage.isReasoningExpanded, }; } } @@ -358,6 +676,167 @@ 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({ + content: t('消息已复制到剪贴板'), + duration: 2, + }); + }).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'; + textArea.style.opacity = '0'; + textArea.style.pointerEvents = 'none'; + textArea.style.zIndex = '-1'; + textArea.setAttribute('readonly', ''); + + document.body.appendChild(textArea); + + // 选中文本 + if (textArea.select) { + textArea.select(); + } + if (textArea.setSelectionRange) { + textArea.setSelectionRange(0, text.length); + } + + // 使用 execCommand 复制 + const successful = document.execCommand('copy'); + document.body.removeChild(textArea); + + if (successful) { + Toast.success({ + content: t('消息已复制到剪贴板'), + duration: 2, + }); + } else { + throw new Error('execCommand copy failed'); + } + } catch (err) { + console.error('回退复制方案也失败:', err); + + // 提供更详细的错误信息 + let errorMessage = t('复制失败,请手动选择文本复制'); + + if (window.location.protocol === 'http:' && window.location.hostname !== 'localhost') { + errorMessage = t('复制功能需要 HTTPS 环境,请手动复制'); + } else if (!navigator.clipboard && !document.execCommand) { + errorMessage = t('浏览器不支持复制功能,请手动复制'); + } + + Toast.error({ + content: errorMessage, + duration: 4, + }); + } + }, [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); + return newMessages; + } + } + return prevMessages; + }); + }, [onMessageSend]); + + // 处理消息删除 + const handleMessageDelete = useCallback((targetMessage) => { + Modal.confirm({ + title: t('确认删除'), + content: t('确定要删除这条消息吗?'), + okText: t('确定'), + cancelText: t('取消'), + okButtonProps: { + type: 'danger', + }, + 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, + }); + return prevMessages.filter((_, index) => index !== messageIndex && index !== messageIndex + 1); + } + } + + // 否则只删除当前消息 + Toast.success({ + content: t('消息已删除'), + duration: 2, + }); + return prevMessages.filter(msg => msg.id !== targetMessage.id); + }); + }, + }); + }, [setMessage, t]); + const onStopGenerator = useCallback(() => { if (sseSourceRef.current) { sseSourceRef.current.close(); @@ -403,11 +882,26 @@ const Playground = () => { } }, [setMessage]); + const DebugToggle = () => { + return ( + + ); + }; + const SettingsToggle = () => { if (!styleState.isMobile) return null; return ( + ); } @@ -455,17 +953,71 @@ const Playground = () => { return ; }, []); + // 自定义操作按钮渲染 + const renderChatBoxAction = useCallback((props) => { + const { message } = props; + + // 对于正在加载或未完成的消息,只显示部分按钮 + const isLoading = message.status === 'loading' || message.status === 'incomplete'; + + return ( +
+ {/* 重试按钮 - 只在消息完成或出错时显示 */} + {!isLoading && ( + +
+ ); + }, [handleMessageReset, handleMessageCopy, handleMessageDelete, t]); + const renderCustomChatContent = useCallback( ({ message, className }) => { if (message.status === 'error') { return ( -
- {message.content || t('请求发生错误')} +
+ + {message.content || t('请求发生错误')} +
); } @@ -494,6 +1046,8 @@ const Playground = () => { let thoughts = []; let replyParts = []; let lastIndex = 0; + + // 使用更安全的正则表达式,只匹配完整的 think 标签对 const thinkTagRegex = /([\s\S]*?)<\/think>/g; let match; @@ -505,35 +1059,54 @@ const Playground = () => { } replyParts.push(fullContent.substring(lastIndex)); - currentDisplayableFinalContent = replyParts.join('').trim(); + // 处理剩余的内容,移除未闭合的 think 标签 + let finalContent = replyParts.join(''); - if (thoughts.length > 0) { - currentExtractedThinkingContent = thoughts.join('\n\n---\n\n'); - thinkingSource = ' tags'; - } - - if (isThinkingStatus && currentDisplayableFinalContent.includes(''); + // 如果还有未闭合的 标签,将其内容提取到思考区域 + if (isThinkingStatus) { + const lastOpenThinkIndex = finalContent.lastIndexOf(''); if (lastOpenThinkIndex !== -1) { - const fragmentAfterLastOpen = currentDisplayableFinalContent.substring(lastOpenThinkIndex); - if (!fragmentAfterLastOpen.substring("".length).includes('')) { - const unclosedThought = fragmentAfterLastOpen.substring("".length); - if (currentExtractedThinkingContent) { - currentExtractedThinkingContent += (currentExtractedThinkingContent ? '\n\n---\n\n' : '') + unclosedThought; - } else { - currentExtractedThinkingContent = unclosedThought; + const fragmentAfterLastOpen = finalContent.substring(lastOpenThinkIndex); + // 检查是否有对应的闭合标签 + if (!fragmentAfterLastOpen.includes('')) { + // 提取未闭合的思考内容 + const unclosedThought = fragmentAfterLastOpen.substring(''.length); + if (unclosedThought.trim()) { + if (currentExtractedThinkingContent) { + currentExtractedThinkingContent += '\n\n---\n\n' + unclosedThought; + } else { + currentExtractedThinkingContent = unclosedThought; + } + if (!thinkingSource) thinkingSource = ' tags (streaming)'; } - if (!thinkingSource && unclosedThought) thinkingSource = ' tags (streaming)'; - currentDisplayableFinalContent = currentDisplayableFinalContent.substring(0, lastOpenThinkIndex).trim(); + // 移除未闭合的 think 标签部分 + finalContent = finalContent.substring(0, lastOpenThinkIndex); } } } + + currentDisplayableFinalContent = finalContent.trim(); + + if (thoughts.length > 0) { + if (currentExtractedThinkingContent) { + currentExtractedThinkingContent = thoughts.join('\n\n---\n\n') + '\n\n---\n\n' + currentExtractedThinkingContent; + } else { + currentExtractedThinkingContent = thoughts.join('\n\n---\n\n'); + } + thinkingSource = ' tags'; + } } - if (typeof currentDisplayableFinalContent === 'string' && currentDisplayableFinalContent.trim().startsWith("")) { - const startsWithCompleteThinkTagRegex = /^[\s\S]*?<\/think>/; - if (!startsWithCompleteThinkTagRegex.test(currentDisplayableFinalContent.trim())) { - currentDisplayableFinalContent = ""; + // 清理任何剩余的不完整 think 标签 + if (typeof currentDisplayableFinalContent === 'string') { + // 移除任何孤立的 开始标签 + currentDisplayableFinalContent = currentDisplayableFinalContent.replace(/\s*$/g, ''); + // 如果内容以 开始但没有完整的标签对,清空内容 + if (currentDisplayableFinalContent.trim().startsWith("")) { + const startsWithCompleteThinkTagRegex = /^[\s\S]*?<\/think>/; + if (!startsWithCompleteThinkTagRegex.test(currentDisplayableFinalContent.trim())) { + currentDisplayableFinalContent = ""; + } } } } @@ -547,9 +1120,18 @@ const Playground = () => { !finalExtractedThinkingContent && (!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) { return ( -
- - {t('正在思考...')} +
+
+ +
+
+ + {t('正在思考...')} + + + AI 正在分析您的问题 + +
); } @@ -557,56 +1139,66 @@ const Playground = () => { return (
{message.role === 'assistant' && finalExtractedThinkingContent && ( -
+
toggleReasoningExpansion(message.id)} > -
- {headerText} - {thinkingSource && ( - - {thinkingSource} - - )} +
+
+ +
+
+ + {headerText} + + {thinkingSource && ( + + 来源: {thinkingSource} + + )} +
-
- {isThinkingStatus && } - {!isThinkingStatus && (message.isReasoningExpanded ? : )} +
+ {isThinkingStatus && ( +
+ + + 思考中 + +
+ )} + {!isThinkingStatus && ( +
+ {message.isReasoningExpanded ? + : + + } +
+ )}
- + {message.isReasoningExpanded && ( +
+
+
+ +
+
+
+ )}
)} {(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && ( - - )} - {!(finalExtractedThinkingContent) && !(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && message.role === 'assistant' && ( -
+
+ +
)}
); @@ -615,115 +1207,460 @@ const Playground = () => { ); return ( - - {(showSettings || !styleState.isMobile) && ( - - -
- {t('分组')}: -
- { - handleInputChange('model', value); - }} - value={inputs.model} - autoComplete='new-password' - optionList={models} - /> -
- Temperature: -
- { - handleInputChange('temperature', value); - }} - /> -
- MaxTokens: -
- { - handleInputChange('max_tokens', value); - }} - /> +
+ + {(showSettings || !styleState.isMobile) && ( + + +
+
+
+ +
+ + {t('模型设置')} + +
+ +
-
- System: +
+ {/* 分组选择 */} +
+
+ + + {t('分组')} + +
+ handleInputChange('model', value)} + value={inputs.model} + autoComplete='new-password' + optionList={models} + className="!rounded-lg" + /> +
+ + {/* Temperature */} +
+
+
+ + + Temperature + + + {inputs.temperature} + +
+
+ + 控制输出的随机性和创造性 + + handleInputChange('temperature', value)} + className="mt-2" + disabled={!parameterEnabled.temperature} + /> +
+ + {/* Top P */} +
+
+
+ + + Top P + + + {inputs.top_p} + +
+
+ + 核采样,控制词汇选择的多样性 + + handleInputChange('top_p', value)} + className="mt-2" + disabled={!parameterEnabled.top_p} + /> +
+ + {/* Frequency Penalty */} +
+
+
+ + + Frequency Penalty + + + {inputs.frequency_penalty} + +
+
+ + 频率惩罚,减少重复词汇的出现 + + handleInputChange('frequency_penalty', value)} + className="mt-2" + disabled={!parameterEnabled.frequency_penalty} + /> +
+ + {/* Presence Penalty */} +
+
+
+ + + Presence Penalty + + + {inputs.presence_penalty} + +
+
+ + 存在惩罚,鼓励讨论新话题 + + handleInputChange('presence_penalty', value)} + className="mt-2" + disabled={!parameterEnabled.presence_penalty} + /> +
+ + {/* MaxTokens */} +
+
+
+ + + Max Tokens + +
+
+ handleInputChange('max_tokens', value)} + className="!rounded-lg" + disabled={!parameterEnabled.max_tokens} + /> +
+ + {/* Seed */} +
+
+
+ + + Seed + + + (可选,用于复现结果) + +
+
+ handleInputChange('seed', value === '' ? null : value)} + className="!rounded-lg" + disabled={!parameterEnabled.seed} + /> +
+ + {/* Stream Toggle */} +
+
+
+ + + 流式输出 + +
+ +
+
+ + {/* System Prompt */} +
+
+ + + System Prompt + +
+