Explorar o código

⚡ perf: optimize CodeViewer performance for large content rendering

- Add intelligent content truncation for payloads over 50K characters
- Implement tiered performance handling based on content size
- Use useMemo and useCallback for memory optimization and caching
- Add progressive loading with async processing for very large content
- Introduce performance warning indicators and user feedback
- Create expand/collapse functionality with smooth animations
- Skip syntax highlighting for extremely large content (>100K)
- Add loading states and debounce handling for better UX
- Display remaining content size indicators (e.g., +15K)
- Implement chunk-based processing to prevent UI blocking

This optimization ensures that even multi-megabyte JSON responses
won't cause page freezing or performance degradation in the debug
panel, while maintaining full functionality for regular-sized content.
Apple\Apple hai 9 meses
pai
achega
d1b192cd72
Modificáronse 1 ficheiros con 194 adicións e 98 borrados
  1. 194 98
      web/src/components/playground/CodeViewer.js

+ 194 - 98
web/src/components/playground/CodeViewer.js

@@ -1,10 +1,15 @@
-import React, { useState } from 'react';
+import React, { useState, useMemo, useCallback } from 'react';
 import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
-import { Copy } from 'lucide-react';
+import { Copy, ChevronDown, ChevronUp } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { copy } from '../../helpers/utils';
 
