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', standalone: true, imports: [CommonModule], templateUrl: './page-interview.html', styleUrl: './page-interview.scss' }) export class PageInterview implements OnInit, AfterViewInit { // 计时相关 remainingTime = '10:00'; progress = 0; // 面试状态控制 interviewEnded = false; isAnalyzing = false; showSubmitButton = false; showCancelButton = false; isFollowUpQuestion = false; showQuestionCard = true; showRadarChart = false; // 语音相关 isListening = false; isSpeaking = false; 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助手。", "首先,请简单介绍一下您自己。", "您学会那些,专业技能", "您在过去工作中遇到的最大技术挑战是什么?您是如何解决的?", "请描述一个您与团队意见不合时,您是如何处理的案例。" ]; currentQuestionIndex = 0; currentQuestion = this.mainQuestions[0]; // 对话记录 messages: {sender: 'ai' | 'user', text: string}[] = []; // 头像状态 avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f916.svg"; expressionText = "等待开始..."; // 评估指标 metrics = { expressiveness: 0, professionalism: 0, relevance: 0 }; @ViewChild('dialogContainer') dialogContainer!: ElementRef; private radarChart: any; constructor(private aiService: AiApiService) { this.initSpeechRecognition(); } ngOnInit(): void { this.initConversation(); this.initProgressTimer(); } ngAfterViewInit(): void { 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.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 = 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; } } 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.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; 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.mainQuestions[0]); setTimeout(() => { this.askQuestion(1); }, 1500); } askQuestion(index: number): void { if (index >= this.mainQuestions.length || this.interviewEnded) { this.endInterview(); return; } 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); if (this.isSelfIntroduction) { this.startSelfIntroTimer(); } } async askFollowUpQuestion(followUpText: string): Promise { if (this.interviewEnded) return; this.isFollowUpQuestion = true; this.currentQuestion = followUpText; this.isSelfIntroduction = false; this.speechBuffer = []; this.lastFinalResultIndex = 0; this.userAnswer = ''; this.interimTranscript = ''; this.addAIMessage("关于您刚才的回答,我有一个跟进问题..."); setTimeout(() => { 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.speechBuffer = []; this.lastFinalResultIndex = 0; this.isSelfIntroduction = false; this.stopSelfIntroTimer(); 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.scrollToBottom(); } private scrollToBottom(): void { setTimeout(() => { this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight; }, 0); } speakText(text: string): void { if (this.isSpeaking || !text) return; this.isSpeaking = true; this.updateAvatarState('speaking'); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = 'zh-CN'; utterance.rate = 0.9; utterance.pitch = 1; utterance.onend = () => { this.isSpeaking = false; if (!this.interviewEnded) { this.updateAvatarState('listening'); } }; speechSynthesis.speak(utterance); } startVoiceInput(): void { if (!this.recognition || this.isSpeaking || this.isAnalyzing) return; this.isListening = true; this.userAnswer = ''; 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; 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 { 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); 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 }; 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); } } catch (error) { console.error('评估失败:', error); this.addAIMessage("分析完成,让我们继续下一个问题"); this.askQuestion(this.currentQuestionIndex + 1); } finally { this.isAnalyzing = false; this.userAnswer = ''; this.speechBuffer = []; this.lastFinalResultIndex = 0; this.isSelfIntroduction = false; } } cancelAnswer(): void { this.showSubmitButton = false; this.showCancelButton = false; this.userAnswer = ''; this.speechBuffer = []; this.lastFinalResultIndex = 0; this.isSelfIntroduction = false; this.updateAvatarState('listening'); } playQuestion(): void { if (!this.isListening && !this.isSpeaking) { this.speakText(this.currentQuestion); } } updateAvatarState(state: 'speaking' | 'listening' | 'waiting' | 'analyzing' | 'default'): void { 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 { this.showRadarChart = !this.showRadarChart; if (this.showRadarChart) { setTimeout(() => { this.initRadarChart(); }, 0); } } private initRadarChart(): void { if (!this.showRadarChart || this.interviewEnded) return; const chartDom = document.getElementById('radarChart'); if (!chartDom) return; if (this.radarChart) { this.radarChart.dispose(); } this.radarChart = echarts.init(chartDom); const option = { backgroundColor: 'transparent', tooltip: {}, radar: { shape: 'circle', indicator: [ { name: '表达能力', max: 100 }, { name: '专业深度', max: 100 }, { name: '岗位匹配', max: 100 }, { name: '应变能力', max: 100 }, { name: '沟通技巧', max: 100 }, { 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)' } } }, series: [{ type: 'radar', data: [ { 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' } }, { 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' } } ] }] }; this.radarChart.setOption(option); window.addEventListener('resize', () => { this.radarChart?.resize(); }); } }