dvc890 commited on
Commit
1d4fe4a
·
verified ·
1 Parent(s): 801d164

Upload 63 files

Browse files
components/ai/AssessmentPanel.tsx CHANGED
@@ -222,12 +222,24 @@ export const AssessmentPanel: React.FC<AssessmentPanelProps> = ({ currentUser })
222
  if (audioData) {
223
  setStreamedAssessment(prev => ({ ...prev, audio: audioData }));
224
  setAssessmentStatus('COMPLETED');
225
- // Auto-play
226
  playPCMAudio(audioData);
227
  }
228
 
229
  if (data.ttsSkipped || (data.type === 'status' && data.ttsSkipped)) {
230
  setAssessmentStatus('COMPLETED');
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
 
233
  if (data.error || (data.type === 'error')) {
 
222
  if (audioData) {
223
  setStreamedAssessment(prev => ({ ...prev, audio: audioData }));
224
  setAssessmentStatus('COMPLETED');
225
+ // Auto-play remote audio
226
  playPCMAudio(audioData);
227
  }
228
 
229
  if (data.ttsSkipped || (data.type === 'status' && data.ttsSkipped)) {
230
  setAssessmentStatus('COMPLETED');
231
+ // Force Auto-play using local browser TTS as fallback
232
+ const feedbackMatch = accumulatedRaw.match(/## Feedback\s+([\s\S]*?)(?=## Score|$)/i);
233
+ const feedbackText = feedbackMatch ? feedbackMatch[1].trim() : "";
234
+ if (feedbackText) {
235
+ const cleanText = cleanTextForTTS(feedbackText);
236
+ const utterance = new SpeechSynthesisUtterance(cleanText);
237
+ utterance.lang = 'zh-CN';
238
+ utterance.onend = () => setIsPlayingResponse(false);
239
+ utterance.onerror = () => setIsPlayingResponse(false);
240
+ window.speechSynthesis.speak(utterance);
241
+ setIsPlayingResponse(true);
242
+ }
243
  }
244
 
245
  if (data.error || (data.type === 'error')) {
components/ai/ChatPanel.tsx CHANGED
@@ -244,24 +244,33 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
244
  setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: aiTextAccumulated } : m));
245
  }
246
 
247
- // 2. Audio Arrived (Independent of text source)
 
 
 
 
 
248
  else if (data.type === 'audio') {
249
- // Update state preserving existing text
250
- setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, audio: data.audio } : m));
251
  // Auto-play
252
  const tempMsg = { ...newAiMsg, text: aiTextAccumulated, audio: data.audio };
253
  playAudio(tempMsg);
254
  }
255
 
256
- // 3. Status handling
257
  else if (data.type === 'status' && data.ttsSkipped) {
258
- // If TTS skipped, maybe play browser TTS if text exists?
259
- // Only play if it's the final update
 
 
 
 
260
  }
261
 
262
- // 4. Error
263
  else if (data.type === 'error') {
264
- setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: `⚠️ 错误: ${data.message}` } : m));
265
  }
266
  } catch (e) {}
267
  }
@@ -311,8 +320,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
311
  </div>
312
  )}
313
 
314
- {/* Play Button Logic: Show if model, has text, and not currently processing initial response (to prevent early clicks) */}
315
- {(msg.role === 'model' && msg.text && !isChatProcessing) && (
 
 
 
 
 
 
 
316
  <button
317
  onClick={() => playingMessageId === msg.id ? stopPlayback() : playAudio(msg)}
318
  className={`mt-2 flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border transition-colors w-fit ${
@@ -395,7 +411,6 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
395
  </div>
396
 
397
  {/* Right Actions */}
398
- {/* If text or audio or image exists, show SEND. Else show Mic (Hold) */}
399
  {(textInput.trim() || audioAttachment || selectedImages.length > 0) ? (
400
  <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">
401
  {isChatProcessing ? <Loader2 className="animate-spin" size={20}/> : <Send size={20}/>}
 
244
  setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: aiTextAccumulated } : m));
245
  }
246
 
247
+ // 2. Status Update (e.g. Generating TTS)
248
+ else if (data.type === 'status' && data.status === 'tts') {
249
+ setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, isGeneratingAudio: true } : m));
250
+ }
251
+
252
+ // 3. Audio Arrived
253
  else if (data.type === 'audio') {
254
+ // Update state: save audio, clear generating flag
255
+ setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, audio: data.audio, isGeneratingAudio: false } : m));
256
  // Auto-play
257
  const tempMsg = { ...newAiMsg, text: aiTextAccumulated, audio: data.audio };
258
  playAudio(tempMsg);
259
  }
260
 
261
+ // 4. TTS Skipped (play browser TTS)
262
  else if (data.type === 'status' && data.ttsSkipped) {
263
+ setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, isGeneratingAudio: false } : m));
264
+ // Fallback play
265
+ if (aiTextAccumulated) {
266
+ const tempMsg = { ...newAiMsg, text: aiTextAccumulated };
267
+ playAudio(tempMsg);
268
+ }
269
  }
270
 
271
+ // 5. Error
272
  else if (data.type === 'error') {
273
+ setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: `⚠️ 错误: ${data.message}`, isGeneratingAudio: false } : m));
274
  }
275
  } catch (e) {}
276
  }
 
320
  </div>
321
  )}
322
 
323
+ {msg.isGeneratingAudio && (
324
+ <div className="flex items-center gap-2 text-blue-600 py-1 mt-1 bg-blue-50 px-2 rounded-lg w-fit animate-pulse">
325
+ <Loader2 className="animate-spin" size={12}/>
326
+ <span className="text-[10px] font-bold">正在合成语音...</span>
327
+ </div>
328
+ )}
329
+
330
+ {/* Play Button Logic */}
331
+ {(msg.role === 'model' && (msg.text || msg.audio) && !isChatProcessing && !msg.isGeneratingAudio) && (
332
  <button
333
  onClick={() => playingMessageId === msg.id ? stopPlayback() : playAudio(msg)}
334
  className={`mt-2 flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border transition-colors w-fit ${
 
411
  </div>
412
 
413
  {/* Right Actions */}
 
414
  {(textInput.trim() || audioAttachment || selectedImages.length > 0) ? (
415
  <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">
416
  {isChatProcessing ? <Loader2 className="animate-spin" size={20}/> : <Send size={20}/>}
types.ts CHANGED
@@ -394,7 +394,8 @@ export interface AIChatMessage {
394
  role: 'user' | 'model';
395
  text?: string;
396
  audio?: string;
397
- images?: string[]; // Added: Array of Base64 strings for images
398
  isAudioMessage?: boolean;
 
399
  timestamp: number;
400
  }
 
394
  role: 'user' | 'model';
395
  text?: string;
396
  audio?: string;
397
+ images?: string[];
398
  isAudioMessage?: boolean;
399
+ isGeneratingAudio?: boolean; // New status flag for UI
400
  timestamp: number;
401
  }