Browse Source

upate interview

0235702 1 day ago
parent
commit
3408d56deb

+ 11 - 4
interview-web/src/modules/interview/mobile/page-interview/page-interview.html

@@ -74,12 +74,13 @@
         <div class="voice-controls">
             <!-- 麦克风按钮 -->
             <button class="voice-btn" 
-                    (mousedown)="startVoiceInput()" 
-                    (mouseup)="stopVoiceInput()"
-                    [class.active]="isListening"
-                    [disabled]="isSpeaking || isAnalyzing">
+          (touchstart)="startVoiceInput()" (touchend)="stopVoiceInput()"
+          (mousedown)="startVoiceInput()" (mouseup)="stopVoiceInput()"
+          (mouseleave)="stopVoiceInput()"
+          [class.active]="isListening">
                 <i class="fas fa-microphone"></i>
                 <div class="voice-wave"></div>
+                <span class="press-hint">长按说话</span>
             </button>
             
             <!-- 播放问题按钮 -->
@@ -131,6 +132,12 @@
         }
     </div>
     
+    @if (isListening && silenceDuration > 0) {
+    <div class="silence-timer">
+            思考时间: {{ MAX_SILENCE - silenceDuration }}秒
+    </div>
+}
+
     <!-- 分析仪表盘 -->
     @if (!interviewEnded) {
     <div class="dashboard-section">

+ 36 - 1
interview-web/src/modules/interview/mobile/page-interview/page-interview.scss

@@ -266,12 +266,47 @@ body {
   outline: none;
 }
 
+.voice-btn {
+  position: relative;
+  
+  .press-hint {
+    position: absolute;
+    bottom: -25px;
+    left: 50%;
+    transform: translateX(-50%);
+    font-size: 12px;
+    color: #718096;
+    white-space: nowrap;
+  }
+}
+
+/* 思考计时器样式 */
+.silence-timer {
+  margin-top: 10px;
+  padding: 8px 12px;
+  background: #f8f9fa;
+  border-radius: 15px;
+  font-size: 14px;
+  color: #4a5568;
+  text-align: center;
+  animation: pulse-opacity 1.5s infinite;
+}
+
+@keyframes pulse-opacity {
+  0%, 100% { opacity: 0.7; }
+  50% { opacity: 1; }
+}
+
 .voice-btn:hover {
   transform: scale(1.05);
 }
 
 .voice-btn.active {
-  animation: pulse 1.5s infinite;
+  box-shadow: 0 0 0 3px rgba(42, 92, 170, 0.3);
+  
+  .press-hint {
+    display: none;
+  }
 }
 
 .voice-hint {

+ 214 - 49
interview-web/src/modules/interview/mobile/page-interview/page-interview.ts

@@ -30,6 +30,22 @@ export class PageInterview implements OnInit, AfterViewInit {
   userAnswer = '';
   recognition: any;
   interimTranscript = '';
+  private voiceEndTimer: any;
+  private readonly VOICE_END_DELAY = 1500;
+  private speechBuffer: string[] = [];
+  private lastFinalResultIndex = 0;
+  
+  // 思考时间控制
+  private silenceTimer: any;
+  silenceDuration = 0;
+  readonly MAX_SILENCE = 5;
+
+  // 自我介绍相关
+  private isSelfIntroduction = false;
+  private minSelfIntroDuration = 5000; // 至少5秒自我介绍时间
+  private selfIntroTimer: any;
+  private minSelfIntroWords = 30; // 至少30字的自我介绍
+
   // 问题列表
   mainQuestions = [
     "欢迎参加本次AI面试,我是您的面试官AI助手。",
@@ -45,7 +61,7 @@ export class PageInterview implements OnInit, AfterViewInit {
   messages: {sender: 'ai' | 'user', text: string}[] = [];
   
   // 头像状态
-  avatarImage = "assets/default-avatar.svg";
+  avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f916.svg";
   expressionText = "等待开始...";
   
   // 评估指标
@@ -59,7 +75,6 @@ export class PageInterview implements OnInit, AfterViewInit {
   private radarChart: any;
 
   constructor(private aiService: AiApiService) {
-    // 初始化语音识别
     this.initSpeechRecognition();
   }
 
@@ -72,52 +87,128 @@ export class PageInterview implements OnInit, AfterViewInit {
     this.initRadarChart();
   }
 
-
-
   private initSpeechRecognition() {
     const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
     if (SpeechRecognition) {
       this.recognition = new SpeechRecognition();
       this.recognition.lang = 'zh-CN';
-      this.recognition.interimResults = true; // 启用临时结果
+      this.recognition.interimResults = true;
       this.recognition.continuous = true;
 
+      this.recognition.onsoundstart = () => {
+        this.updateAvatarState('listening');
+        this.resetSilenceTimer();
+      };
+
+      this.recognition.onsoundend = () => {
+        this.stopSilenceTimer();
+      };
+
       this.recognition.onresult = (event: any) => {
+        this.resetSilenceTimer();
+        
         let finalTranscript = '';
         let interimTranscript = '';
+        let hasNewFinalResult = false;
 
-        for (let i = event.resultIndex; i < event.results.length; i++) {
+        for (let i = this.lastFinalResultIndex; i < event.results.length; i++) {
           const transcript = event.results[i][0].transcript;
           if (event.results[i].isFinal) {
+            this.speechBuffer.push(transcript);
             finalTranscript += transcript;
+            hasNewFinalResult = true;
+            this.lastFinalResultIndex = i + 1;
           } else {
-            interimTranscript += transcript;
+            interimTranscript = transcript;
           }
         }
 
-        if (finalTranscript) {
-          this.userAnswer = finalTranscript;
-          this.showSubmitButton = true;
-          this.showCancelButton = true;
+        if (this.speechBuffer.length > 0) {
+          this.userAnswer = this.speechBuffer.join(' ');
         }
+
+        if (hasNewFinalResult) {
+          this.resetVoiceEndTimer();
+        }
+
         this.interimTranscript = interimTranscript;
       };
 
       this.recognition.onerror = (event: any) => {
         console.error('语音识别错误:', event.error);
         this.updateAvatarState('listening');
+        this.stopVoiceEndTimer();
+        
+        if (this.isListening && event.error !== 'no-speech') {
+          setTimeout(() => {
+            if (this.isListening) {
+              this.recognition.start();
+            }
+          }, 500);
+        }
       };
 
       this.recognition.onend = () => {
         if (this.isListening) {
-          this.stopVoiceInput();
+          this.recognition.start();
         }
       };
     }
   }
 
+  private resetVoiceEndTimer() {
+    this.stopVoiceEndTimer();
+    this.voiceEndTimer = setTimeout(() => {
+      if (this.userAnswer.trim().length > 0) {
+        this.showSubmitButton = true;
+        this.showCancelButton = true;
+      }
+    }, this.VOICE_END_DELAY);
+  }
+
+  private stopVoiceEndTimer() {
+    if (this.voiceEndTimer) {
+      clearTimeout(this.voiceEndTimer);
+      this.voiceEndTimer = null;
+    }
+  }
+
+  private startSelfIntroTimer() {
+    this.stopSelfIntroTimer();
+    this.selfIntroTimer = setTimeout(() => {
+      if (this.isListening && this.userAnswer.trim().length > 0) {
+        this.stopVoiceInput();
+        this.showSubmitButton = true;
+        this.showCancelButton = true;
+      }
+    }, this.minSelfIntroDuration);
+  }
+
+  private stopSelfIntroTimer() {
+    if (this.selfIntroTimer) {
+      clearTimeout(this.selfIntroTimer);
+      this.selfIntroTimer = null;
+    }
+  }
+
+  private resetSilenceTimer() {
+    this.stopSilenceTimer();
+    this.silenceDuration = 0;
+    
+    this.silenceTimer = setInterval(() => {
+      this.silenceDuration++;
+    }, 1000);
+  }
+
+  private stopSilenceTimer() {
+    if (this.silenceTimer) {
+      clearInterval(this.silenceTimer);
+      this.silenceTimer = null;
+    }
+  }
+
   private initProgressTimer(): void {
-    const totalSeconds = 10 * 60; // 10分钟 = 600秒
+    const totalSeconds = 10 * 60;
     let elapsedSeconds = 0;
     
     const timer = setInterval(() => {
@@ -129,19 +220,17 @@ export class PageInterview implements OnInit, AfterViewInit {
       elapsedSeconds++;
       this.progress = Math.min((elapsedSeconds / totalSeconds) * 100, 100);
       
-      // 更新时间显示
       const remainingSeconds = totalSeconds - elapsedSeconds;
       const minutes = Math.floor(remainingSeconds / 60);
       const seconds = remainingSeconds % 60;
       this.remainingTime = `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
       
-      // 时间到自动结束
       if (elapsedSeconds >= totalSeconds) {
         clearInterval(timer);
         this.addAIMessage("时间到,本次面试结束");
         this.interviewEnded = true;
       }
-    }, 1000); // 改为每秒更新一次
+    }, 1000);
   }
 
   initConversation(): void {
@@ -160,10 +249,20 @@ export class PageInterview implements OnInit, AfterViewInit {
     this.currentQuestionIndex = index;
     this.isFollowUpQuestion = false;
     this.currentQuestion = this.mainQuestions[index];
+    this.isSelfIntroduction = index === 1; // 第二个问题是自我介绍
+    
+    this.speechBuffer = [];
+    this.lastFinalResultIndex = 0;
+    this.userAnswer = '';
+    this.interimTranscript = '';
     
     this.addAIMessage(this.currentQuestion);
     this.updateAvatarState('speaking');
-    this.speakText(this.currentQuestion); // 自动语音播放问题
+    this.speakText(this.currentQuestion);
+
+    if (this.isSelfIntroduction) {
+      this.startSelfIntroTimer();
+    }
   }
 
   async askFollowUpQuestion(followUpText: string): Promise<void> {
@@ -171,6 +270,12 @@ export class PageInterview implements OnInit, AfterViewInit {
     
     this.isFollowUpQuestion = true;
     this.currentQuestion = followUpText;
+    this.isSelfIntroduction = false;
+    
+    this.speechBuffer = [];
+    this.lastFinalResultIndex = 0;
+    this.userAnswer = '';
+    this.interimTranscript = '';
     
     this.addAIMessage("关于您刚才的回答,我有一个跟进问题...");
     setTimeout(() => {
@@ -192,6 +297,10 @@ export class PageInterview implements OnInit, AfterViewInit {
     this.messages = [];
     this.metrics = { expressiveness: 0, professionalism: 0, relevance: 0 };
     this.progress = 0;
+    this.speechBuffer = [];
+    this.lastFinalResultIndex = 0;
+    this.isSelfIntroduction = false;
+    this.stopSelfIntroTimer();
     this.initConversation();
   }
 
@@ -243,21 +352,82 @@ export class PageInterview implements OnInit, AfterViewInit {
     
     this.isListening = true;
     this.userAnswer = '';
-    this.updateAvatarState('listening');
-    this.recognition.start();
+    this.interimTranscript = '';
+    this.showSubmitButton = false;
+    this.showCancelButton = false;
+    this.speechBuffer = [];
+    this.lastFinalResultIndex = 0;
+    
+    // 调整自我介绍问题的识别参数
+    if (this.isSelfIntroduction) {
+      this.recognition.continuous = true;
+      this.recognition.interimResults = true;
+      this.recognition.maxAlternatives = 1;
+    }
+    
+    try {
+      this.recognition.start();
+    } catch (e) {
+      console.error('语音识别启动失败:', e);
+      setTimeout(() => this.startVoiceInput(), 500);
+    }
   }
 
   stopVoiceInput(): void {
+    if (!this.isListening) return;
+    
     this.isListening = false;
-    if (this.recognition) {
+    this.stopSilenceTimer();
+    this.stopVoiceEndTimer();
+    this.stopSelfIntroTimer();
+    
+    try {
       this.recognition.stop();
+    } catch (e) {
+      console.error('语音识别停止失败:', e);
     }
+    
+    setTimeout(() => {
+      if (this.userAnswer.trim().length > 0) {
+        this.showSubmitButton = true;
+        this.showCancelButton = true;
+      }
+    }, 500);
+    
     this.updateAvatarState('waiting');
   }
 
+  private getWordCount(text: string): number {
+    // 简单的中文字数统计
+    return text.replace(/[^\u4e00-\u9fa5]/g, '').length;
+  }
+
+  private validateSelfIntroduction(): boolean {
+    if (!this.isSelfIntroduction) return true;
+    
+    const wordCount = this.getWordCount(this.userAnswer);
+    if (wordCount < this.minSelfIntroWords) {
+      this.addAIMessage(`您的自我介绍需要至少${this.minSelfIntroWords}字,请再详细介绍一下。`);
+      return false;
+    }
+    return true;
+  }
+
   async submitAnswer(): Promise<void> {
     if (!this.userAnswer || this.isAnalyzing) return;
     
+    // 验证自我介绍是否符合要求
+    if (!this.validateSelfIntroduction()) {
+      // 不符合要求,重置状态让用户重新回答
+      this.userAnswer = '';
+      this.speechBuffer = [];
+      this.lastFinalResultIndex = 0;
+      this.showSubmitButton = false;
+      this.showCancelButton = false;
+      this.startVoiceInput();
+      return;
+    }
+    
     this.showSubmitButton = false;
     this.showCancelButton = false;
     this.addUserMessage(this.userAnswer);
@@ -271,31 +441,25 @@ export class PageInterview implements OnInit, AfterViewInit {
         this.userAnswer
       );
 
-      // 更新评估指标
       this.metrics = {
         expressiveness: evaluation.metrics.expressiveness,
         professionalism: evaluation.metrics.professionalism,
         relevance: evaluation.metrics.relevance
       };
 
-      // 添加AI反馈
       this.addAIMessage(evaluation.feedback);
 
-      // 检查回答相关性
       if (evaluation.metrics.relevance < 30) {
         this.addAIMessage("您的回答与问题相关性较低,本次面试结束。");
         this.interviewEnded = true;
         return;
       }
 
-      // 处理问题流程
       if (!this.isFollowUpQuestion && this.currentQuestionIndex !== 1) {
-        // 主问题(非自我介绍)需要追问
         setTimeout(() => {
           this.askFollowUpQuestion(evaluation.followUpQuestion);
         }, 1500);
       } else {
-        // 进入下一个问题
         setTimeout(() => {
           this.askQuestion(this.currentQuestionIndex + 1);
         }, 1500);
@@ -307,6 +471,9 @@ export class PageInterview implements OnInit, AfterViewInit {
     } finally {
       this.isAnalyzing = false;
       this.userAnswer = '';
+      this.speechBuffer = [];
+      this.lastFinalResultIndex = 0;
+      this.isSelfIntroduction = false;
     }
   }
 
@@ -314,6 +481,9 @@ export class PageInterview implements OnInit, AfterViewInit {
     this.showSubmitButton = false;
     this.showCancelButton = false;
     this.userAnswer = '';
+    this.speechBuffer = [];
+    this.lastFinalResultIndex = 0;
+    this.isSelfIntroduction = false;
     this.updateAvatarState('listening');
   }
 
@@ -324,27 +494,24 @@ export class PageInterview implements OnInit, AfterViewInit {
   }
 
   updateAvatarState(state: 'speaking' | 'listening' | 'waiting' | 'analyzing' | 'default'): void {
-    switch(state) {
-      case 'speaking':
-        this.avatarImage = "assets/ai-speaking.svg";
-        this.expressionText = "正在提问...";
-        break;
-      case 'listening':
-        this.avatarImage = "assets/ai-listening.svg";
-        this.expressionText = "正在聆听...";
-        break;
-      case 'waiting':
-        this.avatarImage = "assets/ai-waiting.svg";
-        this.expressionText = "等待确认...";
-        break;
-      case 'analyzing':
-        this.avatarImage = "assets/ai-analyzing.svg";
-        this.expressionText = "分析回答中...";
-        break;
-      default:
-        this.avatarImage = "assets/default-avatar.svg";
-        this.expressionText = "AI面试官";
-    }
+    const emojiMap = {
+      'speaking': '1f5e3',
+      'listening': '1f3a4',
+      'waiting': '1f914',
+      'analyzing': '1f9d0',
+      'default': '1f916'
+    };
+    
+    const stateTexts = {
+      'speaking': "正在提问...",
+      'listening': "正在聆听...",
+      'waiting': "等待确认...",
+      'analyzing': "分析回答中...",
+      'default': "AI面试官"
+    };
+
+    this.avatarImage = `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${emojiMap[state]}.svg`;
+    this.expressionText = stateTexts[state];
   }
 
   toggleRadarChart(): void {
@@ -421,6 +588,4 @@ export class PageInterview implements OnInit, AfterViewInit {
       this.radarChart?.resize();
     });
   }
-
-  
 }