初始化项目,添加所有工程文件
This commit is contained in:
commit
205235867e
201
Project_Design.md
Normal file
201
Project_Design.md
Normal file
@ -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简历解析
|
||||
- 多岗位预设
|
||||
- 语音输入支持
|
||||
|
||||
### 中期扩展
|
||||
- 用户账号系统
|
||||
- 历史记录保存
|
||||
- 面试报告导出
|
||||
|
||||
### 长期扩展
|
||||
- 职业规划建议
|
||||
- 薪资谈判指导
|
||||
- 行业趋势分析
|
||||
129
README.md
Normal file
129
README.md
Normal file
@ -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
|
||||
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
284
app.py
Normal file
284
app.py
Normal file
@ -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
|
||||
)
|
||||
93
config.py
Normal file
93
config.py
Normal file
@ -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": """你是一位智能求职助手,专门帮助求职者解决求职相关问题。
|
||||
|
||||
你可以帮助用户:
|
||||
- 解答简历制作和优化问题
|
||||
- 提供面试技巧和建议
|
||||
- 分析岗位要求和匹配度
|
||||
- 分享行业知识和职业发展建议
|
||||
|
||||
请用专业、友好、鼓励的语气与用户交流。回复要简洁有用,避免冗长。"""
|
||||
}
|
||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
flask>=2.3.0
|
||||
requests>=2.31.0
|
||||
python-dotenv>=1.0.0
|
||||
gunicorn>=21.0.0
|
||||
BIN
services/__pycache__/deepseek_service.cpython-312.pyc
Normal file
BIN
services/__pycache__/deepseek_service.cpython-312.pyc
Normal file
Binary file not shown.
126
services/deepseek_service.py
Normal file
126
services/deepseek_service.py
Normal file
@ -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()
|
||||
537
static/css/style.css
Normal file
537
static/css/style.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
402
static/js/main.js
Normal file
402
static/js/main.js
Normal file
@ -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 '<p>未能生成优化建议,请重试。</p>';
|
||||
|
||||
let html = text
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return '<p>' + html + '</p>';
|
||||
}
|
||||
|
||||
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 = '<h4>面试综合评估</h4>';
|
||||
feedbackData.forEach(msg => {
|
||||
html += `<p><strong>${msg.role === 'assistant' ? '面试官' : '你'}:</strong>${msg.content}</p>`;
|
||||
});
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
154
templates/index.html
Normal file
154
templates/index.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>面试官 - AI面试助手</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">👔</span>
|
||||
<h1>面试官</h1>
|
||||
</div>
|
||||
<p class="tagline">AI驱动的智能面试助手,帮助你轻松应对面试</p>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<section class="feature-section">
|
||||
<div class="feature-card" onclick="showFeature('resume')">
|
||||
<div class="feature-icon">📄</div>
|
||||
<h3>简历优化</h3>
|
||||
<p>AI分析简历,提供针对性优化建议,让你的简历脱颖而出</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" onclick="showFeature('interview')">
|
||||
<div class="feature-icon">🎯</div>
|
||||
<h3>模拟面试</h3>
|
||||
<p>模拟真实面试场景,AI扮演面试官进行问答练习</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" onclick="showFeature('feedback')">
|
||||
<div class="feature-icon">💡</div>
|
||||
<h3>反馈建议</h3>
|
||||
<p>实时评估回答质量,提供具体改进建议和参考范例</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="resume-section" class="tool-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>📄 简历优化助手</h2>
|
||||
<button class="back-btn" onclick="showFeature(null)">返回</button>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="target-position">目标岗位(选填)</label>
|
||||
<input type="text" id="target-position" placeholder="例如:产品经理、Java开发工程师">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="resume-content">简历内容</label>
|
||||
<textarea id="resume-content" rows="10" placeholder="请粘贴你的简历内容,或描述你的工作经验和技能..."></textarea>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" onclick="optimizeResume()">开始优化</button>
|
||||
|
||||
<div id="resume-result" class="result-box" style="display: none;">
|
||||
<h3>优化建议</h3>
|
||||
<div id="resume-suggestions" class="suggestions-content"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="interview-section" class="tool-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>🎯 模拟面试</h2>
|
||||
<button class="back-btn" onclick="showFeature(null)">返回</button>
|
||||
</div>
|
||||
|
||||
<div id="interview-setup" class="interview-setup">
|
||||
<div class="input-group">
|
||||
<label for="job-position">目标岗位</label>
|
||||
<input type="text" id="job-position" placeholder="例如:产品经理、数据分析师" required>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>难度级别</label>
|
||||
<div class="difficulty-options">
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="difficulty" value="junior" checked>
|
||||
<span>初级</span>
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="difficulty" value="intermediate">
|
||||
<span>中级</span>
|
||||
</label>
|
||||
<label class="radio-label">
|
||||
<input type="radio" name="difficulty" value="senior">
|
||||
<span>高级</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" onclick="startInterview()">开始面试</button>
|
||||
</div>
|
||||
|
||||
<div id="interview-active" class="interview-active" style="display: none;">
|
||||
<div class="interview-info">
|
||||
<span id="current-position"></span>
|
||||
<span id="current-phase" class="phase-badge"></span>
|
||||
</div>
|
||||
|
||||
<div id="interview-messages" class="interview-messages"></div>
|
||||
|
||||
<div class="interview-input-area">
|
||||
<textarea id="answer-input" rows="3" placeholder="请输入你的回答..."></textarea>
|
||||
<div class="input-actions">
|
||||
<button class="action-btn secondary" onclick="requestFeedback()">请求反馈</button>
|
||||
<button class="action-btn primary" onclick="submitAnswer()">提交回答</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="interview-controls">
|
||||
<button class="control-btn" onclick="endInterview()">结束面试</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="interview-feedback" class="interview-feedback" style="display: none;">
|
||||
<h3>面试反馈</h3>
|
||||
<div id="feedback-content" class="feedback-content"></div>
|
||||
<button class="submit-btn" onclick="resetInterview()">重新开始</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="chat-section" class="tool-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<h2>💬 求职问答</h2>
|
||||
<button class="back-btn" onclick="showFeature(null)">返回</button>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
<div id="chat-messages" class="chat-messages"></div>
|
||||
|
||||
<div class="chat-input-area">
|
||||
<input type="text" id="chat-input" placeholder="输入你的问题,例如:如何准备产品经理面试?">
|
||||
<button class="send-btn" onclick="sendChatMessage()">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>Powered by DeepSeek AI</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div id="loading-overlay" class="loading-overlay" style="display: none;">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>AI思考中,请稍候...</p>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user