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

---
 src/app/chat/page.tsx | 1154 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 1,107 insertions(+), 47 deletions(-)

diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx
index 415bf60..5530946 100644
--- a/src/app/chat/page.tsx
+++ b/src/app/chat/page.tsx
@@ -1,70 +1,1130 @@
 'use client';
 
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef, useCallback, useContext, createContext } from 'react';
 import Link from 'next/link';
 import { useRouter } from 'next/navigation';
+import Image from 'next/image';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeRaw from 'rehype-raw';
+import rehypeSanitize from 'rehype-sanitize';
+import dynamic from 'next/dynamic';
+import { ReactNode } from 'react';
+
+// 鍒涘缓涓�涓秷鎭畬鎴愮姸鎬佺殑Context
+const MessageCompletionContext = createContext<boolean>(true);
+
+// 鍔ㄦ�佸鍏� ECharts锛岀‘淇濆畠鍙湪瀹㈡埛绔覆鏌�
+const ReactECharts = dynamic(() => import('echarts-for-react'), { ssr: false });
+
+interface Message {
+  role: 'user' | 'assistant';
+  content: string | null;
+  timestamp: number;
+  id: string;
+  conversation_id?: string;
+  feedback?: 'like' | 'dislike' | null;
+  metadata?: {
+    usage?: {
+      prompt_tokens: number;
+      completion_tokens: number;
+      total_tokens: number;
+      total_price: string;
+    };
+  };
+}
+
+const BASE_URL = 'http://121.43.139.99:7000';
+
+// 榛樿鐢ㄦ埛澶村儚
+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 }) {
+  // 鍒ゆ柇鏄惁鏄疛avaScript浠g爜锛屾娴嬫槸鍚﹀寘鍚浘琛ㄧ浉鍏崇壒寰�
+  const isEchartsCode = useCallback(() => {
+    if (language !== 'javascript') return false;
+    
+    // 妫�鏌ユ槸鍚﹀寘鍚獷Charts鐗规湁鐨勯厤缃」
+    return value.includes('option') && 
+      (value.includes('series') || 
+       value.includes('chart') || 
+       value.includes('echarts') ||
+       value.includes('xAxis') ||
+       value.includes('yAxis'));
+  }, [language, value]);
+  
+  // 妫�鏌ユ秷鎭槸鍚﹀畬鏁� - 閫氳繃鐖剁粍浠朵紶閫掔殑isMessageComplete鐘舵��
+  const isComplete = useContext(MessageCompletionContext);
+  
+  // 鍦ㄦ秷鎭湭瀹屾垚鏃舵樉绀哄姞杞藉姩鐢�
+  if (language === 'javascript' && !isComplete) {
+    return (
+      <div className="w-full bg-gray-50 rounded-md my-4 p-6 text-center">
+        <div className="flex flex-col items-center justify-center">
+          <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-400 mb-4"></div>
+          <p className="text-gray-500">浠g爜鍔犺浇涓�...</p>
+          <p className="text-xs text-gray-400 mt-2">绛夊緟娑堟伅瀹屾垚鍚庡皢娓叉煋鍥捐〃</p>
+        </div>
+      </div>
+    );
+  }
+  
+  // 娑堟伅瀹屾垚鍚庯紝濡傛灉鏄浘琛ㄤ唬鐮佸垯娓叉煋涓哄浘琛�
+  if (isEchartsCode() && isComplete) {
+    return <EchartsRenderer code={value} />;
+  }
+  
+  // 鏅�欽avaScript浠g爜鎴栧叾浠栬瑷�鐨勪唬鐮佸潡锛岀洿鎺ユ樉绀轰唬鐮�
+  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();
-  const [token, setToken] = useState<string | null>(null);
+  const [apiKey, setApiKey] = useState<string>('');
+  const [showApiKeyInput, setShowApiKeyInput] = useState(false);
+  const [message, setMessage] = useState('');
+  const [messages, setMessages] = useState<Message[]>([]);
+  const [isStreaming, setIsStreaming] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [showError, setShowError] = useState(false);
+  const [conversationId, setConversationId] = useState<string | null>(null);
+  const [isMessageComplete, setIsMessageComplete] = useState(true);
+  const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
+  
+  // 娣诲姞涓�涓柊鐨勭姸鎬佹潵鎺у埗娑堟伅鐨勬樉绀�
+  const [showMessages, setShowMessages] = useState(false);
+
+  // 鍦ㄧ粍浠堕《閮ㄥ鍔犱竴涓己鍒舵洿鏂拌鏁板櫒
+  const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
+
+  // 娣诲姞鐘舵�佹潵鎺у埗鎬濊�冨唴瀹圭殑鏄剧ず/闅愯棌
+  const [expandedThinkMessages, setExpandedThinkMessages] = useState<Record<string, boolean>>({});
+
+  // 鍦ㄧ粍浠堕《閮ㄦ坊鍔犳樉绀�/闅愯棌瀵嗙爜鐨勭姸鎬�
+  const [showApiKey, setShowApiKey] = useState(false);
+
+  const messagesEndRef = useRef<HTMLDivElement>(null);
+  const errorTimeoutRef = useRef<any>(null);
+
+  // 鍦ㄧ粍浠堕《閮ㄦ坊鍔犱竴涓紩鐢紝鐢ㄤ簬璺熻釜缁勪欢鏄惁宸插嵏杞�
+  const isMountedRef = useRef(true);
 
   useEffect(() => {
-    // 鑾峰彇token
-    const storedToken = localStorage.getItem('token');
-    setToken(storedToken);
-
-    // 濡傛灉娌℃湁token锛岀洿鎺ヨ烦杞埌鐧诲綍椤甸潰
-    if (!storedToken) {
-      router.push('/login');
+    // 鑾峰彇瀛樺偍鐨凙PI Key
+    const storedApiKey = localStorage.getItem('api-key');
+    if (storedApiKey) {
+      setApiKey(storedApiKey);
+    } else {
+      setShowApiKeyInput(true);
     }
   }, []);
 
-  // 濡傛灉娌℃湁token锛屼笉娓叉煋浠讳綍鍐呭
-  if (!token) {
-    return null;
-  }
+  useEffect(() => {
+    // 婊氬姩鍒版渶鏂版秷鎭�
+    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+  }, [messages]);
+
+  // 澶勭悊閿欒鏄剧ず鍜岃嚜鍔ㄦ秷澶�
+  const showErrorMessage = useCallback((message: string) => {
+    setError(message);
+    // 鍏堣缃负false纭繚鍔ㄧ敾鑳介噸鏂拌Е鍙�
+    setShowError(false);
+    // 浣跨敤 requestAnimationFrame 纭繚鐘舵�佸彉鍖栬姝g‘娓叉煋
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => {
+        setShowError(true);
+      });
+    });
+
+    if (errorTimeoutRef.current) {
+      clearTimeout(errorTimeoutRef.current);
+    }
+
+    errorTimeoutRef.current = setTimeout(() => {
+      setShowError(false);
+      setTimeout(() => {
+        setError(null);
+      }, 400);
+    }, 3000);
+  }, []);
+
+  // 缁勪欢鍗歌浇鏃舵竻闄ゅ畾鏃跺櫒
+  useEffect(() => {
+    const cleanup = () => {
+      if (errorTimeoutRef.current) {
+        clearTimeout(errorTimeoutRef.current);
+      }
+    };
+    return cleanup;
+  }, []);
+
+  // 娣诲姞缁勪欢鍗歌浇鏃剁殑娓呯悊宸ヤ綔
+  useEffect(() => {
+    // 缁勪欢鎸傝浇鏃讹紝璁剧疆涓簍rue
+    isMountedRef.current = true;
+    
+    // 缁勪欢鍗歌浇鏃讹紝璁剧疆涓篺alse
+    return () => {
+      isMountedRef.current = false;
+      
+      // 娓呴櫎鎵�鏈夊畾鏃跺櫒
+      if (errorTimeoutRef.current) {
+        clearTimeout(errorTimeoutRef.current);
+      }
+    };
+  }, []);
+
+  const handleApiKeySubmit = () => {
+    if (apiKey.trim()) {
+      localStorage.setItem('api-key', apiKey);
+      setShowApiKeyInput(false);
+      setError(null);
+    }
+  };
+
+  const handleSendMessage = async () => {
+    if (!message.trim() || !isMessageComplete) return;
+    if (!apiKey) {
+      showErrorMessage('璇峰厛璁剧疆API Key');
+      return;
+    }
+
+    setIsStreaming(true);
+    setIsMessageComplete(false);
+
+    // 鍒涘缓鏂版秷鎭�
+    const userMessage: Message = {
+      role: 'user',
+      content: message.trim(),
+      timestamp: Date.now(),
+      id: 'user-' + Date.now().toString()
+    };
+
+    const assistantMessage: Message = {
+      role: 'assistant',
+      content: '',
+      timestamp: Date.now(),
+      id: 'temp-' + Date.now().toString()
+    };
+
+    // 浣跨敤鍑芥暟寮忔洿鏂扮‘淇濈姸鎬佹洿鏂扮殑涓�鑷存��
+    setMessages(prevMessages => {
+      const newMessages = [...prevMessages];
+      // 濡傛灉瀛樺湪鏈畬鎴愮殑鍔╂墜娑堟伅锛屽厛绉婚櫎瀹�
+      if (!isMessageComplete && currentMessageId) {
+        const lastMessage = newMessages[newMessages.length - 1];
+        if (lastMessage && lastMessage.role === 'assistant' && lastMessage.id === currentMessageId) {
+          newMessages.pop();
+        }
+      }
+      return [...newMessages, userMessage, assistantMessage];
+    });
+
+    setCurrentMessageId(assistantMessage.id);
+    setMessage('');
+
+    let controller: AbortController | null = new AbortController();
+    
+    try {
+      const response = await fetch(`${BASE_URL}/v1/chat-messages`, {
+        method: 'POST',
+        headers: {
+          'Authorization': `Bearer ${apiKey}`,
+          'Content-Type': 'application/json',
+        },
+        mode: 'cors',
+        body: JSON.stringify({
+          inputs: {},
+          query: userMessage.content,
+          response_mode: 'streaming',
+          conversation_id: conversationId || '',
+          user: 'abc-123',
+          files: []
+        }),
+        signal: controller.signal // 娣诲姞涓淇″彿
+      });
+
+      console.log("API鍝嶅簲鐘舵��:", response.status, response.statusText);
+      
+      if (!response.ok) {
+        const errorText = await response.text();
+        console.error("API閿欒鍝嶅簲:", errorText);
+        let errorMessage;
+        try {
+          const errorJson = JSON.parse(errorText);
+          errorMessage = errorJson.error || `閿欒: ${response.status}`;
+        } catch {
+          errorMessage = `閿欒: ${response.status}`;
+        }
+        throw new Error(errorMessage);
+      }
+
+      // 纭繚response.body瀛樺湪
+      if (!response.body) {
+        throw new Error('鍝嶅簲娌℃湁鎻愪緵鏁版嵁娴�');
+      }
+
+      const reader = response.body.getReader();
+      const decoder = new TextDecoder();
+      let buffer = '';
+
+      while (true) {
+        try {
+          const { done, value } = await reader.read();
+          if (done) {
+            console.log("娴佸紡鍝嶅簲鎺ユ敹瀹屾瘯");
+            setIsMessageComplete(true);
+            break;
+          }
+
+          const chunk = decoder.decode(value, { stream: true });
+          console.log("鎺ユ敹鍒版暟鎹潡:", chunk);
+          buffer += chunk;
+          const lines = buffer.split('\n');
+          buffer = lines.pop() || '';
+
+          for (const line of lines) {
+            if (line.trim() === '') continue;
+            
+            console.log("澶勭悊琛屾暟鎹�:", line);
+            
+            if (line.startsWith('data: ')) {
+              try {
+                const jsonStr = line.slice(6);
+                console.log("瀹屾暣鍘熷鏁版嵁:", line);
+                console.log("瑙f瀽JSON瀛楃涓�:", jsonStr);
+                const data = JSON.parse(jsonStr);
+                console.log("瑙f瀽鍚庣殑鏁版嵁瀵硅薄:", data);
+                
+                // 蹇界暐ping浜嬩欢
+                if (data.event === 'ping') {
+                  console.log("蹇界暐ping浜嬩欢");
+                  continue;
+                }
+
+                switch (data.event) {
+                  case 'message':
+                    console.log(`鏀跺埌message浜嬩欢:`, data);
+                    
+                    // 鎻愬彇message鏁版嵁
+                    const messageId = data.message_id;
+                    const answerChunk = data.answer || '';
+                    const convId = data.conversation_id;
+                    
+                    // 濡傛灉涓嶅湪寰幆涓紝鐩存帴璋冪敤鏇存柊鍑芥暟
+                    handleMessageChunk(messageId, answerChunk, convId);
+                    break;
+
+                  case 'message_end':
+                    console.log('鏀跺埌message_end浜嬩欢:', data);
+                    
+                    // 妫�鏌ユ槸鍚︽湁鏈�缁堢瓟妗�
+                    if (data.metadata && data.id) {
+                      setIsMessageComplete(true);
+                      setConversationId(data.conversation_id || null);
+                    }
+                    break;
+                    
+                  case 'workflow_finished':
+                    console.log('宸ヤ綔娴佸畬鎴�:', data);
+                    
+                    // 妫�鏌ュ伐浣滄祦杈撳嚭鏄惁鏈夌瓟妗�
+                    if (data.data && data.data.outputs && data.data.outputs.answer) {
+                      // 濡傛灉宸ヤ綔娴佹湁鏈�缁堢瓟妗堬紝鏇存柊娑堟伅鍐呭
+                      const finalAnswer = data.data.outputs.answer;
+                      const messageId = data.message_id;
+                      
+                      if (finalAnswer) {
+                        console.log('浠庡伐浣滄祦鑾峰彇鏈�缁堢瓟妗�:', finalAnswer);
+                        handleFinalAnswer(messageId, finalAnswer, data.conversation_id);
+                      }
+                    }
+                    break;
+                    
+                  case 'node_finished':
+                    // 妫�鏌ヨ妭鐐圭被鍨嬫槸鍚︿负answer鑺傜偣
+                    if (data.data && data.data.node_type === 'answer' && data.data.outputs && data.data.outputs.answer) {
+                      console.log('浠巃nswer鑺傜偣鑾峰彇绛旀:', data.data.outputs.answer);
+                      const answer = data.data.outputs.answer;
+                      if (answer) {
+                        handleFinalAnswer(data.message_id, answer, data.conversation_id);
+                      }
+                    }
+                    break;
+                    
+                  // 澶勭悊鍏朵粬宸ヤ綔娴佷簨浠�
+                  case 'workflow_started':
+                  case 'node_started':
+                    // 杩欎簺浜嬩欢鍙渶璁板綍锛屼笉闇�瑕佹洿鏂癠I
+                    console.log(`宸ヤ綔娴佷簨浠� ${data.event}:`, data);
+                    break;
+                    
+                  case 'error':
+                    console.error('鏈嶅姟鍣ㄨ繑鍥為敊璇簨浠�:', data);
+                    setIsMessageComplete(true);
+                    throw new Error(data.message || '鍙戦�佹秷鎭椂鍑洪敊');
+                }
+              } catch (e) {
+                console.error('瑙f瀽SSE鏁版嵁鍑洪敊:', e, '鍘熷琛�:', line);
+                setIsMessageComplete(true);
+                throw e;
+              }
+            }
+          }
+        } catch (err) {
+          console.error('澶勭悊娴佸紡鍝嶅簲鏃跺嚭閿�:', err);
+          throw err;
+        }
+      }
+    } catch (err) {
+      // 妫�鏌ユ槸鍚︽槸涓閿欒
+      if (err instanceof Error && err.name === 'AbortError') {
+        console.log('璇锋眰琚腑姝紝鍙兘鏄粍浠跺嵏杞藉鑷寸殑');
+        return; // 涓閿欒涓嶉渶瑕佹樉绀虹粰鐢ㄦ埛
+      }
+      
+      console.error('鑱婂ぉ璇锋眰閿欒:', err);
+      let errorMsg = err instanceof Error ? err.message : '鍙戦�佹秷鎭椂鍑洪敊';
+      
+      // 鍙湁鍦ㄧ粍浠朵粛鐒舵寕杞芥椂鎵嶆洿鏂癠I
+      if (isMountedRef.current) {
+        showErrorMessage(errorMsg);
+        
+        setMessages(prev => {
+          const newMessages = [...prev];
+          const lastMessage = newMessages[newMessages.length - 1];
+          if (lastMessage?.role === 'assistant') {
+            lastMessage.content = `鎶辨瓑锛屾棤娉曡幏鍙栧洖澶�: ${errorMsg}`;
+          }
+          return newMessages;
+        });
+      }
+    } finally {
+      // 娓呴櫎鎺у埗鍣�
+      controller = null;
+      
+      // 鍙湁鍦ㄧ粍浠朵粛鐒舵寕杞芥椂鎵嶆洿鏂癠I
+      if (isMountedRef.current) {
+        setIsStreaming(false);
+        setIsMessageComplete(true);
+      }
+    }
+  };
+
+  // 淇敼handleMessageChunk鍑芥暟锛屾坊鍔犵粍浠舵寕杞芥鏌�
+  const handleMessageChunk = useCallback((messageId: string, answerChunk: string, convId?: string) => {
+    // 濡傛灉缁勪欢宸插嵏杞斤紝鍒欎笉鎵ц鏇存柊
+    if (!isMountedRef.current) return;
+    
+    console.log(`澶勭悊娑堟伅鐗囨: ID=${messageId}, 鐗囨="${answerChunk}"`);
+    
+    // 浣跨敤鍑芥暟寮忔洿鏂扮‘淇濊幏鍙栨渶鏂扮姸鎬�
+    setMessages(prevMessages => {
+      // 濡傛灉缁勪欢宸插嵏杞斤紝鍒欎笉鎵ц鏇存柊
+      if (!isMountedRef.current) return prevMessages;
+      
+      // 鍒涘缓娑堟伅鏁扮粍鐨勬嫹璐�
+      const newMessages = [...prevMessages];
+      
+      // 鏌ユ壘鏈�鍚庝竴鏉″姪鎵嬫秷鎭�
+      const lastMessage = newMessages[newMessages.length - 1];
+      if (lastMessage?.role !== 'assistant') {
+        console.warn('鎵句笉鍒板姪鎵嬫秷鎭潵鏇存柊');
+        return prevMessages; // 涓嶉渶瑕佹洿鏂�
+      }
+      
+      // 鏇存柊娑堟伅鍐呭鍜孖D
+      const updatedContent = (lastMessage.content || '') + answerChunk;
+      
+      // 鍒涘缓娑堟伅鐨勬柊鍓湰浠ョ‘淇漅eact妫�娴嬪埌鍙樺寲
+      const updatedMessage = {
+        ...lastMessage,
+        id: messageId,
+        content: updatedContent,
+        timestamp: Date.now()
+      };
+      
+      if (convId) {
+        updatedMessage.conversation_id = convId;
+      }
+      
+      // 鏇存柊褰撳墠姝e湪澶勭悊鐨勬秷鎭疘D
+      if (isMountedRef.current) {
+        setCurrentMessageId(messageId);
+      }
+      
+      // 鏇挎崲鏈�鍚庝竴鏉℃秷鎭�
+      newMessages[newMessages.length - 1] = updatedMessage;
+      
+      console.log(`鏇存柊鍚庣殑鍐呭闀垮害: ${updatedContent.length}`);
+      
+      // 瑙﹀彂寮哄埗鏇存柊
+      if (isMountedRef.current) {
+        setTimeout(() => {
+          if (isMountedRef.current) {
+            setForceUpdateCounter(count => count + 1);
+          }
+        }, 0);
+      }
+      
+      return newMessages;
+    });
+  }, []);
+
+  // 淇敼handleFinalAnswer鍑芥暟锛屾坊鍔犵粍浠舵寕杞芥鏌�
+  const handleFinalAnswer = useCallback((messageId: string, answer: string, convId?: string) => {
+    // 濡傛灉缁勪欢宸插嵏杞斤紝鍒欎笉鎵ц鏇存柊
+    if (!isMountedRef.current) return;
+    
+    console.log(`璁剧疆鏈�缁堢瓟妗�: ID=${messageId}, 鍐呭="${answer}"`);
+    
+    setMessages(prevMessages => {
+      // 濡傛灉缁勪欢宸插嵏杞斤紝鍒欎笉鎵ц鏇存柊
+      if (!isMountedRef.current) return prevMessages;
+      
+      const newMessages = [...prevMessages];
+      const lastMessage = newMessages[newMessages.length - 1];
+      
+      if (lastMessage?.role !== 'assistant') {
+        console.warn('鎵句笉鍒板姪鎵嬫秷鎭潵鏇存柊鏈�缁堢瓟妗�');
+        return prevMessages;
+      }
+      
+      // 鍒涘缓娑堟伅鐨勬柊鍓湰锛岃缃渶缁堢瓟妗�
+      const updatedMessage = {
+        ...lastMessage,
+        id: messageId,
+        content: answer,
+        timestamp: Date.now()
+      };
+      
+      if (convId) {
+        updatedMessage.conversation_id = convId;
+      }
+      
+      // 鏇存柊鐘舵��
+      if (isMountedRef.current) {
+        setIsMessageComplete(true);
+        setCurrentMessageId(messageId);
+      }
+      
+      // 鏇挎崲鏈�鍚庝竴鏉℃秷鎭�
+      newMessages[newMessages.length - 1] = updatedMessage;
+      
+      // 瑙﹀彂寮哄埗鏇存柊
+      if (isMountedRef.current) {
+        setTimeout(() => {
+          if (isMountedRef.current) {
+            setForceUpdateCounter(count => count + 1);
+          }
+        }, 0);
+      }
+      
+      return newMessages;
+    });
+  }, []);
+
+  // 鏇存柊鍙戦�佹寜閽殑绂佺敤鐘舵��
+  const isSendDisabled = isStreaming || !message.trim() || !isMessageComplete;
+
+  const handleKeyPress = (e: React.KeyboardEvent) => {
+    if (e.key === 'Enter' && !e.shiftKey) {
+      e.preventDefault();
+      handleSendMessage();
+    }
+  };
+
+  useEffect(() => {
+    // 椤甸潰鍔犺浇鍚庡欢杩熸樉绀烘秷鎭垪琛紝閬垮厤闂儊
+    const timer = setTimeout(() => {
+      setShowMessages(true);
+    }, 100);
+    return () => clearTimeout(timer);
+  }, []);
+
+  // 娣诲姞鑷畾涔塁SS鍔ㄧ敾鏍峰紡
+  useEffect(() => {
+    const style = document.createElement('style');
+    style.innerHTML = `
+      @keyframes dotBounce {
+        0%, 80%, 100% { transform: translateY(0); }
+        40% { transform: translateY(-6px); }
+      }
+      .dot-animation span:nth-child(1) { animation: dotBounce 1.4s -0.32s infinite ease-in-out; }
+      .dot-animation span:nth-child(2) { animation: dotBounce 1.4s -0.16s infinite ease-in-out; }
+      .dot-animation span:nth-child(3) { animation: dotBounce 1.4s 0s infinite ease-in-out; }
+    `;
+    document.head.appendChild(style);
+
+    return () => {
+      document.head.removeChild(style);
+    };
+  }, []);
+
+  // 娣诲姞澶勭悊鎬濊�冨唴瀹圭殑杈呭姪鍑芥暟
+  const parseMessageContent = (content: string | null) => {
+    if (!content) return { mainContent: '', thinkContent: null };
+    
+    // 鍖归厤<think>鏍囩鍐呭 (涓嶅尯鍒嗗ぇ灏忓啓)
+    const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/i);
+    
+    if (thinkMatch) {
+      // 鎻愬彇鎬濊�冨唴瀹�
+      const thinkContent = thinkMatch[1].trim();
+      // 绉婚櫎<think>鏍囩,淇濈暀涓昏鍐呭
+      const mainContent = content.replace(/<think>[\s\S]*?<\/think>/i, '').trim();
+      return { mainContent, thinkContent };
+    }
+    
+    return { mainContent: content, thinkContent: null };
+  };
+
+  // 鍒囨崲鎬濊�冨唴瀹圭殑鏄剧ず/闅愯棌
+  const toggleThinkContent = (messageId: string) => {
+    setExpandedThinkMessages(prev => ({
+      ...prev,
+      [messageId]: !prev[messageId]
+    }));
+  };
+
+  // 娣诲姞缁勪欢鍗歌浇鏃剁殑娓呯悊鍑芥暟
+  useEffect(() => {
+    return () => {
+      // 缁勪欢鍗歌浇鏃剁殑娓呯悊宸ヤ綔
+      console.log('鑱婂ぉ缁勪欢鍗歌浇锛屾竻鐞嗚祫婧�');
+    };
+  }, []);
 
   return (
-    <div className="min-h-screen bg-[#0A1033] text-white">
-      {/* 椤堕儴瀵艰埅 */}
-      <div className="bg-[#131C41] p-4 border-b border-[#6ADBFF]/20">
-        <div className="max-w-4xl mx-auto flex justify-between items-center">
-          <Link href="/" className="text-[#6ADBFF] hover:underline flex items-center gap-2">
-            <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
-              <path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
-            </svg>
-            杩斿洖棣栭〉
-          </Link>
-          <h1 className="text-xl font-bold">AI鍔╂墜瀵硅瘽</h1>
+    <div className="min-h-screen bg-gradient-to-b from-gray-50 to-white text-gray-900 flex flex-col">
+      {/* API Key Modal */}
+      {showApiKeyInput && (
+        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
+          <div className="bg-white rounded-2xl p-6 w-[400px] space-y-4 shadow-xl">
+            <div className="flex justify-between items-center">
+              <h2 className="text-xl font-semibold">璁剧疆 API Key</h2>
+              <button
+                onClick={() => setShowApiKeyInput(false)}
+                className="text-gray-400 hover:text-gray-600"
+              >
+                <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                  <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
+                </svg>
+              </button>
+            </div>
+            <div className="space-y-2">
+              <div className="relative">
+                <input
+                  type={showApiKey ? "text" : "password"}
+                  value={apiKey}
+                  onChange={(e) => setApiKey(e.target.value)}
+                  placeholder="璇疯緭鍏ユ偍鐨� API Key"
+                  className="w-full px-4 py-2 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-100/50"
+                />
+                <button
+                  type="button"
+                  onClick={() => setShowApiKey(!showApiKey)}
+                  className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
+                >
+                  {showApiKey ? (
+                    // 鐪肩潧鍏抽棴鍥炬爣
+                    <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                      <path fillRule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clipRule="evenodd" />
+                      <path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
+                    </svg>
+                  ) : (
+                    // 鐪肩潧鍥炬爣
+                    <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
+                      <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
+                      <path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
+                    </svg>
+                  )}
+                </button>
+              </div>
+            </div>
+            <div className="flex justify-end gap-2">
+              <button
+                onClick={() => setShowApiKeyInput(false)}
+                className="px-4 py-2 text-gray-600 hover:text-gray-900"
+              >
+                鍙栨秷
+              </button>
+              <button
+                onClick={handleApiKeySubmit}
+                className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600"
+              >
+                纭畾
+              </button>
+            </div>
+          </div>
         </div>
-      </div>
+      )}
+
+      {/* 閿欒鎻愮ず */}
+      {error && (
+        <div 
+          className={`fixed top-20 left-1/2 transform -translate-x-1/2 
+            flex items-center gap-2 px-4 py-2 text-sm text-white
+            transition-all duration-400 ease-out
+            ${showError 
+              ? 'opacity-100 translate-y-0 scale-100' 
+              : 'opacity-0 -translate-y-2 scale-95'
+            }
+            before:content-[''] before:absolute before:inset-0 before:bg-red-500 
+            before:rounded-lg before:opacity-90 before:-z-10
+            after:content-[''] after:absolute after:inset-0 after:bg-red-500/50
+            after:blur-md after:rounded-lg after:-z-20`}
+        >
+          <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
+            <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
+          </svg>
+          <span className="relative">{error}</span>
+        </div>
+      )}
+
+      {/* API Key 鎸夐挳 */}
+      <button
+        onClick={() => setShowApiKeyInput(true)}
+        className="fixed top-24 right-6 z-10 w-9 h-9 flex items-center justify-center rounded-full bg-white/80 hover:bg-white shadow-sm border border-gray-100 transition-colors"
+        title="璁剧疆API Key"
+      >
+        <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+          <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
+        </svg>
+      </button>
 
       {/* 鑱婂ぉ鍖哄煙 */}
-      <div className="max-w-4xl mx-auto p-4">
-        <div className="space-y-4 mb-20">
-          <div className="flex justify-start">
-            <div className="max-w-[80%] rounded-lg p-4 bg-[#131C41]">
-              <p>浣犲ソ锛佹杩庢潵鍒拌亰澶╁銆�</p>
+      <div className="flex-1 flex flex-col h-screen">
+        <div className="flex-1 overflow-y-auto pt-20 pb-32">
+          <div className="max-w-4xl mx-auto px-6">
+            <div className={`space-y-6 ${showMessages ? 'opacity-100' : 'opacity-0'} transition-opacity duration-200`}>
+              {messages.length === 0 ? (
+                <div className="flex items-center justify-center h-[200px] text-gray-400">
+                  <p>鍙戦�佹秷鎭紑濮嬪璇�</p>
+                </div>
+              ) : (
+                messages.map((msg, index) => (
+                  <div
+                    key={msg.id}
+                    className={`flex items-start gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
+                  >
+                    <div className="relative flex-shrink-0">
+                      <div className="w-8 h-8 rounded-lg overflow-hidden shadow-inner bg-gray-50">
+                        <Image 
+                          src={msg.role === 'assistant' ? "/images/logo.jpg" : DEFAULT_USER_AVATAR}
+                          alt={msg.role === 'assistant' ? "AI鍔╂墜" : "鐢ㄦ埛"}
+                          width={32}
+                          height={32}
+                          className="w-full h-full object-cover"
+                        />
+                      </div>
+                    </div>
+                    <div className={`flex-1 min-w-0 ${msg.role === 'user' ? 'text-right' : ''}`}>
+                      <div className={`${
+                        msg.role === 'assistant' 
+                          ? 'bg-white rounded-xl shadow-sm border border-gray-100' 
+                          : 'bg-[#E8F4FF] rounded-xl'
+                        } inline-block max-w-[85%] relative overflow-hidden`}>
+                        
+                        {/* 璋冭瘯淇℃伅 - 绉婚櫎杩欓儴鍒� */}
+                        {/* 
+                        {process.env.NODE_ENV !== 'production' && (
+                          <div className="bg-gray-100 px-2 py-1 text-xs text-gray-500">
+                            闀垮害: {(msg.content || '').length} | 璁℃暟: {forceUpdateCounter}
+                          </div>
+                        )}
+                        */}
+                        
+                        {msg.role === 'assistant' && (
+                          <>{(() => {
+                            // 瑙f瀽娑堟伅鍐呭锛屾彁鍙栨�濊�冮儴鍒�
+                            const { mainContent, thinkContent } = parseMessageContent(msg.content);
+                            const isExpanded = expandedThinkMessages[msg.id] || false;
+                            const thinkingDuration = 8; // 鎬濊�冩椂闂�(绉�)
+                            
+                            return (
+                              <>
+                                {/* 鎬濊�冨唴瀹瑰尯鍩� (濡傛灉瀛樺湪) */}
+                                {thinkContent && (
+                                  <div className="border-b border-gray-200">
+                                    <button 
+                                      onClick={() => toggleThinkContent(msg.id)}
+                                      className="w-full flex items-center justify-between px-3 py-2 bg-gray-50 hover:bg-gray-100 transition-colors text-sm text-gray-700"
+                                    >
+                                      <div className="flex items-center space-x-2">
+                                        <span>宸叉繁搴︽�濊�� (鐢ㄦ椂 {thinkingDuration} 绉�)</span>
+                                      </div>
+                                      <svg 
+                                        className={`w-4 h-4 transform transition-transform ${isExpanded ? 'rotate-180' : ''}`}
+                                        fill="none" 
+                                        stroke="currentColor" 
+                                        viewBox="0 0 24 24"
+                                      >
+                                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
+                                      </svg>
+                                    </button>
+                                    
+                                    {/* 鍙姌鍙犵殑鎬濊�冨唴瀹� */}
+                                    {isExpanded && (
+                                      <div className="bg-gray-50 px-4 py-3 text-sm text-gray-700 border-t border-gray-200 whitespace-pre-wrap">
+                                        {thinkContent}
+                                      </div>
+                                    )}
+                                  </div>
+                                )}
+                                
+                                {/* 涓昏鍐呭 */}
+                                <div className="p-3">
+                                  <div className="text-gray-800 leading-relaxed">
+                                    {mainContent ? (
+                                      <MessageCompletionContext.Provider value={isMessageComplete && msg.id === currentMessageId}>
+                                        <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>
+                                      </MessageCompletionContext.Provider>
+                                    ) : (
+                                      msg.role === 'assistant' && !isMessageComplete ? '澶勭悊鍥炲涓�...' : ''
+                                    )}
+                                  </div>
+                                </div>
+                                
+                                {/* 鍔犺浇鎸囩ず鍣� */}
+                                {msg.role === 'assistant' && !isMessageComplete && (
+                                  <div className="absolute bottom-1 right-2">
+                                    <div className="flex space-x-1">
+                                      <div className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse delay-0"></div>
+                                      <div className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse delay-150"></div>
+                                      <div className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse delay-300"></div>
+                                    </div>
+                                  </div>
+                                )}
+                              </>
+                            );
+                          })()}</>
+                        )}
+                        
+                        {/* 鐢ㄦ埛娑堟伅绠�鍗曟樉绀� */}
+                        {msg.role === 'user' && (
+                          <div className="p-3">
+                            <div className="text-gray-800 leading-relaxed whitespace-pre-wrap">
+                              {msg.content}
+                            </div>
+                          </div>
+                        )}
+                      </div>
+                      <div className="mt-0.5 text-xs text-gray-400">
+                        {msg.role === 'user' && new Date(msg.timestamp).toLocaleTimeString()}
+                      </div>
+                    </div>
+                  </div>
+                ))
+              )}
+              <div ref={messagesEndRef} />
+            </div>
+          </div>
+        </div>
+
+        {/* 杈撳叆鍖哄煙 */}
+        <div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-white/95 pt-4 pb-6">
+          <div className="max-w-4xl mx-auto px-6">
+            <div className="relative">
+              <div className="absolute -top-6 left-1/2 -translate-x-1/2 w-48 h-[1px] bg-gradient-to-r from-transparent via-gray-200 to-transparent"></div>
+              
+              <div className="flex gap-4 items-start">
+                <div className="flex-1 relative">
+                  <textarea
+                    value={message}
+                    onChange={(e) => setMessage(e.target.value)}
+                    onKeyDown={handleKeyPress}
+                    placeholder="杈撳叆娑堟伅..."
+                    disabled={isStreaming}
+                    className="w-full resize-none rounded-xl border-0 bg-gray-50 px-4 py-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-100/50 focus:bg-white min-h-[48px] max-h-32 shadow-inner transition-all duration-300 ease-in-out hover:bg-gray-100/70 disabled:opacity-50 disabled:cursor-not-allowed"
+                    style={{ height: '48px' }}
+                  />
+                  <div className="absolute right-4 bottom-2 text-xs text-gray-400 bg-gray-50 px-2">
+                    鎸塃nter鍙戦�侊紝Shift+Enter鎹㈣
+                  </div>
+                </div>
+                <button
+                  onClick={handleSendMessage}
+                  disabled={isSendDisabled}
+                  className="h-12 px-5 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none"
+                >
+                  <span>{isStreaming ? '鍥炲涓�...' : (isMessageComplete ? '鍙戦��' : '澶勭悊涓�...')}</span>
+                  <svg xmlns="http://www.w3.org/2000/svg" className={`h-4 w-4 transform rotate-45 ${isStreaming ? 'animate-pulse' : ''}`} viewBox="0 0 20 20" fill="currentColor">
+                    <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
+                  </svg>
+                </button>
+              </div>
             </div>
           </div>
         </div>
       </div>
-
-      {/* 杈撳叆鍖哄煙 */}
-      <div className="fixed bottom-0 left-0 right-0 bg-[#131C41] border-t border-[#6ADBFF]/20 p-4">
-        <div className="max-w-4xl mx-auto flex gap-4">
-          <input
-            type="text"
-            placeholder="杈撳叆娑堟伅..."
-            className="flex-1 bg-[#1E2B63] rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#6ADBFF]/50"
-          />
-          <button
-            className="bg-gradient-to-r from-[#6ADBFF] to-[#5E72EB] text-white px-6 py-2 rounded-lg font-medium hover:opacity-90 transition-opacity"
-          >
-            鍙戦��
-          </button>
-        </div>
-      </div>
     </div>
   );
-}
+}
\ No newline at end of file

--
Gitblit v1.9.3