From 6379adfd761b0a6cddd03d42dd96f1a619cda2c0 Mon Sep 17 00:00:00 2001
From: hongjli <3117313295@qq.com>
Date: 星期四, 24 四月 2025 16:48:42 +0800
Subject: [PATCH] 聊天页面

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

diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx
index 415bf60..21ff564 100644
--- a/src/app/chat/page.tsx
+++ b/src/app/chat/page.tsx
@@ -1,70 +1,486 @@
 'use client';
 
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useRef, useCallback } 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';
+
+interface Message {
+  role: 'user' | 'assistant';
+  content: string;
+  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";
 
 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 messagesEndRef = useRef<HTMLDivElement>(null);
+  const errorTimeoutRef = useRef<any>(null);
 
   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;
+  }, []);
+
+  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('');
+
+    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: []
+        })
+      });
+
+      if (!response.ok) {
+        const errorText = await response.text();
+        let errorMessage;
+        try {
+          const errorJson = JSON.parse(errorText);
+          errorMessage = errorJson.error || `Error: ${response.status}`;
+        } catch {
+          errorMessage = `Error: ${response.status}`;
+        }
+        throw new Error(errorMessage);
+      }
+
+      const reader = response.body?.getReader();
+      if (!reader) throw new Error('No reader available');
+
+      const decoder = new TextDecoder();
+      let buffer = '';
+
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) {
+          setIsMessageComplete(true);
+          break;
+        }
+
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split('\n');
+        buffer = lines.pop() || '';
+
+        for (const line of lines) {
+          if (line.trim() === '') continue;
+          if (line.startsWith('data: ')) {
+            try {
+              const data = JSON.parse(line.slice(6));
+              
+              // 蹇界暐ping浜嬩欢
+              if (data.event === 'ping') continue;
+
+              switch (data.event) {
+                case 'message':
+                  setMessages(prev => {
+                    const newMessages = [...prev];
+                    const lastMessage = newMessages[newMessages.length - 1];
+                    if (lastMessage?.role === 'assistant' && lastMessage.id === currentMessageId) {
+                      lastMessage.content = data.answer || lastMessage.content;
+                      if (data.message_id) {
+                        lastMessage.id = data.message_id;
+                        setCurrentMessageId(data.message_id);
+                      }
+                    }
+                    return newMessages;
+                  });
+                  break;
+
+                case 'message_end':
+                  if (data.conversation_id) {
+                    setConversationId(data.conversation_id);
+                    setMessages(prev => {
+                      const newMessages = [...prev];
+                      const lastMessage = newMessages[newMessages.length - 1];
+                      if (lastMessage?.role === 'assistant' && lastMessage.id === currentMessageId) {
+                        lastMessage.conversation_id = data.conversation_id;
+                        if (data.metadata) {
+                          lastMessage.metadata = data.metadata;
+                        }
+                      }
+                      return newMessages;
+                    });
+                  }
+                  setIsMessageComplete(true);
+                  break;
+
+                case 'error':
+                  console.error('Error event received:', data);
+                  setIsMessageComplete(true);
+                  throw new Error(data.message || '鍙戦�佹秷鎭椂鍑洪敊');
+              }
+            } catch (e) {
+              console.error('Error parsing SSE data:', e);
+              if (!messages[messages.length - 1]?.content) {
+                setIsMessageComplete(true);
+                throw e;
+              }
+            }
+          }
+        }
+      }
+    } catch (err) {
+      console.error('Chat error:', err);
+      showErrorMessage(err instanceof Error ? err.message : '鍙戦�佹秷鎭椂鍑洪敊');
+      
+      setMessages(prev => {
+        const newMessages = [...prev];
+        const lastMessage = newMessages[newMessages.length - 1];
+        if (lastMessage?.role === 'assistant') {
+          lastMessage.content = '鎶辨瓑锛屾秷鎭彂閫佸け璐ワ紝璇风◢鍚庨噸璇曘��';
+        }
+        return newMessages;
+      });
+    } finally {
+      setIsStreaming(false);
+    }
+  };
+
+  // 鏇存柊鍙戦�佹寜閽殑绂佺敤鐘舵��
+  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);
+  }, []);
 
   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>
-      </div>
-
-      {/* 鑱婂ぉ鍖哄煙 */}
-      <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="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">
+              <input
+                type="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"
+              />
+            </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>
+      )}
 
-      {/* 杈撳叆鍖哄煙 */}
-      <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>
+      {/* 閿欒鎻愮ず */}
+      {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="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.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>
+                    {msg.role === 'assistant' && (
+                      <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></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 min-h-[42px]`}>
+                      {msg.role === 'assistant' && !isMessageComplete && currentMessageId === msg.id && (
+                        <div className="absolute left-0 top-0 w-full h-full flex items-center justify-center">
+                          <div className="w-5 h-5 border-2 border-blue-500/30 border-t-blue-500/80 rounded-full animate-spin"></div>
+                        </div>
+                      )}
+                      <div className={`flex items-center px-4 min-h-[42px] ${
+                        msg.role === 'assistant' && !isMessageComplete && currentMessageId === msg.id ? 'invisible' : ''
+                      }`}>
+                        <div className="prose prose-sm max-w-none text-[14px] leading-[1.3]
+                          prose-p:my-0 prose-p:leading-[1.3]
+                          prose-headings:font-medium prose-headings:text-gray-800
+                          prose-h1:text-lg prose-h1:my-2
+                          prose-h2:text-base prose-h2:my-2
+                          prose-h3:text-base prose-h3:my-1.5
+                          prose-ul:my-1.5 prose-ul:pl-4 prose-li:my-0.5
+                          prose-ol:my-1.5 prose-ol:pl-4
+                          prose-code:px-1 prose-code:py-0.5 prose-code:bg-gray-100 prose-code:rounded prose-code:text-gray-800 prose-code:before:content-[''] prose-code:after:content-['']
+                          prose-pre:my-2 prose-pre:p-2.5 prose-pre:bg-gray-800 prose-pre:rounded-lg
+                          prose-a:text-blue-500 prose-a:no-underline hover:prose-a:underline
+                          prose-blockquote:my-2 prose-blockquote:pl-3 prose-blockquote:border-l-4 prose-blockquote:border-gray-200
+                          prose-strong:font-medium prose-strong:text-gray-800
+                          prose-table:my-2 prose-tr:border-gray-200 prose-td:py-1 prose-td:px-2">
+                          {msg.content.split('\n').map((line, i) => (
+                            <div key={i} className="my-0">
+                              {line.trim() && (
+                                <ReactMarkdown 
+                                  remarkPlugins={[remarkGfm]}
+                                  rehypePlugins={[rehypeRaw, rehypeSanitize]}
+                                >
+                                  {line.trim()}
+                                </ReactMarkdown>
+                              )}
+                            </div>
+                          ))}
+                        </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>
   );
 }
+

--
Gitblit v1.9.3