diff --git a/web/src/pages/Playground/Playground.js b/web/src/pages/Playground/Playground.js index e8138c01..8ed81dee 100644 --- a/web/src/pages/Playground/Playground.js +++ b/web/src/pages/Playground/Playground.js @@ -1,10 +1,11 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { UserContext } from '../../context/User/index.js'; import { API, getUserIdFromLocalStorage, showError, + getLogo, } from '../../helpers/index.js'; import { Card, @@ -16,38 +17,54 @@ import { TextArea, Typography, Button, - Highlight, + MarkdownRender, + Tag, } from '@douyinfe/semi-ui'; import { SSE } from 'sse'; -import { IconSetting } from '@douyinfe/semi-icons'; +import { IconSetting, IconSpin, IconChevronRight, IconChevronUp } from '@douyinfe/semi-icons'; import { StyleContext } from '../../context/Style/index.js'; import { useTranslation } from 'react-i18next'; -import { renderGroupOption, truncateText } from '../../helpers/render.js'; - -const roleInfo = { - user: { - name: 'User', - avatar: - 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png', - }, - assistant: { - name: 'Assistant', - avatar: 'logo.png', - }, - system: { - name: 'System', - avatar: - 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png', - }, -}; +import { renderGroupOption, truncateText, stringToColor } from '../../helpers/render.js'; let id = 4; function getId() { return `${id++}`; } +const generateAvatarDataUrl = (username) => { + if (!username) { + return 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'; + } + const firstLetter = username[0].toUpperCase(); + const bgColor = stringToColor(username); + const svg = ` + + + ${firstLetter} + + `; + return `data:image/svg+xml;base64,${btoa(svg)}`; +}; + const Playground = () => { const { t } = useTranslation(); + const [userState, userDispatch] = useContext(UserContext); + + const roleInfo = { + user: { + name: userState?.user?.username || 'User', + avatar: generateAvatarDataUrl(userState?.user?.username), + }, + assistant: { + name: 'Assistant', + avatar: getLogo(), + }, + system: { + name: 'System', + avatar: + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png', + }, + }; const defaultMessage = [ { @@ -61,17 +78,18 @@ const Playground = () => { id: '3', createAt: 1715676751919, content: t('你好,请问有什么可以帮助您的吗?'), + reasoningContent: '', + isReasoningExpanded: false, }, ]; const [inputs, setInputs] = useState({ - model: 'gpt-4o-mini', + model: 'deepseek-r1', group: '', max_tokens: 0, temperature: 0, }); const [searchParams, setSearchParams] = useSearchParams(); - const [userState, userDispatch] = useContext(UserContext); const [status, setStatus] = useState({}); const [systemPrompt, setSystemPrompt] = useState( 'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.', @@ -97,7 +115,7 @@ const Playground = () => { } loadModels(); loadGroups(); - }, []); + }, [searchParams, t]); const loadModels = async () => { let res = await API.get(`/api/user/models`); @@ -121,7 +139,7 @@ const Playground = () => { label: truncateText(info.desc, '50%'), value: group, ratio: info.ratio, - fullLabel: info.desc, // 保存完整文本用于tooltip + fullLabel: info.desc, })); if (localGroupOptions.length === 0) { @@ -186,7 +204,6 @@ const Playground = () => { payload: JSON.stringify(payload), }); source.addEventListener('message', (e) => { - // 只有收到 [DONE] 时才结束 if (e.data === '[DONE]') { source.close(); completeMessage(); @@ -194,14 +211,19 @@ const Playground = () => { } let payload = JSON.parse(e.data); - // 检查是否有 delta content - if (payload.choices?.[0]?.delta?.content) { - generateMockResponse(payload.choices[0].delta.content); + 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.addEventListener('error', (e) => { - generateMockResponse(e.data); + streamMessageUpdate(e.data, 'content'); completeMessage('error'); }); @@ -230,7 +252,6 @@ const Playground = () => { }, ]; - // 将 getPayload 移到这里 const getPayload = () => { let systemMessage = getSystemMessage(); let messages = newMessage.map((item) => { @@ -252,11 +273,12 @@ const Playground = () => { }; }; - // 使用更新后的消息状态调用 handleSSE handleSSE(getPayload()); newMessage.push({ role: 'assistant', content: '', + reasoningContent: '', + isReasoningExpanded: true, createAt: Date.now(), id: getId(), status: 'loading', @@ -264,39 +286,44 @@ const Playground = () => { return newMessage; }); }, - [getSystemMessage], + [getSystemMessage, inputs, setMessage], ); const completeMessage = useCallback((status = 'complete') => { - // console.log("Complete Message: ", status) setMessage((prevMessage) => { const lastMessage = prevMessage[prevMessage.length - 1]; - // only change the status if the last message is not complete and not error if (lastMessage.status === 'complete' || lastMessage.status === 'error') { return prevMessage; } - return [...prevMessage.slice(0, -1), { ...lastMessage, status: status }]; + return [...prevMessage.slice(0, -1), { ...lastMessage, status: status, isReasoningExpanded: false }]; }); - }, []); + }, [setMessage]); - const generateMockResponse = useCallback((content) => { - // console.log("Generate Mock Response: ", content); - setMessage((message) => { - const lastMessage = message[message.length - 1]; + const streamMessageUpdate = useCallback((textChunk, type) => { + setMessage((prevMessage) => { + const lastMessage = prevMessage[prevMessage.length - 1]; let newMessage = { ...lastMessage }; if ( lastMessage.status === 'loading' || lastMessage.status === 'incomplete' ) { - newMessage = { - ...newMessage, - content: (lastMessage.content || '') + content, - status: 'incomplete', - }; + if (type === 'reasoning') { + newMessage = { + ...newMessage, + reasoningContent: (lastMessage.reasoningContent || '') + textChunk, + status: 'incomplete', + }; + } else if (type === 'content') { + newMessage = { + ...newMessage, + content: (lastMessage.content || '') + textChunk, + status: 'incomplete', + }; + } } - return [...message.slice(0, -1), newMessage]; + return [...prevMessage.slice(0, -1), newMessage]; }); - }, []); + }, [setMessage]); const SettingsToggle = () => { if (!styleState.isMobile) return null; @@ -340,7 +367,6 @@ const Playground = () => { }} onClick={onClick} > - {/*{uploadNode}*/} {inputNode} {sendNode} @@ -351,6 +377,152 @@ const Playground = () => { return ; }, []); + const renderCustomChatContent = useCallback( + ({ message, className }) => { + const toggleReasoningExpansion = (messageId) => { + setMessage(prevMessages => + prevMessages.map(msg => + msg.id === messageId && msg.role === 'assistant' + ? { ...msg, isReasoningExpanded: !msg.isReasoningExpanded } + : msg + ) + ); + }; + + const isThinkingStatus = message.status === 'loading' || message.status === 'incomplete'; + let currentExtractedThinkingContent = null; + let currentDisplayableFinalContent = message.content || ""; + let thinkingSource = null; + + if (message.role === 'assistant') { + if (message.reasoningContent) { + currentExtractedThinkingContent = message.reasoningContent; + thinkingSource = 'reasoningContent'; + } else if (message.content && message.content.includes('([\s\S]*?)<\/think>/g; + let match; + + thinkTagRegex.lastIndex = 0; + while ((match = thinkTagRegex.exec(fullContent)) !== null) { + replyParts.push(fullContent.substring(lastIndex, match.index)); + thoughts.push(match[1]); + lastIndex = match.index + match[0].length; + } + replyParts.push(fullContent.substring(lastIndex)); + + currentDisplayableFinalContent = replyParts.join('').trim(); + + if (thoughts.length > 0) { + currentExtractedThinkingContent = thoughts.join('\n\n---\n\n'); + thinkingSource = ' tags'; + } + + if (isThinkingStatus && currentDisplayableFinalContent.includes(''); + 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; + } + if (!thinkingSource && unclosedThought) thinkingSource = ' tags (streaming)'; + currentDisplayableFinalContent = currentDisplayableFinalContent.substring(0, lastOpenThinkIndex).trim(); + } + } + } + } + + if (typeof currentDisplayableFinalContent === 'string' && currentDisplayableFinalContent.trim().startsWith("")) { + const startsWithCompleteThinkTagRegex = /^[\s\S]*?<\/think>/; + if (!startsWithCompleteThinkTagRegex.test(currentDisplayableFinalContent.trim())) { + currentDisplayableFinalContent = ""; + } + } + } + + const headerText = isThinkingStatus ? t('思考中...') : t('思考过程'); + const finalExtractedThinkingContent = currentExtractedThinkingContent; + const finalDisplayableFinalContent = currentDisplayableFinalContent; + + if (message.role === 'assistant' && + isThinkingStatus && + !finalExtractedThinkingContent && + (!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) { + return ( +
+ + {t('正在思考...')} +
+ ); + } + + return ( +
+ {message.role === 'assistant' && finalExtractedThinkingContent && ( +
+
toggleReasoningExpansion(message.id)} + > +
+ {headerText} + {thinkingSource && ( + + {thinkingSource} + + )} +
+
+ {isThinkingStatus && } + {!isThinkingStatus && (message.isReasoningExpanded ? : )} +
+
+
+ +
+
+ )} + + {(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && ( + + )} + {!(finalExtractedThinkingContent) && !(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && message.role === 'assistant' && ( +
+ )} +
+ ); + }, + [t, setMessage], + ); + return ( {(showSettings || !styleState.isMobile) && ( @@ -429,7 +601,6 @@ const Playground = () => { autoComplete='new-password' autosize defaultValue={systemPrompt} - // value={systemPrompt} onChange={(value) => { setSystemPrompt(value); }} @@ -442,6 +613,7 @@ const Playground = () => { { return
; },