-// VS Code 深色主题样式
+const PERFORMANCE_CONFIG = {
+  MAX_DISPLAY_LENGTH: 50000, // 最大显示字符数
+  PREVIEW_LENGTH: 5000, // 预览长度
+  VERY_LARGE_MULTIPLIER: 2, // 超大内容倍数
+};
+
 const codeThemeStyles = {
   container: {
     backgroundColor: '#1e1e1e',
@@ -28,10 +33,8 @@ const codeThemeStyles = {
     wordBreak: 'normal',
     background: '#1e1e1e',
   },
-  copyButton: {
+  actionButton: {
     position: 'absolute',
-    top: '12px',
-    right: '12px',
     zIndex: 10,
     backgroundColor: 'rgba(45, 45, 45, 0.9)',
     border: '1px solid rgba(255, 255, 255, 0.1)',
@@ -39,7 +42,7 @@ const codeThemeStyles = {
     borderRadius: '6px',
     transition: 'all 0.2s ease',
   },
-  copyButtonHover: {
+  actionButtonHover: {
     backgroundColor: 'rgba(60, 60, 60, 0.95)',
     borderColor: 'rgba(255, 255, 255, 0.2)',
     transform: 'scale(1.05)',
@@ -54,136 +57,172 @@ const codeThemeStyles = {
     fontStyle: 'italic',
     backgroundColor: 'var(--semi-color-fill-0)',
     borderRadius: '8px',
-  }
+  },
+  performanceWarning: {
+    padding: '8px 12px',
+    backgroundColor: 'rgba(255, 193, 7, 0.1)',
+    border: '1px solid rgba(255, 193, 7, 0.3)',
+    borderRadius: '6px',
+    color: '#ffc107',
+    fontSize: '12px',
+    marginBottom: '8px',
+    display: 'flex',
+    alignItems: 'center',
+    gap: '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'; // 数字颜色 (绿色)
+      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 值颜色 (蓝色)
+        color = /:$/.test(match) ? '#9cdcfe' : '#ce9178';
+      } else if (/true|false|null/.test(match)) {
+        color = '#569cd6';
       }
       return `<span style="color: ${color}">${match}</span>`;
     }
   );
 };
 
+const isJsonLike = (content, language) => {
+  if (language === 'json') return true;
+  const trimmed = content.trim();
+  return (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
+    (trimmed.startsWith('[') && trimmed.endsWith(']'));
+};
+
+const formatContent = (content) => {
+  if (!content) return '';
+
+  if (typeof content === 'object') {
+    try {
+      return JSON.stringify(content, null, 2);
+    } catch (e) {
+      return String(content);
+    }
+  }
+
+  if (typeof content === 'string') {
+    try {
+      const parsed = JSON.parse(content);
+      return JSON.stringify(parsed, null, 2);
+    } catch (e) {
+      return content;
+    }
+  }
+
+  return String(content);
+};
+
 const CodeViewer = ({ content, title, language = 'json' }) => {
   const { t } = useTranslation();
   const [copied, setCopied] = useState(false);
   const [isHoveringCopy, setIsHoveringCopy] = useState(false);
+  const [isExpanded, setIsExpanded] = useState(false);
+  const [isProcessing, setIsProcessing] = useState(false);
 
-  const handleCopy = async () => {
-    try {
-      let textToCopy = content;
+  const formattedContent = useMemo(() => formatContent(content), [content]);
 
-      // 如果是对象,转换为格式化的 JSON 字符串
-      if (typeof content === 'object' && content !== null) {
-        textToCopy = JSON.stringify(content, null, 2);
-      }
+  const contentMetrics = useMemo(() => {
+    const length = formattedContent.length;
+    const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH;
+    const isVeryLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER;
+    return { length, isLarge, isVeryLarge };
+  }, [formattedContent.length]);
 
-      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 displayContent = useMemo(() => {
+    if (!contentMetrics.isLarge || isExpanded) {
+      return formattedContent;
     }
-  };
+    return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) +
+      '\n\n// ... 内容被截断以提升性能 ...';
+  }, [formattedContent, contentMetrics.isLarge, isExpanded]);
 
-  // 格式化内容
-  const getFormattedContent = () => {
-    if (!content) return '';
+  const highlightedContent = useMemo(() => {
+    if (contentMetrics.isVeryLarge && !isExpanded) {
+      return displayContent;
+    }
 
-    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;
-      }
+    if (isJsonLike(displayContent, language)) {
+      return highlightJson(displayContent);
     }
 
-    return String(content);
-  };
-
-  // 获取高亮的 HTML
-  const getHighlightedContent = () => {
-    const formattedContent = getFormattedContent();
-
-    // 尝试检测是否为 JSON 格式
-    const isJsonLike = () => {
-      if (language === 'json') return true;
-
-      // 自动检测:如果内容看起来像 JSON,就用 JSON 高亮
-      const trimmed = formattedContent.trim();
-      const looksLikeJson = (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
-        (trimmed.startsWith('[') && trimmed.endsWith(']'));
-
-      // 调试日志
-      if (process.env.NODE_ENV === 'development') {
-        console.log('CodeViewer Debug:', {
-          language,
-          contentType: typeof content,
-          trimmedStart: trimmed.substring(0, 10),
-          looksLikeJson,
-          willHighlight: looksLikeJson
-        });
-      }
+    return displayContent;
+  }, [displayContent, language, contentMetrics.isVeryLarge, isExpanded]);
+
+  const handleCopy = useCallback(async () => {
+    try {
+      const textToCopy = typeof content === 'object' && content !== null
+        ? JSON.stringify(content, null, 2)
+        : content;
 
-      return looksLikeJson;
-    };
+      const success = await copy(textToCopy);
+      setCopied(true);
+      Toast.success(t('已复制到剪贴板'));
+      setTimeout(() => setCopied(false), 2000);
 
-    if (isJsonLike()) {
-      return highlightJson(formattedContent);
+      if (!success) {
+        throw new Error('Copy operation failed');
+      }
+    } catch (err) {
+      Toast.error(t('复制失败'));
+      console.error('Copy failed:', err);
     }
+  }, [content, t]);
 
-    // 对于非 JSON 内容,使用简单的文本高亮
-    return formattedContent;
-  };
+  const handleToggleExpand = useCallback(() => {
+    if (contentMetrics.isVeryLarge && !isExpanded) {
+      setIsProcessing(true);
+      setTimeout(() => {
+        setIsExpanded(true);
+        setIsProcessing(false);
+      }, 100);
+    } else {
+      setIsExpanded(!isExpanded);
+    }
+  }, [isExpanded, contentMetrics.isVeryLarge]);
 
   if (!content) {
+    const placeholderText = {
+      preview: t('正在构造请求体预览...'),
+      request: t('暂无请求数据'),
+      response: t('暂无响应数据')
+    }[title] || t('暂无数据');
+
     return (
       <div style={codeThemeStyles.noContent}>
-        <span>
-          {title === 'preview' ? t('正在构造请求体预览...') :
-            title === 'request' ? t('暂无请求数据') :
-              t('暂无响应数据')}
-        </span>
+        <span>{placeholderText}</span>
       </div>
     );
   }
 
+  const warningTop = contentMetrics.isLarge ? '52px' : '12px';
+  const contentPadding = contentMetrics.isLarge ? '52px' : '16px';
+
   return (
     <div style={codeThemeStyles.container} className="h-full">
+      {/* 性能警告 */}
+      {contentMetrics.isLarge && (
+        <div style={codeThemeStyles.performanceWarning}>
+          <span>⚡</span>
+          <span>
+            {contentMetrics.isVeryLarge
+              ? t('内容较大,已启用性能优化模式')
+              : t('内容较大,部分功能可能受限')}
+          </span>
+        </div>
+      )}
+
       {/* 复制按钮 */}
       <div
         style={{
-          ...codeThemeStyles.copyButton,
-          ...(isHoveringCopy ? codeThemeStyles.copyButtonHover : {})
+          ...codeThemeStyles.actionButton,
+          ...(isHoveringCopy ? codeThemeStyles.actionButtonHover : {}),
+          top: warningTop,
+          right: '12px',
         }}
         onMouseEnter={() => setIsHoveringCopy(true)}
         onMouseLeave={() => setIsHoveringCopy(false)}
@@ -206,10 +245,67 @@ const CodeViewer = ({ content, title, language = 'json' }) => {
 
       {/* 代码内容 */}
       <div
-        style={codeThemeStyles.content}
+        style={{
+          ...codeThemeStyles.content,
+          paddingTop: contentPadding,
+        }}
         className="model-settings-scroll"
-        dangerouslySetInnerHTML={{ __html: getHighlightedContent() }}
-      />
+      >
+        {isProcessing ? (
+          <div style={{
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'center',
+            height: '200px',
+            color: '#888'
+          }}>
+            <div style={{
+              width: '20px',
+              height: '20px',
+              border: '2px solid #444',
+              borderTop: '2px solid #888',
+              borderRadius: '50%',
+              animation: 'spin 1s linear infinite',
+              marginRight: '8px'
+            }} />
+            {t('正在处理大内容...')}
+          </div>
+        ) : (
+          <div dangerouslySetInnerHTML={{ __html: highlightedContent }} />
+        )}
+      </div>
+
+      {/* 展开/收起按钮 */}
+      {contentMetrics.isLarge && !isProcessing && (
+        <div style={{
+          ...codeThemeStyles.actionButton,
+          bottom: '12px',
+          left: '50%',
+          transform: 'translateX(-50%)',
+        }}>
+          <Tooltip content={isExpanded ? t('收起内容') : t('显示完整内容')}>
+            <Button
+              icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
+              onClick={handleToggleExpand}
+              size="small"
+              theme="borderless"
+              style={{
+                backgroundColor: 'transparent',
+                border: 'none',
+                color: '#d4d4d4',
+                padding: '6px 12px',
+              }}
+            >
+              {isExpanded ? t('收起') : t('展开')}
+              {!isExpanded && (
+                <span style={{ fontSize: '11px', opacity: 0.7, marginLeft: '4px' }}>
+                  (+{Math.round((contentMetrics.length - PERFORMANCE_CONFIG.PREVIEW_LENGTH) / 1000)}K)
+                </span>
+              )}
+            </Button>
+          </Tooltip>
+        </div>
+      )}
     </div>
   );
 };