Browse Source

feat page-interview

0235702 1 day ago
parent
commit
b0b186723b

+ 86 - 0
interview-web/src/lib/ai-api.service.ts

@@ -0,0 +1,86 @@
+import { Injectable } from '@angular/core';
+import { TestCompletion, generateInterviewEvaluation } from './completion';
+
+export interface TestMessage {
+    role: string;
+    content: string;
+}
+
+@Injectable({
+    providedIn: 'root'
+})
+export class AiApiService {
+    private positionRequirements = `
+    岗位要求:
+    - 3年以上相关工作经验
+    - 精通至少一种主流编程语言
+    - 良好的沟通能力和团队协作精神
+    - 解决问题的能力
+    `;
+
+    constructor() { }
+
+    async evaluateInterviewAnswer(
+        question: string,
+        answer: string,
+        onMessage?: (content: string) => void
+    ) {
+        try {
+            const evaluation = await generateInterviewEvaluation(
+                question,
+                answer,
+                this.positionRequirements,
+                onMessage
+            );
+            
+            // 确保返回标准化的评估结果
+            return {
+                score: evaluation.score || 0,
+                feedback: evaluation.feedback || "暂无反馈",
+                followUpQuestion: evaluation.followUpQuestion || "能否详细说明一下?",
+                metrics: {
+                    expressiveness: evaluation.metrics?.expressiveness || 0,
+                    professionalism: evaluation.metrics?.professionalism || 0,
+                    relevance: evaluation.metrics?.relevance || 0
+                }
+            };
+        } catch (error) {
+            console.error('评估失败:', error);
+            return this.getDefaultEvaluation();
+        }
+    }
+
+    private getDefaultEvaluation() {
+        return {
+            score: 70,
+            feedback: "回答基本符合要求,但缺乏具体细节",
+            followUpQuestion: "能否详细说明一下?",
+            metrics: {
+                expressiveness: 70,
+                professionalism: 65,
+                relevance: 60
+            }
+        };
+    }
+
+    async generateFollowUpQuestion(
+        conversationHistory: TestMessage[],
+        onMessage?: (content: string) => void
+    ): Promise<string> {
+        const prompt = `根据以下对话历史,生成一个有针对性的追问问题:
+${conversationHistory.map(msg => `${msg.role}: ${msg.content}`).join('\n')}
+
+要求:
+- 问题应针对候选人的回答深入挖掘
+- 问题应简洁明了
+- 只返回问题内容,不要包含其他说明`;
+
+        const messages: TestMessage[] = [
+            { role: "user", content: prompt }
+        ];
+
+        const completion = new TestCompletion(messages);
+        const result = await completion.sendMessage(messages, onMessage);
+        return result || "能否详细说明一下?";
+    }
+}  

+ 187 - 0
interview-web/src/lib/completion.ts

