Spaces:
Sleeping
Sleeping
Update components/ai/ChatPanel.tsx
Browse files- components/ai/ChatPanel.tsx +37 -25
components/ai/ChatPanel.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
|
| 2 |
import React, { useState, useRef, useEffect } from 'react';
|
| 3 |
import { AIChatMessage, User } from '../../types';
|
| 4 |
-
import { Bot, Mic, Square, Volume2, Send, Sparkles, Loader2, Image as ImageIcon, Trash2, X, StopCircle, Globe, Brain, Search } from 'lucide-react';
|
| 5 |
import ReactMarkdown from 'react-markdown';
|
| 6 |
import remarkGfm from 'remark-gfm';
|
| 7 |
import { blobToBase64, base64ToUint8Array, decodePCM, cleanTextForTTS, compressImage } from '../../utils/mediaHelpers';
|
|
@@ -164,6 +164,11 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 164 |
}
|
| 165 |
};
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
const handleSubmit = async () => {
|
| 168 |
if ((!textInput.trim() && !audioAttachment && selectedImages.length === 0) || isChatProcessing) return;
|
| 169 |
|
|
@@ -200,7 +205,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 200 |
timestamp: Date.now()
|
| 201 |
};
|
| 202 |
|
| 203 |
-
const newAiMsg: AIChatMessage = { id: newAiMsgId, role: 'model', text: '', thought: '', timestamp: Date.now() };
|
| 204 |
|
| 205 |
setMessages(prev => [...prev, newUserMsg, newAiMsg]);
|
| 206 |
if (enableThinking) setIsThinkingExpanded(prev => ({ ...prev, [newAiMsgId]: true }));
|
|
@@ -259,7 +264,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 259 |
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, thought: aiThoughtAccumulated } : m));
|
| 260 |
}
|
| 261 |
else if (data.type === 'search') {
|
| 262 |
-
// Enable search visual
|
| 263 |
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, isSearching: true } : m));
|
| 264 |
}
|
| 265 |
else if (data.type === 'status' && data.status === 'tts') {
|
|
@@ -284,6 +289,9 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 284 |
}
|
| 285 |
}
|
| 286 |
}
|
|
|
|
|
|
|
|
|
|
| 287 |
} catch (error: any) {
|
| 288 |
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: '抱歉,连接断开或发生错误。', isSearching: false } : m));
|
| 289 |
} finally {
|
|
@@ -304,7 +312,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 304 |
<Trash2 size={14}/> 清除
|
| 305 |
</button>
|
| 306 |
</div>
|
| 307 |
-
|
| 308 |
{/* Chat History */}
|
| 309 |
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-4 custom-scrollbar">
|
| 310 |
{messages.map(msg => (
|
|
@@ -330,11 +337,11 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 330 |
)}
|
| 331 |
</div>
|
| 332 |
)}
|
| 333 |
-
|
| 334 |
-
{/* Search Status Bubble */}
|
| 335 |
{msg.role === 'model' && msg.isSearching && (
|
| 336 |
-
<div className="flex items-center gap-2 bg-blue-50 text-blue-600 px-3 py-2 rounded-xl mb-2 text-xs border border-blue-100 animate-pulse">
|
| 337 |
-
<Globe size={14} className="animate-spin
|
| 338 |
<span>正在联网搜索相关信息...</span>
|
| 339 |
</div>
|
| 340 |
)}
|
|
@@ -363,17 +370,26 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 363 |
)}
|
| 364 |
|
| 365 |
{(msg.role === 'model' && (msg.text || msg.audio) && !isChatProcessing && !msg.isGeneratingAudio) && (
|
| 366 |
-
<
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
)}
|
| 378 |
</div>
|
| 379 |
</div>
|
|
@@ -382,7 +398,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 382 |
<div ref={messagesEndRef} />
|
| 383 |
</div>
|
| 384 |
|
| 385 |
-
{/*
|
| 386 |
<div className="p-4 bg-white border-t border-gray-200 shrink-0 z-20">
|
| 387 |
<div className="max-w-4xl mx-auto flex flex-col gap-2">
|
| 388 |
|
|
@@ -399,13 +415,12 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 399 |
<button
|
| 400 |
onClick={() => setEnableSearch(!enableSearch)}
|
| 401 |
className={`flex items-center gap-1 text-xs px-2 py-1 rounded-md border transition-all ${enableSearch ? 'bg-blue-50 text-blue-600 border-blue-200' : 'bg-gray-50 text-gray-500 border-transparent hover:bg-gray-100'}`}
|
| 402 |
-
title="开启后AI将联网搜索最新信息 (
|
| 403 |
>
|
| 404 |
<Globe size={14}/> 联网搜索
|
| 405 |
</button>
|
| 406 |
</div>
|
| 407 |
</div>
|
| 408 |
-
|
| 409 |
{/* Attachments Preview */}
|
| 410 |
{(selectedImages.length > 0 || audioAttachment) && (
|
| 411 |
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
@@ -426,7 +441,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 426 |
)}
|
| 427 |
</div>
|
| 428 |
)}
|
| 429 |
-
|
| 430 |
<div className="flex items-end gap-2 bg-gray-100 p-2 rounded-2xl border border-gray-200">
|
| 431 |
{/* Image Button */}
|
| 432 |
<button onClick={() => fileInputRef.current?.click()} className="p-2 text-gray-500 hover:bg-white hover:text-blue-600 rounded-full transition-colors shrink-0">
|
|
@@ -441,7 +455,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 441 |
onChange={handleImageSelect}
|
| 442 |
onClick={(e) => (e.currentTarget.value = '')} // RESET VALUE
|
| 443 |
/>
|
| 444 |
-
|
| 445 |
{/* Input Area */}
|
| 446 |
<div className="flex-1 min-h-[40px] flex items-center">
|
| 447 |
{audioAttachment ? (
|
|
@@ -464,7 +477,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 464 |
/>
|
| 465 |
)}
|
| 466 |
</div>
|
| 467 |
-
|
| 468 |
{/* Right Actions */}
|
| 469 |
{(textInput.trim() || audioAttachment || selectedImages.length > 0) ? (
|
| 470 |
<button onClick={handleSubmit} disabled={isChatProcessing} className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-all shrink-0 shadow-sm disabled:opacity-50">
|
|
|
|
| 1 |
|
| 2 |
import React, { useState, useRef, useEffect } from 'react';
|
| 3 |
import { AIChatMessage, User } from '../../types';
|
| 4 |
+
import { Bot, Mic, Square, Volume2, Send, Sparkles, Loader2, Image as ImageIcon, Trash2, X, StopCircle, Globe, Brain, Search, Copy, ChevronDown, ChevronRight } from 'lucide-react';
|
| 5 |
import ReactMarkdown from 'react-markdown';
|
| 6 |
import remarkGfm from 'remark-gfm';
|
| 7 |
import { blobToBase64, base64ToUint8Array, decodePCM, cleanTextForTTS, compressImage } from '../../utils/mediaHelpers';
|
|
|
|
| 164 |
}
|
| 165 |
};
|
| 166 |
|
| 167 |
+
const handleCopy = (text: string) => {
|
| 168 |
+
navigator.clipboard.writeText(text);
|
| 169 |
+
setToast({ show: true, message: '已复制', type: 'success' });
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
const handleSubmit = async () => {
|
| 173 |
if ((!textInput.trim() && !audioAttachment && selectedImages.length === 0) || isChatProcessing) return;
|
| 174 |
|
|
|
|
| 205 |
timestamp: Date.now()
|
| 206 |
};
|
| 207 |
|
| 208 |
+
const newAiMsg: AIChatMessage = { id: newAiMsgId, role: 'model', text: '', thought: '', timestamp: Date.now(), isSearching: enableSearch };
|
| 209 |
|
| 210 |
setMessages(prev => [...prev, newUserMsg, newAiMsg]);
|
| 211 |
if (enableThinking) setIsThinkingExpanded(prev => ({ ...prev, [newAiMsgId]: true }));
|
|
|
|
| 264 |
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, thought: aiThoughtAccumulated } : m));
|
| 265 |
}
|
| 266 |
else if (data.type === 'search') {
|
| 267 |
+
// Enable search visual explicitly
|
| 268 |
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, isSearching: true } : m));
|
| 269 |
}
|
| 270 |
else if (data.type === 'status' && data.status === 'tts') {
|
|
|
|
| 289 |
}
|
| 290 |
}
|
| 291 |
}
|
| 292 |
+
// Final cleanup
|
| 293 |
+
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, isSearching: false } : m));
|
| 294 |
+
|
| 295 |
} catch (error: any) {
|
| 296 |
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: '抱歉,连接断开或发生错误。', isSearching: false } : m));
|
| 297 |
} finally {
|
|
|
|
| 312 |
<Trash2 size={14}/> 清除
|
| 313 |
</button>
|
| 314 |
</div>
|
|
|
|
| 315 |
{/* Chat History */}
|
| 316 |
<div className="flex-1 overflow-y-auto p-4 space-y-4 pb-4 custom-scrollbar">
|
| 317 |
{messages.map(msg => (
|
|
|
|
| 337 |
)}
|
| 338 |
</div>
|
| 339 |
)}
|
| 340 |
+
|
| 341 |
+
{/* Search Status Bubble (Added) */}
|
| 342 |
{msg.role === 'model' && msg.isSearching && (
|
| 343 |
+
<div className="flex items-center gap-2 bg-blue-50 text-blue-600 px-3 py-2 rounded-xl mb-2 text-xs border border-blue-100 animate-pulse w-fit">
|
| 344 |
+
<Globe size={14} className="animate-spin"/>
|
| 345 |
<span>正在联网搜索相关信息...</span>
|
| 346 |
</div>
|
| 347 |
)}
|
|
|
|
| 370 |
)}
|
| 371 |
|
| 372 |
{(msg.role === 'model' && (msg.text || msg.audio) && !isChatProcessing && !msg.isGeneratingAudio) && (
|
| 373 |
+
<div className="flex gap-2 mt-2">
|
| 374 |
+
<button
|
| 375 |
+
onClick={() => playingMessageId === msg.id ? stopPlayback() : playAudio(msg)}
|
| 376 |
+
className={`flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border transition-colors w-fit ${
|
| 377 |
+
playingMessageId === msg.id
|
| 378 |
+
? 'bg-blue-100 text-blue-600 border-blue-200 animate-pulse'
|
| 379 |
+
: 'bg-gray-50 text-gray-600 hover:bg-gray-100 border border-gray-200'
|
| 380 |
+
}`}
|
| 381 |
+
>
|
| 382 |
+
{playingMessageId === msg.id ? <Square size={14} fill="currentColor"/> : <Volume2 size={14}/>}
|
| 383 |
+
{playingMessageId === msg.id ? '朗读中...' : '朗读'}
|
| 384 |
+
</button>
|
| 385 |
+
<button
|
| 386 |
+
onClick={() => handleCopy(msg.text || '')}
|
| 387 |
+
className="flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border bg-gray-50 text-gray-600 hover:bg-gray-100 border-gray-200 transition-colors w-fit"
|
| 388 |
+
title="复制"
|
| 389 |
+
>
|
| 390 |
+
<Copy size={14}/>
|
| 391 |
+
</button>
|
| 392 |
+
</div>
|
| 393 |
)}
|
| 394 |
</div>
|
| 395 |
</div>
|
|
|
|
| 398 |
<div ref={messagesEndRef} />
|
| 399 |
</div>
|
| 400 |
|
| 401 |
+
{/* Input Area */}
|
| 402 |
<div className="p-4 bg-white border-t border-gray-200 shrink-0 z-20">
|
| 403 |
<div className="max-w-4xl mx-auto flex flex-col gap-2">
|
| 404 |
|
|
|
|
| 415 |
<button
|
| 416 |
onClick={() => setEnableSearch(!enableSearch)}
|
| 417 |
className={`flex items-center gap-1 text-xs px-2 py-1 rounded-md border transition-all ${enableSearch ? 'bg-blue-50 text-blue-600 border-blue-200' : 'bg-gray-50 text-gray-500 border-transparent hover:bg-gray-100'}`}
|
| 418 |
+
title="开启后AI将联网搜索最新信息 (仅部分模型支持)"
|
| 419 |
>
|
| 420 |
<Globe size={14}/> 联网搜索
|
| 421 |
</button>
|
| 422 |
</div>
|
| 423 |
</div>
|
|
|
|
| 424 |
{/* Attachments Preview */}
|
| 425 |
{(selectedImages.length > 0 || audioAttachment) && (
|
| 426 |
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
|
|
| 441 |
)}
|
| 442 |
</div>
|
| 443 |
)}
|
|
|
|
| 444 |
<div className="flex items-end gap-2 bg-gray-100 p-2 rounded-2xl border border-gray-200">
|
| 445 |
{/* Image Button */}
|
| 446 |
<button onClick={() => fileInputRef.current?.click()} className="p-2 text-gray-500 hover:bg-white hover:text-blue-600 rounded-full transition-colors shrink-0">
|
|
|
|
| 455 |
onChange={handleImageSelect}
|
| 456 |
onClick={(e) => (e.currentTarget.value = '')} // RESET VALUE
|
| 457 |
/>
|
|
|
|
| 458 |
{/* Input Area */}
|
| 459 |
<div className="flex-1 min-h-[40px] flex items-center">
|
| 460 |
{audioAttachment ? (
|
|
|
|
| 477 |
/>
|
| 478 |
)}
|
| 479 |
</div>
|
|
|
|
| 480 |
{/* Right Actions */}
|
| 481 |
{(textInput.trim() || audioAttachment || selectedImages.length > 0) ? (
|
| 482 |
<button onClick={handleSubmit} disabled={isChatProcessing} className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-all shrink-0 shadow-sm disabled:opacity-50">
|