|
@@ -19,12 +19,47 @@ import {
|
|
|
Button,
|
|
Button,
|
|
|
MarkdownRender,
|
|
MarkdownRender,
|
|
|
Tag,
|
|
Tag,
|
|
|
|
|
+ Tabs,
|
|
|
|
|
+ TabPane,
|
|
|
|
|
+ Toast,
|
|
|
|
|
+ Tooltip,
|
|
|
|
|
+ Modal,
|
|
|
} from '@douyinfe/semi-ui';
|
|
} from '@douyinfe/semi-ui';
|
|
|
import { SSE } from 'sse';
|
|
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 { StyleContext } from '../../context/Style/index.js';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
import { renderGroupOption, truncateText, stringToColor } from '../../helpers/render.js';
|
|
import { renderGroupOption, truncateText, stringToColor } from '../../helpers/render.js';
|
|
|
|
|
+import { IconSend } from '@douyinfe/semi-icons';
|
|
|
|
|
|
|
|
let id = 4;
|
|
let id = 4;
|
|
|
function getId() {
|
|
function getId() {
|
|
@@ -89,6 +124,19 @@ const Playground = () => {
|
|
|
group: '',
|
|
group: '',
|
|
|
max_tokens: 0,
|
|
max_tokens: 0,
|
|
|
temperature: 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 [searchParams, setSearchParams] = useSearchParams();
|
|
|
const [status, setStatus] = useState({});
|
|
const [status, setStatus] = useState({});
|
|
@@ -99,6 +147,13 @@ const Playground = () => {
|
|
|
const [models, setModels] = useState([]);
|
|
const [models, setModels] = useState([]);
|
|
|
const [groups, setGroups] = useState([]);
|
|
const [groups, setGroups] = useState([]);
|
|
|
const [showSettings, setShowSettings] = useState(true);
|
|
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 [styleState, styleDispatch] = useContext(StyleContext);
|
|
|
const sseSourceRef = useRef(null);
|
|
const sseSourceRef = useRef(null);
|
|
|
|
|
|
|
@@ -106,6 +161,13 @@ const Playground = () => {
|
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleParameterToggle = (paramName) => {
|
|
|
|
|
+ setParameterEnabled(prev => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ [paramName]: !prev[paramName]
|
|
|
|
|
+ }));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (searchParams.get('expired')) {
|
|
if (searchParams.get('expired')) {
|
|
|
showError(t('未登录或登录已过期,请重新登录!'));
|
|
showError(t('未登录或登录已过期,请重新登录!'));
|
|
@@ -184,12 +246,6 @@ const Playground = () => {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- const commonOuterStyle = {
|
|
|
|
|
- border: '1px solid var(--semi-color-border)',
|
|
|
|
|
- borderRadius: '16px',
|
|
|
|
|
- margin: '0px 8px',
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
const getSystemMessage = () => {
|
|
const getSystemMessage = () => {
|
|
|
if (systemPrompt !== '') {
|
|
if (systemPrompt !== '') {
|
|
|
return {
|
|
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 || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 处理 <think> 标签格式的思维链
|
|
|
|
|
+ if (content.includes('<think>')) {
|
|
|
|
|
+ const thinkTagRegex = /<think>([\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) => {
|
|
let handleSSE = (payload) => {
|
|
|
|
|
+ // 记录请求数据并自动切换到请求体标签
|
|
|
|
|
+ setDebugData(prev => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ request: payload,
|
|
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
|
|
+ response: null
|
|
|
|
|
+ }));
|
|
|
|
|
+ setActiveDebugTab('request');
|
|
|
|
|
+
|
|
|
let source = new SSE('/pg/chat/completions', {
|
|
let source = new SSE('/pg/chat/completions', {
|
|
|
headers: {
|
|
headers: {
|
|
|
'Content-Type': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
@@ -214,16 +420,32 @@ const Playground = () => {
|
|
|
// 保存 source 引用以便后续停止生成
|
|
// 保存 source 引用以便后续停止生成
|
|
|
sseSourceRef.current = source;
|
|
sseSourceRef.current = source;
|
|
|
|
|
|
|
|
|
|
+ let responseData = '';
|
|
|
|
|
+ let hasReceivedFirstResponse = false;
|
|
|
|
|
+
|
|
|
source.addEventListener('message', (e) => {
|
|
source.addEventListener('message', (e) => {
|
|
|
if (e.data === '[DONE]') {
|
|
if (e.data === '[DONE]') {
|
|
|
source.close();
|
|
source.close();
|
|
|
sseSourceRef.current = null;
|
|
sseSourceRef.current = null;
|
|
|
|
|
+ // 记录完整响应
|
|
|
|
|
+ setDebugData(prev => ({
|
|
|
|
|
+ ...prev,
|
|
|
|
|
+ response: responseData
|
|
|
|
|
+ }));
|
|
|
completeMessage();
|
|
completeMessage();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
let payload = JSON.parse(e.data);
|
|
let payload = JSON.parse(e.data);
|
|
|
|
|
+ responseData += e.data + '\n';
|
|
|
|
|
+
|
|
|
|
|
+ // 收到第一个响应时自动切换到响应标签
|
|
|
|
|
+ if (!hasReceivedFirstResponse) {
|
|
|
|
|
+ setActiveDebugTab('response');
|
|
|
|
|
+ hasReceivedFirstResponse = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const delta = payload.choices?.[0]?.delta;
|
|
const delta = payload.choices?.[0]?.delta;
|
|
|
if (delta) {
|
|
if (delta) {
|
|
|
if (delta.reasoning_content) {
|
|
if (delta.reasoning_content) {
|
|
@@ -235,6 +457,15 @@ const Playground = () => {
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Failed to parse SSE message:', 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');
|
|
streamMessageUpdate(t('解析响应数据时发生错误'), 'content');
|
|
|
completeMessage('error');
|
|
completeMessage('error');
|
|
|
}
|
|
}
|
|
@@ -243,6 +474,22 @@ const Playground = () => {
|
|
|
source.addEventListener('error', (e) => {
|
|
source.addEventListener('error', (e) => {
|
|
|
console.error('SSE Error:', e);
|
|
console.error('SSE Error:', e);
|
|
|
const errorMessage = e.data || t('请求发生错误');
|
|
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');
|
|
streamMessageUpdate(errorMessage, 'content');
|
|
|
completeMessage('error');
|
|
completeMessage('error');
|
|
|
sseSourceRef.current = null;
|
|
sseSourceRef.current = null;
|
|
@@ -252,6 +499,20 @@ const Playground = () => {
|
|
|
source.addEventListener('readystatechange', (e) => {
|
|
source.addEventListener('readystatechange', (e) => {
|
|
|
if (e.readyState >= 2) {
|
|
if (e.readyState >= 2) {
|
|
|
if (source.status !== undefined && source.status !== 200) {
|
|
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();
|
|
source.close();
|
|
|
streamMessageUpdate(t('连接已断开'), 'content');
|
|
streamMessageUpdate(t('连接已断开'), 'content');
|
|
|
completeMessage('error');
|
|
completeMessage('error');
|
|
@@ -263,6 +524,19 @@ const Playground = () => {
|
|
|
source.stream();
|
|
source.stream();
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Failed to start SSE stream:', 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');
|
|
streamMessageUpdate(t('建立连接时发生错误'), 'content');
|
|
|
completeMessage('error');
|
|
completeMessage('error');
|
|
|
}
|
|
}
|
|
@@ -293,17 +567,44 @@ const Playground = () => {
|
|
|
if (systemMessage) {
|
|
if (systemMessage) {
|
|
|
messages.unshift(systemMessage);
|
|
messages.unshift(systemMessage);
|
|
|
}
|
|
}
|
|
|
- return {
|
|
|
|
|
|
|
+ const payload = {
|
|
|
messages: messages,
|
|
messages: messages,
|
|
|
- stream: true,
|
|
|
|
|
|
|
+ stream: inputs.stream,
|
|
|
model: inputs.model,
|
|
model: inputs.model,
|
|
|
group: inputs.group,
|
|
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({
|
|
newMessage.push({
|
|
|
role: 'assistant',
|
|
role: 'assistant',
|
|
|
content: '',
|
|
content: '',
|
|
@@ -347,10 +648,27 @@ const Playground = () => {
|
|
|
status: 'incomplete',
|
|
status: 'incomplete',
|
|
|
};
|
|
};
|
|
|
} else if (type === 'content') {
|
|
} else if (type === 'content') {
|
|
|
|
|
+ // 当开始接收 content 时,说明思考部分已经完成,应该折叠思考面板
|
|
|
|
|
+ const shouldCollapseReasoning = !lastMessage.content && lastMessage.reasoningContent && lastMessage.isReasoningExpanded;
|
|
|
|
|
+
|
|
|
|
|
+ const newContent = (lastMessage.content || '') + textChunk;
|
|
|
|
|
+
|
|
|
|
|
+ // 检测 </think> 标签的完成
|
|
|
|
|
+ let shouldCollapseFromThinkTag = false;
|
|
|
|
|
+ if (lastMessage.isReasoningExpanded && newContent.includes('</think>')) {
|
|
|
|
|
+ // 检查是否有完整的 <think>...</think> 对
|
|
|
|
|
+ const thinkMatches = newContent.match(/<think>/g);
|
|
|
|
|
+ const thinkCloseMatches = newContent.match(/<\/think>/g);
|
|
|
|
|
+ if (thinkMatches && thinkCloseMatches && thinkCloseMatches.length >= thinkMatches.length) {
|
|
|
|
|
+ shouldCollapseFromThinkTag = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
newMessage = {
|
|
newMessage = {
|
|
|
...newMessage,
|
|
...newMessage,
|
|
|
- content: (lastMessage.content || '') + textChunk,
|
|
|
|
|
|
|
+ content: newContent,
|
|
|
status: 'incomplete',
|
|
status: 'incomplete',
|
|
|
|
|
+ isReasoningExpanded: (shouldCollapseReasoning || shouldCollapseFromThinkTag) ? false : lastMessage.isReasoningExpanded,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -358,6 +676,167 @@ const Playground = () => {
|
|
|
});
|
|
});
|
|
|
}, [setMessage]);
|
|
}, [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(() => {
|
|
const onStopGenerator = useCallback(() => {
|
|
|
if (sseSourceRef.current) {
|
|
if (sseSourceRef.current) {
|
|
|
sseSourceRef.current.close();
|
|
sseSourceRef.current.close();
|
|
@@ -403,11 +882,26 @@ const Playground = () => {
|
|
|
}
|
|
}
|
|
|
}, [setMessage]);
|
|
}, [setMessage]);
|
|
|
|
|
|
|
|
|
|
+ const DebugToggle = () => {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ icon={showDebugPanel ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
|
|
|
+ onClick={() => setShowDebugPanel(!showDebugPanel)}
|
|
|
|
|
+ theme="borderless"
|
|
|
|
|
+ type="tertiary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ className="!rounded-lg !text-gray-600 hover:!text-purple-600 hover:!bg-purple-50"
|
|
|
|
|
+ >
|
|
|
|
|
+ {showDebugPanel ? t('隐藏调试') : t('显示调试')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const SettingsToggle = () => {
|
|
const SettingsToggle = () => {
|
|
|
if (!styleState.isMobile) return null;
|
|
if (!styleState.isMobile) return null;
|
|
|
return (
|
|
return (
|
|
|
<Button
|
|
<Button
|
|
|
- icon={<IconSetting />}
|
|
|
|
|
|
|
+ icon={<Settings size={16} />}
|
|
|
style={{
|
|
style={{
|
|
|
position: 'absolute',
|
|
position: 'absolute',
|
|
|
left: showSettings ? -10 : -20,
|
|
left: showSettings ? -10 : -20,
|
|
@@ -433,20 +927,24 @@ const Playground = () => {
|
|
|
detailProps;
|
|
detailProps;
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div
|
|
|
|
|
- style={{
|
|
|
|
|
- margin: '8px 16px',
|
|
|
|
|
- display: 'flex',
|
|
|
|
|
- flexDirection: 'row',
|
|
|
|
|
- alignItems: 'flex-end',
|
|
|
|
|
- borderRadius: 16,
|
|
|
|
|
- padding: 10,
|
|
|
|
|
- border: '1px solid var(--semi-color-border)',
|
|
|
|
|
- }}
|
|
|
|
|
- onClick={onClick}
|
|
|
|
|
- >
|
|
|
|
|
- {inputNode}
|
|
|
|
|
- {sendNode}
|
|
|
|
|
|
|
+ <div className="p-4">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="flex items-end gap-3 p-4 bg-gray-50 rounded-2xl shadow-sm hover:shadow-md transition-shadow"
|
|
|
|
|
+ style={{ border: '1px solid var(--semi-color-border)' }}
|
|
|
|
|
+ onClick={onClick}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ {inputNode}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme="solid"
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ className="!rounded-lg !bg-purple-500 hover:!bg-purple-600 flex-shrink-0"
|
|
|
|
|
+ icon={<IconSend />}
|
|
|
|
|
+ >
|
|
|
|
|
+ {t('发送')}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -455,17 +953,71 @@ const Playground = () => {
|
|
|
return <CustomInputRender {...props} />;
|
|
return <CustomInputRender {...props} />;
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
+ // 自定义操作按钮渲染
|
|
|
|
|
+ const renderChatBoxAction = useCallback((props) => {
|
|
|
|
|
+ const { message } = props;
|
|
|
|
|
+
|
|
|
|
|
+ // 对于正在加载或未完成的消息,只显示部分按钮
|
|
|
|
|
+ const isLoading = message.status === 'loading' || message.status === 'incomplete';
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="flex items-center gap-0.5">
|
|
|
|
|
+ {/* 重试按钮 - 只在消息完成或出错时显示 */}
|
|
|
|
|
+ {!isLoading && (
|
|
|
|
|
+ <Tooltip content={t('重试')} position="top">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme="borderless"
|
|
|
|
|
+ type="tertiary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={<RefreshCw size={14} />}
|
|
|
|
|
+ onClick={() => handleMessageReset(message)}
|
|
|
|
|
+ className="!rounded-md !text-gray-400 hover:!text-blue-600 hover:!bg-blue-50 !w-7 !h-7 !p-0 transition-all"
|
|
|
|
|
+ aria-label={t('重试')}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 复制按钮 - 只在有内容时显示 */}
|
|
|
|
|
+ {message.content && (
|
|
|
|
|
+ <Tooltip content={t('复制')} position="top">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme="borderless"
|
|
|
|
|
+ type="tertiary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={<Copy size={14} />}
|
|
|
|
|
+ onClick={() => handleMessageCopy(message)}
|
|
|
|
|
+ className="!rounded-md !text-gray-400 hover:!text-green-600 hover:!bg-green-50 !w-7 !h-7 !p-0 transition-all"
|
|
|
|
|
+ aria-label={t('复制')}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 删除按钮 - 只在消息完成或出错时显示,AI输出时隐藏 */}
|
|
|
|
|
+ {!isLoading && (
|
|
|
|
|
+ <Tooltip content={t('删除')} position="top">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme="borderless"
|
|
|
|
|
+ type="tertiary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={<Trash2 size={14} />}
|
|
|
|
|
+ onClick={() => handleMessageDelete(message)}
|
|
|
|
|
+ className="!rounded-md !text-gray-400 hover:!text-red-600 hover:!bg-red-50 !w-7 !h-7 !p-0 transition-all"
|
|
|
|
|
+ aria-label={t('删除')}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Tooltip>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }, [handleMessageReset, handleMessageCopy, handleMessageDelete, t]);
|
|
|
|
|
+
|
|
|
const renderCustomChatContent = useCallback(
|
|
const renderCustomChatContent = useCallback(
|
|
|
({ message, className }) => {
|
|
({ message, className }) => {
|
|
|
if (message.status === 'error') {
|
|
if (message.status === 'error') {
|
|
|
return (
|
|
return (
|
|
|
- <div className={className} style={{
|
|
|
|
|
- display: 'flex',
|
|
|
|
|
- alignItems: 'center',
|
|
|
|
|
- padding: '12px',
|
|
|
|
|
- color: 'var(--semi-color-danger)'
|
|
|
|
|
- }}>
|
|
|
|
|
- <Typography.Text type="danger">{message.content || t('请求发生错误')}</Typography.Text>
|
|
|
|
|
|
|
+ <div className={`${className} flex items-center p-4 bg-red-50 rounded-xl`}>
|
|
|
|
|
+ <Typography.Text type="danger" className="text-sm">
|
|
|
|
|
+ {message.content || t('请求发生错误')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -494,6 +1046,8 @@ const Playground = () => {
|
|
|
let thoughts = [];
|
|
let thoughts = [];
|
|
|
let replyParts = [];
|
|
let replyParts = [];
|
|
|
let lastIndex = 0;
|
|
let lastIndex = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 使用更安全的正则表达式,只匹配完整的 think 标签对
|
|
|
const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
const thinkTagRegex = /<think>([\s\S]*?)<\/think>/g;
|
|
|
let match;
|
|
let match;
|
|
|
|
|
|
|
@@ -505,35 +1059,54 @@ const Playground = () => {
|
|
|
}
|
|
}
|
|
|
replyParts.push(fullContent.substring(lastIndex));
|
|
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 = '<think> tags';
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (isThinkingStatus && currentDisplayableFinalContent.includes('<think')) {
|
|
|
|
|
- const lastOpenThinkIndex = currentDisplayableFinalContent.lastIndexOf('<think>');
|
|
|
|
|
|
|
+ // 如果还有未闭合的 <think> 标签,将其内容提取到思考区域
|
|
|
|
|
+ if (isThinkingStatus) {
|
|
|
|
|
+ const lastOpenThinkIndex = finalContent.lastIndexOf('<think>');
|
|
|
if (lastOpenThinkIndex !== -1) {
|
|
if (lastOpenThinkIndex !== -1) {
|
|
|
- const fragmentAfterLastOpen = currentDisplayableFinalContent.substring(lastOpenThinkIndex);
|
|
|
|
|
- if (!fragmentAfterLastOpen.substring("<think>".length).includes('</think>')) {
|
|
|
|
|
- const unclosedThought = fragmentAfterLastOpen.substring("<think>".length);
|
|
|
|
|
- if (currentExtractedThinkingContent) {
|
|
|
|
|
- currentExtractedThinkingContent += (currentExtractedThinkingContent ? '\n\n---\n\n' : '') + unclosedThought;
|
|
|
|
|
- } else {
|
|
|
|
|
- currentExtractedThinkingContent = unclosedThought;
|
|
|
|
|
|
|
+ const fragmentAfterLastOpen = finalContent.substring(lastOpenThinkIndex);
|
|
|
|
|
+ // 检查是否有对应的闭合标签
|
|
|
|
|
+ if (!fragmentAfterLastOpen.includes('</think>')) {
|
|
|
|
|
+ // 提取未闭合的思考内容
|
|
|
|
|
+ const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length);
|
|
|
|
|
+ if (unclosedThought.trim()) {
|
|
|
|
|
+ if (currentExtractedThinkingContent) {
|
|
|
|
|
+ currentExtractedThinkingContent += '\n\n---\n\n' + unclosedThought;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ currentExtractedThinkingContent = unclosedThought;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!thinkingSource) thinkingSource = '<think> tags (streaming)';
|
|
|
}
|
|
}
|
|
|
- if (!thinkingSource && unclosedThought) thinkingSource = '<think> 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 = '<think> tags';
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (typeof currentDisplayableFinalContent === 'string' && currentDisplayableFinalContent.trim().startsWith("<think>")) {
|
|
|
|
|
- const startsWithCompleteThinkTagRegex = /^<think>[\s\S]*?<\/think>/;
|
|
|
|
|
- if (!startsWithCompleteThinkTagRegex.test(currentDisplayableFinalContent.trim())) {
|
|
|
|
|
- currentDisplayableFinalContent = "";
|
|
|
|
|
|
|
+ // 清理任何剩余的不完整 think 标签
|
|
|
|
|
+ if (typeof currentDisplayableFinalContent === 'string') {
|
|
|
|
|
+ // 移除任何孤立的 <think> 开始标签
|
|
|
|
|
+ currentDisplayableFinalContent = currentDisplayableFinalContent.replace(/<think>\s*$/g, '');
|
|
|
|
|
+ // 如果内容以 <think> 开始但没有完整的标签对,清空内容
|
|
|
|
|
+ if (currentDisplayableFinalContent.trim().startsWith("<think>")) {
|
|
|
|
|
+ const startsWithCompleteThinkTagRegex = /^<think>[\s\S]*?<\/think>/;
|
|
|
|
|
+ if (!startsWithCompleteThinkTagRegex.test(currentDisplayableFinalContent.trim())) {
|
|
|
|
|
+ currentDisplayableFinalContent = "";
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -547,9 +1120,18 @@ const Playground = () => {
|
|
|
!finalExtractedThinkingContent &&
|
|
!finalExtractedThinkingContent &&
|
|
|
(!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
|
(!finalDisplayableFinalContent || finalDisplayableFinalContent.trim() === '')) {
|
|
|
return (
|
|
return (
|
|
|
- <div className={className} style={{ display: 'flex', alignItems: 'center', padding: '12px' }}>
|
|
|
|
|
- <IconSpin spin />
|
|
|
|
|
- <Typography.Text type="secondary" style={{ marginLeft: '8px' }}>{t('正在思考...')}</Typography.Text>
|
|
|
|
|
|
|
+ <div className={`${className} flex items-center gap-4 p-6 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-2xl`}>
|
|
|
|
|
+ <div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
|
|
|
|
+ <Loader2 className="animate-spin text-white" size={20} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex flex-col">
|
|
|
|
|
+ <Typography.Text strong className="text-gray-800 text-base">
|
|
|
|
|
+ {t('正在思考...')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Typography.Text className="text-gray-500 text-sm">
|
|
|
|
|
+ AI 正在分析您的问题
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
@@ -557,56 +1139,66 @@ const Playground = () => {
|
|
|
return (
|
|
return (
|
|
|
<div className={className}>
|
|
<div className={className}>
|
|
|
{message.role === 'assistant' && finalExtractedThinkingContent && (
|
|
{message.role === 'assistant' && finalExtractedThinkingContent && (
|
|
|
- <div style={{
|
|
|
|
|
- background: 'var(--semi-color-tertiary-light-hover)',
|
|
|
|
|
- borderRadius: '16px',
|
|
|
|
|
- marginBottom: '8px',
|
|
|
|
|
- overflow: 'hidden',
|
|
|
|
|
- }}>
|
|
|
|
|
|
|
+ <div className="bg-gradient-to-br from-indigo-50 via-purple-50 to-pink-50 rounded-2xl mb-4 overflow-hidden shadow-sm backdrop-blur-sm">
|
|
|
<div
|
|
<div
|
|
|
- style={{
|
|
|
|
|
- display: 'flex',
|
|
|
|
|
- alignItems: 'center',
|
|
|
|
|
- justifyContent: 'space-between',
|
|
|
|
|
- padding: '8px 12px',
|
|
|
|
|
- cursor: 'pointer',
|
|
|
|
|
- height: 'auto',
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ className="flex items-center justify-between p-5 cursor-pointer hover:bg-gradient-to-r hover:from-white/40 hover:to-purple-50/60 transition-all"
|
|
|
onClick={() => toggleReasoningExpansion(message.id)}
|
|
onClick={() => toggleReasoningExpansion(message.id)}
|
|
|
>
|
|
>
|
|
|
- <div style={{ display: 'flex', alignItems: 'center' }}>
|
|
|
|
|
- <Typography.Text strong={message.isReasoningExpanded} style={{ fontSize: '13px', color: 'var(--semi-color-text-1)' }}>{headerText}</Typography.Text>
|
|
|
|
|
- {thinkingSource && (
|
|
|
|
|
- <Tag size="small" color='green' shape="circle" style={{ marginLeft: '8px' }}>
|
|
|
|
|
- {thinkingSource}
|
|
|
|
|
- </Tag>
|
|
|
|
|
- )}
|
|
|
|
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
|
|
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg">
|
|
|
|
|
+ <Brain className="text-white" size={16} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex flex-col">
|
|
|
|
|
+ <Typography.Text strong className="text-gray-800 text-base">
|
|
|
|
|
+ {headerText}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ {thinkingSource && (
|
|
|
|
|
+ <Typography.Text className="text-gray-500 text-xs mt-0.5">
|
|
|
|
|
+ 来源: {thinkingSource}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div>
|
|
|
|
|
- {isThinkingStatus && <IconSpin spin />}
|
|
|
|
|
- {!isThinkingStatus && (message.isReasoningExpanded ? <IconChevronUp size="small" /> : <IconChevronRight size="small" />)}
|
|
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ {isThinkingStatus && (
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Loader2 className="animate-spin text-purple-500" size={18} />
|
|
|
|
|
+ <Typography.Text className="text-purple-600 text-sm font-medium">
|
|
|
|
|
+ 思考中
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {!isThinkingStatus && (
|
|
|
|
|
+ <div className="w-6 h-6 rounded-full bg-purple-100 flex items-center justify-center">
|
|
|
|
|
+ {message.isReasoningExpanded ?
|
|
|
|
|
+ <ChevronUp size={16} className="text-purple-600" /> :
|
|
|
|
|
+ <ChevronRight size={16} className="text-purple-600" />
|
|
|
|
|
+ }
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div
|
|
<div
|
|
|
- style={{
|
|
|
|
|
- maxHeight: message.isReasoningExpanded ? '160px' : '0px',
|
|
|
|
|
- overflowY: message.isReasoningExpanded ? 'auto' : 'hidden',
|
|
|
|
|
- overflowX: 'hidden',
|
|
|
|
|
- transition: 'max-height 0.3s ease-in-out, padding 0.3s ease-in-out',
|
|
|
|
|
- padding: message.isReasoningExpanded ? '0px 12px 12px 12px' : '0px 12px',
|
|
|
|
|
- boxSizing: 'border-box',
|
|
|
|
|
- }}
|
|
|
|
|
|
|
+ className={`transition-all duration-500 ease-out ${message.isReasoningExpanded ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
|
|
|
|
+ } overflow-hidden`}
|
|
|
>
|
|
>
|
|
|
- <MarkdownRender raw={finalExtractedThinkingContent} />
|
|
|
|
|
|
|
+ {message.isReasoningExpanded && (
|
|
|
|
|
+ <div className="p-5 pt-4">
|
|
|
|
|
+ <div className="bg-white/70 backdrop-blur-sm rounded-xl p-4 shadow-inner overflow-x-auto max-h-50 overflow-y-auto">
|
|
|
|
|
+ <div className="prose prose-sm prose-purple max-w-none">
|
|
|
|
|
+ <MarkdownRender raw={finalExtractedThinkingContent} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
{(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && (
|
|
{(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && (
|
|
|
- <MarkdownRender raw={finalDisplayableFinalContent} />
|
|
|
|
|
- )}
|
|
|
|
|
- {!(finalExtractedThinkingContent) && !(finalDisplayableFinalContent && finalDisplayableFinalContent.trim() !== '') && message.role === 'assistant' && (
|
|
|
|
|
- <div></div>
|
|
|
|
|
|
|
+ <div className="prose prose-sm prose-gray max-w-none overflow-x-auto">
|
|
|
|
|
+ <MarkdownRender raw={finalDisplayableFinalContent} />
|
|
|
|
|
+ </div>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
@@ -615,115 +1207,460 @@ const Playground = () => {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <Layout style={{ height: '100%' }}>
|
|
|
|
|
- {(showSettings || !styleState.isMobile) && (
|
|
|
|
|
- <Layout.Sider
|
|
|
|
|
- style={{ display: styleState.isMobile ? 'block' : 'initial' }}
|
|
|
|
|
- >
|
|
|
|
|
- <Card style={commonOuterStyle}>
|
|
|
|
|
- <div style={{ marginTop: 10 }}>
|
|
|
|
|
- <Typography.Text strong>{t('分组')}:</Typography.Text>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Select
|
|
|
|
|
- placeholder={t('请选择分组')}
|
|
|
|
|
- name='group'
|
|
|
|
|
- required
|
|
|
|
|
- selection
|
|
|
|
|
- onChange={(value) => {
|
|
|
|
|
- handleInputChange('group', value);
|
|
|
|
|
- }}
|
|
|
|
|
- value={inputs.group}
|
|
|
|
|
- autoComplete='new-password'
|
|
|
|
|
- optionList={groups}
|
|
|
|
|
- renderOptionItem={renderGroupOption}
|
|
|
|
|
- style={{ width: '100%' }}
|
|
|
|
|
- />
|
|
|
|
|
- <div style={{ marginTop: 10 }}>
|
|
|
|
|
- <Typography.Text strong>{t('模型')}:</Typography.Text>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Select
|
|
|
|
|
- placeholder={t('请选择模型')}
|
|
|
|
|
- name='model'
|
|
|
|
|
- required
|
|
|
|
|
- selection
|
|
|
|
|
- searchPosition='dropdown'
|
|
|
|
|
- filter
|
|
|
|
|
- onChange={(value) => {
|
|
|
|
|
- handleInputChange('model', value);
|
|
|
|
|
- }}
|
|
|
|
|
- value={inputs.model}
|
|
|
|
|
- autoComplete='new-password'
|
|
|
|
|
- optionList={models}
|
|
|
|
|
- />
|
|
|
|
|
- <div style={{ marginTop: 10 }}>
|
|
|
|
|
- <Typography.Text strong>Temperature:</Typography.Text>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Slider
|
|
|
|
|
- step={0.1}
|
|
|
|
|
- min={0.1}
|
|
|
|
|
- max={1}
|
|
|
|
|
- value={inputs.temperature}
|
|
|
|
|
- onChange={(value) => {
|
|
|
|
|
- handleInputChange('temperature', value);
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- <div style={{ marginTop: 10 }}>
|
|
|
|
|
- <Typography.Text strong>MaxTokens:</Typography.Text>
|
|
|
|
|
- </div>
|
|
|
|
|
- <Input
|
|
|
|
|
- placeholder='MaxTokens'
|
|
|
|
|
- name='max_tokens'
|
|
|
|
|
- required
|
|
|
|
|
- autoComplete='new-password'
|
|
|
|
|
- defaultValue={0}
|
|
|
|
|
- value={inputs.max_tokens}
|
|
|
|
|
- onChange={(value) => {
|
|
|
|
|
- handleInputChange('max_tokens', value);
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <div className="min-h-screen bg-gray-50">
|
|
|
|
|
+ <Layout style={{ height: '100vh', background: 'transparent' }}>
|
|
|
|
|
+ {(showSettings || !styleState.isMobile) && (
|
|
|
|
|
+ <Layout.Sider
|
|
|
|
|
+ style={{
|
|
|
|
|
+ background: 'transparent',
|
|
|
|
|
+ borderRight: 'none',
|
|
|
|
|
+ flexShrink: 0,
|
|
|
|
|
+ minWidth: 320,
|
|
|
|
|
+ maxWidth: 320,
|
|
|
|
|
+ height: 'calc(100vh - 100px)',
|
|
|
|
|
+ }}
|
|
|
|
|
+ width={320}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Card className="!rounded-2xl h-full flex flex-col" bodyStyle={{ padding: '24px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-6 flex-shrink-0">
|
|
|
|
|
+ <div className="flex items-center">
|
|
|
|
|
+ <div className="w-10 h-10 rounded-full bg-gradient-to-r from-purple-500 to-blue-500 flex items-center justify-center mr-3">
|
|
|
|
|
+ <SlidersHorizontal size={20} className="text-white" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Typography.Title heading={5} className="mb-0">
|
|
|
|
|
+ {t('模型设置')}
|
|
|
|
|
+ </Typography.Title>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <DebugToggle />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-6 overflow-y-auto flex-1 pr-2 model-settings-scroll">
|
|
|
|
|
+ {/* 分组选择 */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <Users size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ {t('分组')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Select
|
|
|
|
|
+ placeholder={t('请选择分组')}
|
|
|
|
|
+ name='group'
|
|
|
|
|
+ required
|
|
|
|
|
+ selection
|
|
|
|
|
+ onChange={(value) => handleInputChange('group', value)}
|
|
|
|
|
+ value={inputs.group}
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ optionList={groups}
|
|
|
|
|
+ renderOptionItem={renderGroupOption}
|
|
|
|
|
+ style={{ width: '100%' }}
|
|
|
|
|
+ className="!rounded-lg"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 模型选择 */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <Sparkles size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ {t('模型')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Select
|
|
|
|
|
+ placeholder={t('请选择模型')}
|
|
|
|
|
+ name='model'
|
|
|
|
|
+ required
|
|
|
|
|
+ selection
|
|
|
|
|
+ searchPosition='dropdown'
|
|
|
|
|
+ filter
|
|
|
|
|
+ onChange={(value) => handleInputChange('model', value)}
|
|
|
|
|
+ value={inputs.model}
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ optionList={models}
|
|
|
|
|
+ className="!rounded-lg"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Temperature */}
|
|
|
|
|
+ <div className={`transition-opacity duration-200 ${!parameterEnabled.temperature ? 'opacity-50' : ''}`}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Thermometer size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ Temperature
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Tag size="small" className="!rounded-full">
|
|
|
|
|
+ {inputs.temperature}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={parameterEnabled.temperature ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={parameterEnabled.temperature ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={parameterEnabled.temperature ? <Check size={10} /> : <X size={10} />}
|
|
|
|
|
+ onClick={() => handleParameterToggle('temperature')}
|
|
|
|
|
+ className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
|
|
+ 控制输出的随机性和创造性
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Slider
|
|
|
|
|
+ step={0.1}
|
|
|
|
|
+ min={0.1}
|
|
|
|
|
+ max={1}
|
|
|
|
|
+ value={inputs.temperature}
|
|
|
|
|
+ onChange={(value) => handleInputChange('temperature', value)}
|
|
|
|
|
+ className="mt-2"
|
|
|
|
|
+ disabled={!parameterEnabled.temperature}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Top P */}
|
|
|
|
|
+ <div className={`transition-opacity duration-200 ${!parameterEnabled.top_p ? 'opacity-50' : ''}`}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Target size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ Top P
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Tag size="small" className="!rounded-full">
|
|
|
|
|
+ {inputs.top_p}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={parameterEnabled.top_p ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={parameterEnabled.top_p ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={parameterEnabled.top_p ? <Check size={10} /> : <X size={10} />}
|
|
|
|
|
+ onClick={() => handleParameterToggle('top_p')}
|
|
|
|
|
+ className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
|
|
+ 核采样,控制词汇选择的多样性
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Slider
|
|
|
|
|
+ step={0.1}
|
|
|
|
|
+ min={0.1}
|
|
|
|
|
+ max={1}
|
|
|
|
|
+ value={inputs.top_p}
|
|
|
|
|
+ onChange={(value) => handleInputChange('top_p', value)}
|
|
|
|
|
+ className="mt-2"
|
|
|
|
|
+ disabled={!parameterEnabled.top_p}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Frequency Penalty */}
|
|
|
|
|
+ <div className={`transition-opacity duration-200 ${!parameterEnabled.frequency_penalty ? 'opacity-50' : ''}`}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Repeat size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ Frequency Penalty
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Tag size="small" className="!rounded-full">
|
|
|
|
|
+ {inputs.frequency_penalty}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={parameterEnabled.frequency_penalty ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={parameterEnabled.frequency_penalty ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={parameterEnabled.frequency_penalty ? <Check size={10} /> : <X size={10} />}
|
|
|
|
|
+ onClick={() => handleParameterToggle('frequency_penalty')}
|
|
|
|
|
+ className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
|
|
+ 频率惩罚,减少重复词汇的出现
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Slider
|
|
|
|
|
+ step={0.1}
|
|
|
|
|
+ min={-2}
|
|
|
|
|
+ max={2}
|
|
|
|
|
+ value={inputs.frequency_penalty}
|
|
|
|
|
+ onChange={(value) => handleInputChange('frequency_penalty', value)}
|
|
|
|
|
+ className="mt-2"
|
|
|
|
|
+ disabled={!parameterEnabled.frequency_penalty}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div style={{ marginTop: 10 }}>
|
|
|
|
|
- <Typography.Text strong>System:</Typography.Text>
|
|
|
|
|
|
|
+ {/* Presence Penalty */}
|
|
|
|
|
+ <div className={`transition-opacity duration-200 ${!parameterEnabled.presence_penalty ? 'opacity-50' : ''}`}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Ban size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ Presence Penalty
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Tag size="small" className="!rounded-full">
|
|
|
|
|
+ {inputs.presence_penalty}
|
|
|
|
|
+ </Tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={parameterEnabled.presence_penalty ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={parameterEnabled.presence_penalty ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={parameterEnabled.presence_penalty ? <Check size={10} /> : <X size={10} />}
|
|
|
|
|
+ onClick={() => handleParameterToggle('presence_penalty')}
|
|
|
|
|
+ className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Typography.Text className="text-xs text-gray-500 mb-2">
|
|
|
|
|
+ 存在惩罚,鼓励讨论新话题
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Slider
|
|
|
|
|
+ step={0.1}
|
|
|
|
|
+ min={-2}
|
|
|
|
|
+ max={2}
|
|
|
|
|
+ value={inputs.presence_penalty}
|
|
|
|
|
+ onChange={(value) => handleInputChange('presence_penalty', value)}
|
|
|
|
|
+ className="mt-2"
|
|
|
|
|
+ disabled={!parameterEnabled.presence_penalty}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* MaxTokens */}
|
|
|
|
|
+ <div className={`transition-opacity duration-200 ${!parameterEnabled.max_tokens ? 'opacity-50' : ''}`}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Hash size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ Max Tokens
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={parameterEnabled.max_tokens ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={parameterEnabled.max_tokens ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={parameterEnabled.max_tokens ? <Check size={10} /> : <X size={10} />}
|
|
|
|
|
+ onClick={() => handleParameterToggle('max_tokens')}
|
|
|
|
|
+ className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder='MaxTokens'
|
|
|
|
|
+ name='max_tokens'
|
|
|
|
|
+ required
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ defaultValue={0}
|
|
|
|
|
+ value={inputs.max_tokens}
|
|
|
|
|
+ onChange={(value) => handleInputChange('max_tokens', value)}
|
|
|
|
|
+ className="!rounded-lg"
|
|
|
|
|
+ disabled={!parameterEnabled.max_tokens}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Seed */}
|
|
|
|
|
+ <div className={`transition-opacity duration-200 ${!parameterEnabled.seed ? 'opacity-50' : ''}`}>
|
|
|
|
|
+ <div className="flex items-center justify-between mb-2">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Shuffle size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ Seed
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ <Typography.Text className="text-xs text-gray-400">
|
|
|
|
|
+ (可选,用于复现结果)
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={parameterEnabled.seed ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={parameterEnabled.seed ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ icon={parameterEnabled.seed ? <Check size={10} /> : <X size={10} />}
|
|
|
|
|
+ onClick={() => handleParameterToggle('seed')}
|
|
|
|
|
+ className="!rounded-full !w-6 !h-6 !p-0 !min-w-0"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder='随机种子 (留空为随机)'
|
|
|
|
|
+ name='seed'
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ value={inputs.seed || ''}
|
|
|
|
|
+ onChange={(value) => handleInputChange('seed', value === '' ? null : value)}
|
|
|
|
|
+ className="!rounded-lg"
|
|
|
|
|
+ disabled={!parameterEnabled.seed}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Stream Toggle */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <ToggleLeft size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ 流式输出
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ theme={inputs.stream ? 'solid' : 'borderless'}
|
|
|
|
|
+ type={inputs.stream ? 'primary' : 'tertiary'}
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ onClick={() => handleInputChange('stream', !inputs.stream)}
|
|
|
|
|
+ className="!rounded-full"
|
|
|
|
|
+ >
|
|
|
|
|
+ {inputs.stream ? '开启' : '关闭'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* System Prompt */}
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="flex items-center gap-2 mb-2">
|
|
|
|
|
+ <Type size={16} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text strong className="text-sm">
|
|
|
|
|
+ System Prompt
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <TextArea
|
|
|
|
|
+ placeholder='System Prompt'
|
|
|
|
|
+ name='system'
|
|
|
|
|
+ required
|
|
|
|
|
+ autoComplete='new-password'
|
|
|
|
|
+ autosize
|
|
|
|
|
+ defaultValue={systemPrompt}
|
|
|
|
|
+ onChange={(value) => setSystemPrompt(value)}
|
|
|
|
|
+ className="!rounded-lg"
|
|
|
|
|
+ maxHeight={200}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </Layout.Sider>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ <Layout.Content className="relative flex-1 overflow-hidden">
|
|
|
|
|
+ <div className="px-4 overflow-hidden flex gap-4" style={{ height: 'calc(100vh - 100px)' }}>
|
|
|
|
|
+ <div className="flex-1 flex flex-col">
|
|
|
|
|
+ <SettingsToggle />
|
|
|
|
|
+ <Card
|
|
|
|
|
+ className="!rounded-2xl h-full"
|
|
|
|
|
+ bodyStyle={{ padding: 0, height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
|
|
|
|
+ >
|
|
|
|
|
+ {/* 聊天头部 */}
|
|
|
|
|
+ <div className="px-6 py-4 bg-gradient-to-r from-purple-500 to-blue-500 rounded-t-2xl">
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ <div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur flex items-center justify-center">
|
|
|
|
|
+ <MessageSquare size={20} className="text-white" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Typography.Title heading={5} className="!text-white mb-0">
|
|
|
|
|
+ {t('AI 对话')}
|
|
|
|
|
+ </Typography.Title>
|
|
|
|
|
+ <Typography.Text className="!text-white/80 text-sm">
|
|
|
|
|
+ {inputs.model || t('选择模型开始对话')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 聊天内容区域 */}
|
|
|
|
|
+ <div className="flex-1 overflow-hidden">
|
|
|
|
|
+ <Chat
|
|
|
|
|
+ chatBoxRenderConfig={{
|
|
|
|
|
+ renderChatBoxContent: renderCustomChatContent,
|
|
|
|
|
+ renderChatBoxAction: renderChatBoxAction,
|
|
|
|
|
+ }}
|
|
|
|
|
+ renderInputArea={renderInputArea}
|
|
|
|
|
+ roleConfig={roleInfo}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ height: '100%',
|
|
|
|
|
+ maxWidth: '100%',
|
|
|
|
|
+ overflow: 'hidden'
|
|
|
|
|
+ }}
|
|
|
|
|
+ chats={message}
|
|
|
|
|
+ onMessageSend={onMessageSend}
|
|
|
|
|
+ onMessageCopy={handleMessageCopy}
|
|
|
|
|
+ onMessageReset={handleMessageReset}
|
|
|
|
|
+ onMessageDelete={handleMessageDelete}
|
|
|
|
|
+ showClearContext
|
|
|
|
|
+ showStopGenerate
|
|
|
|
|
+ onStopGenerator={onStopGenerator}
|
|
|
|
|
+ onClear={() => setMessage([])}
|
|
|
|
|
+ className="h-full"
|
|
|
|
|
+ placeholder={t('请输入您的问题...')}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Card>
|
|
|
</div>
|
|
</div>
|
|
|
- <TextArea
|
|
|
|
|
- placeholder='System Prompt'
|
|
|
|
|
- name='system'
|
|
|
|
|
- required
|
|
|
|
|
- autoComplete='new-password'
|
|
|
|
|
- autosize
|
|
|
|
|
- defaultValue={systemPrompt}
|
|
|
|
|
- onChange={(value) => {
|
|
|
|
|
- setSystemPrompt(value);
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- </Card>
|
|
|
|
|
- </Layout.Sider>
|
|
|
|
|
- )}
|
|
|
|
|
- <Layout.Content>
|
|
|
|
|
- <div style={{ height: '100%', position: 'relative' }}>
|
|
|
|
|
- <SettingsToggle />
|
|
|
|
|
- <Chat
|
|
|
|
|
- chatBoxRenderConfig={{
|
|
|
|
|
- renderChatBoxContent: renderCustomChatContent,
|
|
|
|
|
- renderChatBoxAction: () => {
|
|
|
|
|
- return <div></div>;
|
|
|
|
|
- },
|
|
|
|
|
- }}
|
|
|
|
|
- renderInputArea={renderInputArea}
|
|
|
|
|
- roleConfig={roleInfo}
|
|
|
|
|
- style={commonOuterStyle}
|
|
|
|
|
- chats={message}
|
|
|
|
|
- onMessageSend={onMessageSend}
|
|
|
|
|
- showClearContext
|
|
|
|
|
- showStopGenerate
|
|
|
|
|
- onStopGenerator={onStopGenerator}
|
|
|
|
|
- onClear={() => {
|
|
|
|
|
- setMessage([]);
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </Layout.Content>
|
|
|
|
|
- </Layout>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {/* 调试面板 */}
|
|
|
|
|
+ {showDebugPanel && (
|
|
|
|
|
+ <div className="w-96 flex-shrink-0">
|
|
|
|
|
+ <Card className="!rounded-2xl h-full flex flex-col" bodyStyle={{ padding: '24px', height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
|
|
|
+ <div className="flex items-center mb-6 flex-shrink-0">
|
|
|
|
|
+ <div className="w-10 h-10 rounded-full bg-gradient-to-r from-green-500 to-blue-500 flex items-center justify-center mr-3">
|
|
|
|
|
+ <Code size={20} className="text-white" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Typography.Title heading={5} className="mb-0">
|
|
|
|
|
+ {t('调试信息')}
|
|
|
|
|
+ </Typography.Title>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex-1 overflow-hidden debug-panel">
|
|
|
|
|
+ <Tabs
|
|
|
|
|
+ type="line"
|
|
|
|
|
+ className="h-full"
|
|
|
|
|
+ style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
|
|
|
|
+ activeKey={activeDebugTab}
|
|
|
|
|
+ onChange={setActiveDebugTab}
|
|
|
|
|
+ >
|
|
|
|
|
+ <TabPane tab={
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <FileText size={16} />
|
|
|
|
|
+ {t('请求体')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ } itemKey="request">
|
|
|
|
|
+ <div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
|
|
|
|
+ {debugData.request ? (
|
|
|
|
|
+ <pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
|
|
|
|
+ {JSON.stringify(debugData.request, null, 2)}
|
|
|
|
|
+ </pre>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Typography.Text type="secondary" className="text-sm">
|
|
|
|
|
+ {t('暂无请求数据')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </TabPane>
|
|
|
|
|
+
|
|
|
|
|
+ <TabPane tab={
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Zap size={16} />
|
|
|
|
|
+ {t('响应内容')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ } itemKey="response">
|
|
|
|
|
+ <div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
|
|
|
|
+ {debugData.response ? (
|
|
|
|
|
+ <pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
|
|
|
|
+ {debugData.response}
|
|
|
|
|
+ </pre>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Typography.Text type="secondary" className="text-sm">
|
|
|
|
|
+ {t('暂无响应数据')}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </TabPane>
|
|
|
|
|
+ </Tabs>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {debugData.timestamp && (
|
|
|
|
|
+ <div className="flex items-center gap-2 mt-4 pt-4 flex-shrink-0">
|
|
|
|
|
+ <Clock size={14} className="text-gray-500" />
|
|
|
|
|
+ <Typography.Text className="text-xs text-gray-500">
|
|
|
|
|
+ {t('最后更新')}: {new Date(debugData.timestamp).toLocaleString()}
|
|
|
|
|
+ </Typography.Text>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Layout.Content>
|
|
|
|
|
+ </Layout>
|
|
|
|
|
+ </div>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|
|
|
|
|
|