123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- 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<void> {
- 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<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);
-
- 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();
- });
- }
- }
|