初始化项目,添加所有工程文件

This commit is contained in:
PierreCashon 2026-01-07 16:30:31 +08:00
commit 205235867e
11 changed files with 1930 additions and 0 deletions

201
Project_Design.md Normal file
View 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 APIdeepseek-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
View 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

Binary file not shown.

284
app.py Normal file
View 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
View 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
View File

@ -0,0 +1,4 @@
flask>=2.3.0
requests>=2.31.0
python-dotenv>=1.0.0
gunicorn>=21.0.0

Binary file not shown.

View 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
View 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
View 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
View 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>