From e9b0e1af02b05de795420ccac832823e62eb8516 Mon Sep 17 00:00:00 2001
From: hongjli <3117313295@qq.com>
Date: 星期日, 27 四月 2025 20:21:52 +0800
Subject: [PATCH] 渲染echarts图

---
 src/app/chat/page.tsx |  307 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 303 insertions(+), 4 deletions(-)

diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx
index 1301f70..e4d65cd 100644
--- a/src/app/chat/page.tsx
+++ b/src/app/chat/page.tsx
@@ -8,6 +8,11 @@
 import remarkGfm from 'remark-gfm';
 import rehypeRaw from 'rehype-raw';
 import rehypeSanitize from 'rehype-sanitize';
+import dynamic from 'next/dynamic';
+import { ReactNode } from 'react';
+
+// 鍔ㄦ�佸鍏� ECharts锛岀‘淇濆畠鍙湪瀹㈡埛绔覆鏌�
+const ReactECharts = dynamic(() => import('echarts-for-react'), { ssr: false });
 
 interface Message {
   role: 'user' | 'assistant';
@@ -31,6 +36,274 @@
 // 榛樿鐢ㄦ埛澶村儚
 const DEFAULT_USER_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E";
 const AI_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234F46E5'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 1c4.96 0 9 4.04 9 9s-4.04 9-9 9-9-4.04-9-9 4.04-9 9-9zm0 3.5c-2.48 0-4.5 2.02-4.5 4.5h1.5c0-1.66 1.34-3 3-3s3 1.34 3 3h1.5c0-2.48-2.02-4.5-4.5-4.5zM8 13h2v2H8v-2zm6 0h2v2h-2v-2z'/%3E%3C/svg%3E";
+
+// 娣诲姞鐢ㄤ簬瑙f瀽鍜屾覆鏌� ECharts 鐨勭粍浠�
+function EchartsRenderer({ code }: { code: string }) {
+  const [error, setError] = useState<string | null>(null);
+  const [isLoaded, setIsLoaded] = useState(false);
+  const chartContainerRef = useRef<HTMLDivElement>(null);
+  const codeRef = useRef<string>(code);
+  const renderAttemptedRef = useRef(false);
+  
+  // 鏇存柊浠g爜寮曠敤
+  useEffect(() => {
+    codeRef.current = code;
+  }, [code]);
+
+  // 寤惰繜鍒濆鍖栫洿鍒版秷鎭ǔ瀹�
+  useEffect(() => {
+    // 涓虹‘淇濇祦寮忓唴瀹圭ǔ瀹氾紝璁剧疆寤惰繜
+    const timer = setTimeout(() => {
+      setIsLoaded(true);
+    }, 500);
+    
+    return () => clearTimeout(timer);
+  }, []);
+
+  // 涓昏娓叉煋閫昏緫 - 浠呭湪缁勪欢鏍囪涓哄凡鍔犺浇鍚庢墽琛�
+  useEffect(() => {
+    // 濡傛灉缁勪欢鏈爣璁颁负宸插姞杞芥垨宸茬粡灏濊瘯杩囨覆鏌擄紝鍒欓��鍑�
+    if (!isLoaded || renderAttemptedRef.current) return;
+    
+    // 纭繚鍙湪瀹㈡埛绔墽琛�
+    if (typeof window === 'undefined' || !chartContainerRef.current) return;
+    
+    // 鏍囪涓哄凡灏濊瘯娓叉煋锛岄槻姝㈤噸澶嶆覆鏌�
+    renderAttemptedRef.current = true;
+    
+    // 闃叉浠g爜鎵ц涓殑鍙橀噺鍐茬獊
+    const safeCode = codeRef.current.replace(/window\.option/g, '_uniqueOptionVar');
+    
+    // 绛夊緟DOM鏇存柊瀹屾垚
+    setTimeout(() => {
+      let chartInstance: any = null;
+      
+      const initChart = async () => {
+        try {
+          // 鍔ㄦ�佸鍏charts
+          const echarts = await import('echarts');
+          
+          if (!chartContainerRef.current) {
+            console.warn('鍥捐〃瀹瑰櫒宸蹭笉瀛樺湪');
+            return;
+          }
+          
+          // 鍒濆鍖栧浘琛�
+          chartInstance = echarts.init(chartContainerRef.current);
+          
+          // 浣跨敤Function鏋勯�犲櫒锛屼絾娣诲姞棰濆鐨勪繚鎶�
+          try {
+            const getFinalOption = new Function(`
+              "use strict";
+              let option;
+              let _uniqueOptionVar;
+              try {
+                ${safeCode}
+                return option || _uniqueOptionVar;
+              } catch (e) {
+                console.error("ECharts鍐呴儴鎵ц閿欒:", e);
+                return null;
+              }
+            `);
+            
+            const chartOption = getFinalOption();
+            
+            if (chartOption) {
+              chartInstance.setOption(chartOption);
+              setError(null);
+            } else {
+              console.warn('鏈幏鍙栧埌鏈夋晥鐨勫浘琛ㄩ厤缃紝浣跨敤澶囩敤閰嶇疆');
+              useFallbackOption(chartInstance);
+            }
+          } catch (e) {
+            console.error('鎵ц鍥捐〃浠g爜閿欒:', e);
+            setError(e instanceof Error ? e.message : '鍥捐〃浠g爜鎵ц閿欒');
+            useFallbackOption(chartInstance);
+          }
+          
+          // 娣诲姞鍝嶅簲寮忚皟鏁�
+          const handleResize = () => {
+            chartInstance && chartInstance.resize();
+          };
+          
+          window.addEventListener('resize', handleResize);
+          
+          // 纭繚缁勪欢鍗歌浇鏃舵竻鐞嗚祫婧�
+          return () => {
+            window.removeEventListener('resize', handleResize);
+            if (chartInstance) {
+              try {
+                chartInstance.dispose();
+              } catch (e) {
+                console.warn('鍥捐〃瀹炰緥閿�姣佸け璐�', e);
+              }
+            }
+          };
+        } catch (e) {
+          console.error('鍥捐〃鍒濆鍖栧け璐�:', e);
+          setError('鍔犺浇鍥捐〃搴撳け璐�');
+          return () => {};
+        }
+      };
+      
+      // 澶囩敤閰嶇疆鍑芥暟
+      const useFallbackOption = (instance: any) => {
+        try {
+          // 绠�鍗曠殑澶囩敤閰嶇疆
+          const fallbackOption = {
+            title: {
+              text: '鏁版嵁鍙鍖栧浘琛�',
+              subtext: '(鍘熷浠g爜瑙f瀽澶辫触锛屾樉绀哄鐢ㄥ浘琛�)'
+            },
+            tooltip: {
+              trigger: 'axis'
+            },
+            legend: {
+              data: ['鏁版嵁']
+            },
+            xAxis: {
+              type: 'category',
+              data: ['椤圭洰1', '椤圭洰2', '椤圭洰3', '椤圭洰4', '椤圭洰5']
+            },
+            yAxis: {
+              type: 'value'
+            },
+            series: [{
+              name: '鏁版嵁',
+              type: 'bar',
+              data: [5, 20, 36, 10, 10]
+            }]
+          };
+          
+          // 妫�娴嬫槸鍚︽槸鐑姏鍥句唬鐮�
+          if (safeCode.includes('heatmap') || 
+              safeCode.includes('visualMap') || 
+              safeCode.includes('椋庨櫓')) {
+            (fallbackOption.series as any) = [{
+              type: 'heatmap',
+              name: '椋庨櫓鍊�',  // 娣诲姞缂哄け鐨刵ame灞炴��
+              data: [
+                [0, 0, 5], [0, 1, 7], [0, 2, 3],
+                [1, 0, 7], [1, 1, 8], [1, 2, 6],
+                [2, 0, 9], [2, 1, 10], [2, 2, 8]
+              ] as any[]
+            }];
+            
+            fallbackOption.title.text = 'VIP瀹㈡埛璁㈠崟浜や粯椋庨櫓鐑姏鍥�';
+            fallbackOption.title.subtext = '鏁版嵁瑙f瀽澶辫触锛屾樉绀虹ず渚嬬儹鍔涘浘';
+            
+            // 娣诲姞鐑姏鍥炬墍闇�鐨勫叾浠栭厤缃�
+            Object.assign(fallbackOption, {
+              tooltip: {
+                position: 'top'
+              },
+              xAxis: {
+                type: 'category',
+                data: ['浣�', '涓�', '楂�'],
+                name: '寤惰繜椋庨櫓'
+              },
+              yAxis: {
+                type: 'category',
+                data: ['浣�', '涓�', '楂�'],
+                name: '杩濈害鎴愭湰'
+              },
+              visualMap: {
+                min: 1,
+                max: 10,
+                calculable: true,
+                orient: 'horizontal',
+                left: 'center',
+                bottom: '15%'
+              }
+            });
+          }
+          
+          // 璁剧疆澶囩敤閰嶇疆
+          instance.setOption(fallbackOption);
+        } catch (e) {
+          console.error('搴旂敤澶囩敤閰嶇疆澶辫触:', e);
+        }
+      };
+      
+      // 鍒濆鍖栧浘琛ㄥ苟鑾峰彇娓呯悊鍑芥暟
+      const cleanupPromise = initChart();
+      
+      // 瀹屽叏閲嶅啓娓呯悊鍑芥暟閫昏緫锛岄伩鍏嶄娇鐢≒romise鐨勪笉纭畾杩斿洖绫诲瀷
+      // 鍒濆鍖栧浘琛�
+      initChart().then(cleanupFn => {
+        // 瀛樺偍娓呯悊鍑芥暟渚涗互鍚庝娇鐢�
+        if (typeof cleanupFn === 'function') {
+          // 浣跨敤ref瀛樺偍娓呯悊鍑芥暟
+          const currentCleanup = cleanupFn;
+          // 缁勪欢鍗歌浇鏃舵墽琛�
+          return () => currentCleanup();
+        }
+      }).catch(e => {
+        console.error('鍒濆鍖栧浘琛ㄥけ璐�:', e);
+      });
+      
+      // 杩斿洖涓�涓┖鐨勬竻鐞嗗嚱鏁帮紝閬垮厤绫诲瀷閿欒
+      return () => {};
+    }, 100);
+  }, [isLoaded]);
+
+  // 娓叉煋鍥捐〃瀹瑰櫒
+  return (
+    <div className="relative my-4">
+      {error && (
+        <div className="absolute top-0 left-0 right-0 bg-red-50 border border-red-200 text-red-600 px-3 py-2 rounded-md text-xs z-10">
+          {error}
+          <button 
+            className="ml-2 text-red-700 hover:text-red-900" 
+            onClick={() => setError(null)}
+          >
+            脳
+          </button>
+        </div>
+      )}
+      <div 
+        ref={chartContainerRef} 
+        className="w-full bg-white border border-gray-200 rounded-lg overflow-hidden" 
+        style={{ height: '400px' }}
+      />
+    </div>
+  );
+}
+
+// 淇敼浠g爜鍧楁覆鏌撶粍浠讹紝娣诲姞鏇村ソ鐨勭被鍨嬫娴�
+function CodeBlockRenderer({ language, value }: { language: string; value: string }) {
+  // 鏇寸簿纭湴妫�娴婨Charts浠g爜
+  const isEchartsCode = () => {
+    if (language !== 'javascript') return false;
+    
+    // 妫�鏌ユ槸鍚﹀寘鍚獷Charts鐗规湁鐨勯厤缃」
+    const hasEchartsConfig = 
+      value.includes('option =') || 
+      value.includes('const option') || 
+      value.includes('let option') || 
+      value.includes('series') &&
+      (value.includes('type:') || value.includes('tooltip:') || value.includes('xAxis:'));
+      
+    return hasEchartsConfig;
+  };
+  
+  // 濡傛灉鏄� ECharts 浠g爜鍒欐覆鏌撳浘琛�
+  if (isEchartsCode()) {
+    return <EchartsRenderer code={value} />;
+  }
+  
+  // 鍚﹀垯鎸夋櫘閫氫唬鐮佸潡娓叉煋
+  return (
+    <div className="bg-gray-800 rounded-md my-2 overflow-hidden">
+      <div className="flex items-center justify-between px-4 py-2 border-b border-gray-700">
+        <span className="text-xs text-gray-400">{language}</span>
+      </div>
+      <pre className="p-4 text-gray-300 overflow-x-auto">
+        <code>{value}</code>
+      </pre>
+    </div>
+  );
+}
 
 export default function Page() {
   const router = useRouter();
@@ -729,8 +1002,35 @@
                                 
                                 {/* 涓昏鍐呭 */}
                                 <div className="p-3">
-                                  <div className="text-gray-800 leading-relaxed whitespace-pre-wrap">
-                                    {mainContent || (msg.role === 'assistant' && !isMessageComplete ? '姝e湪鎬濊��...' : '')}
+                                  <div className="text-gray-800 leading-relaxed">
+                                    {mainContent ? (
+                                      <ReactMarkdown
+                                        remarkPlugins={[remarkGfm]}
+                                        rehypePlugins={[rehypeRaw, rehypeSanitize]}
+                                        components={{
+                                          // @ts-ignore - ReactMarkdown 缁勪欢绫诲瀷瀹氫箟鐨勫吋瀹规�ч棶棰�
+                                          code: ({ node, inline, className, children, ...props }) => {
+                                            const match = /language-(\w+)/.exec(className || '');
+                                            const language = match ? match[1] : '';
+                                            const value = String(children).replace(/\n$/, '');
+                                            
+                                            if (!inline && match) {
+                                              return <CodeBlockRenderer language={language} value={value} />;
+                                            }
+                                            
+                                            return (
+                                              <code className={className} {...props}>
+                                                {children}
+                                              </code>
+                                            );
+                                          }
+                                        }}
+                                      >
+                                        {mainContent}
+                                      </ReactMarkdown>
+                                    ) : (
+                                      msg.role === 'assistant' && !isMessageComplete ? '澶勭悊鍥炲涓�...' : ''
+                                    )}
                                   </div>
                                 </div>
                                 
@@ -808,5 +1108,4 @@
       </div>
     </div>
   );
-}
-
+}
\ No newline at end of file

--
Gitblit v1.9.3