2 Commits 22865c9b54 ... 8190661114

Author SHA1 Message Date
  0235702 8190661114 merge: with others 1 day ago
  0235702 b0b186723b feat page-interview 2 days ago

+ 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
+    }
+}

+ 167 - 116
interview-web/src/modules/interview/mobile/page-interview/page-interview.html

@@ -1,132 +1,183 @@
 <div class="interview-container">
 <div class="interview-container">
-  <!-- 头部区域 -->
-  <div class="interview-header">
-    <div class="logo">AI面试官</div>
-    <div class="timer">
-      <i class="fas fa-clock"></i> 
-      剩余时间: {{ remainingTime }}
-      <span *ngIf="remainingMinutes === 0 && remainingSeconds <= 30" class="time-warning">
-        (即将结束)
-      </span>
-    </div>
-  </div>
-  
-  <!-- 数字人区域 -->
-  <div class="avatar-section">
-    <div class="avatar-container">
-      <img [src]="avatarImage" alt="AI头像" class="avatar-img" 
-      (error)="avatarImage = 'assets/default-avatar.svg'">
-      <div class="voice-wave" [style.animation]="isListening ? 'wave 2s infinite' : 'none'"></div>
-    </div>
-    <div class="avatar-expression">
-      <span class="expression-dot"></span>
-      <span>{{ expressionText }}</span>
+    <!-- 头部区域 -->
+    <div class="interview-header">
+        <div class="logo">AI面试官</div>
+        <div class="timer">
+            <i class="fas fa-clock"></i> 剩余时间: {{ remainingTime }}
+        </div>
     </div>
     </div>
-  </div>
-  
-  <!-- 对话区域 -->
-  <div class="dialog-container" #dialogContainer>
-    @for (message of messages; track message) {
-    <div class="message" [ngClass]="{'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user'}">
-      <div class="message-bubble" [ngClass]="{'ai-bubble': message.sender === 'ai', 'user-bubble': message.sender === 'user'}">
-        {{ message.text }}
-      </div>
-      <div class="message-meta" [ngClass]="{'ai-meta': message.sender === 'ai', 'user-meta': message.sender === 'user'}">
-        <i [class]="message.sender === 'ai' ? 'fas fa-robot' : 'fas fa-user'"></i> 
-        {{ message.sender === 'ai' ? 'AI面试官' : '您' }}
-      </div>
-    </div>}
-  </div>
-  
-  <!-- 问题卡片 -->
-  @if (showQuestionCard) {
-  <div class="question-card">
-    <div class="question-text">
-      {{ currentQuestion }}
+    
+    <!-- 数字人区域 -->
+    <div class="avatar-section">
+        <div class="avatar-container">
+            <img [src]="avatarImage" alt="AI头像" class="avatar-img" 
+                 (error)="avatarImage = 'assets/default-avatar.svg'">
+            <div class="voice-wave" [style.animation]="isListening ? 'wave 2s infinite' : 'none'"></div>
+        </div>
+        <div class="avatar-expression">
+            <span class="expression-dot"></span>
+            <span>{{ expressionText }}</span>
+        </div>
     </div>
     </div>
-    <div class="question-progress">
-      <span>问题 {{ currentQuestionIndex + 1 }}/{{ questions.length }}</span>
-      <div class="progress-bar">
-        <div class="progress-fill" [style.width]="progress + '%'"></div>
-      </div>
-      <span>{{ progress }}%</span>
+    
+    <!-- 修改后的对话区域 -->
+    <div class="dialog-container" #dialogContainer>
+        @for (message of messages; track $index) {
+        <div class="message" [ngClass]="{'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user'}">
+            <div class="message-bubble" [ngClass]="{'ai-bubble': message.sender === 'ai', 'user-bubble': message.sender === 'user'}">
+                {{ message.text }}
+            </div>
+            <div class="message-meta" [ngClass]="{'ai-meta': message.sender === 'ai', 'user-meta': message.sender === 'user'}">
+                <i [class]="message.sender === 'ai' ? 'fas fa-robot' : 'fas fa-user'"></i> 
+                {{ message.sender === 'ai' ? 'AI面试官' : '您' }}
+            </div>
+        </div>
+        }
+
+        <!-- 实时语音转文字显示 -->
+        @if (isListening && interimTranscript) {
+        <div class="message message-user">
+            <div class="message-bubble user-bubble">
+                {{ interimTranscript }}
+                <div class="recording-indicator">
+                    <span class="dot"></span>
+                    <span class="dot"></span>
+                    <span class="dot"></span>
+                </div>
+            </div>
+            <div class="message-meta user-meta">
+                <i class="fas fa-user"></i> 您 (正在说话)
+            </div>
+        </div>
+        }
     </div>
     </div>
-  </div>}
-  
-  <!-- 语音输入区域 -->
-  <div class="voice-input-section">
-    <div class="voice-controls">
-      <button class="voice-btn" 
-              (mousedown)="startVoiceInput()" 
-              (mouseup)="stopVoiceInput()"
-              [class.active]="isListening">
-        <i class="fas fa-microphone"></i>
-        <div class="voice-wave"></div>
-      </button>
-      <button class="voice-btn" 
-              (click)="playQuestion()"
-              style="background: linear-gradient(135deg, #48BB78, #38A169);">
-        <i class="fas fa-play"></i>
-      </button>
+    
+    <!-- 问题卡片 -->
+    @if (showQuestionCard && !interviewEnded) {
+    <div class="question-card">
+        <div class="question-text">
+            {{ currentQuestion }}
+        </div>
+        <div class="question-progress">
+            <span>问题 {{ currentQuestionIndex + 1 }}/{{ mainQuestions.length }}</span>
+            <div class="progress-bar">
+                <div class="progress-fill" [style.width]="progress + '%'"></div>
+            </div>
+            <span>{{ progress }}%</span>
+        </div>
     </div>
     </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>
     }
     }
