dvc890 commited on
Commit
703b6f3
·
verified ·
1 Parent(s): 586757f

Update components/ai/ChatPanel.tsx

Browse files
Files changed (1) hide show
  1. 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-slow"/>
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
- <button
367
- onClick={() => playingMessageId === msg.id ? stopPlayback() : playAudio(msg)}
368
- className={`mt-2 flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border transition-colors w-fit ${
369
- playingMessageId === msg.id
370
- ? 'bg-blue-100 text-blue-600 border-blue-200 animate-pulse'
371
- : 'bg-gray-50 text-gray-600 hover:bg-gray-100 border border-gray-200'
372
- }`}
373
- >
374
- {playingMessageId === msg.id ? <Square size={14} fill="currentColor"/> : <Volume2 size={14}/>}
375
- {playingMessageId === msg.id ? '朗读中...' : '朗读'}
376
- </button>
 
 
 
 
 
 
 
 
 
377
  )}
378
  </div>
379
  </div>
@@ -382,7 +398,7 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
382
  <div ref={messagesEndRef} />
383
  </div>
384
 
385
- {/* Improved Input Area */}
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将联网搜索最新信息 (Doubao模型支持)"
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">