From 229af563f799c9e9daad503d5353b6d29e7f3b34 Mon Sep 17 00:00:00 2001 From: hongjli <3117313295@qq.com> Date: 星期五, 25 四月 2025 14:35:11 +0800 Subject: [PATCH] 聊天页面优化 --- src/app/chat/page.tsx | 612 ++++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 469 insertions(+), 143 deletions(-) diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 21ff564..1301f70 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -11,7 +11,7 @@ interface Message { role: 'user' | 'assistant'; - content: string; + content: string | null; timestamp: number; id: string; conversation_id?: string; @@ -48,8 +48,20 @@ // 娣诲姞涓�涓柊鐨勭姸鎬佹潵鎺у埗娑堟伅鐨勬樉绀� 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(() => { // 鑾峰彇瀛樺偍鐨凙PI Key @@ -98,6 +110,22 @@ } }; return cleanup; + }, []); + + // 娣诲姞缁勪欢鍗歌浇鏃剁殑娓呯悊宸ヤ綔 + useEffect(() => { + // 缁勪欢鎸傝浇鏃讹紝璁剧疆涓簍rue + isMountedRef.current = true; + + // 缁勪欢鍗歌浇鏃讹紝璁剧疆涓篺alse + return () => { + isMountedRef.current = false; + + // 娓呴櫎鎵�鏈夊畾鏃跺櫒 + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + }; }, []); const handleApiKeySubmit = () => { @@ -149,6 +177,8 @@ setCurrentMessageId(assistantMessage.id); setMessage(''); + let controller: AbortController | null = new AbortController(); + try { const response = await fetch(`${BASE_URL}/v1/chat-messages`, { method: 'POST', @@ -164,112 +194,289 @@ 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 || `Error: ${response.status}`; + errorMessage = errorJson.error || `閿欒: ${response.status}`; } catch { - errorMessage = `Error: ${response.status}`; + errorMessage = `閿欒: ${response.status}`; } throw new Error(errorMessage); } - const reader = response.body?.getReader(); - if (!reader) throw new Error('No reader available'); + // 纭繚response.body瀛樺湪 + if (!response.body) { + throw new Error('鍝嶅簲娌℃湁鎻愪緵鏁版嵁娴�'); + } + const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { - const { done, value } = await reader.read(); - if (done) { - setIsMessageComplete(true); - break; - } + try { + const { done, value } = await reader.read(); + if (done) { + console.log("娴佸紡鍝嶅簲鎺ユ敹瀹屾瘯"); + setIsMessageComplete(true); + break; + } - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; + 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; - if (line.startsWith('data: ')) { - try { - const data = JSON.parse(line.slice(6)); - - // 蹇界暐ping浜嬩欢 - if (data.event === 'ping') continue; + 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': - 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); + 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); } } - 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; - } + 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); } - 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) { + } + 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) { - console.error('Chat error:', err); - showErrorMessage(err instanceof Error ? err.message : '鍙戦�佹秷鎭椂鍑洪敊'); + // 妫�鏌ユ槸鍚︽槸涓閿欒 + if (err instanceof Error && err.name === 'AbortError') { + console.log('璇锋眰琚腑姝紝鍙兘鏄粍浠跺嵏杞藉鑷寸殑'); + return; // 涓閿欒涓嶉渶瑕佹樉绀虹粰鐢ㄦ埛 + } - setMessages(prev => { - const newMessages = [...prev]; - const lastMessage = newMessages[newMessages.length - 1]; - if (lastMessage?.role === 'assistant') { - lastMessage.content = '鎶辨瓑锛屾秷鎭彂閫佸け璐ワ紝璇风◢鍚庨噸璇曘��'; - } - return newMessages; - }); + 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 { - setIsStreaming(false); + // 娓呴櫎鎺у埗鍣� + 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; @@ -287,6 +494,59 @@ 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 ( @@ -307,13 +567,34 @@ </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 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 @@ -371,74 +652,119 @@ <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> + {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="mt-0.5 text-xs text-gray-400"> - {msg.role === 'user' && new Date(msg.timestamp).toLocaleTimeString()} + <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 whitespace-pre-wrap"> + {mainContent || (msg.role === 'assistant' && !isMessageComplete ? '姝e湪鎬濊��...' : '')} + </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> - ))} + )) + )} <div ref={messagesEndRef} /> </div> </div> -- Gitblit v1.9.3