-    <div class="voice-hint">
-      点击播放按钮可重复听取问题<br>
-      按住麦克风按钮开始回答
-    </div>
-  </div>
-  
-  <!-- 分析仪表盘 -->
-  <div class="dashboard-section">
-    <div class="dashboard-title">
-      <h3>实时分析面板</h3>
-      <button class="toggle-btn" (click)="toggleRadarChart()">
-        <i [class]="showRadarChart ? 'fas fa-chart-bar' : 'fas fa-chart-radar'"></i> 
-        {{ showRadarChart ? '隐藏雷达图' : '显示雷达图' }}
-      </button>
-    </div>
     
     
-    <div class="dashboard-content">
-      <div class="metric-item expressiveness">
-        <div class="metric-header">
-          <span class="metric-name">表达能力</span>
-          <span class="metric-value">{{ metrics.expressiveness }}/100</span>
+    <!-- 语音输入区域 -->
+    <div class="voice-input-section">
+        @if (!interviewEnded) {
+        <div class="voice-controls">
+            <!-- 麦克风按钮 -->
+            <button class="voice-btn" 
+                    (mousedown)="startVoiceInput()" 
+                    (mouseup)="stopVoiceInput()"
+                    [class.active]="isListening"
+                    [disabled]="isSpeaking || isAnalyzing">
+                <i class="fas fa-microphone"></i>
+                <div class="voice-wave"></div>
+            </button>
+            
+            <!-- 播放问题按钮 -->
+            <button class="voice-btn" 
+                    (click)="playQuestion()"
+                    [disabled]="isListening || isAnalyzing"
+                    style="background: linear-gradient(135deg, #48BB78, #38A169);">
+                <i class="fas fa-play"></i>
+            </button>
         </div>
         </div>
-        <div class="metric-bar">
-          <div class="metric-fill" [style.width]="metrics.expressiveness + '%'"></div>
+        
+        <!-- 确认/取消按钮 -->
+        @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>
-      </div>
-      
-      <div class="metric-item professionalism">
-        <div class="metric-header">
-          <span class="metric-name">专业度</span>
-          <span class="metric-value">{{ metrics.professionalism }}/100</span>
+        }
+        
+        <!-- 操作提示 -->
+        <div class="voice-hint">
+            @if (isListening) {
+            正在聆听,请回答问题...
+            } @else if (isSpeaking) {
+            正在播报问题...
+            } @else if (isAnalyzing) {
+            正在分析您的回答...
+            } @else {
+            按住麦克风按钮开始回答,点击播放按钮重复听取问题
+            }
         </div>
         </div>