@@ -0,0 +1,187 @@
+
+export interface TestMessage {
+    role: string;
+    content: string;
+}
+
+export class TestCompletion {
+    token: string = "r:60abef69e7cd8181b146ceaba1fdbf02";
+    messageList: TestMessage[] = [];
+    stream: boolean = true;
+
+    constructor(messageList?: TestMessage[]) {
+        this.messageList = messageList || this.messageList;
+    }
+
+    async sendMessage(
+        messageList?: TestMessage[] | null,
+        onMessage?: (content: string) => void
+    ): Promise<any> {
+        this.messageList = messageList || this.messageList;
+        
+        const body = {
+            "messages": this.messageList,
+            "stream": this.stream,
+            "model": "fmode-4.5-128k",
+            "temperature": 0.5,
+            "presence_penalty": 0,
+            "frequency_penalty": 0,
+            "token": "Bearer " + this.token
+        };
+
+        try {
+            const response = await fetch("https://server.fmode.cn/api/apig/aigc/gpt/v1/chat/completions", {
+                headers: {
+                    "Content-Type": "application/json"
+                },
+                body: JSON.stringify(body),
+                method: "POST",
+                mode: "cors",
+                credentials: "omit"
+            });
+
+            if (!response.ok) {
+                throw new Error(`HTTP error! Status: ${response.status}`);
+            }
+
+            if (!this.stream) {
+                const data = await response.json();
+                return data?.choices?.[0]?.message?.content;
+            }
+
+            if (!response.body) {
+                throw new Error("No response body in stream mode");
+            }
+
+            const reader = response.body.getReader();
+            const decoder = new TextDecoder("utf-8");
+            let accumulatedContent = "";
+
+            try {
+                while (true) {
+                    const { done, value } = await reader.read();
+                    if (done) break;
+
+                    const chunk = decoder.decode(value, { stream: true });
+                    const lines = chunk.split('\n').filter(line => line.trim() !== '');
+
+                    for (const line of lines) {
+                        if (line.startsWith('data:') && !line.includes('[DONE]')) {
+                            try {
+                                const jsonStr = line.substring(5).trim();
+                                const data = JSON.parse(jsonStr);
+                                const content = data?.choices?.[0]?.delta?.content || '';
+                                
+                                if (content) {
+                                    accumulatedContent += content;
+                                    if (onMessage) {
+                                        onMessage(accumulatedContent);
+                                    }
+                                }
+                            } catch (e) {
+                                console.error("Error parsing stream data:", e);
+                            }
+                        }
+                    }
+                }
+            } finally {
+                reader.releaseLock();
+            }
+
+            return accumulatedContent;
+        } catch (error) {
+            console.error("API request failed:", error);
+            throw error;
+        }
+    }
+}
+
+/**
+ * 生成面试评估报告
+ */
+export async function generateInterviewEvaluation(
+    question: string,
+    answer: string,
+    positionRequirements: string,
+    onMessage?: (content: string) => void
+): Promise<{
+    score: number;
+    feedback: string;
+    followUpQuestion: string;
+    metrics: {
+        expressiveness: number;
+        professionalism: number;
+        relevance: number;
+    };
+}> {
+    const evaluationPrompt = `请根据以下面试对话生成评估报告:
+问题:${question}
+回答:${answer}
+岗位要求:${positionRequirements}
+
+请返回包含以下内容的JSON对象:
+1. score: 整体评分(0-100)
+2. feedback: 具体反馈意见
+3. followUpQuestion: 针对回答的追问问题
+4. metrics: {
+    expressiveness: 表达能力评分(0-100),
+    professionalism: 专业度评分(0-100),
+    relevance: 岗位匹配度评分(0-100)
+}
+
+要求:
+- 反馈意见应具体指出回答中的优缺点
+- 追问问题应能深入了解候选人的能力
+- 评分应基于岗位要求合理给出`;
+
+    const messages: TestMessage[] = [
+        {
+            role: "user",
+            content: evaluationPrompt
+        }
+    ];
+
+    const completion = new TestCompletion(messages);
+    let fullContent = "";
+    
+    await completion.sendMessage(null, (content) => {
+        fullContent = content;
+        if (onMessage) onMessage(content);
+    });
+
+    try {
+        const result = extractJSON(fullContent);
+        if (!result) {
+            throw new Error("无法解析评估结果");
+        }
+        return result;
+    } catch (e) {
+        console.error("评估结果解析失败:", e);
+        console.log("原始内容:", fullContent);
+        throw new Error("生成的评估报告格式不正确");
+    }
+}
+
+function extractJSON(str: string) {
+    let stack = 0;
+    let startIndex = -1;
+    let result = null;
+
+    for (let i = 0; i < str.length; i++) {
+        if (str[i] === '{') {
+            if (stack === 0) startIndex = i;
+            stack++;
+        } else if (str[i] === '}') {
+            stack--;
+            if (stack === 0 && startIndex !== -1) {
+                try {
+                    result = JSON.parse(str.slice(startIndex, i + 1));
+                    break;
+                } catch (e) {
+                    startIndex = -1;
+                }
+            }
+        }
+    }
+    return result;
+}

+ 431 - 0
interview-web/src/lib/ncloud.ts

