page-interview.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { Component, OnInit, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import * as echarts from 'echarts';
  4. @Component({
  5. selector: 'app-page-interview',
  6. standalone: true,
  7. imports: [CommonModule],
  8. templateUrl: './page-interview.html',
  9. styleUrl: './page-interview.scss'
  10. })
  11. export class PageInterview implements OnInit, AfterViewInit {
  12. remainingTime = '04:32';
  13. isAnalyzing = false;
  14. showSubmitButton = false;
  15. userAnswer = '';
  16. // 在组件中定义Base64编码的SVG
  17. avatarImages = {
  18. default: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQjkwMCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMThTMCAyNy45NDEgMCAxOCA4LjA1OSAwIDE4IDBzMTggOC4wNTkgMTggMTgiLz48cGF0aCBmaWxsPSIjNjYyMjEyIiBkPSJNMTguODQ1IDEzLjE0OGMuMTM0LS4yNDYuMzU2LS4zNjkuNjU3LS4zNjkuMzA5IDAgLjUyOS4xMjMuNjY0LjM2OS4xMzYuMjQ1LjIwNC41NzkuMjA0IDEuMDAzIDAgLjQxNS0uMDY4Ljc0LS4yMDQuOTg1LS4xMzUuMjQ0LS4zNTUuMzY2LS42NjQuMzY2LS4zMDEgMC0uNTIzLS4xMjItLjY1Ny0uMzY2LS4xMzUtLjI0NS0uMjAyLS41Ny0uMjAyLS45ODUgMC0uNDI0LjA2Ny0uNzU4LjIwMi0xLjAwM3ptLTUuNDQ4IDBjLjEzNS0uMjQ2LjM1Ni0uMzY5LjY1OC0uMzY5LjMwOSAwIC41MjkuMTIzLjY2NC4zNjkuMTM2LjI0NS4yMDQuNTc5LjIwNCAxLjAwMyAwIC40MTUtLjA2OC43NC0uMjA0Ljk4NS0uMTM1LjI0NC0uMzU1LjM2Ni0uNjY0LjM2Ni0uMzAyIDAtLjUyMy0uMTIyLS42NTgtLjM2Ni0uMTM0LS4yNDUtLjIwMS0uNTctLjIwMS0uOTg1IDAtLjQyNC4wNjctLjc1OC4yMDEtMS4wMDN6Ii8+PHBhdGggZmlsbD0iIzY2MjIxMiIgZD0iTTE4IDIxLjUzYy0yLjc2NCAwLTQuOTUxLS40MTktNC45NTEtMS4wNTQgMC0uNjM1IDIuMTg3LTEuMDU0IDQuOTUxLTEuMDU0IDIuNzYzIDAgNC45NTEuNDE5IDQuOTUxIDEuMDU0IDAgLjYzNS0yLjE4OCAxLjA1NC00Ljk1MSAxLjA1NHptMC0yLjEwOGMtMy4wMTcgMC01LjQ1MS0uNDk0LTUuNDUxLTEuMDU0czIuNDM0LTEuMDU0IDUuNDUxLTEuMDU0YzMuMDE4IDAgNS40NTEuNDk0IDUuNDUxIDEuMDU0cy0yLjQzMyAxLjA1NC01LjQ1MSAxLjA1NHoiLz48L3N2Zz4=',
  19. speaking: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQjkwMCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMThTMCAyNy45NDEgMCAxOCA4LjA1OSAwIDE4IDBzMTggOC4wNTkgMTggMTgiLz48cGF0aCBmaWxsPSIjNjYyMjEyIiBkPSJNMTggMjEuNTNjLTIuNzY0IDAtNC45NTEtLjQxOS00Ljk1MS0xLjA1NCAwLS42MzUgMi4xODctMS4wNTQgNC45NTEtMS4wNTQgMi43NjMgMCA0Ljk1MS40MTkgNC45NTEgMS4wNTQgMCAuNjM1LTIuMTg4IDEuMDU0LTQuOTUxIDEuMDU0em0wLTIuMTA4Yy0zLjAxNyAwLTUuNDUxLS40OTQtNS40NTEtMS4wNTRzMi40MzQtMS4wNTQgNS40NTEtMS4wNTRjMy4wMTggMCA1LjQ1MS40OTQgNS40NTEgMS4wNTRzLTIuNDMzIDEuMDU0LTUuNDUxIDEuMDU0eiIvPjwvc3ZnPg==',
  20. // 其他表情...
  21. };
  22. // 问题相关
  23. questions = [
  24. "欢迎参加本次AI面试,我是您的面试官AI助手。",
  25. "首先,请简单介绍一下您自己。",
  26. "您在过去工作中遇到的最大技术挑战是什么?您是如何解决的?",
  27. "请描述一个您与团队意见不合时,您是如何处理的案例。"
  28. ];
  29. currentQuestionIndex = 0;
  30. currentQuestion = this.questions[0];
  31. showQuestionCard = false;
  32. progress = 30;
  33. // 对话相关
  34. messages: {sender: 'ai' | 'user', text: string}[] = [];
  35. // 语音相关
  36. isListening = false;
  37. isSpeaking = false;
  38. // 头像和表情相关
  39. avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f913.svg";
  40. expressionText = "正在聆听您的回答...";
  41. // 仪表盘相关
  42. showRadarChart = false;
  43. metrics = {
  44. expressiveness: 82,
  45. professionalism: 65,
  46. relevance: 73
  47. };
  48. // 预设回答
  49. presetAnswers = [
  50. "我是张伟,有5年Java开发经验,擅长分布式系统设计。",
  51. "我们曾遇到高并发下的数据库瓶颈,我通过引入Redis缓存和分库分表解决了问题。",
  52. "有一次在技术方案选择上,我通过数据对比和原型验证说服了团队采用我的方案。"
  53. ];
  54. @ViewChild('dialogContainer') dialogContainer!: ElementRef;
  55. private radarChart: any;
  56. ngOnInit(): void {
  57. this.initConversation();
  58. this.startProgressTimer();
  59. }
  60. ngAfterViewInit(): void {
  61. // 初始化雷达图
  62. this.initRadarChart();
  63. }
  64. // 初始化对话
  65. initConversation(): void {
  66. this.addAIMessage(this.questions[0]);
  67. setTimeout(() => {
  68. this.askQuestion(1);
  69. }, 1500);
  70. }
  71. // 提问问题
  72. askQuestion(index: number): void {
  73. if (index >= this.questions.length) return;
  74. this.currentQuestionIndex = index;
  75. this.currentQuestion = this.questions[index];
  76. // 添加到对话框
  77. this.addAIMessage(this.currentQuestion);
  78. // 更新头像状态
  79. this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
  80. this.expressionText = "等待您的回答...";
  81. }
  82. // 添加AI消息
  83. addAIMessage(text: string): void {
  84. this.messages.push({
  85. sender: 'ai',
  86. text: text
  87. });
  88. // 滚动到底部
  89. setTimeout(() => {
  90. this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
  91. }, 0);
  92. // 自动语音播报
  93. this.speakText(text);
  94. }
  95. // 添加用户消息
  96. addUserMessage(text: string): void {
  97. this.messages.push({
  98. sender: 'user',
  99. text: text
  100. });
  101. // 滚动到底部
  102. setTimeout(() => {
  103. this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
  104. }, 0);
  105. }
  106. // 语音播报
  107. speakText(text: string): void {
  108. if (this.isSpeaking) return;
  109. this.isSpeaking = true;
  110. this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f5e3.svg";
  111. this.expressionText = "正在播报问题...";
  112. // 使用Web Speech API
  113. const utterance = new SpeechSynthesisUtterance(text);
  114. utterance.lang = 'zh-CN';
  115. utterance.rate = 0.9;
  116. utterance.pitch = 1;
  117. utterance.onend = () => {
  118. this.isSpeaking = false;
  119. this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
  120. this.expressionText = "等待您的回答...";
  121. };
  122. speechSynthesis.speak(utterance);
  123. }
  124. // 开始语音输入
  125. startVoiceInput(): void {
  126. this.isListening = true;
  127. this.expressionText = "正在聆听您的回答...";
  128. console.log('语音输入开始');
  129. }
  130. // 停止语音输入
  131. stopVoiceInput(): void {
  132. this.isListening = false;
  133. // 模拟语音识别结果
  134. this.userAnswer = this.presetAnswers[this.currentQuestionIndex - 1] || "这是我的回答...";
  135. this.showSubmitButton = true; // 显示提交按钮
  136. // 不再自动提交,等待用户点击
  137. this.avatarImage = this.avatarImages.default;
  138. this.expressionText = "请确认您的回答";
  139. }
  140. submitAnswer(): void {
  141. this.showSubmitButton = false;
  142. this.addUserMessage(this.userAnswer);
  143. // 开始分析
  144. this.isAnalyzing = true;
  145. this.avatarImage = this.avatarImages.default;
  146. this.expressionText = "正在分析您的回答...";
  147. // 模拟分析过程 (2秒)
  148. setTimeout(() => {
  149. this.isAnalyzing = false;
  150. // 问下一题或结束
  151. if (this.currentQuestionIndex < this.questions.length - 1) {
  152. this.askQuestion(this.currentQuestionIndex + 1);
  153. } else {
  154. this.addAIMessage("感谢您的回答,面试即将结束,正在生成最终报告...");
  155. this.avatarImage = this.avatarImages.speaking;
  156. this.expressionText = "生成最终评估中...";
  157. }
  158. }, 2000);
  159. }
  160. // 播放问题
  161. playQuestion(): void {
  162. if (!this.isSpeaking) {
  163. this.speakText(this.questions[this.currentQuestionIndex]);
  164. }
  165. }
  166. // 切换雷达图
  167. toggleRadarChart(): void {
  168. this.showRadarChart = !this.showRadarChart;
  169. if (this.showRadarChart) {
  170. setTimeout(() => {
  171. this.initRadarChart();
  172. }, 0);
  173. }
  174. }
  175. // 初始化雷达图
  176. initRadarChart(): void {
  177. if (!this.showRadarChart) return;
  178. const chartDom = document.getElementById('radarChart');
  179. if (!chartDom) return;
  180. if (this.radarChart) {
  181. this.radarChart.dispose();
  182. }
  183. this.radarChart = echarts.init(chartDom);
  184. const option = {
  185. backgroundColor: 'transparent',
  186. tooltip: {},
  187. radar: {
  188. shape: 'circle',
  189. indicator: [
  190. { name: '表达能力', max: 100 },
  191. { name: '专业深度', max: 100 },
  192. { name: '岗位匹配', max: 100 },
  193. { name: '应变能力', max: 100 },
  194. { name: '沟通技巧', max: 100 },
  195. { name: '知识广度', max: 100 }
  196. ],
  197. radius: '65%',
  198. axisName: {
  199. color: '#4A5568'
  200. },
  201. splitArea: {
  202. areaStyle: {
  203. color: ['rgba(42, 92, 170, 0.1)']
  204. }
  205. },
  206. axisLine: {
  207. lineStyle: {
  208. color: 'rgba(42, 92, 170, 0.3)'
  209. }
  210. },
  211. splitLine: {
  212. lineStyle: {
  213. color: 'rgba(42, 92, 170, 0.3)'
  214. }
  215. }
  216. },
  217. series: [{
  218. type: 'radar',
  219. data: [
  220. {
  221. value: [82, 65, 73, 68, 75, 60],
  222. name: '当前表现',
  223. areaStyle: {
  224. color: 'rgba(42, 92, 170, 0.4)'
  225. },
  226. lineStyle: {
  227. width: 2,
  228. color: 'rgba(42, 92, 170, 0.8)'
  229. },
  230. itemStyle: {
  231. color: '#2A5CAA'
  232. }
  233. },
  234. {
  235. value: [70, 80, 85, 75, 65, 70],
  236. name: '岗位要求',
  237. areaStyle: {
  238. color: 'rgba(255, 107, 53, 0.2)'
  239. },
  240. lineStyle: {
  241. width: 2,
  242. color: 'rgba(255, 107, 53, 0.8)'
  243. },
  244. itemStyle: {
  245. color: '#FF6B35'
  246. }
  247. }
  248. ]
  249. }]
  250. };
  251. this.radarChart.setOption(option);
  252. // 响应式调整
  253. window.addEventListener('resize', () => {
  254. this.radarChart?.resize();
  255. });
  256. }
  257. // 进度条计时器
  258. startProgressTimer(): void {
  259. setInterval(() => {
  260. this.progress = Math.min(this.progress + 10, 100);
  261. }, 5000);
  262. }
  263. }