|  | @@ -1,6 +1,7 @@
 | 
	
		
			
				|  |  | -import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
 | 
	
		
			
				|  |  | +import { Component, OnInit, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
 | 
	
		
			
				|  |  |  import { CommonModule } from '@angular/common';
 | 
	
		
			
				|  |  |  import * as echarts from 'echarts';
 | 
	
		
			
				|  |  | +import { AiApiService } from '../../../../lib/ai-api.service';
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  @Component({
 | 
	
		
			
				|  |  |    selector: 'app-page-interview',
 | 
	
	
		
			
				|  | @@ -9,197 +10,343 @@ import * as echarts from 'echarts';
 | 
	
		
			
				|  |  |    templateUrl: './page-interview.html',
 | 
	
		
			
				|  |  |    styleUrl: './page-interview.scss'
 | 
	
		
			
				|  |  |  })
 | 
	
		
			
				|  |  | -export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
 | 
	
		
			
				|  |  | -  // 倒计时相关属性
 | 
	
		
			
				|  |  | -  remainingTime: string = '30:00';
 | 
	
		
			
				|  |  | -  remainingMinutes: number = 30;
 | 
	
		
			
				|  |  | -  remainingSeconds: number = 0;
 | 
	
		
			
				|  |  | -  timerInterval: any;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  // 其他现有属性保持不变
 | 
	
		
			
				|  |  | +export class PageInterview implements OnInit, AfterViewInit {
 | 
	
		
			
				|  |  | +  // 计时相关
 | 
	
		
			
				|  |  | +  remainingTime = '10:00'; 
 | 
	
		
			
				|  |  | +  progress = 0;
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  | +  // 面试状态控制
 | 
	
		
			
				|  |  | +  interviewEnded = false;
 | 
	
		
			
				|  |  |    isAnalyzing = false;
 | 
	
		
			
				|  |  |    showSubmitButton = false;
 | 
	
		
			
				|  |  | -  userAnswer = '';
 | 
	
		
			
				|  |  | +  showCancelButton = false;
 | 
	
		
			
				|  |  | +  isFollowUpQuestion = false;
 | 
	
		
			
				|  |  | +  showQuestionCard = true;
 | 
	
		
			
				|  |  | +  showRadarChart = false;
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  | -  avatarImages = {
 | 
	
		
			
				|  |  | -    default: 'data:image/svg+xml;base64,...',
 | 
	
		
			
				|  |  | -    speaking: 'data:image/svg+xml;base64,...',
 | 
	
		
			
				|  |  | -  };
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  questions = [
 | 
	
		
			
				|  |  | -    "欢迎参加本次AI面试,我是您的面试官AI助手。",
 | 
	
		
			
				|  |  | +  // 语音相关
 | 
	
		
			
				|  |  | +  isListening = false;
 | 
	
		
			
				|  |  | +  isSpeaking = false;
 | 
	
		
			
				|  |  | +  userAnswer = '';
 | 
	
		
			
				|  |  | +  recognition: any;
 | 
	
		
			
				|  |  | +  interimTranscript = '';
 | 
	
		
			
				|  |  | +  // 问题列表
 | 
	
		
			
				|  |  | +  mainQuestions = [
 | 
	
		
			
				|  |  | +    "欢迎参加本次AI面试,我是您的面试官AI助手。",
 | 
	
		
			
				|  |  |      "首先,请简单介绍一下您自己。",
 | 
	
		
			
				|  |  |      "您在过去工作中遇到的最大技术挑战是什么?您是如何解决的?",
 | 
	
		
			
				|  |  |      "请描述一个您与团队意见不合时,您是如何处理的案例。"
 | 
	
		
			
				|  |  |    ];
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  |    currentQuestionIndex = 0;
 | 
	
		
			
				|  |  | -  currentQuestion = this.questions[0];
 | 
	
		
			
				|  |  | -  showQuestionCard = false;
 | 
	
		
			
				|  |  | -  progress = 30;
 | 
	
		
			
				|  |  | +  currentQuestion = this.mainQuestions[0];
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  | +  // 对话记录
 | 
	
		
			
				|  |  |    messages: {sender: 'ai' | 'user', text: string}[] = [];
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  | -  isListening = false;
 | 
	
		
			
				|  |  | -  isSpeaking = false;
 | 
	
		
			
				|  |  | +  // 头像状态
 | 
	
		
			
				|  |  | +  avatarImage = "assets/default-avatar.svg";
 | 
	
		
			
				|  |  | +  expressionText = "等待开始...";
 | 
	
		
			
				|  |  |    
 | 
	
		
			
				|  |  | -  avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f913.svg";
 | 
	
		
			
				|  |  | -  expressionText = "正在聆听您的回答...";
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  showRadarChart = false;
 | 
	
		
			
				|  |  | +  // 评估指标
 | 
	
		
			
				|  |  |    metrics = {
 | 
	
		
			
				|  |  | -    expressiveness: 82,
 | 
	
		
			
				|  |  | -    professionalism: 65,
 | 
	
		
			
				|  |  | -    relevance: 73
 | 
	
		
			
				|  |  | +    expressiveness: 0,
 | 
	
		
			
				|  |  | +    professionalism: 0,
 | 
	
		
			
				|  |  | +    relevance: 0
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  presetAnswers = [
 | 
	
		
			
				|  |  | -    "我是张伟,有5年Java开发经验,擅长分布式系统设计。",
 | 
	
		
			
				|  |  | -    "我们曾遇到高并发下的数据库瓶颈,我通过引入Redis缓存和分库分表解决了问题。",
 | 
	
		
			
				|  |  | -    "有一次在技术方案选择上,我通过数据对比和原型验证说服了团队采用我的方案。"
 | 
	
		
			
				|  |  | -  ];
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    @ViewChild('dialogContainer') dialogContainer!: ElementRef;
 | 
	
		
			
				|  |  |    private radarChart: any;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  constructor(private aiService: AiApiService) {
 | 
	
		
			
				|  |  | +    // 初始化语音识别
 | 
	
		
			
				|  |  | +    this.initSpeechRecognition();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    ngOnInit(): void {
 | 
	
		
			
				|  |  |      this.initConversation();
 | 
	
		
			
				|  |  | -    this.startCountdown(); // 启动倒计时
 | 
	
		
			
				|  |  | +    this.initProgressTimer();
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    ngAfterViewInit(): void {
 | 
	
		
			
				|  |  |      this.initRadarChart();
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  ngOnDestroy(): void {
 | 
	
		
			
				|  |  | -    if (this.timerInterval) {
 | 
	
		
			
				|  |  | -      clearInterval(this.timerInterval);
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  // 倒计时相关方法
 | 
	
		
			
				|  |  | -  private startCountdown() {
 | 
	
		
			
				|  |  | -    this.updateTimeDisplay();
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  | -    this.timerInterval = setInterval(() => {
 | 
	
		
			
				|  |  | -      if (this.remainingSeconds > 0) {
 | 
	
		
			
				|  |  | -        this.remainingSeconds--;
 | 
	
		
			
				|  |  | -      } else {
 | 
	
		
			
				|  |  | -        if (this.remainingMinutes > 0) {
 | 
	
		
			
				|  |  | -          this.remainingMinutes--;
 | 
	
		
			
				|  |  | -          this.remainingSeconds = 59;
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | -          clearInterval(this.timerInterval);
 | 
	
		
			
				|  |  | -          this.handleTimeUp();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  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.continuous = true;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      this.recognition.onresult = (event: any) => {
 | 
	
		
			
				|  |  | +        let finalTranscript = '';
 | 
	
		
			
				|  |  | +        let interimTranscript = '';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        for (let i = event.resultIndex; i < event.results.length; i++) {
 | 
	
		
			
				|  |  | +          const transcript = event.results[i][0].transcript;
 | 
	
		
			
				|  |  | +          if (event.results[i].isFinal) {
 | 
	
		
			
				|  |  | +            finalTranscript += transcript;
 | 
	
		
			
				|  |  | +          } else {
 | 
	
		
			
				|  |  | +            interimTranscript += transcript;
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -      this.updateTimeDisplay();
 | 
	
		
			
				|  |  | -    }, 1000);
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  private updateTimeDisplay() {
 | 
	
		
			
				|  |  | -    const mins = this.remainingMinutes.toString().padStart(2, '0');
 | 
	
		
			
				|  |  | -    const secs = this.remainingSeconds.toString().padStart(2, '0');
 | 
	
		
			
				|  |  | -    this.remainingTime = `${mins}:${secs}`;
 | 
	
		
			
				|  |  | +        if (finalTranscript) {
 | 
	
		
			
				|  |  | +          this.userAnswer = finalTranscript;
 | 
	
		
			
				|  |  | +          this.showSubmitButton = true;
 | 
	
		
			
				|  |  | +          this.showCancelButton = true;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        this.interimTranscript = interimTranscript;
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      this.recognition.onerror = (event: any) => {
 | 
	
		
			
				|  |  | +        console.error('语音识别错误:', event.error);
 | 
	
		
			
				|  |  | +        this.updateAvatarState('listening');
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      this.recognition.onend = () => {
 | 
	
		
			
				|  |  | +        if (this.isListening) {
 | 
	
		
			
				|  |  | +          this.stopVoiceInput();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  private handleTimeUp() {
 | 
	
		
			
				|  |  | -    this.addAIMessage("面试时间已结束,系统将自动提交您的回答");
 | 
	
		
			
				|  |  | -    this.submitAnswer();
 | 
	
		
			
				|  |  | +  private initProgressTimer(): void {
 | 
	
		
			
				|  |  | +    const totalSeconds = 10 * 60; // 10分钟 = 600秒
 | 
	
		
			
				|  |  | +    let elapsedSeconds = 0;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    const timer = setInterval(() => {
 | 
	
		
			
				|  |  | +      if (this.interviewEnded) {
 | 
	
		
			
				|  |  | +        clearInterval(timer);
 | 
	
		
			
				|  |  | +        return;
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      
 | 
	
		
			
				|  |  | +      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); // 改为每秒更新一次
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  // 其他现有方法保持不变
 | 
	
		
			
				|  |  |    initConversation(): void {
 | 
	
		
			
				|  |  | -    this.addAIMessage(this.questions[0]);
 | 
	
		
			
				|  |  | +    this.addAIMessage(this.mainQuestions[0]);
 | 
	
		
			
				|  |  |      setTimeout(() => {
 | 
	
		
			
				|  |  |        this.askQuestion(1);
 | 
	
		
			
				|  |  |      }, 1500);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    askQuestion(index: number): void {
 | 
	
		
			
				|  |  | -    if (index >= this.questions.length) return;
 | 
	
		
			
				|  |  | +    if (index >= this.mainQuestions.length || this.interviewEnded) {
 | 
	
		
			
				|  |  | +      this.endInterview();
 | 
	
		
			
				|  |  | +      return;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  |      this.currentQuestionIndex = index;
 | 
	
		
			
				|  |  | -    this.currentQuestion = this.questions[index];
 | 
	
		
			
				|  |  | +    this.isFollowUpQuestion = false;
 | 
	
		
			
				|  |  | +    this.currentQuestion = this.mainQuestions[index];
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  |      this.addAIMessage(this.currentQuestion);
 | 
	
		
			
				|  |  | -    this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
 | 
	
		
			
				|  |  | -    this.expressionText = "等待您的回答...";
 | 
	
		
			
				|  |  | +    this.updateAvatarState('speaking');
 | 
	
		
			
				|  |  | +    this.speakText(this.currentQuestion); // 自动语音播放问题
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  addAIMessage(text: string): void {
 | 
	
		
			
				|  |  | -    this.messages.push({ sender: 'ai', text: text });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  async askFollowUpQuestion(followUpText: string): Promise<void> {
 | 
	
		
			
				|  |  | +    if (this.interviewEnded) return;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    this.isFollowUpQuestion = true;
 | 
	
		
			
				|  |  | +    this.currentQuestion = followUpText;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  | +    this.addAIMessage("关于您刚才的回答,我有一个跟进问题...");
 | 
	
		
			
				|  |  |      setTimeout(() => {
 | 
	
		
			
				|  |  | -      this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
 | 
	
		
			
				|  |  | -    }, 0);
 | 
	
		
			
				|  |  | -    this.speakText(text);
 | 
	
		
			
				|  |  | +      this.addAIMessage(this.currentQuestion);
 | 
	
		
			
				|  |  | +      this.updateAvatarState('speaking');
 | 
	
		
			
				|  |  | +    }, 1000);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  endInterview(): void {
 | 
	
		
			
				|  |  | +    this.interviewEnded = true;
 | 
	
		
			
				|  |  | +    this.addAIMessage("感谢您参与本次面试,祝您有美好的一天!");
 | 
	
		
			
				|  |  | +    this.updateAvatarState('default');
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  restartInterview(): void {
 | 
	
		
			
				|  |  | +    this.interviewEnded = false;
 | 
	
		
			
				|  |  | +    this.currentQuestionIndex = 0;
 | 
	
		
			
				|  |  | +    this.currentQuestion = this.mainQuestions[0];
 | 
	
		
			
				|  |  | +    this.messages = [];
 | 
	
		
			
				|  |  | +    this.metrics = { expressiveness: 0, professionalism: 0, relevance: 0 };
 | 
	
		
			
				|  |  | +    this.progress = 0;
 | 
	
		
			
				|  |  | +    this.initConversation();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  addAIMessage(text: string): void {
 | 
	
		
			
				|  |  | +    this.messages.push({
 | 
	
		
			
				|  |  | +      sender: 'ai',
 | 
	
		
			
				|  |  | +      text: text
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    this.scrollToBottom();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    addUserMessage(text: string): void {
 | 
	
		
			
				|  |  | -    this.messages.push({ sender: 'user', text: text });
 | 
	
		
			
				|  |  | +    this.messages.push({
 | 
	
		
			
				|  |  | +      sender: 'user',
 | 
	
		
			
				|  |  | +      text: text
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    this.scrollToBottom();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private scrollToBottom(): void {
 | 
	
		
			
				|  |  |      setTimeout(() => {
 | 
	
		
			
				|  |  |        this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
 | 
	
		
			
				|  |  |      }, 0);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    speakText(text: string): void {
 | 
	
		
			
				|  |  | -    if (this.isSpeaking) return;
 | 
	
		
			
				|  |  | +    if (this.isSpeaking || !text) return;
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  |      this.isSpeaking = true;
 | 
	
		
			
				|  |  | -    this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f5e3.svg";
 | 
	
		
			
				|  |  | -    this.expressionText = "正在播报问题...";
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  | +    this.updateAvatarState('speaking');
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      const utterance = new SpeechSynthesisUtterance(text);
 | 
	
		
			
				|  |  |      utterance.lang = 'zh-CN';
 | 
	
		
			
				|  |  |      utterance.rate = 0.9;
 | 
	
		
			
				|  |  |      utterance.pitch = 1;
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      utterance.onend = () => {
 | 
	
		
			
				|  |  |        this.isSpeaking = false;
 | 
	
		
			
				|  |  | -      this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
 | 
	
		
			
				|  |  | -      this.expressionText = "等待您的回答...";
 | 
	
		
			
				|  |  | +      if (!this.interviewEnded) {
 | 
	
		
			
				|  |  | +        this.updateAvatarState('listening');
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  |      };
 | 
	
		
			
				|  |  | -    
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      speechSynthesis.speak(utterance);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    startVoiceInput(): void {
 | 
	
		
			
				|  |  | +    if (!this.recognition || this.isSpeaking || this.isAnalyzing) return;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  |      this.isListening = true;
 | 
	
		
			
				|  |  | -    this.expressionText = "正在聆听您的回答...";
 | 
	
		
			
				|  |  | +    this.userAnswer = '';
 | 
	
		
			
				|  |  | +    this.updateAvatarState('listening');
 | 
	
		
			
				|  |  | +    this.recognition.start();
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    stopVoiceInput(): void {
 | 
	
		
			
				|  |  |      this.isListening = false;
 | 
	
		
			
				|  |  | -    this.userAnswer = this.presetAnswers[this.currentQuestionIndex - 1] || "这是我的回答...";
 | 
	
		
			
				|  |  | -    this.showSubmitButton = true;
 | 
	
		
			
				|  |  | -    this.avatarImage = this.avatarImages.default;
 | 
	
		
			
				|  |  | -    this.expressionText = "请确认您的回答";
 | 
	
		
			
				|  |  | +    if (this.recognition) {
 | 
	
		
			
				|  |  | +      this.recognition.stop();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    this.updateAvatarState('waiting');
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  submitAnswer(): void {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  async submitAnswer(): Promise<void> {
 | 
	
		
			
				|  |  | +    if (!this.userAnswer || this.isAnalyzing) return;
 | 
	
		
			
				|  |  | +    
 | 
	
		
			
				|  |  |      this.showSubmitButton = false;
 | 
	
		
			
				|  |  | +    this.showCancelButton = false;
 | 
	
		
			
				|  |  |      this.addUserMessage(this.userAnswer);
 | 
	
		
			
				|  |  | -    this.isAnalyzing = true;
 | 
	
		
			
				|  |  | -    this.avatarImage = this.avatarImages.default;
 | 
	
		
			
				|  |  | -    this.expressionText = "正在分析您的回答...";
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  | -    setTimeout(() => {
 | 
	
		
			
				|  |  | -      this.isAnalyzing = false;
 | 
	
		
			
				|  |  | -      if (this.currentQuestionIndex < this.questions.length - 1) {
 | 
	
		
			
				|  |  | -        this.askQuestion(this.currentQuestionIndex + 1);
 | 
	
		
			
				|  |  | +    this.isAnalyzing = true;
 | 
	
		
			
				|  |  | +    this.updateAvatarState('analyzing');
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    try {
 | 
	
		
			
				|  |  | +      const evaluation = await this.aiService.evaluateInterviewAnswer(
 | 
	
		
			
				|  |  | +        this.currentQuestion,
 | 
	
		
			
				|  |  | +        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 {
 | 
	
		
			
				|  |  | -        this.addAIMessage("感谢您的回答,面试即将结束,正在生成最终报告...");
 | 
	
		
			
				|  |  | -        this.avatarImage = this.avatarImages.speaking;
 | 
	
		
			
				|  |  | -        this.expressionText = "生成最终评估中...";
 | 
	
		
			
				|  |  | +        // 进入下一个问题
 | 
	
		
			
				|  |  | +        setTimeout(() => {
 | 
	
		
			
				|  |  | +          this.askQuestion(this.currentQuestionIndex + 1);
 | 
	
		
			
				|  |  | +        }, 1500);
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  | -    }, 2000);
 | 
	
		
			
				|  |  | +    } catch (error) {
 | 
	
		
			
				|  |  | +      console.error('评估失败:', error);
 | 
	
		
			
				|  |  | +      this.addAIMessage("分析完成,让我们继续下一个问题");
 | 
	
		
			
				|  |  | +      this.askQuestion(this.currentQuestionIndex + 1);
 | 
	
		
			
				|  |  | +    } finally {
 | 
	
		
			
				|  |  | +      this.isAnalyzing = false;
 | 
	
		
			
				|  |  | +      this.userAnswer = '';
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  cancelAnswer(): void {
 | 
	
		
			
				|  |  | +    this.showSubmitButton = false;
 | 
	
		
			
				|  |  | +    this.showCancelButton = false;
 | 
	
		
			
				|  |  | +    this.userAnswer = '';
 | 
	
		
			
				|  |  | +    this.updateAvatarState('listening');
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    playQuestion(): void {
 | 
	
		
			
				|  |  | -    if (!this.isSpeaking) {
 | 
	
		
			
				|  |  | -      this.speakText(this.questions[this.currentQuestionIndex]);
 | 
	
		
			
				|  |  | +    if (!this.isListening && !this.isSpeaking) {
 | 
	
		
			
				|  |  | +      this.speakText(this.currentQuestion);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  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面试官";
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    toggleRadarChart(): void {
 | 
	
		
			
				|  |  |      this.showRadarChart = !this.showRadarChart;
 | 
	
		
			
				|  |  |      if (this.showRadarChart) {
 | 
	
	
		
			
				|  | @@ -208,9 +355,9 @@ export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
 | 
	
		
			
				|  |  |        }, 0);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -  initRadarChart(): void {
 | 
	
		
			
				|  |  | -    if (!this.showRadarChart) return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  private initRadarChart(): void {
 | 
	
		
			
				|  |  | +    if (!this.showRadarChart || this.interviewEnded) return;
 | 
	
		
			
				|  |  |      
 | 
	
		
			
				|  |  |      const chartDom = document.getElementById('radarChart');
 | 
	
		
			
				|  |  |      if (!chartDom) return;
 | 
	
	
		
			
				|  | @@ -235,55 +382,34 @@ export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
 | 
	
		
			
				|  |  |            { name: '知识广度', max: 100 }
 | 
	
		
			
				|  |  |          ],
 | 
	
		
			
				|  |  |          radius: '65%',
 | 
	
		
			
				|  |  | -        axisName: {
 | 
	
		
			
				|  |  | -          color: '#4A5568'
 | 
	
		
			
				|  |  | -        },
 | 
	
		
			
				|  |  | -        splitArea: {
 | 
	
		
			
				|  |  | -          areaStyle: {
 | 
	
		
			
				|  |  | -            color: ['rgba(42, 92, 170, 0.1)']
 | 
	
		
			
				|  |  | -          }
 | 
	
		
			
				|  |  | -        },
 | 
	
		
			
				|  |  | -        axisLine: {
 | 
	
		
			
				|  |  | -          lineStyle: {
 | 
	
		
			
				|  |  | -            color: 'rgba(42, 92, 170, 0.3)'
 | 
	
		
			
				|  |  | -          }
 | 
	
		
			
				|  |  | -        },
 | 
	
		
			
				|  |  | -        splitLine: {
 | 
	
		
			
				|  |  | -          lineStyle: {
 | 
	
		
			
				|  |  | -            color: 'rgba(42, 92, 170, 0.3)'
 | 
	
		
			
				|  |  | -          }
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | +        axisName: { color: '#4A5568' },
 | 
	
		
			
				|  |  | +        splitArea: { areaStyle: { color: ['rgba(42, 92, 170, 0.1)'] } },
 | 
	
		
			
				|  |  | +        axisLine: { lineStyle: { color: 'rgba(42, 92, 170, 0.3)' } },
 | 
	
		
			
				|  |  | +        splitLine: { lineStyle: { color: 'rgba(42, 92, 170, 0.3)' } }
 | 
	
		
			
				|  |  |        },
 | 
	
		
			
				|  |  |        series: [{
 | 
	
		
			
				|  |  |          type: 'radar',
 | 
	
		
			
				|  |  |          data: [
 | 
	
		
			
				|  |  |            {
 | 
	
		
			
				|  |  | -            value: [82, 65, 73, 68, 75, 60],
 | 
	
		
			
				|  |  | +            value: [
 | 
	
		
			
				|  |  | +              this.metrics.expressiveness,
 | 
	
		
			
				|  |  | +              this.metrics.professionalism,
 | 
	
		
			
				|  |  | +              this.metrics.relevance,
 | 
	
		
			
				|  |  | +              (this.metrics.expressiveness + this.metrics.professionalism) / 2,
 | 
	
		
			
				|  |  | +              this.metrics.expressiveness,
 | 
	
		
			
				|  |  | +              this.metrics.professionalism
 | 
	
		
			
				|  |  | +            ],
 | 
	
		
			
				|  |  |              name: '当前表现',
 | 
	
		
			
				|  |  | -            areaStyle: {
 | 
	
		
			
				|  |  | -              color: 'rgba(42, 92, 170, 0.4)'
 | 
	
		
			
				|  |  | -            },
 | 
	
		
			
				|  |  | -            lineStyle: {
 | 
	
		
			
				|  |  | -              width: 2,
 | 
	
		
			
				|  |  | -              color: 'rgba(42, 92, 170, 0.8)'
 | 
	
		
			
				|  |  | -            },
 | 
	
		
			
				|  |  | -            itemStyle: {
 | 
	
		
			
				|  |  | -              color: '#2A5CAA'
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | +            areaStyle: { color: 'rgba(42, 92, 170, 0.4)' },
 | 
	
		
			
				|  |  | +            lineStyle: { width: 2, color: 'rgba(42, 92, 170, 0.8)' },
 | 
	
		
			
				|  |  | +            itemStyle: { color: '#2A5CAA' }
 | 
	
		
			
				|  |  |            },
 | 
	
		
			
				|  |  |            {
 | 
	
		
			
				|  |  |              value: [70, 80, 85, 75, 65, 70],
 | 
	
		
			
				|  |  |              name: '岗位要求',
 | 
	
		
			
				|  |  | -            areaStyle: {
 | 
	
		
			
				|  |  | -              color: 'rgba(255, 107, 53, 0.2)'
 | 
	
		
			
				|  |  | -            },
 | 
	
		
			
				|  |  | -            lineStyle: {
 | 
	
		
			
				|  |  | -              width: 2,
 | 
	
		
			
				|  |  | -              color: 'rgba(255, 107, 53, 0.8)'
 | 
	
		
			
				|  |  | -            },
 | 
	
		
			
				|  |  | -            itemStyle: {
 | 
	
		
			
				|  |  | -              color: '#FF6B35'
 | 
	
		
			
				|  |  | -            }
 | 
	
		
			
				|  |  | +            areaStyle: { color: 'rgba(255, 107, 53, 0.2)' },
 | 
	
		
			
				|  |  | +            lineStyle: { width: 2, color: 'rgba(255, 107, 53, 0.8)' },
 | 
	
		
			
				|  |  | +            itemStyle: { color: '#FF6B35' }
 | 
	
		
			
				|  |  |            }
 | 
	
		
			
				|  |  |          ]
 | 
	
		
			
				|  |  |        }]
 | 
	
	
		
			
				|  | @@ -295,4 +421,6 @@ export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
 | 
	
		
			
				|  |  |        this.radarChart?.resize();
 | 
	
		
			
				|  |  |      });
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  
 | 
	
		
			
				|  |  |  }
 |