✨ feat: enhance debug panel with real-time preview and collapsible tabs
- Add real-time request body preview that updates when parameters change - Implement pre-constructed payload generation for debugging without sending requests - Add support for image URLs in preview payload construction - Upgrade debug panel to card-style tabs with custom arrow navigation - Add collapsible functionality and dropdown menu for tab selection - Integrate image-enabled messages with proper multimodal content structure - Refactor tab state management with internal useState and external sync - Remove redundant status labels and clean up component structure - Set preview tab as default active tab for better UX - Maintain backward compatibility with existing debug functionality This enhancement significantly improves the debugging experience by allowing developers to see exactly what request will be sent before actually sending it, with real-time updates as they adjust parameters, models, or image settings.
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Typography,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Button,
|
||||
Dropdown,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Code,
|
||||
FileText,
|
||||
Zap,
|
||||
Clock,
|
||||
X,
|
||||
Eye,
|
||||
Send,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -24,6 +26,61 @@ const DebugPanel = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [activeKey, setActiveKey] = useState(activeDebugTab);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveKey(activeDebugTab);
|
||||
}, [activeDebugTab]);
|
||||
|
||||
const handleTabChange = (key) => {
|
||||
setActiveKey(key);
|
||||
onActiveDebugTabChange(key);
|
||||
};
|
||||
|
||||
const renderArrow = (items, pos, handleArrowClick, defaultNode) => {
|
||||
const style = {
|
||||
width: 32,
|
||||
height: 32,
|
||||
margin: '0 12px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: '100%',
|
||||
background: 'rgba(var(--semi-grey-1), 1)',
|
||||
color: 'var(--semi-color-text)',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
{items.map(item => {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={item.itemKey}
|
||||
onClick={() => handleTabChange(item.itemKey)}
|
||||
>
|
||||
{item.tab}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
{pos === 'start' ? (
|
||||
<div style={style} onClick={handleArrowClick}>
|
||||
←
|
||||
</div>
|
||||
) : (
|
||||
<div style={style} onClick={handleArrowClick}>
|
||||
→
|
||||
</div>
|
||||
)}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="!rounded-2xl h-full flex flex-col"
|
||||
@@ -44,7 +101,6 @@ const DebugPanel = ({
|
||||
</Typography.Title>
|
||||
</div>
|
||||
|
||||
{/* 移动端关闭按钮 */}
|
||||
{styleState.isMobile && onCloseDebugPanel && (
|
||||
<Button
|
||||
icon={<X size={16} />}
|
||||
@@ -59,23 +115,46 @@ const DebugPanel = ({
|
||||
|
||||
<div className="flex-1 overflow-hidden debug-panel">
|
||||
<Tabs
|
||||
type="line"
|
||||
renderArrow={renderArrow}
|
||||
type="card"
|
||||
collapsible
|
||||
className="h-full"
|
||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||
activeKey={activeDebugTab}
|
||||
onChange={onActiveDebugTabChange}
|
||||
activeKey={activeKey}
|
||||
onChange={handleTabChange}
|
||||
>
|
||||
<TabPane tab={
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={16} />
|
||||
{t('请求体')}
|
||||
<Eye size={16} />
|
||||
{t('预览请求体')}
|
||||
</div>
|
||||
} itemKey="preview">
|
||||
<div className="h-full overflow-y-auto bg-gray-50 rounded-lg p-4 model-settings-scroll">
|
||||
{debugData.previewRequest ? (
|
||||
<pre className="debug-code text-gray-700 whitespace-pre-wrap break-words">
|
||||
{JSON.stringify(debugData.previewRequest, null, 2)}
|
||||
</pre>
|
||||
) : (
|
||||
<Typography.Text type="secondary" className="text-sm">
|
||||
{t('正在构造请求体预览...')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab={
|
||||
<div className="flex items-center gap-2">
|
||||
<Send 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>
|
||||
<>
|
||||
<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('暂无请求数据')}
|
||||
@@ -105,14 +184,20 @@ const DebugPanel = ({
|
||||
</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>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-4 pt-4 flex-shrink-0">
|
||||
{(debugData.timestamp || debugData.previewTimestamp) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock size={14} className="text-gray-500" />
|
||||
<Typography.Text className="text-xs text-gray-500">
|
||||
{activeKey === 'preview' && debugData.previewTimestamp
|
||||
? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}`
|
||||
: debugData.timestamp
|
||||
? `${t('最后请求')}: ${new Date(debugData.timestamp).toLocaleString()}`
|
||||
: ''}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,9 +38,6 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab
|
||||
<Typography.Text strong className="text-sm">
|
||||
图片地址
|
||||
</Typography.Text>
|
||||
<Typography.Text className="text-xs text-gray-400">
|
||||
(多模态对话)
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
|
||||
@@ -181,7 +181,7 @@ const SettingsPanel = ({
|
||||
|
||||
{/* 桌面端的配置管理放在底部 */}
|
||||
{!styleState.isMobile && (
|
||||
<div className="flex-shrink-0 mt-4 pt-3">
|
||||
<div className="flex-shrink-0 pt-3">
|
||||
<ConfigManager
|
||||
currentConfig={currentConfig}
|
||||
onConfigImport={onConfigImport}
|
||||
|
||||
@@ -102,13 +102,149 @@ const Playground = () => {
|
||||
response: null,
|
||||
timestamp: null
|
||||
});
|
||||
const [activeDebugTab, setActiveDebugTab] = useState('request');
|
||||
const [activeDebugTab, setActiveDebugTab] = useState('preview');
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const sseSourceRef = useRef(null);
|
||||
const chatRef = useRef(null);
|
||||
|
||||
const saveConfigTimeoutRef = useRef(null);
|
||||
|
||||
const [previewPayload, setPreviewPayload] = useState(null);
|
||||
|
||||
const constructPreviewPayload = useCallback(() => {
|
||||
try {
|
||||
let systemMessage = null;
|
||||
if (systemPrompt !== '') {
|
||||
systemMessage = {
|
||||
role: 'system',
|
||||
id: '1',
|
||||
createAt: 1715676751919,
|
||||
content: systemPrompt,
|
||||
};
|
||||
}
|
||||
|
||||
let messages = message.map((item) => {
|
||||
return {
|
||||
role: item.role,
|
||||
content: item.content,
|
||||
};
|
||||
});
|
||||
|
||||
if (messages.length === 0 || messages.every(msg => msg.role !== 'user')) {
|
||||
const validImageUrls = inputs.imageUrls ? inputs.imageUrls.filter(url => url.trim() !== '') : [];
|
||||
|
||||
if (inputs.imageEnabled && validImageUrls.length > 0) {
|
||||
const messageContent = [
|
||||
{
|
||||
type: 'text',
|
||||
text: '你好'
|
||||
},
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: url.trim(),
|
||||
},
|
||||
})),
|
||||
];
|
||||
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: messageContent
|
||||
});
|
||||
} else {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: '你好'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const lastUserMessageIndex = messages.length - 1;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === 'user') {
|
||||
if (inputs.imageEnabled && inputs.imageUrls) {
|
||||
const validImageUrls = inputs.imageUrls.filter(url => url.trim() !== '');
|
||||
if (validImageUrls.length > 0) {
|
||||
let textContent = '示例消息';
|
||||
|
||||
if (typeof messages[i].content === 'string') {
|
||||
textContent = messages[i].content;
|
||||
} else if (Array.isArray(messages[i].content)) {
|
||||
const textPart = messages[i].content.find(item => item.type === 'text');
|
||||
if (textPart && textPart.text) {
|
||||
textContent = textPart.text;
|
||||
}
|
||||
}
|
||||
|
||||
messages[i] = {
|
||||
...messages[i],
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: textContent
|
||||
},
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: url.trim(),
|
||||
},
|
||||
})),
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
} catch (error) {
|
||||
console.error('构造预览请求体失败:', error);
|
||||
return null;
|
||||
}
|
||||
}, [inputs, parameterEnabled, systemPrompt, message]);
|
||||
|
||||
useEffect(() => {
|
||||
const newPreviewPayload = constructPreviewPayload();
|
||||
setPreviewPayload(newPreviewPayload);
|
||||
|
||||
setDebugData(prev => ({
|
||||
...prev,
|
||||
previewRequest: newPreviewPayload,
|
||||
previewTimestamp: new Date().toISOString()
|
||||
}));
|
||||
}, [constructPreviewPayload]);
|
||||
|
||||
const debouncedSaveConfig = useCallback(() => {
|
||||
if (saveConfigTimeoutRef.current) {
|
||||
clearTimeout(saveConfigTimeoutRef.current);
|
||||
|
||||
Reference in New Issue
Block a user