@@ -0,0 +1,431 @@
+// CloudObject.ts
+
+let serverURL = `https://dev.fmode.cn/parse`;
+if (location.protocol == "http:") {
+    serverURL = `http://dev.fmode.cn:1337/parse`;
+}
+
+export class CloudObject {
+    className: string;
+    id: string | undefined = undefined;
+    createdAt: any;
+    updatedAt: any;
+    data: Record<string, any> = {};
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    toPointer() {
+        return { "__type": "Pointer", "className": this.className, "objectId": this.id };
+    }
+
+    set(json: Record<string, any>) {
+        Object.keys(json).forEach(key => {
+            if (["objectId", "id", "createdAt", "updatedAt"].indexOf(key) > -1) {
+                return;
+            }
+            this.data[key] = json[key];
+        });
+    }
+
+    get(key: string) {
+        return this.data[key] || null;
+    }
+
+    async save() {
+        let method = "POST";
+        let url = serverURL + `/classes/${this.className}`;
+
+        // 更新
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        const body = JSON.stringify(this.data);
+        const response = await fetch(url, {
+            headers: {
+                "content-type": "application/json;charset=UTF-8",
+                "x-parse-application-id": "dev"
+            },
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        return this;
+    }
+
+    async destroy() {
+        if (!this.id) return;
+        const response = await fetch(serverURL + `/classes/${this.className}/${this.id}`, {
+            headers: {
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "DELETE",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result) {
+            this.id = undefined;
+        }
+        return true;
+    }
+}
+
+// CloudQuery.ts
+export class CloudQuery {
+    className: string;
+    queryParams: Record<string, any> = { where: {} };
+
+    constructor(className: string) {
+        this.className = className;
+    }
+
+    include(...fileds: string[]) {
+        this.queryParams["include"] = fileds;
+    }
+    greaterThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gt"] = value;
+    }
+
+    greaterThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$gte"] = value;
+    }
+
+    lessThan(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lt"] = value;
+    }
+
+    lessThanAndEqualTo(key: string, value: any) {
+        if (!this.queryParams["where"][key]) this.queryParams["where"][key] = {};
+        this.queryParams["where"][key]["$lte"] = value;
+    }
+
+    equalTo(key: string, value: any) {
+        if (!this.queryParams["where"]) this.queryParams["where"] = {};
+        this.queryParams["where"][key] = value;
+    }
+
+    async get(id: string) {
+        const url = serverURL + `/classes/${this.className}/${id}?`;
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        if (json) {
+            let existsObject = this.dataToObj(json)
+            return existsObject;
+        }
+        return null
+    }
+
+    async find(): Promise<Array<CloudObject>> {
+        let url = serverURL + `/classes/${this.className}?`;
+
+        let queryStr = ``
+        Object.keys(this.queryParams).forEach(key => {
+            let paramStr = JSON.stringify(this.queryParams[key]);
+            if (key == "include") {
+                paramStr = this.queryParams[key]?.join(",")
+            }
+            if (queryStr) {
+                url += `${key}=${paramStr}`;
+            } else {
+                url += `&${key}=${paramStr}`;
+            }
+        })
+        // if (Object.keys(this.queryParams["where"]).length) {
+
+        // }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        let list = json?.results || []
+        let objList = list.map((item: any) => this.dataToObj(item))
+        return objList || [];
+    }
+
+
+    async first() {
+        let url = serverURL + `/classes/${this.className}?`;
+
+        if (Object.keys(this.queryParams["where"]).length) {
+            const whereStr = JSON.stringify(this.queryParams["where"]);
+            url += `where=${whereStr}&limit=1`;
+        }
+
+        const response = await fetch(url, {
+            headers: {
+                "if-none-match": "W/\"1f0-ghxH2EwTk6Blz0g89ivf2adBDKY\"",
+                "x-parse-application-id": "dev"
+            },
+            body: null,
+            method: "GET",
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const json = await response?.json();
+        const exists = json?.results?.[0] || null;
+        if (exists) {
+            let existsObject = this.dataToObj(exists)
+            return existsObject;
+        }
+        return null
+    }
+
+    dataToObj(exists: any): CloudObject {
+        let existsObject = new CloudObject(this.className);
+        Object.keys(exists).forEach(key => {
+            if (exists[key]?.__type == "Object") {
+                exists[key] = this.dataToObj(exists[key])
+            }
+        })
+        existsObject.set(exists);
+        existsObject.id = exists.objectId;
+        existsObject.createdAt = exists.createdAt;
+        existsObject.updatedAt = exists.updatedAt;
+        return existsObject;
+    }
+}
+
+// CloudUser.ts
+export class CloudUser extends CloudObject {
+    constructor() {
+        super("_User"); // 假设用户类在Parse中是"_User"
+        // 读取用户缓存信息
+        let userCacheStr = localStorage.getItem("NCloud/dev/User")
+        if (userCacheStr) {
+            let userData = JSON.parse(userCacheStr)
+            // 设置用户信息
+            this.id = userData?.objectId;
+            this.sessionToken = userData?.sessionToken;
+            this.data = userData; // 保存用户数据
+        }
+    }
+
+    sessionToken: string | null = ""
+    /** 获取当前用户信息 */
+    async current() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return null;
+        }
+        return this;
+        // const response = await fetch(serverURL + `/users/me`, {
+        //     headers: {
+        //         "x-parse-application-id": "dev",
+        //         "x-parse-session-token": this.sessionToken // 使用sessionToken进行身份验证
+        //     },
+        //     method: "GET"
+        // });
+
+        // const result = await response?.json();
+        // if (result?.error) {
+        //     console.error(result?.error);
+        //     return null;
+        // }
+        // return result;
+    }
+
+    /** 登录 */
+    async login(username: string, password: string): Promise<CloudUser | null> {
+        const response = await fetch(serverURL + `/login`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify({ username, password }),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
+        return this;
+    }
+
+    /** 登出 */
+    async logout() {
+        if (!this.sessionToken) {
+            console.error("用户未登录");
+            return;
+        }
+
+        const response = await fetch(serverURL + `/logout`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "x-parse-session-token": this.sessionToken
+            },
+            method: "POST"
+        });
+
+        let result = await response?.json();
+
+        if (result?.error) {
+            console.error(result?.error);
+            if (result?.error == "Invalid session token") {
+                this.clearUserCache()
+                return true;
+            }
+            return false;
+        }
+
+        this.clearUserCache()
+        return true;
+    }
+    clearUserCache() {
+        // 清除用户信息
+        localStorage.removeItem("NCloud/dev/User")
+        this.id = undefined;
+        this.sessionToken = null;
+        this.data = {};
+    }
+
+    /** 注册 */
+    async signUp(username: string, password: string, additionalData: Record<string, any> = {}) {
+        const userData = {
+            username,
+            password,
+            ...additionalData // 合并额外的用户数据
+        };
+
+        const response = await fetch(serverURL + `/users`, {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            body: JSON.stringify(userData),
+            method: "POST"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+            return null;
+        }
+
+        // 设置用户信息
+        // 缓存用户信息
+        console.log(result)
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(result))
+        this.id = result?.objectId;
+        this.sessionToken = result?.sessionToken;
+        this.data = result; // 保存用户数据
+        return this;
+    }
+
+    override async save() {
+        let method = "POST";
+        let url = serverURL + `/users`;
+
+        // 更新用户信息
+        if (this.id) {
+            url += `/${this.id}`;
+            method = "PUT";
+        }
+
+        let data: any = JSON.parse(JSON.stringify(this.data))
+        delete data.createdAt
+        delete data.updatedAt
+        delete data.ACL
+        delete data.objectId
+        const body = JSON.stringify(data);
+        let headersOptions: any = {
+            "content-type": "application/json;charset=UTF-8",
+            "x-parse-application-id": "dev",
+            "x-parse-session-token": this.sessionToken, // 添加sessionToken以进行身份验证
+        }
+        const response = await fetch(url, {
+            headers: headersOptions,
+            body: body,
+            method: method,
+            mode: "cors",
+            credentials: "omit"
+        });
+
+        const result = await response?.json();
+        if (result?.error) {
+            console.error(result?.error);
+        }
+        if (result?.objectId) {
+            this.id = result?.objectId;
+        }
+        localStorage.setItem("NCloud/dev/User", JSON.stringify(this.data))
+        return this;
+    }
+}
+
+export class CloudApi {
+    async fetch(path: string, body: any, options?: {
+        method: string
+        body: any
+    }) {
+
+        let reqOpts: any = {
+            headers: {
+                "x-parse-application-id": "dev",
+                "Content-Type": "application/json"
+            },
+            method: options?.method || "POST",
+            mode: "cors",
+            credentials: "omit"
+        }
+        if (body || options?.body) {
+            reqOpts.body = JSON.stringify(body || options?.body);
+            reqOpts.json = true;
+        }
+        let host = `https://dev.fmode.cn`
+        // host = `http://127.0.0.1:1337`
+        let url = `${host}/api/` + path
+        console.log(url, reqOpts)
+        const response = await fetch(url, reqOpts);
+        let json = await response.json();
+        return json
+    }
+}

+ 1 - 1
interview-web/src/modules/interview/mobile/mobile.routes.ts

@@ -34,5 +34,5 @@ export const MOBILE_ROUTES: Routes = [
   {
     path: 'interview/mobile/page-job-hunting',
     loadComponent: () => import('./page-job-hunting/page-job-hunting').then(m => m.PageJobHunting)
-  }
+  },
 ];

