Spaces:
Sleeping
Sleeping
Upload 63 files
Browse files- components/ai/AssessmentPanel.tsx +13 -1
- components/ai/ChatPanel.tsx +26 -11
- types.ts +2 -1
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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
else if (data.type === 'audio') {
|
| 249 |
-
// Update state
|
| 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 |
-
//
|
| 257 |
else if (data.type === 'status' && data.ttsSkipped) {
|
| 258 |
-
|
| 259 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
-
//
|
| 263 |
else if (data.type === 'error') {
|
| 264 |
-
setMessages(prev => prev.map(m => m.id === newAiMsgId ? { ...m, text: `⚠️ 错误: ${data.message}
|
| 265 |
}
|
| 266 |
} catch (e) {}
|
| 267 |
}
|
|
@@ -311,8 +320,15 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ currentUser }) => {
|
|
| 311 |
</div>
|
| 312 |
)}
|
| 313 |
|
| 314 |
-
{
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[];
|
| 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 |
}
|