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,
Tag,
Tabs,
TabPane,
Toast,
Tooltip,
Modal,
} from '@douyinfe/semi-ui';
import { SSE } from 'sse';
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() {
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 = `
`;
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,
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({});
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 [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);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const handleParameterToggle = (paramName) => {
setParameterEnabled(prev => ({
...prev,
[paramName]: !prev[paramName]
}));
};
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);
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 getSystemMessage = () => {
if (systemPrompt !== '') {
return {
role: 'system',
id: '1',
createAt: 1715676751919,
content: systemPrompt,
};
}
};
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()
};
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 || '';
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
};
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',
'New-Api-User': getUserIdFromLocalStorage(),
},
method: 'POST',
payload: JSON.stringify(payload),
});
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) {
streamMessageUpdate(delta.reasoning_content, '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}`
}));
setActiveDebugTab('response');
streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
completeMessage('error');
}
});
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;
source.close();
});
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');
}
}
});
try {
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');
}
};
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);
}
const payload = {
messages: messages,
stream: inputs.stream,
model: inputs.model,
group: inputs.group,
};
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;
};
const payload = getPayload();
if (inputs.stream) {
handleSSE(payload);
} else {
handleNonStreamRequest(payload);
}
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') {
const newContent = (lastMessage.content || '') + textChunk;
newMessage = {
...newMessage,
content: newContent,
status: 'incomplete',
isReasoningExpanded: lastMessage.isReasoningExpanded,
};
}
}
return [...prevMessage.slice(0, -1), newMessage];
});
}, [setMessage]);
const handleMessageCopy = useCallback((message) => {
if (!message.content) return;
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);
fallbackCopyToClipboard(message.content);
});
} else {
fallbackCopyToClipboard(message.content);
}
}, [t]);
const fallbackCopyToClipboard = useCallback((text) => {
try {
if (!document.execCommand) {
throw new Error('execCommand not supported');
}
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);
}
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();
sseSourceRef.current = null;
setMessage((prevMessage) => {
const lastMessage = prevMessage[prevMessage.length - 1];
if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') {
return [...prevMessage.slice(0, -1), {
...lastMessage,
status: 'complete',
isReasoningExpanded: false
}];
}
return prevMessage;
});
}
}, [setMessage]);
const DebugToggle = () => {
return (
:
{JSON.stringify(debugData.request, null, 2)}
) : (
{debugData.response}
) : (