+ 1 - 1
interview-web/src/modules/interview/mobile/nav-mobile-tabs/nav-mobile-tabs.html

@@ -1,4 +1,4 @@
-<p>nav-mobile-tabs works!</p>
+
 <nav class="bottom-nav">
   <div class="nav-container">
     <div 

+ 20 - 15
interview-web/src/modules/interview/mobile/page-interview/page-interview.html

@@ -34,14 +34,14 @@
         </div>}
     </div>
     
-    <!-- 问题卡片 (保留但隐藏) -->
-     @if (showQuestionCard) {
+    <!-- 问题卡片 -->
+    @if (showQuestionCard) {
     <div class="question-card">
         <div class="question-text">
             {{ currentQuestion }}
         </div>
         <div class="question-progress">
-            <span>问题 {{ currentQuestionIndex + 1 }}/{{ questions.length }}</span>
+            <span>问题 {{ currentQuestionIndex + 1 }}/{{ mainQuestions.length }}</span>
             <div class="progress-bar">
                 <div class="progress-fill" [style.width]="progress + '%'"></div>
             </div>
@@ -65,14 +65,20 @@
                 <i class="fas fa-play"></i>
             </button>
         </div>
-        @if (showSubmitButton) {
-        <button class="submit-btn" (click)="submitAnswer()" [disabled]="isAnalyzing">
-            <svg class="check-icon" viewBox="0 0 24 24" width="16" height="16">
-                <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" style="color: burlywood;"/>
-            </svg>
-                    <div style="color: black;">确认上传并分析</div>
-        </button>
-  }
+        @if (showSubmitButton || showCancelButton) {
+        <div class="answer-confirm-buttons">
+            <button class="submit-btn" (click)="submitAnswer()" [disabled]="isAnalyzing">
+                <svg class="check-icon" viewBox="0 0 24 24" width="16" height="16">
+                    <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" style="color: burlywood;"/>
+                </svg>
+                <div style="color: black;">确认回答</div>
+            </button>
+            <button class="cancel-btn" (click)="cancelAnswer()" [disabled]="isAnalyzing">
+                <i class="fas fa-times"></i>
+                取消回答
+            </button>
+        </div>
+        }
         <div class="voice-hint">
             点击播放按钮可重复听取问题<br>
             按住麦克风按钮开始回答
@@ -122,9 +128,8 @@
         </div>
         
         <!-- 雷达图容器 -->
-         @if (showRadarChart){
-            <div id="radarChart" class="radar-chart" *ngIf="showRadarChart"></div>
-         }
-        
+        @if (showRadarChart) {
+        <div id="radarChart" class="radar-chart"></div>
+        }
     </div>
 </div>

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

@@ -423,4 +423,4 @@ body {
 .check-icon {
   margin-right: 8px;
   vertical-align: middle;
-}
+}

+ 121 - 70
interview-web/src/modules/interview/mobile/page-interview/page-interview.ts

@@ -1,6 +1,7 @@
 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',
@@ -10,25 +11,30 @@ import * as echarts from 'echarts';
   styleUrl: './page-interview.scss'
 })
 export class PageInterview implements OnInit, AfterViewInit {
-remainingTime = '04:32';
-isAnalyzing = false;
-showSubmitButton = false;
-userAnswer = '';
-// 在组件中定义Base64编码的SVG
-avatarImages = {
-  default: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQjkwMCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMThTMCAyNy45NDEgMCAxOCA4LjA1OSAwIDE4IDBzMTggOC4wNTkgMTggMTgiLz48cGF0aCBmaWxsPSIjNjYyMjEyIiBkPSJNMTguODQ1IDEzLjE0OGMuMTM0LS4yNDYuMzU2LS4zNjkuNjU3LS4zNjkuMzA5IDAgLjUyOS4xMjMuNjY0LjM2OS4xMzYuMjQ1LjIwNC41NzkuMjA0IDEuMDAzIDAgLjQxNS0uMDY4Ljc0LS4yMDQuOTg1LS4xMzUuMjQ0LS4zNTUuMzY2LS42NjQuMzY2LS4zMDEgMC0uNTIzLS4xMjItLjY1Ny0uMzY2LS4xMzUtLjI0NS0uMjAyLS41Ny0uMjAyLS45ODUgMC0uNDI0LjA2Ny0uNzU4LjIwMi0xLjAwM3ptLTUuNDQ4IDBjLjEzNS0uMjQ2LjM1Ni0uMzY5LjY1OC0uMzY5LjMwOSAwIC41MjkuMTIzLjY2NC4zNjkuMTM2LjI0NS4yMDQuNTc5LjIwNCAxLjAwMyAwIC40MTUtLjA2OC43NC0uMjA0Ljk4NS0uMTM1LjI0NC0uMzU1LjM2Ni0uNjY0LjM2Ni0uMzAyIDAtLjUyMy0uMTIyLS42NTgtLjM2Ni0uMTM0LS4yNDUtLjIwMS0uNTctLjIwMS0uOTg1IDAtLjQyNC4wNjctLjc1OC4yMDEtMS4wMDN6Ii8+PHBhdGggZmlsbD0iIzY2MjIxMiIgZD0iTTE4IDIxLjUzYy0yLjc2NCAwLTQuOTUxLS40MTktNC45NTEtMS4wNTQgMC0uNjM1IDIuMTg3LTEuMDU0IDQuOTUxLTEuMDU0IDIuNzYzIDAgNC45NTEuNDE5IDQuOTUxIDEuMDU0IDAgLjYzNS0yLjE4OCAxLjA1NC00Ljk1MSAxLjA1NHptMC0yLjEwOGMtMy4wMTcgMC01LjQ1MS0uNDk0LTUuNDUxLTEuMDU0czIuNDM0LTEuMDU0IDUuNDUxLTEuMDU0YzMuMDE4IDAgNS40NTEuNDk0IDUuNDUxIDEuMDU0cy0yLjQzMyAxLjA1NC01LjQ1MSAxLjA1NHoiLz48L3N2Zz4=',
-  speaking: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQjkwMCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMThTMCAyNy45NDEgMCAxOCA4LjA1OSAwIDE4IDBzMTggOC4wNTkgMTggMTgiLz48cGF0aCBmaWxsPSIjNjYyMjEyIiBkPSJNMTggMjEuNTNjLTIuNzY0IDAtNC45NTEtLjQxOS00Ljk1MS0xLjA1NCAwLS42MzUgMi4xODctMS4wNTQgNC45NTEtMS4wNTQgMi43NjMgMCA0Ljk1MS40MTkgNC45NTEgMS4wNTQgMCAuNjM1LTIuMTg4IDEuMDU0LTQuOTUxIDEuMDU0em0wLTIuMTA4Yy0zLjAxNyAwLTUuNDUxLS40OTQtNS40NTEtMS4wNTRzMi40MzQtMS4wNTQgNS40NTEtMS4wNTRjMy4wMTggMCA1LjQ1MS40OTQgNS40NTEgMS4wNTRzLTIuNDMzIDEuMDU0LTUuNDUxIDEuMDU0eiIvPjwvc3ZnPg==',
-  // 其他表情...
-};
-  // 问题相关
-  questions = [
+  remainingTime = '04:32';
+  isAnalyzing = false;
+  showSubmitButton = false;
+  showCancelButton = false; // 添加缺失的属性
+  userAnswer = '';
+  isFollowUpQuestion = false; // 添加缺失的属性
+  
+  // 面试问题相关
+  mainQuestions = [
     "欢迎参加本次AI面试,我是您的面试官AI助手。",
     "首先,请简单介绍一下您自己。",
     "您在过去工作中遇到的最大技术挑战是什么?您是如何解决的?",
     "请描述一个您与团队意见不合时,您是如何处理的案例。"
   ];
+  
+  followUpQuestions = [
+    "", // 第一个问题不需要追问
+    "您能详细说明一下您最擅长的技术领域吗?",
+    "在这个解决方案中,您学到了什么重要的经验?",
+    "这次经历如何影响了您后续的团队协作方式?"
+  ];
+  
   currentQuestionIndex = 0;
-  currentQuestion = this.questions[0];
+  currentQuestion = this.mainQuestions[0];
   showQuestionCard = false;
   progress = 30;
   
@@ -51,87 +57,112 @@ avatarImages = {
     relevance: 73
   };
   
-  // 预设回答
-  presetAnswers = [
-    "我是张伟,有5年Java开发经验,擅长分布式系统设计。",
-    "我们曾遇到高并发下的数据库瓶颈,我通过引入Redis缓存和分库分表解决了问题。",
-    "有一次在技术方案选择上,我通过数据对比和原型验证说服了团队采用我的方案。"
-  ];
-  
   @ViewChild('dialogContainer') dialogContainer!: ElementRef;
   private radarChart: any;
 
+  constructor(private aiService: AiApiService) {}
+
   ngOnInit(): void {
     this.initConversation();
     this.startProgressTimer();
   }
   
   ngAfterViewInit(): void {
-    // 初始化雷达图
     this.initRadarChart();
   }
   
-  // 初始化对话
+  // 添加缺失的方法
+  updateAvatarState(state: "speaking" | "listening" | "waiting" | "analyzing"): void {
+    switch(state) {
+      case "speaking":
+        this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f5e3.svg";
+        this.expressionText = "正在播报问题...";
+        break;
+      case "listening":
+        this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
+        this.expressionText = "正在聆听您的回答...";
+        break;
+      case "waiting":
+        this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f914.svg";
+        this.expressionText = "请确认您的回答";
+        break;
+      case "analyzing":
+        this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f9d0.svg";
+        this.expressionText = "正在分析您的回答...";
+        break;
+    }
+  }
+  
   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.endInterview();
+      return;
+    }
     
     this.currentQuestionIndex = index;
-    this.currentQuestion = this.questions[index];
+    this.isFollowUpQuestion = false;
+    this.currentQuestion = this.mainQuestions[index];
     
-    // 添加到对话框
     this.addAIMessage(this.currentQuestion);
+    this.updateAvatarState("speaking");
+  }
+  
+  askFollowUpQuestion(): void {
+    if (this.currentQuestionIndex >= this.followUpQuestions.length || 
+        !this.followUpQuestions[this.currentQuestionIndex]) {
+      this.askQuestion(this.currentQuestionIndex + 1);
+      return;
+    }
     
-    // 更新头像状态
-    this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
-    this.expressionText = "等待您的回答...";
+    this.isFollowUpQuestion = true;
+    this.currentQuestion = this.followUpQuestions[this.currentQuestionIndex];
+    
+    this.addAIMessage(this.currentQuestion);
+    this.updateAvatarState("speaking");
+  }
+  
+  endInterview(): void {
+    this.addAIMessage("感谢您的回答,面试即将结束,正在生成最终报告...");
+    this.updateAvatarState("analyzing");
   }
   
