import React, { useCallback, useContext, useEffect, useState, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { UserContext } from '../../context/User/index.js'; import { API, getUserIdFromLocalStorage, showError, getLogo, } from '../../helpers/index.js'; import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button, MarkdownRender, Tag, } from '@douyinfe/semi-ui'; import { SSE } from 'sse'; import { IconSetting, IconSpin, IconChevronRight, IconChevronUp } from '@douyinfe/semi-icons'; import { StyleContext } from '../../context/Style/index.js'; import { useTranslation } from 'react-i18next'; 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 = [ { role: 'user', id: '2', createAt: 1715676751919, content: t('你好'), }, { role: 'assistant', id: '3', createAt: 1715676751919, content: t('你好,请问有什么可以帮助您的吗?'), reasoningContent: '', isReasoningExpanded: false, }, ]; const defaultModel = 'deepseek-r1'; const [inputs, setInputs] = useState({ model: defaultModel, group: '', max_tokens: 0, temperature: 0, }); const [searchParams, setSearchParams] = useSearchParams(); 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.', ); const [message, setMessage] = useState(defaultMessage); const [models, setModels] = useState([]); const [groups, setGroups] = useState([]); const [showSettings, setShowSettings] = useState(true); const [styleState, styleDispatch] = useContext(StyleContext); const sseSourceRef = useRef(null); const handleInputChange = (name, value) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; useEffect(() => { if (searchParams.get('expired')) { showError(t('未登录或登录已过期,请重新登录!')); } let status = localStorage.getItem('status'); if (status) { status = JSON.parse(status); setStatus(status); } loadModels(); loadGroups(); }, [searchParams, t]); const loadModels = async () => { let res = await API.get(`/api/user/models`); const { success, message, data } = res.data; if (success) { let localModelOptions = data.map((model) => ({ label: model, 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 })); } } else { showError(t(message)); } }; const loadGroups = async () => { let res = await API.get(`/api/user/self/groups`); const { success, message, data } = res.data; if (success) { let localGroupOptions = Object.entries(data).map(([group, info]) => ({ label: truncateText(info.desc, '50%'), value: group, ratio: info.ratio, fullLabel: info.desc, })); if (localGroupOptions.length === 0) { localGroupOptions = [ { label: t('用户分组'), value: '', ratio: 1, }, ]; } else { const localUser = JSON.parse(localStorage.getItem('user')); const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group); if (userGroup) { const userGroupIndex = localGroupOptions.findIndex( (g) => g.value === userGroup, ); if (userGroupIndex > -1) { const userGroupOption = localGroupOptions.splice( userGroupIndex, 1, )[0]; localGroupOptions.unshift(userGroupOption); } } } setGroups(localGroupOptions); handleInputChange('group', localGroupOptions[0].value); } else { showError(t(message)); } }; const commonOuterStyle = { border: '1px solid var(--semi-color-border)', borderRadius: '16px', margin: '0px 8px', }; const getSystemMessage = () => { if (systemPrompt !== '') { return { role: 'system', id: '1', createAt: 1715676751919, content: systemPrompt, }; } }; let handleSSE = (payload) => { let source = new SSE('/pg/chat/completions', { headers: { 'Content-Type': 'application/json', 'New-Api-User': getUserIdFromLocalStorage(), }, method: 'POST', payload: JSON.stringify(payload), }); // 保存 source 引用以便后续停止生成 sseSourceRef.current = source; source.addEventListener('message', (e) => { if (e.data === '[DONE]') { source.close(); sseSourceRef.current = null; completeMessage(); return; } try { let payload = JSON.parse(e.data); const delta = payload.choices?.[0]?.delta; if (delta) { if (delta.reasoning_content) { streamMessageUpdate(delta.reasoning_content, 'reasoning'); } if (delta.content) { streamMessageUpdate(delta.content, 'content'); } } } catch (error) { console.error('Failed to parse SSE message:', error); streamMessageUpdate(t('解析响应数据时发生错误'), 'content'); completeMessage('error'); } }); source.addEventListener('error', (e) => { console.error('SSE Error:', e); const errorMessage = e.data || t('请求发生错误'); streamMessageUpdate(errorMessage, 'content'); completeMessage('error'); sseSourceRef.current = null; source.close(); }); source.addEventListener('readystatechange', (e) => { if (e.readyState >= 2) { if (source.status !== undefined && source.status !== 200) { source.close(); streamMessageUpdate(t('连接已断开'), 'content'); completeMessage('error'); } } }); try { source.stream(); } catch (error) { console.error('Failed to start SSE stream:', error); streamMessageUpdate(t('建立连接时发生错误'), 'content'); completeMessage('error'); } }; const onMessageSend = useCallback( (content, attachment) => { console.log('attachment: ', attachment); setMessage((prevMessage) => { const newMessage = [ ...prevMessage, { role: 'user', content: content, createAt: Date.now(), id: getId(), }, ]; const getPayload = () => { let systemMessage = getSystemMessage(); let messages = newMessage.map((item) => { return { role: item.role, content: item.content, }; }); if (systemMessage) { messages.unshift(systemMessage); } return { messages: messages, stream: true, model: inputs.model, group: inputs.group, max_tokens: parseInt(inputs.max_tokens), temperature: inputs.temperature, }; }; handleSSE(getPayload()); newMessage.push({ role: 'assistant', content: '', reasoningContent: '', isReasoningExpanded: true, createAt: Date.now(), id: getId(), status: 'loading', }); return newMessage; }); }, [getSystemMessage, inputs, setMessage], ); const completeMessage = useCallback((status = 'complete') => { setMessage((prevMessage) => { const lastMessage = prevMessage[prevMessage.length - 1]; if (lastMessage.status === 'complete' || lastMessage.status === 'error') { return prevMessage; } return [...prevMessage.slice(0, -1), { ...lastMessage, status: status, isReasoningExpanded: false }]; }); }, [setMessage]); const streamMessageUpdate = useCallback((textChunk, type) => { setMessage((prevMessage) => { const lastMessage = prevMessage[prevMessage.length - 1]; let newMessage = { ...lastMessage }; // 如果消息已经是错误状态,保持错误状态 if (lastMessage.status === 'error') { return prevMessage; } if (lastMessage.status === 'loading' || lastMessage.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 [...prevMessage.slice(0, -1), newMessage]; }); }, [setMessage]); const onStopGenerator = useCallback(() => { if (sseSourceRef.current) { sseSourceRef.current.close(); sseSourceRef.current = null; setMessage((prevMessage) => { const lastMessage = prevMessage[prevMessage.length - 1]; if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') { let content = lastMessage.content || ''; let reasoningContent = lastMessage.reasoningContent || ''; // 处理 标签格式的思维链 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'); } } return [...prevMessage.slice(0, -1), { ...lastMessage, status: 'complete', reasoningContent: reasoningContent, content: content, isReasoningExpanded: false // 停止时折叠思维链面板 }]; } return prevMessage; }); } }, [setMessage]); const SettingsToggle = () => { if (!styleState.isMobile) return null; return (