feat: enhance debug panel with VS Code dark theme and syntax highlighting

- Create new CodeViewer component with VS Code dark theme styling
- Implement custom JSON syntax highlighting with proper color coding
- Add improved copy functionality with hover effects and user feedback
- Refactor DebugPanel to use the new CodeViewer component
- Apply dark background (#1e1e1e) with syntax colors matching VS Code
- Enhance UX with floating copy button and responsive design
- Support automatic JSON formatting and parsing
- Maintain compatibility with existing Semi Design components

The debug panel now displays preview requests, actual requests, and
responses in a professional code editor style, improving readability
and developer experience in the playground interface.
This commit is contained in:
Apple\Apple
2025-05-31 02:47:31 +08:00
parent 71df716787
commit 6242cc31f2
2 changed files with 210 additions and 35 deletions

View File

@@ -0,0 +1,194 @@
import React, { useState } from 'react';
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
import { Copy } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { copy } from '../../helpers/utils';
// VS Code 深色主题样式
const codeThemeStyles = {
container: {
backgroundColor: '#1e1e1e',
color: '#d4d4d4',
fontFamily: 'Consolas, "Courier New", Monaco, "SF Mono", monospace',
fontSize: '13px',
lineHeight: '1.4',
borderRadius: '8px',
border: '1px solid #3c3c3c',
position: 'relative',
overflow: 'hidden',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
},
content: {
height: '100%',
overflowY: 'auto',
overflowX: 'auto',
padding: '16px',
margin: 0,
whiteSpace: 'pre',
wordBreak: 'normal',
background: '#1e1e1e',
},
copyButton: {
position: 'absolute',
top: '12px',
right: '12px',
zIndex: 10,
backgroundColor: 'rgba(45, 45, 45, 0.9)',
border: '1px solid rgba(255, 255, 255, 0.1)',
color: '#d4d4d4',
borderRadius: '6px',
transition: 'all 0.2s ease',
},
copyButtonHover: {
backgroundColor: 'rgba(60, 60, 60, 0.95)',
borderColor: 'rgba(255, 255, 255, 0.2)',
transform: 'scale(1.05)',
},
noContent: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: '#666',
fontSize: '14px',
fontStyle: 'italic',
backgroundColor: 'var(--semi-color-fill-0)',
borderRadius: '8px',
}
};
// 自定义 JSON 高亮器(使用 VS Code 深色主题配色)
const highlightJson = (str) => {
return str.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,
(match) => {
let color = '#b5cea8'; // 数字颜色 (绿色)
if (/^"/.test(match)) {
if (/:$/.test(match)) {
color = '#9cdcfe'; // 键名颜色 (蓝色)
} else {
color = '#ce9178'; // 字符串值颜色 (橙色)
}
} else if (/true|false/.test(match)) {
color = '#569cd6'; // 布尔值颜色 (蓝色)
} else if (/null/.test(match)) {
color = '#569cd6'; // null 值颜色 (蓝色)
}
return `<span style="color: ${color}">${match}</span>`;
}
);
};
const CodeViewer = ({ content, title, language = 'json' }) => {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const [isHoveringCopy, setIsHoveringCopy] = useState(false);
const handleCopy = async () => {
try {
let textToCopy = content;
// 如果是对象,转换为格式化的 JSON 字符串
if (typeof content === 'object' && content !== null) {
textToCopy = JSON.stringify(content, null, 2);
}
const success = await copy(textToCopy);
if (success) {
setCopied(true);
Toast.success(t('已复制到剪贴板'));
setTimeout(() => setCopied(false), 2000);
} else {
Toast.error(t('复制失败'));
}
} catch (err) {
Toast.error(t('复制失败'));
console.error('Copy failed:', err);
}
};
// 格式化内容
const getFormattedContent = () => {
if (!content) return '';
if (typeof content === 'object') {
try {
return JSON.stringify(content, null, 2);
} catch (e) {
return String(content);
}
} else if (typeof content === 'string') {
// 尝试解析并重新格式化 JSON
try {
const parsed = JSON.parse(content);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return content;
}
}
return String(content);
};
// 获取高亮的 HTML
const getHighlightedContent = () => {
const formattedContent = getFormattedContent();
if (language === 'json') {
return highlightJson(formattedContent);
}
// 对于非 JSON 内容,使用简单的文本高亮
return formattedContent;
};
if (!content) {
return (
<div style={codeThemeStyles.noContent}>
<span>
{title === 'preview' ? t('正在构造请求体预览...') :
title === 'request' ? t('暂无请求数据') :
t('暂无响应数据')}
</span>
</div>
);
}
return (
<div style={codeThemeStyles.container} className="h-full">
{/* 复制按钮 */}
<div
style={{
...codeThemeStyles.copyButton,
...(isHoveringCopy ? codeThemeStyles.copyButtonHover : {})
}}
onMouseEnter={() => setIsHoveringCopy(true)}
onMouseLeave={() => setIsHoveringCopy(false)}
>
<Tooltip content={copied ? t('已复制') : t('复制代码')}>
<Button
icon={<Copy size={14} />}
onClick={handleCopy}
size="small"
theme="borderless"
style={{
backgroundColor: 'transparent',
border: 'none',
color: copied ? '#4ade80' : '#d4d4d4',
padding: '6px',
}}
/>
</Tooltip>
</div>
{/* 代码内容 */}
<div
style={codeThemeStyles.content}
className="model-settings-scroll"
dangerouslySetInnerHTML={{ __html: getHighlightedContent() }}
/>
</div>
);
};
export default CodeViewer;

View File

@@ -16,6 +16,7 @@ import {
Send,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import CodeViewer from './CodeViewer';
const DebugPanel = ({
debugData,
@@ -129,17 +130,11 @@ const DebugPanel = ({
{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>
<CodeViewer
content={debugData.previewRequest}
title="preview"
language="json"
/>
</TabPane>
<TabPane tab={
@@ -148,19 +143,11 @@ const DebugPanel = ({
{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>
<CodeViewer
content={debugData.request}
title="request"
language="json"
/>
</TabPane>
<TabPane tab={
@@ -169,17 +156,11 @@ const DebugPanel = ({
{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>
<CodeViewer
content={debugData.response}
title="response"
language="javascript"
/>
</TabPane>
</Tabs>
</div>