-  // 添加AI消息
   addAIMessage(text: string): void {
     this.messages.push({
       sender: 'ai',
       text: text
     });
     
-    // 滚动到底部
     setTimeout(() => {
       this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
     }, 0);
     
-    // 自动语音播报
     this.speakText(text);
   }
   
-  // 添加用户消息
   addUserMessage(text: string): void {
     this.messages.push({
       sender: 'user',
       text: text
     });
     
-    // 滚动到底部
     setTimeout(() => {
       this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
     }, 0);
   }
   
-  // 语音播报
   speakText(text: string): void {
     if (this.isSpeaking) 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");
     
-    // 使用Web Speech API
     const utterance = new SpeechSynthesisUtterance(text);
     utterance.lang = 'zh-CN';
     utterance.rate = 0.9;
@@ -139,64 +170,87 @@ avatarImages = {
     
     utterance.onend = () => {
       this.isSpeaking = false;
-      this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
-      this.expressionText = "等待您的回答...";
+      this.updateAvatarState("listening");
     };
     
     speechSynthesis.speak(utterance);
   }
   
-  // 开始语音输入
   startVoiceInput(): void {
     this.isListening = true;
-    this.expressionText = "正在聆听您的回答...";
+    this.updateAvatarState("listening");
     console.log('语音输入开始');
   }
   
-  // 停止语音输入
   stopVoiceInput(): void {
     this.isListening = false;
     
     // 模拟语音识别结果
-    this.userAnswer = this.presetAnswers[this.currentQuestionIndex - 1] || "这是我的回答...";
-    this.showSubmitButton = true; // 显示提交按钮
+    this.userAnswer = "这是我的回答...";
+    this.showSubmitButton = true;
+    this.showCancelButton = true;
     
-    // 不再自动提交,等待用户点击
-    this.avatarImage = this.avatarImages.default;
-    this.expressionText = "请确认您的回答";
+    this.updateAvatarState("waiting");
   }
-  submitAnswer(): void {
+  
+  async submitAnswer(): Promise<void> {
     this.showSubmitButton = false;
+    this.showCancelButton = false;
     this.addUserMessage(this.userAnswer);
     
-    // 开始分析
     this.isAnalyzing = true;
-    this.avatarImage = this.avatarImages.default;
-    this.expressionText = "正在分析您的回答...";
+    this.updateAvatarState("analyzing");
     
-    // 模拟分析过程 (2秒)
-    setTimeout(() => {
-      this.isAnalyzing = false;
+    try {
+      const evaluation = await this.aiService.evaluateInterviewAnswer(
+        this.currentQuestion,
+        this.userAnswer,
+        (content) => console.log('分析进度:', content)
+      );
       
-      // 问下一题或结束
-      if (this.currentQuestionIndex < this.questions.length - 1) {
-        this.askQuestion(this.currentQuestionIndex + 1);
+      this.metrics = {
+        expressiveness: evaluation.metrics.expressiveness,
+        professionalism: evaluation.metrics.professionalism,
+        relevance: evaluation.metrics.relevance
+      };
+      
+      this.addAIMessage(evaluation.feedback);
+      
+      if (!this.isFollowUpQuestion) {
+        this.addAIMessage("接下来,我有一个跟进问题...");
+        setTimeout(() => {
+          this.currentQuestion = evaluation.followUpQuestion;
+          this.isFollowUpQuestion = true;
+          this.addAIMessage(this.currentQuestion);
+        }, 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]);
+      this.speakText(this.currentQuestion);
     }
   }
   
-  // 切换雷达图
   toggleRadarChart(): void {
     this.showRadarChart = !this.showRadarChart;
     if (this.showRadarChart) {
@@ -206,7 +260,6 @@ avatarImages = {
     }
   }
   
-  // 初始化雷达图
   initRadarChart(): void {
     if (!this.showRadarChart) return;
     
@@ -289,16 +342,14 @@ avatarImages = {
     
     this.radarChart.setOption(option);
     
-    // 响应式调整
     window.addEventListener('resize', () => {
       this.radarChart?.resize();
     });
   }
   
-  // 进度条计时器
   startProgressTimer(): void {
     setInterval(() => {
       this.progress = Math.min(this.progress + 10, 100);
     }, 5000);
   }
-}
+}

+ 3 - 0
interview-web/tsconfig.doc.json

@@ -0,0 +1,3 @@
+{
+    "include": ["src/**/*.ts"]
+}