-        <div class="metric-bar">
-          <div class="metric-fill" [style.width]="metrics.professionalism + '%'"></div>
+        } @else {
+        <!-- 面试结束状态 -->
+        <div class="interview-ended">
+            <h3>面试已结束</h3>
+            <p>感谢您参与本次AI面试</p>
+            <button class="restart-btn" (click)="restartInterview()">
+                <i class="fas fa-redo"></i> 重新开始面试
+            </button>
         </div>
         </div>
-      </div>
-      
-      <div class="metric-item relevance">
-        <div class="metric-header">
-          <span class="metric-name">岗位匹配度</span>
-          <span class="metric-value">{{ metrics.relevance }}/100</span>
+        }
+    </div>
+    
+    <!-- 分析仪表盘 -->
+    @if (!interviewEnded) {
+    <div class="dashboard-section">
+        <div class="dashboard-title">
+            <h3>实时分析面板</h3>
+            <button class="toggle-btn" (click)="toggleRadarChart()">
+                <i [class]="showRadarChart ? 'fas fa-chart-bar' : 'fas fa-chart-radar'"></i> 
+                {{ showRadarChart ? '隐藏雷达图' : '显示雷达图' }}
+            </button>
         </div>
         </div>
-        <div class="metric-bar">
-          <div class="metric-fill" [style.width]="metrics.relevance + '%'"></div>
+        
+        <div class="dashboard-content">
+            <div class="metric-item expressiveness">
+                <div class="metric-header">
+                    <span class="metric-name">表达能力</span>
+                    <span class="metric-value">{{ metrics.expressiveness }}/100</span>
+                </div>
+                <div class="metric-bar">
+                    <div class="metric-fill" [style.width]="metrics.expressiveness + '%'"></div>
+                </div>
+            </div>
+            
+            <div class="metric-item professionalism">
+                <div class="metric-header">
+                    <span class="metric-name">专业度</span>
+                    <span class="metric-value">{{ metrics.professionalism }}/100</span>
+                </div>
+                <div class="metric-bar">
+                    <div class="metric-fill" [style.width]="metrics.professionalism + '%'"></div>
+                </div>
+            </div>
+            
+            <div class="metric-item relevance">
+                <div class="metric-header">
+                    <span class="metric-name">岗位匹配度</span>
+                    <span class="metric-value">{{ metrics.relevance }}/100</span>
+                </div>
+                <div class="metric-bar">
+                    <div class="metric-fill" [style.width]="metrics.relevance + '%'"></div>
+                </div>
+            </div>
         </div>
         </div>
-      </div>
+        
+        <!-- 雷达图容器 -->
+        @if (showRadarChart) {
+        <div id="radarChart" class="radar-chart"></div>
+        }
     </div>
     </div>
-    
-    @if (showRadarChart){
-      <div id="radarChart" class="radar-chart"></div>
     }
     }
-  </div>
 </div>
 </div>

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

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

+ 287 - 159
interview-web/src/modules/interview/mobile/page-interview/page-interview.ts

@@ -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 { CommonModule } from '@angular/common';
 import * as echarts from 'echarts';
 import * as echarts from 'echarts';
