简历优化
+AI分析简历,提供针对性优化建议,让你的简历脱颖而出
+模拟面试
+模拟真实面试场景,AI扮演面试官进行问答练习
+反馈建议
+实时评估回答质量,提供具体改进建议和参考范例
+commit 205235867e1aa46a9d5fa7dbba7ec2f1bfb5644a Author: PierreCashon <1775967499@qq.com> Date: Wed Jan 7 16:30:31 2026 +0800 初始化项目,添加所有工程文件 diff --git a/Project_Design.md b/Project_Design.md new file mode 100644 index 0000000..9378cb0 --- /dev/null +++ b/Project_Design.md @@ -0,0 +1,201 @@ +# 项目设计思路 + +## 一、项目概述 + +### 名称 +面试官(Interviewer) + +### 目标用户 +正在求职的求职者,尤其是: +- 应届毕业生 +- 跳槽求职者 +- 转行求职者 +- 希望提升面试技巧的职场人士 + +### 核心价值 +通过AI技术降低面试准备门槛,提供个性化、即时反馈的面试练习体验,帮助求职者提升成功率。 + +## 二、问题分析 + +### 用户痛点 +1. 简历不知道怎么优化才能脱颖而出 +2. 缺乏面试经验,不知道面试官会问什么 +3. 面试后不知道回答得好不好,如何改进 +4. 准备效率低,缺乏针对性的练习 + +### 市场现状 +- 传统面试辅导价格昂贵(500-2000元/小时) +- 面试题库类产品只提供题目,缺乏交互反馈 +- AI面试产品多面向企业端,个人用户选择少 + +## 三、解决方案 + +### 产品形态 +Web应用(可扩展为小程序、APP) + +### 核心功能设计 + +#### 功能一:简历定制问答 + +**用户场景** +用户有一份简历,想要针对特定岗位进行优化,或者不确定简历内容是否合适。 + +**实现方案** +- 支持文本输入和文件上传(后续扩展PDF解析) +- AI分析简历与目标岗位的匹配度 +- 提供具体的优化建议:关键词补充、表述改进、内容增删 + +**交互示例** +``` +用户:我想应聘产品经理,这是我的一段项目经历: +「我负责了一个电商小程序,从0到1搭建,用了3个月时间。」 + +AI建议: +1. 补充具体职责:负责产品规划、需求分析、原型设计等 +2. 添加数据成果:用户增长XX%、转化率提升XX% +3. 突出技能匹配:强调需求分析、用户调研、数据分析等产品经理核心能力 +``` + +#### 功能二:面试模拟练习 + +**用户场景** +用户想要练习面试,但身边没有面试官,或者不想浪费真实面试机会。 + +**实现方案** +- 用户选择目标岗位、经验级别 +- AI根据岗位特性生成面试问题 +- 支持多轮对话,模拟真实面试节奏 +- 可随时暂停、继续、重新开始 + +**问题类型覆盖** +- 自我介绍类 +- 经历描述类(STAR法则) +- 岗位专业类 +- 情景假设类 +- 压力测试类 +- 职业规划类 + +#### 功能三:练习反馈与改进建议 + +**用户场景** +用户回答完面试问题后,想要知道回答得好不好,如何改进。 + +**实现方案** +- 实时评估用户回答(不等到面试结束) +- 评估维度:内容完整性、逻辑清晰度、专业匹配度、表达流畅度 +- 提供0-100分评分和星级评价 +- 给出具体改进建议 +- 提供参考回答范例(可选) + +**反馈示例** +``` +问题:请介绍一下你的项目管理经验。 + +你的回答:我之前带过两个项目,都按时交付了。 + +AI反馈: +- 评分:45/100 +- 内容完整度:★☆☆☆☆(过于简略,缺少关键信息) +- 改进建议: + 1. 补充项目背景(项目类型、规模、团队人数) + 2. 说明具体职责和贡献 + 3. 强调遇到的挑战和解决方案 + 4. 添加可量化的成果 + +参考回答:我在A公司担任产品经理期间,负责过2个B端SaaS产品的迭代项目... +``` + +## 四、技术架构 + +### 前端 +- 技术选型:原生HTML/CSS/JS(降低学习成本)或 React/Vue +- 核心页面:首页、简历优化页、模拟面试页、反馈页 +- 交互特点:对话式界面,模拟真实面试场景 + +### 后端 +- 技术选型:Flask(轻量级,适合快速开发)或 FastAPI(异步支持好) +- 职责:路由管理、API调用封装、请求处理 + +### AI服务层 +- DeepSeek API(deepseek-chat模型) +- 服务封装:统一调用接口、错误处理、上下文管理 +- 提示词工程:针对不同场景优化系统提示词 + +### 数据流 +``` +用户输入 → 前端收集 → 后端接收 → AI服务调用 → 结果返回 → 前端展示 +``` + +## 五、交互设计 + +### 页面结构 +1. **首页**:功能入口展示,快速开始按钮 +2. **简历优化页**:输入区域、目标岗位选择、结果显示 +3. **模拟面试页**:对话式界面,问题展示区、回答输入区、提交按钮 +4. **反馈页**:评分展示、详细建议、改进后的参考回答 + +### 视觉风格 +- 专业、简洁、智能感 +- 主色调:蓝色系(科技、专业、信任) +- 辅助色:绿色(正面反馈)、橙色(提醒) + +### 响应式设计 +- 适配桌面端和移动端 +- 移动端优先考虑对话体验 + +## 六、开发计划 + +### 第一阶段:MVP(第1-2天) +- 完成项目结构搭建 +- 实现DeepSeek API集成 +- 实现简历问答功能 +- 实现基础对话界面 + +### 第二阶段:完善功能(第3-4天) +- 实现面试模拟对话流程 +- 实现评分反馈系统 +- 优化UI/UX + +### 第三阶段:优化与测试(第5天) +- Bug修复和性能优化 +- 多轮对话测试 +- 用户体验测试 + +## 七、成功指标 + +1. 功能完整性:所有核心功能可正常运行 +2. 响应速度:AI回复时间<3秒 +3. 用户体验:操作流程顺畅,界面清晰 +4. AI质量:回答有针对性,建议有实用价值 + +## 八、风险与应对 + +| 风险 | 应对措施 | +|------|----------| +| API调用失败 | 添加错误提示和重试机制 | +| AI回答质量不稳定 | 优化提示词,添加回答质量过滤 | +| 用户数据安全 | 本地存储敏感信息,不上传服务器 | +| 并发访问压力 | 后续考虑添加请求队列和限流 | + +## 九、成本估算 + +- DeepSeek API:约0.1-0.5元/千token(根据使用量) +- 服务器:云服务器约50-200元/月(可选,本地运行则免费) +- 域名:约50-100元/年(可选) + +## 十、扩展方向 + +### 短期扩展 +- PDF简历解析 +- 多岗位预设 +- 语音输入支持 + +### 中期扩展 +- 用户账号系统 +- 历史记录保存 +- 面试报告导出 + +### 长期扩展 +- 职业规划建议 +- 薪资谈判指导 +- 行业趋势分析 diff --git a/README.md b/README.md new file mode 100644 index 0000000..380f0d2 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# 面试官 + +> 面向求职者的AI面试助手,提供简历定制问答和面试练习反馈服务。 + +## 一句话描述 + +「面试官」是一款基于DeepSeek AI的智能面试助手,帮助求职者优化简历、模拟面试并获取专业改进建议。 + +## 核心功能(MVP) + +### 1. 简历定制问答 +- 用户上传简历或描述求职意向 +- AI分析简历内容,提供优化建议 +- 针对目标岗位定制简历关键词和表述 + +### 2. 面试模拟练习 +- 用户选择目标岗位和难度级别 +- AI扮演面试官进行多轮问答 +- 实时生成针对性的面试问题 + +### 3. 练习反馈与改进建议 +- 实时分析用户回答内容 +- 提供结构化评分(逻辑性、专业性、表达力) +- 给出具体改进建议和参考回答 + +## 交互流程 + +### 简历定制问答流程 +``` +1. 用户打开应用 +2. 点击「简历优化」或输入目标岗位 +3. 上传简历文件或粘贴简历内容 +4. AI分析并展示优化建议 +5. 用户查看、采纳或继续咨询 +``` + +### 面试模拟练习流程 +``` +1. 用户打开应用 +2. 点击「模拟面试」 +3. 选择目标岗位和难度(初级/中级/高级) +4. AI开始面试问答(循环进行) +5. 用户回答每个问题 +6. 点击「结束面试」查看详细反馈 +7. 查看评分和改进建议 +``` + +## 技术栈 + +- **后端**:Python + Flask/FastAPI +- **前端**:HTML/CSS/JavaScript(可扩展为React/Vue) +- **AI服务**:DeepSeek API +- **部署**:本地运行或云服务器 + +## 项目结构 + +``` +面试官/ +├── README.md # 项目说明文档 +├── Project_Design.md # 项目设计思路 +├── app.py # 主应用入口 +├── config.py # 配置文件(API密钥等) +├── requirements.txt # Python依赖 +├── static/ # 静态资源 +│ ├── css/ +│ │ └── style.css # 样式文件 +│ └── js/ +│ └── main.js # 前端脚本 +├── templates/ # HTML模板 +│ └── index.html # 主页面 +└── services/ # 业务逻辑层 + └── deepseek_service.py # DeepSeek API服务 +``` + +## 快速开始 + +### 1. 环境准备 + +确保已安装Python 3.8+: + +```bash +python --version +``` + +### 2. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 3. 配置API密钥 + +编辑`config.py`文件: + +```python +DEEPSEEK_API_KEY = "sk-9f449a2d06f644d082e32863d7c2d37c" +``` + +### 4. 启动应用 + +```bash +python app.py +``` + +### 5. 访问应用 + +打开浏览器访问:http://localhost:5000 + +## API配置 + +本项目使用DeepSeek Chat API,官方文档:https://platform.deepseek.com/ + +### 模型选择 + +- `deepseek-chat`:通用对话模型(推荐) +- `deepseek-reasoner`:推理增强模型 + +## 后续扩展功能 + +- [ ] 多语言支持 +- [ ] 语音面试模拟 +- [ ] 面试题库管理 +- [ ] 用户历史记录 +- [ ] PDF简历解析 +- [ ] 岗位推荐分析 + +## 许可证 + +MIT License diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..c0925b7 Binary files /dev/null and b/__pycache__/config.cpython-312.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..6cb79d4 --- /dev/null +++ b/app.py @@ -0,0 +1,284 @@ +from flask import Flask, render_template, request, jsonify, session +from services.deepseek_service import deepseek_service +from config import Config +import uuid + +app = Flask(__name__) +app.secret_key = "interviewer-secret-key-change-in-production" + +INTERVIEW_PHASES = { + "intro": "自我介绍", + "professional": "专业能力", + "scenario": "情景假设", + "career": "职业规划", + "closing": "面试结束" +} + +QUESTION_COUNTS = { + "intro": 2, + "professional": 4, + "scenario": 2, + "career": 1 +} + +@app.route("/") +def index(): + return render_template("index.html") + + +@app.route("/api/chat", methods=["POST"]) +def chat(): + data = request.json + user_input = data.get("message", "").strip() + system_type = data.get("system_type", "general_assistant") + conversation_key = data.get("conversation_key", "default") + + if not user_input: + return jsonify({"error": "请输入内容"}), 400 + + if conversation_key not in session: + session[conversation_key] = [] + + conversation_history = session[conversation_key] + + try: + result = deepseek_service.chat( + user_input=user_input, + conversation_history=conversation_history, + system_type=system_type + ) + + conversation_history.append({"role": "user", "content": user_input}) + conversation_history.append(result) + session[conversation_key] = conversation_history[-20:] + + return jsonify({"response": result["content"]}) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/resume/optimize", methods=["POST"]) +def optimize_resume(): + data = request.json + resume_content = data.get("resume_content", "").strip() + target_position = data.get("target_position", "").strip() + + if not resume_content: + return jsonify({"error": "请提供简历内容"}), 400 + + try: + result = deepseek_service.optimize_resume(resume_content, target_position) + return jsonify(result) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route("/api/interview/start", methods=["POST"]) +def start_interview(): + data = request.json + job_position = data.get("job_position", "").strip() + difficulty = data.get("difficulty", "intermediate") + + if not job_position: + return jsonify({"error": "请选择目标岗位"}), 400 + + interview_id = str(uuid.uuid4()) + + session[f"interview_{interview_id}"] = { + "job_position": job_position, + "difficulty": difficulty, + "current_phase": "intro", + "question_count": 0, + "conversation_history": [], + "is_active": True + } + + first_question = deepseek_service.generate_interview_question( + job_position=job_position, + difficulty=difficulty, + phase="intro" + ) + + session[f"interview_{interview_id}"]["conversation_history"].append({ + "role": "assistant", + "content": f"你好!我是面试官,现在开始针对{job_position}岗位的面试。\n\n{first_question}" + }) + + return jsonify({ + "interview_id": interview_id, + "job_position": job_position, + "difficulty": difficulty, + "question": first_question, + "phase": "intro" + }) + + +@app.route("/api/interview/answer", methods=["POST"]) +def answer_question(): + data = request.json + interview_id = data.get("interview_id", "").strip() + user_answer = data.get("answer", "").strip() + request_feedback = data.get("request_feedback", False) + + if not interview_id: + return jsonify({"error": "无效的面试ID"}), 400 + + interview_key = f"interview_{interview_id}" + if interview_key not in session: + return jsonify({"error": "面试不存在或已结束"}), 400 + + interview_data = session[interview_key] + + if not interview_data["is_active"]: + return jsonify({"error": "面试已结束"}), 400 + + if not user_answer: + return jsonify({"error": "请输入你的回答"}), 400 + + conversation_history = interview_data["conversation_history"] + + if request_feedback: + last_question = "" + for msg in reversed(conversation_history): + if msg["role"] == "assistant" and "?" in msg["content"]: + last_question = msg["content"] + break + + if last_question: + try: + feedback = deepseek_service.chat_with_feedback( + user_input=last_question, + user_answer=user_answer, + conversation_history=conversation_history[:-1] + ) + conversation_history.append({"role": "user", "content": user_answer}) + conversation_history.append(feedback) + + return jsonify({ + "feedback": feedback["content"], + "ended": False + }) + except Exception as e: + return jsonify({"error": f"生成反馈失败:{str(e)}"}), 500 + + conversation_history.append({"role": "user", "content": user_answer}) + + interview_data["question_count"] += 1 + current_phase = interview_data["current_phase"] + phase_order = ["intro", "professional", "scenario", "career", "closing"] + + current_index = phase_order.index(current_phase) + questions_in_phase = QUESTION_COUNTS.get(current_phase, 1) + + next_question = None + + if interview_data["question_count"] >= questions_in_phase: + if current_index < len(phase_order) - 1: + next_phase = phase_order[current_index + 1] + interview_data["current_phase"] = next_phase + interview_data["question_count"] = 0 + + if next_phase == "closing": + conversation_history.append({ + "role": "assistant", + "content": "面试结束!感谢你的参与。点击下方按钮获取本次面试的详细反馈。" + }) + interview_data["is_active"] = False + + return jsonify({ + "question": None, + "feedback": "面试结束", + "ended": True, + "conversation_history": conversation_history[-10:] + }) + else: + phase_names = { + "intro": "自我介绍", + "professional": "专业能力", + "scenario": "情景假设", + "career": "职业规划" + } + + try: + next_question = deepseek_service.generate_interview_question( + job_position=interview_data["job_position"], + difficulty=interview_data["difficulty"], + conversation_history=conversation_history[:-1], + phase=next_phase + ) + conversation_history.append({ + "role": "assistant", + "content": f"({phase_names.get(next_phase, next_phase)}阶段)\n\n{next_question}" + }) + except Exception as e: + return jsonify({"error": f"生成问题失败:{str(e)}"}), 500 + else: + try: + next_question = deepseek_service.generate_interview_question( + job_position=interview_data["job_position"], + difficulty=interview_data["difficulty"], + conversation_history=conversation_history[:-1], + phase=current_phase + ) + conversation_history.append({"role": "assistant", "content": next_question}) + except Exception as e: + return jsonify({"error": f"生成问题失败:{str(e)}"}), 500 + + session[interview_key] = interview_data + + return jsonify({ + "question": next_question, + "feedback": None, + "ended": False, + "phase": interview_data["current_phase"] + }) + + +@app.route("/api/interview/feedback", methods=["POST"]) +def get_interview_feedback(): + data = request.json + interview_id = data.get("interview_id", "").strip() + conversation_history = data.get("conversation_history", []) + + if not interview_id: + return jsonify({"error": "无效的面试ID"}), 400 + + system_prompt = """作为一位专业的面试评估专家,请对整场面试进行全面评估。 + +请提供: +1. 整体表现评分(0-100分)和评级(优秀/良好/一般/需改进) +2. 各轮回答的详细分析 +3. strengths(优势) +4. areas_for_improvement(需要改进的方面) +5. 具体的准备建议 + +请用中文回复,格式清晰、结构化。""" + + conversation_text = "\n\n".join([ + f"{msg['role']}:{msg['content']}" + for msg in conversation_history[-30:] + ]) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": f"请分析以下面试对话并给出综合反馈:\n\n{conversation_text}"} + ] + + try: + response = deepseek_service._call_api(messages) + feedback = response["choices"][0]["message"]["content"] + + return jsonify({"feedback": feedback}) + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +if __name__ == "__main__": + app.run( + host=Config.APP_HOST, + port=Config.APP_PORT, + debug=Config.DEBUG + ) diff --git a/config.py b/config.py new file mode 100644 index 0000000..422bd95 --- /dev/null +++ b/config.py @@ -0,0 +1,93 @@ +import os +from typing import Optional + +class Config: + DEEPSEEK_API_KEY: str = "sk-9f449a2d06f644d082e32863d7c2d37c" + DEEPSEEK_API_BASE: str = "https://api.deepseek.com" + MODEL_NAME: str = "deepseek-chat" + + MAX_TOKENS: int = 2000 + TEMPERATURE: float = 0.7 + + APP_HOST: str = "0.0.0.0" + APP_PORT: int = 5000 + DEBUG: bool = True + +def get_api_key() -> str: + api_key = os.getenv("DEEPSEEK_API_KEY") + if not api_key: + api_key = Config.DEEPSEEK_API_KEY + return api_key + +SYSTEM_PROMPTS = { + "resume_optimization": """你是一位专业的简历优化顾问和面试辅导专家。你的任务是帮助用户优化简历,提供专业的求职建议。 + +工作流程: +1. 分析用户提供的简历内容或求职意向 +2. 针对目标岗位评估简历的匹配度 +3. 提供具体的优化建议: + - 关键词补充(ATS系统友好) + - 表述方式改进(STAR法则) + - 内容结构优化 + - 量化成果强调 +4. 用专业、鼓励的语气回复 + +请确保: +- 回复简洁有力,重点突出 +- 每次只针对一个具体问题给出建议 +- 提供可操作的改进方案,而不是泛泛而谈 +- 如果信息不足,主动询问用户需要补充的内容""", + + "interview_simulation": """你是一位经验丰富的面试官,负责进行模拟面试。你的任务是扮演目标岗位的面试官,向候选人提问并评估其回答。 + +工作流程: +1. 根据用户选择的岗位和级别,准备针对性的面试问题 +2. 从基础问题开始,逐步深入 +3. 每轮只问一个问题,等待用户回答 +4. 根据用户回答,决定: + - 进入下一个问题 + - 追问细节 + - 给出反馈(如果用户要求结束面试) + +面试问题类型: +- 自我介绍(1-2轮) +- 岗位专业能力(3-5轮) +- 情景假设(1-2轮) +- 职业规划(1轮) + +注意事项: +- 问题要真实、典型、有挑战性 +- 语气专业但友好 +- 适当追问以深入了解 +- 严格遵守用户选择的岗位和级别""", + + "answer_feedback": """你是一位专业的面试辅导专家,负责评估用户的面试回答并提供改进建议。 + +评估维度: +1. 内容完整性(0-25分):是否覆盖问题要点 +2. 逻辑清晰度(0-25分):结构是否清晰,论证是否严密 +3. 专业匹配度(0-25分):是否展示岗位所需的专业能力 +4. 表达流畅度(0-25分):语言是否流畅自然 + +反馈格式: +- 整体评分(X/100)和星级(★☆☆☆☆) +- 每个维度的具体得分和评价 +- 具体的改进建议(3-5条) +- 参考回答示例(可选,用来说明好的回答应该是什么样的) + +要求: +- 评价客观中肯,既要指出不足也要肯定优点 +- 建议要具体可操作,不是泛泛而谈 +- 语言要鼓励性强,帮助用户建立信心 +- 如果回答已经很优秀,可以简短肯定并进入下一题""", + + "general_assistant": """你是一位智能求职助手,专门帮助求职者解决求职相关问题。 + +你可以帮助用户: +- 解答简历制作和优化问题 +- 提供面试技巧和建议 +- 分析岗位要求和匹配度 +- 分享行业知识和职业发展建议 + +请用专业、友好、鼓励的语气与用户交流。回复要简洁有用,避免冗长。""" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d8903a4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask>=2.3.0 +requests>=2.31.0 +python-dotenv>=1.0.0 +gunicorn>=21.0.0 diff --git a/services/__pycache__/deepseek_service.cpython-312.pyc b/services/__pycache__/deepseek_service.cpython-312.pyc new file mode 100644 index 0000000..62b5263 Binary files /dev/null and b/services/__pycache__/deepseek_service.cpython-312.pyc differ diff --git a/services/deepseek_service.py b/services/deepseek_service.py new file mode 100644 index 0000000..cfbf38e --- /dev/null +++ b/services/deepseek_service.py @@ -0,0 +1,126 @@ +import requests +import json +from typing import List, Dict, Optional, Generator +from config import Config, SYSTEM_PROMPTS, get_api_key + + +class DeepSeekService: + def __init__(self): + self.api_key = get_api_key() + self.api_base = Config.DEEPSEEK_API_BASE + self.model = Config.MODEL_NAME + self.max_tokens = Config.MAX_TOKENS + self.temperature = Config.TEMPERATURE + + def _get_headers(self) -> Dict[str, str]: + return { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + def _build_messages(self, system_prompt: str, conversation_history: List[Dict[str, str]], + user_input: str) -> List[Dict[str, str]]: + messages = [{"role": "system", "content": system_prompt}] + messages.extend(conversation_history) + messages.append({"role": "user", "content": user_input}) + return messages + + def _call_api(self, messages: List[Dict[str, str]], stream: bool = False) -> Dict: + payload = { + "model": self.model, + "messages": messages, + "max_tokens": self.max_tokens, + "temperature": self.temperature, + "stream": stream + } + + response = requests.post( + f"{self.api_base}/chat/completions", + headers=self._get_headers(), + json=payload, + timeout=30 + ) + + if response.status_code != 200: + error_msg = f"API调用失败:状态码 {response.status_code}" + try: + error_detail = response.json().get("error", {}) + error_msg += f",错误信息:{error_detail.get('message', '未知错误')}" + except: + error_msg += f",响应内容:{response.text}" + raise Exception(error_msg) + + return response.json() + + def chat(self, user_input: str, conversation_history: List[Dict[str, str]] = None, + system_type: str = "general_assistant") -> Dict[str, str]: + if conversation_history is None: + conversation_history = [] + + system_prompt = SYSTEM_PROMPTS.get(system_type, SYSTEM_PROMPTS["general_assistant"]) + messages = self._build_messages(system_prompt, conversation_history, user_input) + + response = self._call_api(messages) + assistant_message = response["choices"][0]["message"]["content"] + + return {"role": "assistant", "content": assistant_message} + + def chat_with_feedback(self, user_input: str, user_answer: str, + conversation_history: List[Dict[str, str]] = None) -> Dict[str, str]: + if conversation_history is None: + conversation_history = [] + + feedback_prompt = SYSTEM_PROMPTS["answer_feedback"] + + context = f"面试问题:{user_input}\n\n候选人回答:{user_answer}" + messages = self._build_messages(feedback_prompt, conversation_history, context) + + response = self._call_api(messages) + feedback_content = response["choices"][0]["message"]["content"] + + return {"role": "assistant", "content": feedback_content} + + def generate_interview_question(self, job_position: str, difficulty: str, + conversation_history: List[Dict[str, str]] = None, + phase: str = "intro") -> str: + if conversation_history is None: + conversation_history = [] + + system_prompt = SYSTEM_PROMPTS["interview_simulation"] + + context = f""" +目标岗位:{job_position} +难度级别:{difficulty} +面试阶段:{phase} + +请根据以上信息,提出一个针对性的面试问题。 +""" + messages = self._build_messages(system_prompt, conversation_history, context) + + response = self._call_api(messages) + question = response["choices"][0]["message"]["content"] + + return question + + def optimize_resume(self, resume_content: str, target_position: str = None) -> Dict[str, str]: + system_prompt = SYSTEM_PROMPTS["resume_optimization"] + + if target_position: + user_input = f"目标岗位:{target_position}\n\n简历内容:\n{resume_content}" + else: + user_input = f"请分析以下简历内容,提供优化建议:\n\n{resume_content}" + + messages = [{"role": "system", "content": system_prompt}] + messages.append({"role": "user", "content": user_input}) + + response = self._call_api(messages) + optimization_suggestions = response["choices"][0]["message"]["content"] + + return { + "resume_content": resume_content, + "target_position": target_position, + "suggestions": optimization_suggestions + } + + +deepseek_service = DeepSeekService() diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..47afb9b --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,537 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + color: #333; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.header { + text-align: center; + padding: 40px 20px; + color: white; +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + margin-bottom: 15px; +} + +.logo-icon { + font-size: 48px; +} + +.logo h1 { + font-size: 48px; + font-weight: 700; + text-shadow: 2px 2px 4px rgba(0,0,0,0.2); +} + +.tagline { + font-size: 18px; + opacity: 0.9; +} + +.main-content { + flex: 1; + padding: 20px 0; +} + +.feature-section { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 25px; + margin-bottom: 30px; +} + +.feature-card { + background: white; + border-radius: 16px; + padding: 35px 30px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); +} + +.feature-card:hover { + transform: translateY(-8px); + box-shadow: 0 15px 40px rgba(0,0,0,0.15); +} + +.feature-icon { + font-size: 56px; + margin-bottom: 20px; +} + +.feature-card h3 { + font-size: 24px; + color: #333; + margin-bottom: 12px; +} + +.feature-card p { + color: #666; + font-size: 15px; + line-height: 1.6; +} + +.tool-section { + background: white; + border-radius: 16px; + padding: 35px; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 2px solid #f0f0f0; +} + +.section-header h2 { + font-size: 26px; + color: #333; +} + +.back-btn { + background: #f5f5f5; + color: #666; + border: none; + padding: 10px 20px; + border-radius: 8px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.back-btn:hover { + background: #e0e0e0; +} + +.input-group { + margin-bottom: 25px; +} + +.input-group label { + display: block; + font-size: 15px; + font-weight: 600; + color: #333; + margin-bottom: 10px; +} + +.input-group input[type="text"], +.input-group textarea { + width: 100%; + padding: 14px 18px; + border: 2px solid #e0e0e0; + border-radius: 10px; + font-size: 15px; + transition: border-color 0.2s; + font-family: inherit; +} + +.input-group input[type="text"]:focus, +.input-group textarea:focus { + outline: none; + border-color: #667eea; +} + +.input-group textarea { + resize: vertical; + min-height: 150px; +} + +.difficulty-options { + display: flex; + gap: 20px; +} + +.radio-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 15px; +} + +.radio-label input[type="radio"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.submit-btn { + width: 100%; + padding: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 10px; + font-size: 17px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.submit-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4); +} + +.submit-btn:active { + transform: translateY(0); +} + +.result-box { + margin-top: 30px; + padding: 25px; + background: #f8f9ff; + border-radius: 12px; + border-left: 4px solid #667eea; +} + +.result-box h3 { + font-size: 18px; + color: #333; + margin-bottom: 15px; +} + +.suggestions-content { + font-size: 15px; + line-height: 1.8; + color: #444; +} + +.suggestions-content h4 { + color: #667eea; + margin: 15px 0 8px; +} + +.interview-setup { + max-width: 600px; + margin: 0 auto; +} + +.interview-active { + display: flex; + flex-direction: column; + height: calc(100vh - 300px); + min-height: 500px; +} + +.interview-info { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: #f5f5f5; + border-radius: 10px; + margin-bottom: 20px; +} + +#current-position { + font-weight: 600; + color: #333; +} + +.phase-badge { + background: #667eea; + color: white; + padding: 5px 15px; + border-radius: 20px; + font-size: 13px; +} + +.interview-messages { + flex: 1; + overflow-y: auto; + padding: 20px; + background: #fafafa; + border-radius: 12px; + margin-bottom: 20px; + max-height: 400px; +} + +.message { + margin-bottom: 20px; + padding: 15px 20px; + border-radius: 12px; + max-width: 85%; + line-height: 1.6; + font-size: 15px; +} + +.message.interviewer { + background: #667eea; + color: white; + margin-right: auto; +} + +.message.candidate { + background: #e8f5e9; + color: #333; + margin-left: auto; +} + +.message.feedback { + background: #fff3e0; + color: #333; + border-left: 4px solid #ff9800; + margin: 20px 0; +} + +.interview-input-area { + margin-bottom: 15px; +} + +.interview-input-area textarea { + width: 100%; + padding: 14px 18px; + border: 2px solid #e0e0e0; + border-radius: 10px; + font-size: 15px; + resize: none; + font-family: inherit; +} + +.interview-input-area textarea:focus { + outline: none; + border-color: #667eea; +} + +.input-actions { + display: flex; + gap: 15px; + margin-top: 12px; +} + +.action-btn { + flex: 1; + padding: 14px; + border: none; + border-radius: 10px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.action-btn.primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.action-btn.secondary { + background: #f5f5f5; + color: #666; +} + +.action-btn:hover { + transform: translateY(-2px); +} + +.interview-controls { + text-align: center; +} + +.control-btn { + background: #ff6b6b; + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; +} + +.control-btn:hover { + background: #ee5a5a; +} + +.interview-feedback { + padding: 25px; + background: #f8f9ff; + border-radius: 12px; +} + +.interview-feedback h3 { + font-size: 22px; + color: #333; + margin-bottom: 20px; +} + +.feedback-content { + font-size: 15px; + line-height: 1.8; + color: #444; + margin-bottom: 25px; +} + +.chat-container { + display: flex; + flex-direction: column; + height: calc(100vh - 350px); + min-height: 400px; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 20px; + background: #fafafa; + border-radius: 12px; + margin-bottom: 20px; +} + +.chat-message { + margin-bottom: 15px; + padding: 12px 18px; + border-radius: 12px; + max-width: 85%; + line-height: 1.6; + font-size: 15px; +} + +.chat-message.assistant { + background: #e3f2fd; + color: #333; + margin-right: auto; +} + +.chat-message.user { + background: #f3e5f5; + color: #333; + margin-left: auto; +} + +.chat-input-area { + display: flex; + gap: 15px; +} + +.chat-input-area input { + flex: 1; + padding: 14px 18px; + border: 2px solid #e0e0e0; + border-radius: 10px; + font-size: 15px; +} + +.chat-input-area input:focus { + outline: none; + border-color: #667eea; +} + +.send-btn { + padding: 14px 28px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 10px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} + +.send-btn:hover { + transform: translateY(-2px); +} + +.footer { + text-align: center; + padding: 25px; + color: rgba(255,255,255,0.8); + font-size: 14px; +} + +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 4px solid rgba(255,255,255,0.3); + border-top-color: white; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.loading-overlay p { + color: white; + margin-top: 15px; + font-size: 15px; +} + +@media (max-width: 768px) { + .container { + padding: 15px; + } + + .header { + padding: 30px 15px; + } + + .logo h1 { + font-size: 36px; + } + + .feature-section { + grid-template-columns: 1fr; + } + + .tool-section { + padding: 25px 20px; + } + + .difficulty-options { + flex-direction: column; + gap: 12px; + } + + .message { + max-width: 95%; + } + + .input-actions { + flex-direction: column; + } +} diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..1ca8eca --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,402 @@ +let currentFeature = null; +let interviewId = null; +let conversationKey = 'default'; + +function showFeature(feature) { + document.querySelectorAll('.tool-section').forEach(section => { + section.style.display = 'none'; + }); + + document.querySelectorAll('.feature-card').forEach(card => { + card.style.transform = ''; + card.style.boxShadow = ''; + }); + + if (feature === null) { + currentFeature = null; + return; + } + + const featureCard = document.querySelector(`.feature-card[onclick="showFeature('${feature}')"]`); + if (featureCard) { + featureCard.style.transform = 'translateY(-5px)'; + featureCard.style.boxShadow = '0 10px 30px rgba(102, 126, 234, 0.3)'; + } + + currentFeature = feature; + + const sectionMap = { + 'resume': 'resume-section', + 'interview': 'interview-section', + 'feedback': 'interview-section' + }; + + const targetSection = sectionMap[feature]; + if (targetSection) { + document.getElementById(targetSection).style.display = 'block'; + + if (feature === 'interview' || feature === 'feedback') { + resetInterview(); + } else if (feature === 'resume') { + document.getElementById('resume-result').style.display = 'none'; + } + } +} + +function showLoading(show) { + document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none'; +} + +async function optimizeResume() { + const targetPosition = document.getElementById('target-position').value.trim(); + const resumeContent = document.getElementById('resume-content').value.trim(); + + if (!resumeContent) { + alert('请输入简历内容'); + return; + } + + showLoading(true); + + try { + const response = await fetch('/api/resume/optimize', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + resume_content: resumeContent, + target_position: targetPosition + }) + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + const suggestionsDiv = document.getElementById('resume-suggestions'); + suggestionsDiv.innerHTML = formatSuggestions(data.suggestions); + document.getElementById('resume-result').style.display = 'block'; + + } catch (error) { + alert('优化失败:' + error.message); + } finally { + showLoading(false); + } +} + +function formatSuggestions(text) { + if (!text) return '
未能生成优化建议,请重试。
'; + + let html = text + .replace(/\n\n/g, '')
+ .replace(/\n/g, '
');
+
+ return '
' + html + '
'; +} + +async function startInterview() { + const jobPosition = document.getElementById('job-position').value.trim(); + + if (!jobPosition) { + alert('请输入目标岗位'); + return; + } + + const difficulty = document.querySelector('input[name="difficulty"]:checked').value; + + showLoading(true); + + try { + const response = await fetch('/api/interview/start', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + job_position: jobPosition, + difficulty: difficulty + }) + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + interviewId = data.interview_id; + + document.getElementById('interview-setup').style.display = 'none'; + document.getElementById('interview-active').style.display = 'flex'; + + document.getElementById('current-position').textContent = '目标岗位:' + data.job_position; + updatePhaseBadge(data.phase); + + const messagesContainer = document.getElementById('interview-messages'); + messagesContainer.innerHTML = ''; + + addInterviewMessage(data.question, 'interviewer'); + + } catch (error) { + alert('开始面试失败:' + error.message); + } finally { + showLoading(false); + } +} + +function updatePhaseBadge(phase) { + const phaseMap = { + 'intro': '自我介绍', + 'professional': '专业能力', + 'scenario': '情景假设', + 'career': '职业规划', + 'closing': '面试结束' + }; + + document.getElementById('current-phase').textContent = phaseMap[phase] || phase; +} + +function addInterviewMessage(content, type) { + if (!content) return; + + const messagesContainer = document.getElementById('interview-messages'); + const messageDiv = document.createElement('div'); + messageDiv.className = 'message ' + type; + messageDiv.textContent = content; + messagesContainer.appendChild(messageDiv); + messagesContainer.scrollTop = messagesContainer.scrollHeight; +} + +async function submitAnswer() { + const answerInput = document.getElementById('answer-input'); + const answer = answerInput.value.trim(); + + if (!answer) { + alert('请输入你的回答'); + return; + } + + addInterviewMessage(answer, 'candidate'); + answerInput.value = ''; + + showLoading(true); + + try { + const response = await fetch('/api/interview/answer', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + interview_id: interviewId, + answer: answer, + request_feedback: false + }) + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + if (data.ended) { + showFinalFeedback(data.conversation_history); + } else if (data.question) { + addInterviewMessage(data.question, 'interviewer'); + updatePhaseBadge(data.phase); + } + + } catch (error) { + alert('提交回答失败:' + error.message); + } finally { + showLoading(false); + } +} + +async function requestFeedback() { + const answerInput = document.getElementById('answer-input'); + const answer = answerInput.value.trim(); + + if (!answer) { + alert('请先输入你的回答'); + return; + } + + addInterviewMessage(answer, 'candidate'); + answerInput.value = ''; + + showLoading(true); + + try { + const response = await fetch('/api/interview/answer', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + interview_id: interviewId, + answer: answer, + request_feedback: true + }) + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + if (data.feedback) { + addInterviewMessage(data.feedback, 'feedback'); + } + + } catch (error) { + alert('获取反馈失败:' + error.message); + } finally { + showLoading(false); + } +} + +async function endInterview() { + const messages = []; + document.querySelectorAll('.interview-messages .message').forEach(msg => { + messages.push({ + role: msg.classList.contains('interviewer') ? 'assistant' : 'user', + content: msg.textContent + }); + }); + + showLoading(true); + + try { + const response = await fetch('/api/interview/feedback', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + interview_id: interviewId, + conversation_history: messages + }) + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + showFinalFeedback(data.feedback); + + } catch (error) { + alert('获取反馈失败:' + error.message); + } finally { + showLoading(false); + } +} + +function showFinalFeedback(feedbackData) { + document.getElementById('interview-active').style.display = 'none'; + document.getElementById('interview-feedback').style.display = 'block'; + + const feedbackContent = document.getElementById('feedback-content'); + + if (typeof feedbackData === 'string') { + feedbackContent.innerHTML = formatSuggestions(feedbackData); + } else { + let html = '${msg.role === 'assistant' ? '面试官' : '你'}:${msg.content}
`; + }); + feedbackContent.innerHTML = html; + } +} + +function resetInterview() { + interviewId = null; + document.getElementById('interview-setup').style.display = 'block'; + document.getElementById('interview-active').style.display = 'none'; + document.getElementById('interview-feedback').style.display = 'none'; + document.getElementById('interview-messages').innerHTML = ''; + document.getElementById('answer-input').value = ''; + document.getElementById('job-position').value = ''; +} + +async function sendChatMessage() { + const chatInput = document.getElementById('chat-input'); + const message = chatInput.value.trim(); + + if (!message) { + return; + } + + const messagesContainer = document.getElementById('chat-messages'); + + const userMessageDiv = document.createElement('div'); + userMessageDiv.className = 'chat-message user'; + userMessageDiv.textContent = message; + messagesContainer.appendChild(userMessageDiv); + + chatInput.value = ''; + messagesContainer.scrollTop = messagesContainer.scrollHeight; + + showLoading(true); + + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + message: message, + system_type: 'general_assistant', + conversation_key: conversationKey + }) + }); + + const data = await response.json(); + + if (data.error) { + alert(data.error); + return; + } + + const assistantMessageDiv = document.createElement('div'); + assistantMessageDiv.className = 'chat-message assistant'; + assistantMessageDiv.textContent = data.response; + messagesContainer.appendChild(assistantMessageDiv); + + messagesContainer.scrollTop = messagesContainer.scrollHeight; + + } catch (error) { + alert('发送消息失败:' + error.message); + } finally { + showLoading(false); + } +} + +document.addEventListener('DOMContentLoaded', function() { + document.getElementById('chat-input').addEventListener('keypress', function(e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendChatMessage(); + } + }); + + document.getElementById('answer-input').addEventListener('keypress', function(e) { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + submitAnswer(); + } + }); +}); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..829c523 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,154 @@ + + + + + +AI驱动的智能面试助手,帮助你轻松应对面试
+AI分析简历,提供针对性优化建议,让你的简历脱颖而出
+模拟真实面试场景,AI扮演面试官进行问答练习
+实时评估回答质量,提供具体改进建议和参考范例
+