+import { AiApiService } from '../../../../lib/ai-api.service';
 
 
 @Component({
 @Component({
   selector: 'app-page-interview',
   selector: 'app-page-interview',
@@ -9,197 +10,343 @@ import * as echarts from 'echarts';
   templateUrl: './page-interview.html',
   templateUrl: './page-interview.html',
   styleUrl: './page-interview.scss'
   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;
   isAnalyzing = false;
   showSubmitButton = 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;
   currentQuestionIndex = 0;
-  currentQuestion = this.questions[0];
-  showQuestionCard = false;
-  progress = 30;
+  currentQuestion = this.mainQuestions[0];
   
   
+  // 对话记录
   messages: {sender: 'ai' | 'user', text: string}[] = [];
   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 = {
   metrics = {
-    expressiveness: 82,
-    professionalism: 65,
-    relevance: 73
+    expressiveness: 0,
+    professionalism: 0,
+    relevance: 0
   };
   };
-  
-  presetAnswers = [
-    "我是张伟,有5年Java开发经验,擅长分布式系统设计。",
-    "我们曾遇到高并发下的数据库瓶颈,我通过引入Redis缓存和分库分表解决了问题。",
-    "有一次在技术方案选择上,我通过数据对比和原型验证说服了团队采用我的方案。"
-  ];
-  
+
   @ViewChild('dialogContainer') dialogContainer!: ElementRef;
   @ViewChild('dialogContainer') dialogContainer!: ElementRef;
   private radarChart: any;
   private radarChart: any;
 
 
+  constructor(private aiService: AiApiService) {
+    // 初始化语音识别
+    this.initSpeechRecognition();
+  }
+
   ngOnInit(): void {
   ngOnInit(): void {
     this.initConversation();
     this.initConversation();
-    this.startCountdown(); // 启动倒计时
+    this.initProgressTimer();
   }
   }
-  
+
   ngAfterViewInit(): void {
   ngAfterViewInit(): void {
     this.initRadarChart();
     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 {
   initConversation(): void {
-    this.addAIMessage(this.questions[0]);
+    this.addAIMessage(this.mainQuestions[0]);
     setTimeout(() => {
     setTimeout(() => {
       this.askQuestion(1);
       this.askQuestion(1);
     }, 1500);
     }, 1500);
   }
   }
-  
+
   askQuestion(index: number): void {
   askQuestion(index: number): void {
-    if (index >= this.questions.length) return;
+    if (index >= this.mainQuestions.length || this.interviewEnded) {
+      this.endInterview();
+      return;
+    }
     
     
     this.currentQuestionIndex = index;
     this.currentQuestionIndex = index;
-    this.currentQuestion = this.questions[index];
+    this.isFollowUpQuestion = false;
+    this.currentQuestion = this.mainQuestions[index];
+    
     this.addAIMessage(this.currentQuestion);
     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(() => {
     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 {
   addUserMessage(text: string): void {
-    this.messages.push({ sender: 'user', text: text });
+    this.messages.push({
+      sender: 'user',
+      text: text
+    });
+    this.scrollToBottom();
+  }
+
+  private scrollToBottom(): void {
     setTimeout(() => {
     setTimeout(() => {
       this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
       this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
     }, 0);
     }, 0);
   }
   }
-  
+
   speakText(text: string): void {
   speakText(text: string): void {
-    if (this.isSpeaking) return;
+    if (this.isSpeaking || !text) return;
     
     
     this.isSpeaking = true;
     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);
     const utterance = new SpeechSynthesisUtterance(text);
     utterance.lang = 'zh-CN';
     utterance.lang = 'zh-CN';
     utterance.rate = 0.9;
     utterance.rate = 0.9;
     utterance.pitch = 1;
     utterance.pitch = 1;
-    
+
     utterance.onend = () => {
     utterance.onend = () => {
       this.isSpeaking = false;
       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);
     speechSynthesis.speak(utterance);
   }
   }
-  
+
   startVoiceInput(): void {
   startVoiceInput(): void {
+    if (!this.recognition || this.isSpeaking || this.isAnalyzing) return;
+    
     this.isListening = true;
     this.isListening = true;
-    this.expressionText = "正在聆听您的回答...";
+    this.userAnswer = '';
+    this.updateAvatarState('listening');
+    this.recognition.start();
   }
   }
-  
+
   stopVoiceInput(): void {
   stopVoiceInput(): void {
     this.isListening = false;
     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.showSubmitButton = false;
+    this.showCancelButton = false;
     this.addUserMessage(this.userAnswer);
     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 {
       } 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 {
   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 {
   toggleRadarChart(): void {
     this.showRadarChart = !this.showRadarChart;
     this.showRadarChart = !this.showRadarChart;
     if (this.showRadarChart) {
     if (this.showRadarChart) {
@@ -208,9 +355,9 @@ export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
       }, 0);
       }, 0);
     }
     }
   }
   }
-  
-  initRadarChart(): void {
-    if (!this.showRadarChart) return;
+
+  private initRadarChart(): void {
+    if (!this.showRadarChart || this.interviewEnded) return;
     
     
     const chartDom = document.getElementById('radarChart');
     const chartDom = document.getElementById('radarChart');
     if (!chartDom) return;
     if (!chartDom) return;
@@ -235,55 +382,34 @@ export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
           { name: '知识广度', max: 100 }
           { name: '知识广度', max: 100 }
         ],
         ],
         radius: '65%',
         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: [{
       series: [{
         type: 'radar',
         type: 'radar',
         data: [
         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: '当前表现',
             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],
             value: [70, 80, 85, 75, 65, 70],
             name: '岗位要求',
             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();
       this.radarChart?.resize();
     });
     });
   }
   }
+
+  
 }
 }

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

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