Sfoglia il codice sorgente

feat: restore graduation project from PR #726

Original PR: https://github.com/datawhalechina/hello-agents/pull/726
Original commits:
2efd23e45a997a3f57725bf65ded21a45a66cdb2
8a2f9025c61dc15b1e61c2c6d213a5eb77e04c7a
ff92dba5ae90b67dfc818bd962e0d22f583c8935
475d00cf049f46877099fb8765c85e3069d2135d
4ec9493f37669670827f00993b50cadb693960da
1bcf37ab566b5a03e11cbb6c707e57f8dc93f2db

Max3753 1 mese fa
parent
commit
b206842a9c
82 ha cambiato i file con 18927 aggiunte e 0 eliminazioni
  1. 55 0
      Co-creation-projects/Max3753-Way_to_Engineer/.gitignore
  2. 180 0
      Co-creation-projects/Max3753-Way_to_Engineer/README.md
  3. 10 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/.env.example
  4. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/__init__.py
  5. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/__init__.py
  6. 73 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/arch_agent.py
  7. 91 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/coach_agent.py
  8. 65 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/debug_agent.py
  9. 93 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/orchestrator.py
  10. 116 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/review_agent.py
  11. 241 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/tutor_agent.py
  12. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/__init__.py
  13. 36 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/main.py
  14. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/__init__.py
  15. 83 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/assessment.py
  16. 86 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/auth.py
  17. 65 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/chat.py
  18. 101 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/code.py
  19. 22 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/gamification.py
  20. 323 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/learning.py
  21. 82 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/settings.py
  22. 84 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/config.py
  23. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/models/__init__.py
  24. 184 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/models/learning.py
  25. 26 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/models/schemas.py
  26. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/__init__.py
  27. 416 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/assessment_service.py
  28. 440 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/code_executor.py
  29. 307 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/data_store.py
  30. 208 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/gamification_service.py
  31. 510 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/learning_content.py
  32. 1259 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/lesson_content.py
  33. 116 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/llm_service.py
  34. 20 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/requirements.txt
  35. 19 0
      Co-creation-projects/Max3753-Way_to_Engineer/backend/run.py
  36. 24 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/.gitignore
  37. 13 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/index.html
  38. 2703 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/package-lock.json
  39. 32 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/package.json
  40. 0 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/public/favicon.svg
  41. 24 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/public/icons.svg
  42. 208 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/App.vue
  43. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/assets/hero.png
  44. 454 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/AgentConfigPanel.vue
  45. 269 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/AgentNode.vue
  46. 639 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/CodeEditor.vue
  47. 290 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/CodeRunner.vue
  48. 409 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/GamificationPanel.vue
  49. 469 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/MarkdownRenderer.vue
  50. 297 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/QuizWidget.vue
  51. 424 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/SettingsModal.vue
  52. 9 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/counter.ts
  53. 639 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/dark-theme-overrides.css
  54. 230 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/locales/en.ts
  55. 230 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/locales/zh.ts
  56. 26 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/main.ts
  57. 30 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/router/index.ts
  58. 144 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/agentStore.ts
  59. 30 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/authStore.ts
  60. 54 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/langStore.ts
  61. 30 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/themeStore.ts
  62. 77 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/style.css
  63. 92 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/styles/highlight-theme.css
  64. 143 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/types/index.ts
  65. 164 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/utils/helpers.ts
  66. 824 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Assessment.vue
  67. 986 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Chat.vue
  68. 887 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Dashboard.vue
  69. 1732 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Learning.vue
  70. 346 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Login.vue
  71. 592 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Orchestration.vue
  72. 7 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/vite-env.d.ts
  73. 24 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/tsconfig.json
  74. 16 0
      Co-creation-projects/Max3753-Way_to_Engineer/frontend/vite.config.ts
  75. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/agentsView.png
  76. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/chatView.png
  77. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/configView.png
  78. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/coursePath1.png
  79. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/coursePath2.png
  80. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/coursePath3.png
  81. BIN
      Co-creation-projects/Max3753-Way_to_Engineer/image/learningRecords.png
  82. 49 0
      Co-creation-projects/Max3753-Way_to_Engineer/start.bat

+ 55 - 0
Co-creation-projects/Max3753-Way_to_Engineer/.gitignore

@@ -0,0 +1,55 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+venv/
+.venv/
+env/
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS
+.DS_Store
+Thumbs.db
+desktop.ini
+
+# Node.js
+node_modules/
+dist/
+dist-ssr/
+
+# Build outputs
+*.local
+
+# Logs
+logs/
+*.log
+
+# Backend data (user-specific)
+backend/data/
+backend/memory/
+
+# Agent trace files
+*.jsonl
+
+# Sisyphus orchestration
+.sisyphus/
+
+# Frontend assets (Vite scaffold)
+frontend/src/assets/vite.svg
+frontend/src/assets/typescript.svg
+
+# Test coverage
+coverage/
+.nyc_output/

+ 180 - 0
Co-creation-projects/Max3753-Way_to_Engineer/README.md

@@ -0,0 +1,180 @@
+# Way to Engineer
+
+> AI 辅助编程学习平台 — 多 Agent 协作,交互式代码练习
+
+<div align="center">
+  <img src="image/chatView.png" alt="聊天对话界面" width="700">
+</div>
+
+---
+
+## 简介
+
+Way to Engineer 是一个 AI 驱动的编程学习平台。用户通过与多个 AI Agent 对话来学习编程,每个 Agent 扮演不同角色,覆盖从概念讲解到代码审查的完整学习闭环。
+
+---
+
+## 特性预览
+
+### 🤖 多 Agent 智能对话
+
+自动路由到最合适的 AI 角色——编程导师讲解概念、调试助手分析错误、审查员评估代码质量、架构师设计方案、学习教练规划路径。对话中可嵌入交互式测验和代码练习,边学边练。
+
+<div align="center">
+  <img src="image/agentsView.png" alt="Agent 角色视图" width="320">
+  <img src="image/chatView.png" alt="对话界面" width="320">
+</div>
+
+### 📚 结构化学习路径
+
+支持前端、后端、全栈三条学习路径,每节课包含概念讲解、代码示例和练习测验。完成课程后自动解锁下一课,学习进度和提交记录持久化保存。
+
+<div align="center">
+  <img src="image/coursePath1.png" alt="课程路径 1" width="220">
+  <img src="image/coursePath2.png" alt="课程路径 2" width="220">
+  <img src="image/coursePath3.png" alt="课程路径 3" width="220">
+</div>
+
+<div align="center">
+  <img src="image/learningRecords.png" alt="学习记录" width="320">
+</div>
+
+### ⚙️ 灵活配置
+
+运行时切换 LLM 配置(API Base URL、Model ID、API Key),无需重启服务。同时支持暗色主题和中英文界面切换。
+
+<div align="center">
+  <img src="image/configView.png" alt="配置页面" width="500">
+</div>
+
+### 更多功能
+
+- **交互式代码运行** — 聊天中的代码块可一键执行,右侧 Monaco 编辑器提供完整的编码环境
+- **练习反馈** — 写完练习代码后可提交给 AI 审查,获得教学性反馈
+- **水平评估** — AI 生成测验题目,评估用户当前水平并推荐学习起点
+- **游戏化** — XP 经验值、连续学习天数、徽章系统
+
+---
+
+## 技术栈
+
+| 层 | 技术 |
+|---|---|
+| 前端 | Vue 3 + TypeScript + Vite |
+| 后端 | FastAPI + Python 3.10+ |
+| AI 框架 | hello-agents(轻量 LLM Agent 封装) |
+| 代码编辑器 | Monaco Editor |
+| 持久化 | JSON 文件存储 |
+| 代码执行 | 子进程沙箱(subprocess + 安全过滤) |
+
+---
+
+## 快速开始
+
+### 前置要求
+
+- Python 3.10+
+- Node.js 18+
+- 一个 LLM API Key(默认支持 DeepSeek,也可配置其他 OpenAI 兼容接口)
+
+### 1. 克隆并配置后端
+
+```bash
+git clone <repo-url>
+cd Way_to_Engineer/backend
+python -m venv venv
+source venv/bin/activate   # Windows: venv\Scripts\activate
+pip install -r requirements.txt
+cp .env.example .env       # 编辑 .env 填入 API Key
+```
+
+### 2. 启动后端
+
+```bash
+python run.py
+```
+
+后端运行在 `http://localhost:8000`。
+
+### 3. 启动前端
+
+新开一个终端:
+
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+前端运行在 `http://localhost:5173`,Vite 自动代理 `/api` 到后端。
+
+### 4. 开始使用
+
+浏览器访问 `http://localhost:5173`,输入用户名即可开始。
+
+---
+
+## 项目结构
+
+```
+Way_to_Engineer/
+├── backend/
+│   ├── app/
+│   │   ├── agents/           # AI Agent(导师 / 调试 / 审查 / 架构 / 教练)
+│   │   ├── api/routes/       # API 路由
+│   │   ├── models/           # 数据模型
+│   │   ├── services/         # 业务服务
+│   │   └── config.py         # 配置管理
+│   ├── data/                 # 用户数据(不纳入版本控制)
+│   └── requirements.txt
+├── frontend/
+│   ├── src/
+│   │   ├── components/       # 组件(CodeEditor / QuizWidget 等)
+│   │   ├── views/            # 页面
+│   │   ├── stores/           # Pinia 状态管理
+│   │   └── locales/          # 中英文国际化
+│   └── package.json
+├── image/                    # 项目截图
+└── README.md
+```
+
+---
+
+## 配置说明
+
+### 环境变量(`.env`)
+
+| 变量 | 说明 | 默认值 |
+|---|---|---|
+| `DEEPSEEK_API_KEY` | DeepSeek API 密钥 | — |
+| `DEEPSEEK_MODEL_ID` | 模型名称 | `deepseek-chat` |
+| `DEEPSEEK_BASE_URL` | API 地址 | `https://api.deepseek.com/v1` |
+| `LLM_TIMEOUT` | LLM 请求超时(秒) | `60` |
+| `HOST` | 后端监听地址 | `0.0.0.0` |
+| `PORT` | 后端端口 | `8000` |
+
+### 运行时 LLM 配置
+
+登录后在导航栏点击齿轮图标 ⚙️,可在页面中直接修改 LLM 配置(Base URL / Model ID / API Key),修改后即时生效,无需重启服务。页面配置优先于 `.env` 文件。
+
+---
+
+## API 概览
+
+| 方法 | 路径 | 说明 |
+|---|---|---|
+| POST | `/api/chat/` | 发送聊天消息,自动路由到对应 Agent |
+| POST | `/api/code/execute` | 执行代码 |
+| POST | `/api/code/submit` | 提交练习代码获取 AI 反馈 |
+| GET/POST | `/api/settings/llm` | 获取 / 更新 LLM 配置 |
+| POST | `/api/auth/login` | 用户名登录 / 注册 |
+| GET | `/api/learning/paths` | 获取学习路径列表 |
+| GET | `/api/gamification/profile` | 获取游戏化档案 |
+
+完整 API 文档在服务启动后访问 `http://localhost:8000/docs`(Swagger UI)。
+
+---
+
+## License
+
+MIT

+ 10 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/.env.example

@@ -0,0 +1,10 @@
+# LLM API 配置
+DEEPSEEK_API_KEY=your_api_key_here
+DEEPSEEK_MODEL_ID=deepseek-chat
+DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
+LLM_TIMEOUT=60
+
+# 服务器配置
+HOST=0.0.0.0
+PORT=8000
+CORS_ORIGINS=http://localhost:5173,http://localhost:3000

+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/__init__.py


+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/__init__.py


+ 73 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/arch_agent.py

@@ -0,0 +1,73 @@
+"""架构师Agent"""
+
+from hello_agents import SimpleAgent
+from ..services.llm_service import get_llm
+
+
+ARCH_PROMPT = """你是一位资深的软件架构师。你的任务是帮助用户设计和优化软件架构。
+
+**你的职责:**
+1. 设计系统架构和技术选型
+2. 评估架构方案的优缺点
+3. 提供设计模式建议
+4. 解决架构层面的问题
+
+**专业领域:**
+1. **系统架构** - 微服务、单体、Serverless
+2. **数据架构** - 数据库设计、缓存策略
+3. **API设计** - RESTful、GraphQL、gRPC
+4. **设计模式** - 创建型、结构型、行为型
+5. **技术选型** - 框架、工具、中间件选择
+
+**输出质量守则:**
+1. **先确认约束** — 回答问题前先澄清用户的需求规模、团队规模、现有技术栈
+2. **对比方案** — 永远提供至少 2 种方案对比,列举各自的优缺点
+3. **结构化输出** — 用 `##` 二级标题组织内容,让用户可以折叠浏览
+4. **所有代码/配置示例必须用代码块包裹** — 使用 ` ```python ` 、` ```yaml ` 、` ```json ` 等对应语言标记,平台会自动渲染为可交互的代码卡片
+5. **具体而非抽象** — 给出推荐方案时附上具体的技术选型和理由
+6. **既考虑当下也考虑未来** — 明确区分"现在该怎么做"和"未来怎么演进"
+
+**推荐输出结构:**
+```
+## 需求理解
+(澄清核心需求和约束条件)
+
+## 方案对比
+
+### 方案A:[名称]
+(优缺点、适用场景)
+
+### 方案B:[名称]
+(优缺点、适用场景)
+
+## 推荐方案
+(选哪个,为什么,实施建议)
+```"""
+
+
+class ArchAgent:
+    """架构师Agent"""
+    
+    def __init__(self):
+        self.llm = get_llm()
+        self.agent = SimpleAgent(
+            name="架构师",
+            llm=self.llm,
+            system_prompt=ARCH_PROMPT,
+        )
+    
+    def chat(self, message: str, context=None) -> str:
+        """架构咨询"""
+        return self.agent.run(message)
+
+
+# 全局实例
+_arch_agent = None
+
+
+def get_arch_agent() -> ArchAgent:
+    """获取Arch Agent实例(单例)"""
+    global _arch_agent
+    if _arch_agent is None:
+        _arch_agent = ArchAgent()
+    return _arch_agent

+ 91 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/coach_agent.py

@@ -0,0 +1,91 @@
+"""学习教练Agent"""
+
+from hello_agents import SimpleAgent
+from ..services.llm_service import get_llm
+
+
+COACH_SYSTEM_PROMPT = """你是一个专业的学习教练,帮助用户规划学习路径、跟踪进度、提供学习建议。
+
+**你的角色定位:**
+你是用户的学习规划师 + 引路人。不只是分配学习任务,更要解释"为什么要学这个"和"这个有什么实际用处"。你的核心价值是帮用户看清学习地图,同时让每一步都有意义。
+
+**你的职责:**
+1. 根据用户当前进度,推荐下一步学习内容
+2. 解答关于学习路径的问题
+3. 提供学习方法建议(具体可执行,而非泛泛而谈)
+4. 用户完成阶段性目标后给予真诚肯定
+
+**输出质量守则:**
+1. **结构化输出** — 用 `##` 二级标题组织内容(自动折叠),不要一大段文字从头写到尾
+2. **所有代码示例必须用代码块包裹** — 使用 ` ```python ` 格式,平台会自动渲染为可交互的代码卡片(带"运行"按钮)
+3. **先讲解后练习** — 推荐每个练习前,先给一段概念讲解(2-4句话),说明这个知识点是什么、为什么重要、在实际代码中怎么用。不要一上来就扔练习。
+   - 正确做法:先解释"封装是 OOP 的核心思想,把数据和操作绑定在一起...",再给出练习
+   - 错误做法:直接"练习1:创建 BankAccount 类"
+4. **具体而非模糊** — 每一条建议必须附带具体例子(用 ` ``` ` 代码块)
+5. **可操作** — 告诉用户下一步具体做什么,而不是"继续努力"
+6. **代码示例是演示性而非答案性** — 展示概念用法,但保留核心练习让用户自己完成
+7. **不要空洞煽情** — 肯定要简洁真诚,不写大段鸡汤
+
+**你了解以下学习路径:**
+- 前端开发:HTML/CSS → JavaScript → Vue.js → 实战项目
+- 后端开发:Python → REST API → 系统设计 → 实战项目
+- 全栈开发:Web基础 → 前端框架 → 后端开发 → 全栈实战
+
+**输出结构参考(每项推荐应先讲概念后给练习):**
+```
+## 当前进度
+(一句话总结用户当前阶段和完成度)
+
+## 下一步推荐
+
+### 主题1:封装与 @property
+封装是 OOP 的核心——把数据和操作绑定在一起,对外隐藏内部细节。
+Python 用 @property 替代传统的 getter/setter,写起来更优雅。
+```python
+# 演示: @property 的基本用法
+class Temperature:
+    def __init__(self, celsius):
+        self._celsius = celsius
+    @property
+    def fahrenheit(self):
+        return self._celsius * 9/5 + 32
+```
+**练习:** 创建一个 BankAccount 类,实现私有属性 _balance 和 @property...
+
+### 主题2:继承与方法重写
+...
+
+## 学习建议
+(具体可执行的方法建议)
+```"""
+
+
+class CoachAgent:
+    """学习教练Agent"""
+    
+    def __init__(self):
+        self.llm = get_llm()
+        self.agent = SimpleAgent(
+            name="学习教练",
+            llm=self.llm,
+            system_prompt=COACH_SYSTEM_PROMPT,
+        )
+    
+    def chat(self, message: str, context=None) -> str:
+        """与教练对话"""
+        enriched = message
+        if context:
+            enriched = f"{message}\n\n用户上下文:\n{context}"
+        return self.agent.run(enriched)
+
+
+# 全局实例
+_coach_agent = None
+
+
+def get_coach_agent() -> CoachAgent:
+    """获取Coach Agent实例(单例)"""
+    global _coach_agent
+    if _coach_agent is None:
+        _coach_agent = CoachAgent()
+    return _coach_agent

+ 65 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/debug_agent.py

@@ -0,0 +1,65 @@
+"""调试助手Agent"""
+
+from hello_agents import SimpleAgent
+from ..services.llm_service import get_llm
+
+
+DEBUG_PROMPT = """你是一位经验丰富的调试专家。你的任务是帮助用户分析和修复代码bug。
+
+**你的职责:**
+1. 分析错误信息和报错日志
+2. 定位代码问题的可能原因
+3. 提供修复建议和代码示例
+4. 教用户调试技巧和方法
+
+**输出质量守则:**
+1. **先诊断后修复** — 不看完完整错误信息就给修复建议是失职
+2. **结构化输出** — 用 `##` 标题组织:错误分析 → 根因定位 → 修复方案 → 预防措施
+3. **按可能性排序** — 列出多个可能原因时,从最可能的开始,而不是罗列全部可能性
+4. **修复方案附带代码** — 每条修复建议给出可运行的代码示例,用 ` ```python ` 代码块
+5. **教方法而不是给答案** — 解释为什么会出现这个错误,让用户学会自己排查
+
+**推荐输出结构:**
+```
+## 错误分析
+(解析错误信息,告诉用户关键信息在哪里)
+
+## 根因定位
+(指出问题出在哪段代码、什么逻辑上)
+
+## 修复方案
+```python
+# 修改后的代码
+```
+
+## 预防措施
+(如何避免类似问题再次发生)
+```"""
+
+
+class DebugAgent:
+    """调试助手Agent"""
+    
+    def __init__(self):
+        self.llm = get_llm()
+        self.agent = SimpleAgent(
+            name="调试助手",
+            llm=self.llm,
+            system_prompt=DEBUG_PROMPT,
+        )
+    
+    def chat(self, message: str, context=None) -> str:
+        """与调试助手对话"""
+        return self.agent.run(message)
+
+
+# 全局实例
+_debug_agent = None
+
+
+def get_debug_agent() -> DebugAgent:
+    """获取Debug Agent实例(单例)"""
+    global _debug_agent
+    if _debug_agent is None:
+        _debug_agent = DebugAgent()
+    return _debug_agent

+ 93 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/orchestrator.py

@@ -0,0 +1,93 @@
+"""总控Agent - 负责路由和状态管理"""
+
+from typing import Dict, Any, Optional
+from hello_agents import SimpleAgent
+from ..services.llm_service import get_llm
+from .tutor_agent import TutorAgent
+from .debug_agent import DebugAgent
+from .review_agent import ReviewAgent
+from .arch_agent import ArchAgent
+from .coach_agent import CoachAgent
+
+
+ORCHESTRATOR_PROMPT = """你是一个智能路由系统。你的任务是分析用户输入,决定应该交给哪个Agent处理。
+
+**可用的Agent:**
+1. tutor - 编程导师:回答编程概念、解释代码、提供学习建议
+2. debug - 调试助手:分析错误信息、帮助修复代码bug
+3. review - 代码审查员:审查代码质量、发现潜在问题、提供优化建议
+4. arch - 架构师:设计系统架构、技术选型、解决架构问题
+5. coach - 学习教练:规划学习路径、跟踪进度、提供学习建议
+
+**判断规则:**
+- 如果用户在问学习路径、课程推荐、学习计划、进度相关 → 选择 coach
+- 如果用户在问编程概念、原理、怎么用 → 选择 tutor
+- 如果用户在报告错误、贴了报错信息、代码不工作 → 选择 debug
+- 如果用户贴了代码想让帮忙看看、想优化代码 → 选择 review
+- 如果用户在问系统设计、架构、技术选型 → 选择 arch
+- 如果用户输入模棱两可,同时涉及多个领域 → 选择最匹配核心意图的那个,不要选 tutor 作为默认兜底
+
+**关键规则:**
+- 用户问"这段代码有什么问题"→ 优先 debug(检查是否报错),不是 review
+- 用户问"帮我写个XX功能"→ 优先 tutor(指导怎么写),不是 review
+- 用户问"设计一个XX系统"→ 优先 arch,不是 tutor
+- 只有完全无法判断时才用 tutor 兜底
+
+**输出格式(只输出一个词):**
+tutor 或 debug 或 review 或 arch 或 coach
+"""
+
+
+class Orchestrator:
+    """总控Agent"""
+    
+    def __init__(self):
+        self.llm = get_llm()
+        
+        # 创建路由Agent
+        self.router = SimpleAgent(
+            name="路由器",
+            llm=self.llm,
+            system_prompt=ORCHESTRATOR_PROMPT,
+        )
+        
+        # 创建子Agent
+        self.agents = {
+            "tutor": TutorAgent(),
+            "debug": DebugAgent(),
+            "review": ReviewAgent(),
+            "arch": ArchAgent(),
+            "coach": CoachAgent(),
+        }
+        
+        print("Orchestrator初始化完成,已加载Agent:", list(self.agents.keys()))
+    
+    def route(self, user_input: str, context: Optional[Dict[str, Any]] = None) -> str:
+        """路由用户输入到合适的Agent"""
+        # 让LLM判断应该路由到哪个Agent
+        router_response = self.router.run(
+            f"用户输入:{user_input}\n\n请判断应该交给哪个Agent处理。"
+        )
+        
+        # 解析路由结果
+        agent_name = router_response.strip().lower()
+        if agent_name not in self.agents:
+            agent_name = "tutor"  # 默认使用tutor
+        
+        print(f"路由结果: {agent_name}")
+        
+        # 调用对应的Agent(传递上下文)
+        agent = self.agents[agent_name]
+        return agent.chat(user_input, context=context), agent_name
+
+
+# 全局实例
+_orchestrator = None
+
+
+def get_orchestrator() -> Orchestrator:
+    """获取Orchestrator实例(单例)"""
+    global _orchestrator
+    if _orchestrator is None:
+        _orchestrator = Orchestrator()
+    return _orchestrator

+ 116 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/review_agent.py

@@ -0,0 +1,116 @@
+"""代码审查员Agent"""
+
+from hello_agents import SimpleAgent
+from ..services.llm_service import get_llm
+
+
+REVIEW_PROMPT = """你是一位资深的代码审查专家。你的任务是帮助用户审查代码质量。
+
+**你的职责:**
+1. 检查代码风格和规范
+2. 发现潜在的bug和问题
+3. 提供优化建议
+4. 评估代码的可读性和可维护性
+
+**审查维度:**
+1. **代码风格** - 命名规范、缩进、注释
+2. **逻辑正确性** - 算法是否正确、边界情况处理
+3. **性能** - 是否有性能隐患、可优化的地方
+4. **安全性** - 是否有安全漏洞
+5. **可维护性** - 代码结构是否清晰、易于修改
+
+**输出质量守则:**
+1. **结构化输出** — 用 `##` 标题组织:总体评价 → 严重问题 → 优化建议
+2. **评分附理由** — 给出 1-10 分时必须写明扣分点,不能只有分数
+3. **问题要具体** — 明确指出问题所在的代码行和原因,而不是只说"代码风格不好"
+4. **建议可操作** — 每条优化建议附带具体的修改示例
+5. **先肯定后批评** — 每个问题之前先指出代码中做得好的一面
+
+**推荐输出结构:**
+```
+## 总体评价
+评分:X/10
+(一句话概括代码质量)
+
+## 严重问题
+按严重程度列出,每个问题包含:问题描述 → 影响 → 修改建议
+
+## 优化建议
+每条建议附带具体的代码示例
+
+## 值得肯定的地方
+```"""
+
+EXERCISE_REVIEW_PROMPT = """你是一位编程练习导师,你的任务是针对用户提交的练习代码给出教学性反馈。
+
+**你的职责:**
+1. 判断代码是否正确地完成了练习任务(基于代码逻辑和预期目标)
+2. 指出代码中做得好和可以改进的地方
+3. 给出学习建议,帮助用户理解相关概念
+4. 鼓励用户,保持学习动力
+
+**输出质量守则:**
+1. 先肯定用户做得好的地方
+2. 再指出可以改进的地方(如果有)
+3. 如果代码有错误,解释原因以及如何修复
+4. 判断代码是否执行通过、输出是否合理(如有提供执行结果)
+5. 输出简短精炼(100-200字左右),不要过长
+6. 使用 `##` 标题组织输出,便于阅读
+
+**推荐输出结构:**
+```
+## 做得好
+(用户代码中的亮点)
+
+## 改进建议
+(需要改进的地方,如果没有则写"代码看起来不错!")
+
+## 学习建议
+(针对练习主题的学习建议)
+```"""
+
+
+class ReviewAgent:
+    """代码审查员Agent"""
+    
+    def __init__(self):
+        self.llm = get_llm()
+        self.agent = SimpleAgent(
+            name="代码审查员",
+            llm=self.llm,
+            system_prompt=REVIEW_PROMPT,
+        )
+        self.exercise_agent = SimpleAgent(
+            name="练习反馈",
+            llm=self.llm,
+            system_prompt=EXERCISE_REVIEW_PROMPT,
+        )
+    
+    def chat(self, message: str, context=None) -> str:
+        """审查代码"""
+        return self.agent.run(message)
+    
+    def review_exercise(self, code: str, context: dict = None) -> str:
+        """对练习代码给出教学性反馈"""
+        prompt = f"用户提交的练习代码:\n\n```python\n{code}\n```\n\n"
+        if context:
+            if context.get("output"):
+                prompt += f"执行输出:\n{context['output']}\n\n"
+            if context.get("error"):
+                prompt += f"执行错误:\n{context['error']}\n\n"
+            if context.get("lesson_id"):
+                prompt += f"关联课程:{context['lesson_id']}\n"
+        prompt += "\n请对这段练习代码给出教学性反馈。"
+        return self.exercise_agent.run(prompt)
+
+
+# 全局实例
+_review_agent = None
+
+
+def get_review_agent() -> ReviewAgent:
+    """获取Review Agent实例(单例)"""
+    global _review_agent
+    if _review_agent is None:
+        _review_agent = ReviewAgent()
+    return _review_agent

+ 241 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/agents/tutor_agent.py

@@ -0,0 +1,241 @@
+"""编程导师Agent"""
+
+from typing import Dict, Any, Optional
+from hello_agents import SimpleAgent, HelloAgentsLLM
+from ..services.llm_service import get_llm
+
+
+TUTOR_PROMPT = """你是一位经验丰富的编程导师。你的任务是帮助用户学习编程。
+
+**你的职责:**
+1. 用简单易懂的语言解释编程概念
+2. 提供清晰的代码示例
+3. 回答用户的编程问题
+4. 鼓励用户动手实践
+5. **在教学过程中嵌入交互式测验,让学习更有趣**
+
+**交流风格:**
+- 耐心、友好
+- 循序渐进,由浅入深
+- 多用比喻帮助理解
+- 适时给出练习建议
+
+**回复格式要求:**
+- **结构化内容请使用 `##` 二级标题来分割主要章节**(例如 `## 模块一:变量与数据类型`)
+- 这样平台会自动将每个章节渲染为可折叠的卡片,用户可以按需展开/收起
+- 子标题用 `###` 三级标题
+- 代码块使用 ` ```python ` 格式(会自动显示"运行"按钮)
+
+**内联测验格式:**
+你可以在回复中嵌入交互式测验题目,使用以下 JSON 格式(用 ```quiz 包裹):
+
+```quiz
+{
+  "question": "在 Vue 3 中,以下哪个选项可以创建响应式数据?",
+  "options": ["A. let x = 1", "B. ref(1)", "C. Number(1)", "D. String(1)"],
+  "correct": 1,
+  "explanation": "ref() 是 Vue 3 的响应式 API,用于创建基本类型的响应式数据。"
+}
+```
+
+**必填字段说明:**
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `question` | 字符串 | 题目内容,必须明确完整 |
+| `options` | 字符串数组 | 恰好 4 个选项,每个以 `A.` `B.` `C.` `D.` 开头 |
+| `correct` | 整数(不能加引号) | 正确答案的索引(从 0 开始),例如第 2 个选项正确则填 `1`,第 3 个正确则填 `2` |
+| `explanation` | 字符串 | 详细的答案解析(无论对错都显示) |
+| `code` | 字符串(可选) | 代码预测题的完整代码,会显示在题目上方 |
+
+**概念题示例(不含 code):**
+```quiz
+{
+  "question": "在 JavaScript 中,以下哪个关键字用于声明常量?",
+  "options": ["A. var", "B. let", "C. const", "D. static"],
+  "correct": 2,
+  "explanation": "const 用于声明常量,一旦赋值不能重新赋值。"
+}
+```
+
+**代码预测题示例(带 code 字段):**
+```quiz
+{
+  "question": "以上代码执行后,console.log 的输出顺序是什么?",
+  "code": "console.log(1);\nsetTimeout(() => console.log(2), 0);\nconsole.log(3);",
+  "options": ["A. 1 2 3", "B. 1 3 2", "C. 3 2 1", "D. 2 1 3"],
+  "correct": 1,
+  "explanation": "setTimeout 是宏任务,会在当前同步代码执行完后才执行..."
+}
+```
+
+**`correct` 值计算规则(非常重要):**
+1. 先写出 `options` 数组
+2. 在数组中找到正确答案是第几个元素(从 0 开始数)
+3. 将该数字作为 `correct` 的值
+4. 例如:`options` 中第三个选项是正确答案 → `"correct": 2`
+5. 确保 `correct` 的值是数字而非字符串(不加引号)
+
+**自查清单(每个 quiz 生成后必须检查):**
+- [ ] JSON 语法正确(无缺逗号、多余逗号)
+- [ ] `question` 不为空且有明确问题
+- [ ] `options` 恰好有 4 个选项,每条以 `A.` `B.` `C.` `D.` 开头
+- [ ] `correct` 是 0~3 的整数,并且**与 options 数组中的正确答案位置一致**
+- [ ] `explanation` 详细且能独立理解(不依赖外部上下文)
+- [ ] 代码预测题必须包含 `code` 字段,且代码完整可运行
+
+**可执行代码:**
+你在回复中提供的 Python 代码块会自动显示"运行"按钮,用户可以点击执行。善用这个功能:
+
+```python
+# 用户可以直接点击"运行"来执行这段代码
+print("Hello, World!")
+```
+
+建议:
+- 对于可以独立运行的代码示例,使用 ```python 代码块
+- 对于仅作演示用途的代码片段,加注释说明
+- 代码不要太长(建议不超过30行),否则运行体验不好
+- 确保代码可以独立运行,不依赖外部输入
+```
+
+**使用场景:**
+- 讲解完一个知识点后,出一道题检验理解
+- 学习新概念前,出一道先导题激发思考
+- 让用户预测代码输出(带 `code` 字段)
+- 出找bug题,给一段有问题的代码让用户选择错误
+
+**要求:**
+- `correct` 必须是 0~3 的整数索引(不是字母 A/B/C/D)
+- 每段回复最多 1-2 道题,不要太多
+- 题目难度要匹配当前讲解的内容
+- 解析要详细,让用户即使答错也能学到东西
+- **每个 quiz JSON 必须通过上述自查清单**
+
+**下一步学习建议:**
+在课程内容的结尾(出测验题之前),如果有 `[下一课程]` 信息,请用**具体、可操作**的引导:
+- 直接告诉用户下一节课的具体名称和内容
+- 例如:"下一节是《Flexbox布局》,你会学到弹性盒子的完整用法"
+- 不要笼统地说"继续学习更多技术"或列举大方向
+- 如果没有 `[下一课程]` 信息,才使用通用的学习建议
+
+**内联代码练习题格式:**
+你可以使用以下格式生成代码练习题(用户可以在右侧编辑器中补全代码并提交批改):
+
+```exercise:python
+# 题目:编写一个函数计算斐波那契数列的第 n 项
+# 请补全下面的代码:
+
+def fibonacci(n):
+    # 在这里写你的代码
+    pass
+
+# 完成后点击右侧「提交练习反馈」获取批改
+```
+
+**代码练习题指引(重要):**
+生成代码练习题时,请在题目描述中明确告诉用户操作步骤:
+1. 点击「📂 在编辑器中打开」按钮
+2. 在右侧编辑器中补全代码
+3. 点击「📝 提交练习反馈」获取 AI 批改和优化建议
+**不要**写"告诉我你的答案"、"在代码注释中填空"等与实际操作不符的指引。
+
+**测验生成指令:**
+当用户说"已完成学习"或"出测验题"时,请生成 2-3 道题目:
+- 至少 1 道选择题(使用 ```quiz 格式)
+- 至少 1 道代码练习题(使用 ```exercise:语言 格式,让用户在编辑器中补全代码)
+- 也可出代码预测题(使用带 `code` 字段的 ```quiz 格式)
+- 题目难度匹配用户当前水平
+- 选择题的 correct 用索引(从 0 开始)
+- **每个 quiz 都必须通过自查清单,不合格的不要输出**
+
+**当前状态:**
+用户正在学习编程,希望从基础进阶到软件工程师水平。
+"""
+
+
+class TutorAgent:
+    """编程导师Agent"""
+    
+    def __init__(self):
+        self.llm = get_llm()
+        self.agent = SimpleAgent(
+            name="编程导师",
+            llm=self.llm,
+            system_prompt=TUTOR_PROMPT,
+        )
+    
+    def chat(self, message: str, context: Optional[Dict[str, Any]] = None) -> str:
+        """与导师对话"""
+        # 如果有上下文,将上下文信息添加到消息中
+        if context:
+            enhanced_message = self._enhance_message_with_context(message, context)
+            return self.agent.run(enhanced_message)
+        return self.agent.run(message)
+    
+    def _enhance_message_with_context(self, message: str, context: Dict[str, Any]) -> str:
+        """用上下文信息增强用户消息"""
+        context_parts = []
+        
+        # 用户水平信息
+        user_level = context.get("user_level")
+        if user_level:
+            level_text = {
+                "beginner": "入门",
+                "intermediate": "中级",
+                "advanced": "高级"
+            }.get(user_level, user_level)
+            context_parts.append(f"[用户水平: {level_text}]")
+        
+        # 技能掌握情况
+        skill_levels = context.get("skill_levels")
+        if skill_levels:
+            skills_text = ", ".join([
+                f"{k}: {v}%" for k, v in skill_levels.items()
+            ])
+            context_parts.append(f"[技能掌握: {skills_text}]")
+        
+        # 当前课程信息
+        lesson_title = context.get("lesson_title")
+        module_title = context.get("module_title")
+        if lesson_title:
+            context_parts.append(f"[当前课程: {module_title} > {lesson_title}]")
+        
+        # 推荐模块
+        recommended_module = context.get("recommended_module")
+        if recommended_module:
+            context_parts.append(f"[推荐从模块 {recommended_module} 开始]")
+        
+        # 下一课程信息
+        next_lesson = context.get("next_lesson")
+        if next_lesson:
+            context_parts.append(
+                f"[下一课程: {next_lesson['title']} - {next_lesson['description']}]"
+            )
+        
+        # 学习路径
+        path_type = context.get("path_type")
+        if path_type:
+            path_text = {
+                "frontend": "前端开发",
+                "backend": "后端开发",
+                "fullstack": "全栈开发"
+            }.get(path_type, path_type)
+            context_parts.append(f"[学习方向: {path_text}]")
+        
+        if context_parts:
+            context_str = " ".join(context_parts)
+            return f"{context_str}\n\n{message}"
+        
+        return message
+
+
+# 全局实例
+_tutor_agent = None
+
+
+def get_tutor_agent() -> TutorAgent:
+    """获取Tutor Agent实例(单例)"""
+    global _tutor_agent
+    if _tutor_agent is None:
+        _tutor_agent = TutorAgent()
+    return _tutor_agent

+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/__init__.py


+ 36 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/main.py

@@ -0,0 +1,36 @@
+"""FastAPI主应用"""
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from ..config import get_settings
+from .routes import chat, code, learning, assessment, auth, gamification, settings as settings_router
+
+app = FastAPI(
+    title="Way_to_Engineer API",
+    description="AI辅助编程学习平台",
+    version="0.1.0",
+)
+
+# CORS配置
+settings = get_settings()
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=settings.cors_origins.split(","),
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# 注册路由
+app.include_router(chat.router, prefix="/api")
+app.include_router(code.router, prefix="/api")
+app.include_router(learning.router)
+app.include_router(assessment.router, prefix="/api")
+app.include_router(auth.router, prefix="/api")
+app.include_router(gamification.router)
+app.include_router(settings_router.router, prefix="/api")
+
+
+@app.get("/")
+async def root():
+    return {"message": "Way_to_Engineer API is running"}

+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/__init__.py


+ 83 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/assessment.py

@@ -0,0 +1,83 @@
+"""
+水平检测API路由
+"""
+from fastapi import APIRouter, HTTPException, Query
+from typing import Optional
+
+from ...models.learning import (
+    AssessmentStartRequest, AssessmentAnswerRequest,
+    AssessmentCompleteRequest, AssessmentStartResponse,
+    AssessmentAnswerResponse, AssessmentResult
+)
+from ...services.assessment_service import (
+    start_assessment, submit_answer, complete_assessment
+)
+from ...services.data_store import data_store
+
+router = APIRouter(prefix="/assessment", tags=["assessment"])
+
+
+@router.get("/check/{path_type}")
+async def check_assessment(path_type: str, user_id: str = "default"):
+    """检查用户是否已测试过指定路径"""
+    has_test = data_store.has_assessment(user_id, path_type)
+    current = data_store.get_current_assessment(user_id, path_type)
+
+    return {
+        "has_assessment": has_test,
+        "current_result": current,
+    }
+
+
+@router.get("/result/{path_type}")
+async def get_assessment_result(path_type: str, user_id: str = "default"):
+    """获取用户测试结果"""
+    result = data_store.get_current_assessment(user_id, path_type)
+    if not result:
+        raise HTTPException(status_code=404, detail="未找到测试结果")
+    return result
+
+
+@router.get("/history/{path_type}")
+async def get_assessment_history(path_type: str, user_id: str = "default"):
+    """获取用户测试历史"""
+    assessments = data_store.get_user_assessments(user_id, path_type)
+    return {"assessments": assessments}
+
+
+@router.post("/start")
+async def api_start_assessment(request: AssessmentStartRequest):
+    """开始测试"""
+    result = start_assessment(request.path_type, user_id=request.user_id)
+    return result
+
+
+@router.post("/answer")
+async def api_submit_answer(request: AssessmentAnswerRequest):
+    """提交答案"""
+    result = submit_answer(
+        request.session_id,
+        request.question_id,
+        request.answer,
+        user_id=request.user_id,
+    )
+    if "error" in result:
+        raise HTTPException(status_code=400, detail=result["error"])
+    return result
+
+
+@router.post("/complete")
+async def api_complete_assessment(request: AssessmentCompleteRequest):
+    """完成测试"""
+    result = complete_assessment(request.session_id, user_id=request.user_id)
+    if not result:
+        raise HTTPException(status_code=400, detail="会话不存在或已过期")
+    return result
+
+
+@router.post("/save-result")
+async def save_assessment_result(result: AssessmentResult, user_id: str = "default"):
+    """保存测试结果"""
+    result.user_id = user_id
+    data_store.save_assessment(result)
+    return {"message": "保存成功", "result": result}

+ 86 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/auth.py

@@ -0,0 +1,86 @@
+"""
+用户认证API路由
+轻量级用户管理:无密码,仅用户名 + 重名检测
+"""
+from fastapi import APIRouter, HTTPException
+from pydantic import BaseModel
+from ...services.data_store import data_store
+
+router = APIRouter(prefix="/auth", tags=["auth"])
+
+
+class LoginRequest(BaseModel):
+    """登录请求"""
+    username: str
+
+
+class LoginResponse(BaseModel):
+    """登录响应"""
+    username: str
+    is_new_user: bool
+    has_existing_data: bool
+    message: str
+
+
+@router.post("/check")
+async def check_username(request: LoginRequest):
+    """检查用户名是否已存在"""
+    progress = data_store.get_user_progress(request.username)
+    assessments = data_store.get_user_assessments(request.username)
+    
+    exists = progress is not None
+    has_data = exists and (
+        len(assessments) > 0
+        or len(progress.completed_lessons) > 0
+        or progress.current_path is not None
+    )
+    
+    return {
+        "username": request.username,
+        "exists": exists,
+        "has_data": has_data,
+    }
+
+
+@router.post("/login", response_model=LoginResponse)
+async def login(request: LoginRequest):
+    """登录/注册"""
+    username = request.username.strip()
+    if not username:
+        raise HTTPException(status_code=400, detail="用户名不能为空")
+    
+    progress = data_store.get_user_progress(username)
+    assessments = data_store.get_user_assessments(username)
+    
+    is_new_user = progress is None
+    has_existing_data = False
+    
+    if is_new_user:
+        # 新用户:自动创建进度记录
+        from datetime import datetime
+        from ...models.learning import UserProgress
+        progress = UserProgress(
+            user_id=username,
+            started_at=datetime.now(),
+            last_activity_at=datetime.now(),
+        )
+        data_store.save_user_progress(progress)
+        message = f"欢迎新用户 {username}!"
+    else:
+        # 老用户:检查是否有学习数据
+        has_existing_data = (
+            len(assessments) > 0
+            or len(progress.completed_lessons) > 0
+            or progress.current_path is not None
+        )
+        if has_existing_data:
+            message = f"欢迎回来,{username}!将继续你的学习进度。"
+        else:
+            message = f"欢迎回来,{username}!"
+    
+    return LoginResponse(
+        username=username,
+        is_new_user=is_new_user,
+        has_existing_data=has_existing_data,
+        message=message,
+    )

+ 65 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/chat.py

@@ -0,0 +1,65 @@
+"""聊天API"""
+
+from fastapi import APIRouter, Query
+from ...models.schemas import ChatRequest, ChatResponse
+from ...agents.orchestrator import get_orchestrator
+from ...services.data_store import data_store
+from ...services.learning_content import find_next_lesson
+from ...models.learning import UserProgress
+from datetime import datetime
+import uuid
+
+router = APIRouter(prefix="/chat", tags=["聊天"])
+
+
+@router.post("/", response_model=ChatResponse)
+async def chat(request: ChatRequest, user_id: str = Query("default")):
+    """与AI助手对话(自动路由到合适的Agent)"""
+    orchestrator = get_orchestrator()
+    
+    # 生成会话ID
+    conversation_id = request.conversation_id or str(uuid.uuid4())
+    
+    # 注入下一课程信息到上下文(让AI给出具体推荐)
+    context = request.context
+    if context and context.get("lesson_id") and context.get("path_type"):
+        progress = data_store.get_user_progress(user_id)
+        completed = progress.completed_lessons if progress else []
+        next_lesson = find_next_lesson(
+            path_type=context["path_type"],
+            current_lesson_id=context["lesson_id"],
+            completed_lessons=completed,
+        )
+        if next_lesson:
+            context["next_lesson"] = next_lesson
+    
+    # 路由到合适的Agent并获取回复(传递上下文)
+    reply, agent_name = orchestrator.route(request.message, context=context)
+    
+    # 如果是教练回应,保存为学习计划到用户进度
+    if agent_name == "coach":
+        progress = data_store.get_user_progress(user_id)
+        if not progress:
+            progress = UserProgress(
+                user_id=user_id,
+                started_at=datetime.now(),
+                last_activity_at=datetime.now()
+            )
+        progress.learning_plan = reply
+        progress.last_activity_at = datetime.now()
+        data_store.save_user_progress(progress)
+    
+    # 映射agent名称到中文
+    agent_display_name = {
+        "tutor": "编程导师",
+        "debug": "调试助手",
+        "review": "代码审查员",
+        "arch": "架构师",
+        "coach": "学习教练",
+    }.get(agent_name, "编程导师")
+    
+    return ChatResponse(
+        reply=reply,
+        conversation_id=conversation_id,
+        agent_name=agent_display_name,
+    )

+ 101 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/code.py

@@ -0,0 +1,101 @@
+"""代码执行API"""
+
+import uuid
+from datetime import datetime
+from typing import Optional
+
+from fastapi import APIRouter
+from pydantic import BaseModel
+from ...services.code_executor import get_executor
+from ...agents.review_agent import get_review_agent
+from ...services.data_store import data_store
+from ...models.learning import CodeSubmission
+
+router = APIRouter(prefix="/code", tags=["代码执行"])
+
+
+class CodeRequest(BaseModel):
+    """代码执行请求"""
+    code: str
+    language: str = "python"
+
+
+class CodeResponse(BaseModel):
+    """代码执行响应"""
+    success: bool
+    output: str
+    error: str
+    exit_code: int
+
+
+class CodeSubmitRequest(BaseModel):
+    """代码提交审查请求"""
+    code: str
+    language: str = "python"
+    output: str = ""
+    error: str = ""
+    success: bool = True
+    exit_code: int = 0
+    user_id: str = "default"
+    lesson_id: Optional[str] = None
+
+
+class CodeSubmitResponse(BaseModel):
+    """代码提交审查响应"""
+    feedback: str
+    submission_id: str
+    created_at: str
+
+
+@router.post("/execute", response_model=CodeResponse)
+async def execute_code(request: CodeRequest):
+    """执行代码"""
+    executor = get_executor()
+    result = executor.execute(request.code, request.language)
+    
+    return CodeResponse(
+        success=result["success"],
+        output=result["output"],
+        error=result["error"],
+        exit_code=result["exit_code"],
+    )
+
+
+@router.post("/submit", response_model=CodeSubmitResponse)
+async def submit_code(request: CodeSubmitRequest):
+    """提交代码进行AI审查"""
+    # 调用 ReviewAgent 进行练习反馈
+    review_agent = get_review_agent()
+    feedback = review_agent.review_exercise(request.code, context={
+        "output": request.output,
+        "error": request.error,
+        "success": request.success,
+        "exit_code": request.exit_code,
+        "lesson_id": request.lesson_id,
+    })
+    
+    # 构建提交记录
+    submission_id = str(uuid.uuid4())
+    created_at = datetime.now()
+    submission = CodeSubmission(
+        id=submission_id,
+        user_id=request.user_id,
+        code=request.code,
+        language=request.language,
+        output=request.output,
+        error=request.error,
+        success=request.success,
+        exit_code=request.exit_code,
+        lesson_id=request.lesson_id,
+        feedback=feedback,
+        created_at=created_at,
+    )
+    
+    # 持久化
+    data_store.save_submission(submission)
+    
+    return CodeSubmitResponse(
+        feedback=feedback,
+        submission_id=submission_id,
+        created_at=created_at.isoformat(),
+    )

+ 22 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/gamification.py

@@ -0,0 +1,22 @@
+"""
+游戏化API路由 - XP、等级、徽章
+"""
+from fastapi import APIRouter, Query
+from ...services.gamification_service import get_gamification_service
+
+router = APIRouter(prefix="/api/gamification", tags=["gamification"])
+
+
+@router.get("/profile")
+async def get_gamification_profile(user_id: str = Query("default")):
+    """获取用户游戏化档案"""
+    service = get_gamification_service()
+    profile = service.get_profile(user_id)
+    return profile
+
+
+@router.get("/badges")
+async def get_badge_definitions():
+    """获取所有徽章定义"""
+    from ...services.gamification_service import BADGE_DEFINITIONS
+    return {"badges": list(BADGE_DEFINITIONS.values())}

+ 323 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/learning.py

@@ -0,0 +1,323 @@
+from fastapi import APIRouter, HTTPException, Query
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from ...models.learning import (
+    LearningPath, UserProgress, LearningPathData, CoachResponse,
+    CoachRecommendation, ModuleStatus
+)
+from ...services.learning_content import get_learning_path, get_all_paths, find_next_lesson
+from ...services.data_store import data_store
+from ...services.gamification_service import get_gamification_service
+
+router = APIRouter(prefix="/api/learning", tags=["learning"])
+
+
+def get_user_progress(user_id: str = "default") -> UserProgress:
+    """获取用户进度,使用data_store持久化"""
+    progress = data_store.get_user_progress(user_id)
+    if not progress:
+        progress = UserProgress(
+            user_id=user_id,
+            started_at=datetime.now(),
+            last_activity_at=datetime.now()
+        )
+        data_store.save_user_progress(progress)
+    return progress
+
+
+def save_user_progress(progress: UserProgress):
+    """保存用户进度"""
+    progress.last_activity_at = datetime.now()
+    data_store.save_user_progress(progress)
+
+
+@router.get("/paths")
+async def list_learning_paths(user_id: str = Query("default")):
+    """获取所有学习路径概览"""
+    paths = get_all_paths()
+    return {
+        "paths": [
+            {
+                "path": p.path.value,
+                "title": p.title,
+                "description": p.description,
+                "icon": p.icon,
+                "total_modules": len(p.modules),
+                "total_lessons": sum(len(m.lessons) for m in p.modules)
+            }
+            for p in paths
+        ]
+    }
+
+
+@router.get("/paths/{path_type}")
+async def get_learning_path_detail(path_type: LearningPath, user_id: str = Query("default")):
+    """获取指定学习路径详情"""
+    path_data = get_learning_path(path_type)
+    progress = get_user_progress(user_id)
+    
+    # 根据用户进度更新模块状态
+    for module in path_data.modules:
+        module_progress = calculate_module_progress(module.id, progress)
+        module.progress = module_progress
+        
+        if module_progress >= 100:
+            module.status = ModuleStatus.COMPLETED
+        elif module_progress > 0:
+            module.status = ModuleStatus.IN_PROGRESS
+        elif is_module_unlocked(module.order, progress):
+            module.status = ModuleStatus.NOT_STARTED
+        else:
+            module.status = ModuleStatus.LOCKED
+    
+    # 计算总进度
+    total_lessons = sum(len(m.lessons) for m in path_data.modules)
+    completed_lessons = sum(
+        len([l for l in m.lessons if l.id in progress.completed_lessons])
+        for m in path_data.modules
+    )
+    path_data.total_lessons = total_lessons
+    path_data.completed_lessons = completed_lessons
+    path_data.progress = (completed_lessons / total_lessons * 100) if total_lessons > 0 else 0
+    
+    return path_data
+
+
+@router.get("/progress")
+async def get_progress(user_id: str = Query("default")):
+    """获取用户学习进度"""
+    progress = get_user_progress(user_id)
+    return progress
+
+
+@router.post("/select-path/{path_type}")
+async def select_learning_path(path_type: LearningPath, user_id: str = Query("default")):
+    """选择学习路径"""
+    progress = get_user_progress(user_id)
+    progress.current_path = path_type
+    
+    # 获取路径数据,设置第一个模块为当前
+    path_data = get_learning_path(path_type)
+    if path_data.modules:
+        progress.current_module = path_data.modules[0].id
+    
+    save_user_progress(progress)
+    return {"message": f"已选择{path_type.value}路径", "progress": progress}
+
+
+@router.post("/complete-lesson/{lesson_id}")
+async def complete_lesson(lesson_id: str, user_id: str = Query("default")):
+    """标记课程完成"""
+    progress = get_user_progress(user_id)
+    is_new = False
+    
+    if lesson_id not in progress.completed_lessons:
+        progress.completed_lessons.append(lesson_id)
+        is_new = True
+    
+    # 检查是否完成整个模块
+    path_data = get_learning_path(progress.current_path) if progress.current_path else None
+    if path_data:
+        for module in path_data.modules:
+            lesson_ids = [l.id for l in module.lessons]
+            if lesson_id in lesson_ids:
+                if all(lid in progress.completed_lessons for lid in lesson_ids):
+                    if module.id not in progress.completed_modules:
+                        progress.completed_modules.append(module.id)
+                break
+    
+    save_user_progress(progress)
+    
+    # 发放XP奖励
+    xp_awarded = 0
+    new_badges = []
+    if is_new:
+        svc = get_gamification_service()
+        # 基础XP:每节课10XP
+        xp_awarded = 10
+        profile, new_badges = svc.award_xp(user_id, xp_awarded, f"完成课程: {lesson_id}")
+    
+    # 查找下一课程(用于前端自动跳转)
+    next_lesson = None
+    if progress.current_path and is_new:
+        next_lesson = find_next_lesson(
+            path_type=progress.current_path.value,
+            current_lesson_id=lesson_id,
+            completed_lessons=progress.completed_lessons,
+        )
+    
+    return {
+        "message": "课程已标记完成",
+        "progress": progress,
+        "xp_awarded": xp_awarded,
+        "new_badges": new_badges,
+        "next_lesson": next_lesson,
+    }
+
+
+def calculate_module_progress(module_id: str, progress: UserProgress) -> float:
+    """计算模块进度"""
+    path_data = get_learning_path(progress.current_path) if progress.current_path else None
+    if not path_data:
+        return 0.0
+    
+    for module in path_data.modules:
+        if module.id == module_id:
+            total = len(module.lessons)
+            if total == 0:
+                return 0.0
+            completed = sum(1 for l in module.lessons if l.id in progress.completed_lessons)
+            return (completed / total) * 100
+    
+    return 0.0
+
+
+def is_module_unlocked(module_order: int, progress: UserProgress) -> bool:
+    """检查模块是否解锁"""
+    if module_order <= 1:
+        return True
+    
+    path_data = get_learning_path(progress.current_path) if progress.current_path else None
+    if not path_data:
+        return False
+    
+    # 找到前一个模块
+    prev_module = None
+    for m in path_data.modules:
+        if m.order == module_order - 1:
+            prev_module = m
+            break
+    
+    if prev_module:
+        return prev_module.id in progress.completed_modules
+    
+    return False
+
+
+@router.get("/coach")
+async def get_coach_recommendations(user_id: str = Query("default")):
+    """获取学习教练推荐"""
+    progress = get_user_progress(user_id)
+    
+    if not progress.current_path:
+        return CoachResponse(
+            greeting="👋 你好!我是你的学习教练。",
+            recommendations=[
+                CoachRecommendation(
+                    type="select_path",
+                    title="选择学习路径",
+                    description="首先选择一个学习路径开始你的学习之旅",
+                    priority=5
+                )
+            ],
+            encouragement="每个人都有自己的学习节奏,加油!",
+            stats={"total_study_minutes": 0, "completed_lessons": 0},
+            learning_plan=progress.learning_plan  # 如果有AI生成的计划则带上
+        )
+    
+    path_data = get_learning_path(progress.current_path)
+    recommendations = []
+    
+    # 找到下一个未完成的课程
+    next_lesson = None
+    next_module = None
+    for module in path_data.modules:
+        if module.id in progress.completed_modules:
+            continue
+        for lesson in module.lessons:
+            if lesson.id not in progress.completed_lessons:
+                next_lesson = lesson
+                next_module = module
+                break
+        if next_lesson:
+            break
+    
+    if next_lesson and next_module:
+        recommendations.append(
+            CoachRecommendation(
+                type="next_lesson",
+                title=f"继续学习: {next_lesson.title}",
+                description=f"来自 {next_module.title} 模块",
+                module_id=next_module.id,
+                lesson_id=next_lesson.id,
+                priority=5
+            )
+        )
+    
+    # 如果有完成的模块,建议复习
+    if progress.completed_modules:
+        recommendations.append(
+            CoachRecommendation(
+                type="review",
+                title="复习已完成内容",
+                description="巩固已学知识,加深理解",
+                priority=3
+            )
+        )
+    
+    # 生成鼓励语
+    completed_count = len(progress.completed_lessons)
+    if completed_count == 0:
+        encouragement = "🌱 刚开始学习,每一步都是进步!"
+    elif completed_count < 5:
+        encouragement = "💪 开了个好头,继续努力!"
+    elif completed_count < 15:
+        encouragement = "🚀 学习势头很好,保持下去!"
+    else:
+        encouragement = "🌟 你已经学了很多,快要成为专家了!"
+    
+    return CoachResponse(
+        greeting=f"👋 你好!你正在学习 {path_data.title}。",
+        recommendations=recommendations,
+        encouragement=encouragement,
+        stats={
+            "total_study_minutes": progress.total_study_minutes,
+            "completed_lessons": completed_count,
+            "completed_modules": len(progress.completed_modules),
+            "current_path": progress.current_path.value if progress.current_path else None
+        },
+        learning_plan=progress.learning_plan
+    )
+
+
+@router.post("/ai-plan")
+async def save_ai_plan(data: dict, user_id: str = Query("default")):
+    """保存AI生成的个性化学习计划"""
+    plan_text = data.get("plan_text", "")
+    progress = get_user_progress(user_id)
+    progress.learning_plan = plan_text
+    save_user_progress(progress)
+    return {"message": "学习计划已保存"}
+
+
+class SessionUpdate(BaseModel):
+    lesson_id: Optional[str] = None
+    module_id: Optional[str] = None
+    lesson_title: Optional[str] = None
+    module_title: Optional[str] = None
+    path_type: Optional[str] = None
+    last_reply_summary: Optional[str] = None
+
+
+@router.get("/session")
+async def get_session(user_id: str = Query("default")):
+    """获取用户最近的会话数据"""
+    progress = get_user_progress(user_id)
+    return {"session": progress.last_session or None}
+
+
+@router.post("/session")
+async def save_session(update: SessionUpdate, user_id: str = Query("default"), clear: bool = Query(False)):
+    """保存/更新用户会话数据。clear=true 则清除会话"""
+    progress = get_user_progress(user_id)
+    if clear:
+        progress.last_session = None
+        save_user_progress(progress)
+        return {"message": "ok", "session": None}
+    if progress.last_session is None:
+        progress.last_session = {}
+    update_data = update.model_dump(exclude_unset=True)
+    progress.last_session.update(update_data)
+    save_user_progress(progress)
+    return {"message": "ok", "session": progress.last_session}

+ 82 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/api/routes/settings.py

@@ -0,0 +1,82 @@
+"""LLM配置API"""
+
+from fastapi import APIRouter
+from pydantic import BaseModel
+from typing import Optional
+from ...services.llm_service import get_llm_config, reload_llm, reset_llm_config
+
+router = APIRouter(prefix="/settings", tags=["设置"])
+
+
+class LLMConfigResponse(BaseModel):
+    """LLM配置响应"""
+    base_url: str
+    model_id: str
+    api_key: str  # 脱敏后返回
+
+
+class LLMConfigUpdateRequest(BaseModel):
+    """LLM配置更新请求"""
+    base_url: str
+    model_id: str
+    api_key: str
+
+
+class LLMConfigUpdateResponse(BaseModel):
+    """LLM配置更新响应"""
+    success: bool
+    message: str
+    config: LLMConfigResponse
+
+
+@router.get("/llm", response_model=LLMConfigResponse)
+async def get_llm_config_endpoint():
+    """获取当前LLM配置"""
+    config = get_llm_config()
+    return LLMConfigResponse(**config)
+
+
+@router.post("/llm", response_model=LLMConfigUpdateResponse)
+async def update_llm_config_endpoint(request: LLMConfigUpdateRequest):
+    """更新LLM配置并重新初始化"""
+    try:
+        reload_llm({
+            "base_url": request.base_url.rstrip("/"),
+            "model_id": request.model_id,
+            "api_key": request.api_key,
+        })
+        config = get_llm_config()
+        return LLMConfigUpdateResponse(
+            success=True,
+            message="LLM配置已更新",
+            config=LLMConfigResponse(**config),
+        )
+    except Exception as e:
+        return LLMConfigUpdateResponse(
+            success=False,
+            message=f"配置失败: {str(e)}",
+            config=LLMConfigResponse(
+                base_url="", model_id="", api_key=""
+            ),
+        )
+
+
+@router.post("/llm/reset", response_model=LLMConfigUpdateResponse)
+async def reset_llm_config_endpoint():
+    """恢复LLM配置到.env默认值"""
+    try:
+        reset_llm_config()
+        config = get_llm_config()
+        return LLMConfigUpdateResponse(
+            success=True,
+            message="已恢复为默认配置",
+            config=LLMConfigResponse(**config),
+        )
+    except Exception as e:
+        return LLMConfigUpdateResponse(
+            success=False,
+            message=f"重置失败: {str(e)}",
+            config=LLMConfigResponse(
+                base_url="", model_id="", api_key=""
+            ),
+        )

+ 84 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/config.py

@@ -0,0 +1,84 @@
+"""配置管理"""
+
+from pydantic_settings import BaseSettings
+from functools import lru_cache
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+class Settings(BaseSettings):
+    """应用配置"""
+    
+    # 应用基本配置
+    app_name: str = "Way_to_Engineer"
+    app_version: str = "1.0.0"
+    debug: bool = False
+    
+    # 服务器配置
+    host: str = "0.0.0.0"
+    port: int = 12000
+    
+    # CORS配置
+    cors_origins: str = "http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173,http://127.0.0.1:3000"
+
+    # DeepSeek API
+    deepseek_api_key: str = ""
+    deepseek_model_id: str = "deepseek-chat"
+    deepseek_base_url: str = "https://api.deepseek.com/v1"
+    
+    # 日志配置
+    log_level: str = "INFO"
+    
+    # LLM配置
+    llm_timeout: int = 60
+    
+    class Config:
+        env_file = ".env"
+        case_sensitive = False
+        env_file_encoding = "utf-8"
+        
+    def get_cors_origins_list(self) -> list[str]:
+        """获取CORS允许的源列表"""
+        return [origin.strip() for origin in self.cors_origins.split(",")]
+    
+# 创建全局配置实例
+settings = Settings()
+
+# 获取全局配置实例
+def get_settings() -> Settings:
+    return settings
+
+def validate_config():
+    """验证配置"""
+    warnings = []
+    
+    llm_api_key = os.getenv("DEEPSEEK_API_KEY")
+    if not llm_api_key:
+        warnings.append("LLM API Key未设置,将无法使用LLM功能")
+        
+    if warnings:
+        print("\n⚠️  配置警告:")
+        for w in warnings:
+            print(f"  - {w}")
+    
+    return True
+
+def print_config():
+    """打印配置"""
+    print(f"应用名称: {settings.app_name}")
+    print(f"版本: {settings.app_version}")
+    print(f"服务器: {settings.host}:{settings.port}")
+
+    # 检查LLM配置
+    llm_api_key = os.getenv("LLM_API_KEY") or os.getenv("DEEPSEEK_API_KEY")
+    llm_base_url = os.getenv("LLM_BASE_URL") or os.getenv("DEEPSEEK_BASE_URL") or settings.deepseek_base_url
+    llm_model = os.getenv("LLM_MODEL_ID") or os.getenv("DEEPSEEK_MODEL_ID")
+
+    print(f"LLM API Key: {'已配置' if llm_api_key else '未配置'}")
+    print(f"LLM Base URL: {llm_base_url}")
+    print(f"LLM Model: {llm_model}")
+    print(f"日志级别: {settings.log_level}")
+    
+if __name__ == "__main__":
+    print_config()

+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/models/__init__.py


+ 184 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/models/learning.py

@@ -0,0 +1,184 @@
+from pydantic import BaseModel
+from typing import Optional, List, Dict
+from enum import Enum
+from datetime import datetime
+
+
+class LearningPath(str, Enum):
+    FRONTEND = "frontend"
+    BACKEND = "backend"
+    FULLSTACK = "fullstack"
+
+
+class ModuleStatus(str, Enum):
+    NOT_STARTED = "not_started"
+    IN_PROGRESS = "in_progress"
+    COMPLETED = "completed"
+    LOCKED = "locked"
+
+
+class UserLevel(str, Enum):
+    BEGINNER = "beginner"
+    INTERMEDIATE = "intermediate"
+    ADVANCED = "advanced"
+
+
+class LearningModule(BaseModel):
+    id: str
+    title: str
+    description: str
+    icon: str
+    order: int
+    lessons: List["LearningLesson"]
+    status: ModuleStatus = ModuleStatus.NOT_STARTED
+    progress: float = 0.0  # 0-100
+
+
+class LearningLesson(BaseModel):
+    id: str
+    title: str
+    description: str
+    type: str  # "theory", "practice", "quiz", "project"
+    duration_minutes: int
+    is_completed: bool = False
+    content_markdown: Optional[str] = None  # Markdown格式的课程内容
+
+
+class LearningPathData(BaseModel):
+    path: LearningPath
+    title: str
+    description: str
+    icon: str
+    modules: List[LearningModule]
+    total_lessons: int = 0
+    completed_lessons: int = 0
+    progress: float = 0.0
+
+
+class AssessmentResult(BaseModel):
+    """水平检测结果"""
+    user_id: str = "default"
+    path_type: str  # LearningPath value
+    total_questions: int
+    correct_count: int
+    score: float  # 0-100
+    level: UserLevel
+    category_scores: Dict[str, float]  # 各分类得分 {"html_css": 80, "javascript": 60, ...}
+    recommended_start_module: str  # 推荐开始的模块ID
+    completed_at: datetime
+    is_current: bool = True  # 是否为当前有效结果
+
+
+class UserProgress(BaseModel):
+    user_id: str = "default"
+    current_path: Optional[LearningPath] = None
+    completed_modules: List[str] = []
+    completed_lessons: List[str] = []
+    current_module: Optional[str] = None
+    current_lesson: Optional[str] = None
+    started_at: Optional[datetime] = None
+    last_activity_at: Optional[datetime] = None
+    total_study_minutes: int = 0
+    assessments: List[AssessmentResult] = []  # 测试结果历史
+    skill_levels: Dict[str, float] = {}  # 各分类水平 0-100
+    learning_plan: Optional[str] = None  # AI生成的个性化学习计划
+    last_session: Optional[dict] = None  # 用户最近的会话数据
+
+
+class CodeSubmission(BaseModel):
+    """代码提交记录"""
+    id: str
+    user_id: str = "default"
+    code: str
+    language: str = "python"
+    output: str = ""
+    error: str = ""
+    success: bool = True
+    exit_code: int = 0
+    lesson_id: Optional[str] = None
+    feedback: str = ""
+    created_at: datetime
+
+
+class CoachRecommendation(BaseModel):
+    type: str  # "next_lesson", "review", "practice", "challenge"
+    title: str
+    description: str
+    module_id: Optional[str] = None
+    lesson_id: Optional[str] = None
+    priority: int = 1  # 1-5
+
+
+class GamificationProfile(BaseModel):
+    """游戏化个人档案"""
+    user_id: str = "default"
+    total_xp: int = 0
+    level: int = 1
+    streak: int = 0  # 连续学习天数
+    last_active_date: str = ""  # "YYYY-MM-DD"
+    badges: List[str] = []  # 已获得的徽章ID列表
+    xp_log: List[dict] = []  # XP变动记录 [{amount, reason, timestamp}]
+
+
+class CoachResponse(BaseModel):
+    greeting: str
+    recommendations: List[CoachRecommendation]
+    encouragement: str
+    stats: dict
+    learning_plan: Optional[str] = None  # AI生成的个性化学习计划
+
+
+# ===== 水平检测相关模型 =====
+
+class AssessmentQuestion(BaseModel):
+    """检测题目"""
+    id: str
+    category: str  # "html_css", "javascript", "python", "vue", "system_design"
+    difficulty: int  # 1-5
+    content: str
+    question_type: str = "choice"  # "choice" | "code_output" | "code_fill" | "bug_fix"
+    code_snippet: Optional[str] = None  # 代码片段(用于代码类题目)
+    options: List[str]  # 选择题选项
+    correct_answer: str  # "A", "B", "C", "D"
+    explanation: str
+
+
+class AssessmentStartRequest(BaseModel):
+    """开始检测请求"""
+    path_type: str  # LearningPath value
+    user_id: str = "default"
+
+
+class AssessmentAnswerRequest(BaseModel):
+    """提交答案请求"""
+    session_id: str
+    question_id: str
+    answer: str  # "A", "B", "C", "D"
+    path_type: str
+    user_id: str = "default"
+
+
+class AssessmentCompleteRequest(BaseModel):
+    """完成检测请求"""
+    session_id: str
+    path_type: str
+    user_id: str = "default"
+
+
+class AssessmentStartResponse(BaseModel):
+    """开始检测响应"""
+    session_id: str
+    question: AssessmentQuestion
+    current_index: int
+    total_questions: int
+
+
+class AssessmentAnswerResponse(BaseModel):
+    """提交答案响应"""
+    is_correct: bool
+    correct_answer: str
+    explanation: str
+    next_question: Optional[AssessmentQuestion]
+    current_index: int
+    total_questions: int
+    is_completed: bool

+ 26 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/models/schemas.py

@@ -0,0 +1,26 @@
+"""数据模型"""
+
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+from datetime import datetime
+
+
+class ChatMessage(BaseModel):
+    """聊天消息"""
+    role: str  # "user" 或 "assistant"
+    content: str
+    timestamp: datetime = datetime.now()
+
+
+class ChatRequest(BaseModel):
+    """聊天请求"""
+    message: str
+    conversation_id: Optional[str] = None
+    context: Optional[Dict[str, Any]] = None  # 学习上下文
+
+
+class ChatResponse(BaseModel):
+    """聊天响应"""
+    reply: str
+    conversation_id: str
+    agent_name: str = "编程导师"

+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/__init__.py


+ 416 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/assessment_service.py

@@ -0,0 +1,416 @@
+"""
+水平检测服务
+使用AI实时生成测试题目,评估用户水平
+"""
+import json
+import uuid
+from typing import List, Optional, Dict
+from datetime import datetime
+
+from ..models.learning import (
+    AssessmentQuestion, AssessmentResult, UserLevel, LearningPath
+)
+from .llm_service import get_llm
+from .learning_content import get_learning_path
+from .data_store import data_store
+
+
+# 测试配置
+TOTAL_QUESTIONS = 10
+
+
+# 各路径的测试分类定义
+PATH_CATEGORIES = {
+    LearningPath.FRONTEND: {
+        "html_css": "HTML/CSS基础",
+        "javascript": "JavaScript核心",
+        "vue": "Vue.js框架",
+        "browser_apis": "浏览器API与DOM",
+    },
+    LearningPath.BACKEND: {
+        "python": "Python基础",
+        "api_design": "REST API设计",
+        "database": "数据库操作",
+        "system_design": "系统设计",
+    },
+    LearningPath.FULLSTACK: {
+        "html_css": "HTML/CSS",
+        "javascript": "JavaScript",
+        "python": "Python",
+        "vue": "Vue.js",
+        "api_design": "API设计",
+    },
+}
+
+
+def get_categories_for_path(path_type: str) -> Dict[str, str]:
+    """获取指定路径的测试分类"""
+    try:
+        path = LearningPath(path_type)
+        return PATH_CATEGORIES.get(path, PATH_CATEGORIES[LearningPath.FRONTEND])
+    except ValueError:
+        return PATH_CATEGORIES[LearningPath.FRONTEND]
+
+
+def get_modules_for_path(path_type: str) -> List[Dict]:
+    """获取指定路径的模块信息"""
+    try:
+        path = LearningPath(path_type)
+        path_data = get_learning_path(path)
+        return [
+            {
+                "id": m.id,
+                "title": m.title,
+                "description": m.description,
+                "lessons": [l.title for l in m.lessons],
+            }
+            for m in path_data.modules
+        ]
+    except ValueError:
+        return []
+
+
+# ===== AI题目生成 =====
+
+
+def generate_assessment_questions(path_type: str) -> List[AssessmentQuestion]:
+    """使用AI生成测试题目"""
+    categories = get_categories_for_path(path_type)
+    modules = get_modules_for_path(path_type)
+
+    categories_text = ", ".join([f"{k}({v})" for k, v in categories.items()])
+    modules_text = json.dumps([m["title"] for m in modules], ensure_ascii=False)
+
+    prompt = """请为"{path_type}"学习路径生成{total}道编程水平测试题。
+
+测试分类:{categories}
+涉及模块:{modules}
+
+要求:
+1. 混合4种题型:choice(选择题/知识题)、code_output(预测输出)、code_fill(代码填空)、bug_fix(找Bug)
+2. 每道题包含:id(q1到q10)、category(分类key)、difficulty(难度1-5)、question_type(题型)、content(题目描述,用中文)、code_snippet(代码片段,选择题填null)、options(A/B/C/D选项,用中文)、correct_answer(正确答案字母)、explanation(解析,用中文)
+3. 代码题必须包含5-15行的代码片段
+4. 难度分布:简单30%、中等40%、困难30%
+5. 每个分类至少2道题
+6. 所有题目内容、选项、解析必须用中文输出
+
+只输出JSON数组:
+[
+  {{"id":"q1","category":"html_css","difficulty":2,"question_type":"choice","content":"关于HTML语义化标签的说法,正确的是?","code_snippet":null,"options":["A. <div>是语义化标签","B. <header>表示页面头部区域","C. <span>是块级元素","D. <article>只能用于博客文章"],"correct_answer":"B","explanation":"<header>是HTML5语义化标签,表示页面或区块的头部区域。div是无语义容器,span是行内元素,article可用于任何独立内容。"}},
+  {{"id":"q2","category":"javascript","difficulty":3,"question_type":"code_output","content":"以下代码的输出是什么?","code_snippet":"const arr = [1, 2, 3];\\nconst result = arr.map(x => x * 2).filter(x => x > 3);\\nconsole.log(result);","options":["A. [2, 4, 6]","B. [4, 6]","C. [2, 4]","D. [6]"],"correct_answer":"B","explanation":"map将每个元素乘2得到[2,4,6],filter筛选大于3的元素得到[4,6]。"}}
+]""".format(
+        path_type=path_type,
+        total=TOTAL_QUESTIONS,
+        categories=categories_text,
+        modules=modules_text,
+    )
+
+    try:
+        llm = get_llm()
+        messages = [{"role": "user", "content": prompt}]
+        response = llm.invoke(messages)
+        content = response if isinstance(response, str) else str(response)
+
+        # 提取JSON
+        start_idx = content.find("[")
+        end_idx = content.rfind("]") + 1
+
+        if start_idx == -1 or end_idx == 0:
+            print("[ERROR] AI返回格式错误,无法解析题目")
+            return _get_fallback_questions(path_type)
+
+        questions_data = json.loads(content[start_idx:end_idx])
+
+        questions = []
+        for q in questions_data:
+            questions.append(AssessmentQuestion(
+                id=q["id"],
+                category=q["category"],
+                difficulty=q.get("difficulty", 3),
+                content=q["content"],
+                question_type=q.get("question_type", "choice"),
+                code_snippet=q.get("code_snippet"),
+                options=q["options"],
+                correct_answer=q["correct_answer"],
+                explanation=q["explanation"],
+            ))
+
+        print(f"[OK] AI生成了 {len(questions)} 道题目")
+        return questions[:TOTAL_QUESTIONS]
+
+    except Exception as e:
+        print(f"[ERROR] AI生成题目失败: {e}")
+        return _get_fallback_questions(path_type)
+
+
+def _get_fallback_questions(path_type: str) -> List[AssessmentQuestion]:
+    """备用题目(当AI生成失败时)"""
+    categories = get_categories_for_path(path_type)
+    cat_keys = list(categories.keys())
+
+    fallback = []
+    for i in range(TOTAL_QUESTIONS):
+        cat = cat_keys[i % len(cat_keys)]
+        fallback.append(AssessmentQuestion(
+            id=f"q{i+1}",
+            category=cat,
+            difficulty=2,
+            content=f"这是一道关于{categories[cat]}的测试题(备用题目)",
+            question_type="choice",
+            code_snippet=None,
+            options=["A. 选项1", "B. 选项2", "C. 选项3", "D. 选项4"],
+            correct_answer="A",
+            explanation="备用题目解析",
+        ))
+    return fallback
+
+
+# ===== 评估会话管理 =====
+
+
+class AssessmentSession:
+    """评估会话"""
+
+    def __init__(self, session_id: str, path_type: str, questions: List[AssessmentQuestion], user_id: str = "default"):
+        self.session_id = session_id
+        self.path_type = path_type
+        self.user_id = user_id
+        self.questions = questions
+        self.answers: Dict[str, str] = {}  # question_id -> answer
+        self.current_index = 0
+        self.created_at = datetime.now()
+
+    @property
+    def is_completed(self) -> bool:
+        return self.current_index >= len(self.questions)
+
+    @property
+    def total_questions(self) -> int:
+        return len(self.questions)
+
+    def get_current_question(self) -> Optional[AssessmentQuestion]:
+        if self.current_index < len(self.questions):
+            return self.questions[self.current_index]
+        return None
+
+    def submit_answer(self, question_id: str, answer: str) -> Dict:
+        """提交答案"""
+        question = self.questions[self.current_index]
+        if question.id != question_id:
+            return {"error": "题目ID不匹配"}
+
+        self.answers[question_id] = answer
+        is_correct = answer.upper() == question.correct_answer.upper()
+        self.current_index += 1
+
+        next_question = self.get_current_question()
+
+        return {
+            "is_correct": is_correct,
+            "correct_answer": question.correct_answer,
+            "explanation": question.explanation,
+            "next_question": next_question.model_dump() if next_question else None,
+            "current_index": self.current_index,
+            "total_questions": self.total_questions,
+            "is_completed": self.is_completed,
+        }
+
+
+# 会话存储
+_sessions: Dict[str, AssessmentSession] = {}
+
+
+def create_session(path_type: str, user_id: str = "default") -> AssessmentSession:
+    """创建新的评估会话"""
+    session_id = str(uuid.uuid4())[:8]
+    questions = generate_assessment_questions(path_type)
+    session = AssessmentSession(session_id, path_type, questions, user_id=user_id)
+    _sessions[session_id] = session
+    print(f"[INFO] 创建评估会话 {session_id}(用户: {user_id}),{len(questions)}道题")
+    return session
+
+
+def get_session(session_id: str) -> Optional[AssessmentSession]:
+    """获取评估会话"""
+    return _sessions.get(session_id)
+
+
+def delete_session(session_id: str):
+    """删除评估会话"""
+    if session_id in _sessions:
+        del _sessions[session_id]
+
+
+# ===== 评分与结果 =====
+
+
+def calculate_result(session: AssessmentSession, user_id: str = "default") -> AssessmentResult:
+    """计算评估结果"""
+    questions = session.questions
+    answers = session.answers
+    categories = get_categories_for_path(session.path_type)
+
+    # 统计
+    total = len(questions)
+    correct = 0
+    category_correct = {cat: 0 for cat in categories}
+    category_total = {cat: 0 for cat in categories}
+
+    for q in questions:
+        user_answer = answers.get(q.id, "")
+        is_correct = user_answer.upper() == q.correct_answer.upper()
+        if is_correct:
+            correct += 1
+            if q.category in category_correct:
+                category_correct[q.category] += 1
+        if q.category in category_total:
+            category_total[q.category] += 1
+
+    # 总分
+    score = (correct / total * 100) if total > 0 else 0
+
+    # 各分类得分
+    category_scores = {}
+    for cat in categories:
+        if category_total.get(cat, 0) > 0:
+            category_scores[cat] = round(
+                category_correct[cat] / category_total[cat] * 100, 1
+            )
+        else:
+            category_scores[cat] = 0.0
+
+    # 确定水平
+    if score >= 80:
+        level = UserLevel.ADVANCED
+    elif score >= 50:
+        level = UserLevel.INTERMEDIATE
+    else:
+        level = UserLevel.BEGINNER
+
+    # 推荐开始模块
+    recommended_module = _recommend_module(session.path_type, level, category_scores)
+
+    return AssessmentResult(
+        user_id=user_id,
+        path_type=session.path_type,
+        total_questions=total,
+        correct_count=correct,
+        score=round(score, 1),
+        level=level,
+        category_scores=category_scores,
+        recommended_start_module=recommended_module,
+        completed_at=datetime.now(),
+        is_current=True,
+    )
+
+
+def _recommend_module(
+    path_type: str, level: UserLevel, category_scores: Dict[str, float]
+) -> str:
+    """根据水平推荐开始模块"""
+    try:
+        path = LearningPath(path_type)
+        path_data = get_learning_path(path)
+        modules = sorted(path_data.modules, key=lambda m: m.order)
+
+        if level == UserLevel.BEGINNER:
+            return modules[0].id
+
+        # 中级:检查各分类得分,跳过掌握较好的模块
+        for module in modules:
+            # 检查模块相关的分类得分
+            module_cats = _get_module_categories(module.id)
+            avg_score = sum(
+                category_scores.get(cat, 0) for cat in module_cats
+            ) / max(len(module_cats), 1)
+
+            if avg_score < 70:
+                return module.id
+
+        return modules[0].id
+
+    except Exception:
+        return ""
+
+
+def _get_module_categories(module_id: str) -> List[str]:
+    """根据模块ID获取相关分类"""
+    mapping = {
+        "fe-html-css": ["html_css"],
+        "fe-javascript": ["javascript"],
+        "fe-vue": ["vue"],
+        "fe-project": ["html_css", "javascript", "vue"],
+        "be-python": ["python"],
+        "be-api": ["api_design"],
+        "be-system": ["system_design", "database"],
+        "be-project": ["python", "api_design", "system_design"],
+        "fs-web-basics": ["html_css", "javascript"],
+        "fs-frontend": ["vue"],
+        "fs-backend": ["python", "api_design"],
+        "fs-fullstack": ["html_css", "javascript", "python", "vue", "api_design"],
+    }
+    return mapping.get(module_id, [])
+
+
+# ===== 顶层API =====
+
+
+def start_assessment(path_type: str, user_id: str = "default") -> Dict:
+    """开始评估"""
+    session = create_session(path_type, user_id=user_id)
+    question = session.get_current_question()
+
+    return {
+        "session_id": session.session_id,
+        "question": question.model_dump() if question else None,
+        "current_index": session.current_index,
+        "total_questions": session.total_questions,
+    }
+
+
+def submit_answer(session_id: str, question_id: str, answer: str, user_id: str = "default") -> Dict:
+    """提交答案"""
+    session = get_session(session_id)
+    if not session:
+        return {"error": "会话不存在或已过期"}
+
+    result = session.submit_answer(question_id, answer)
+
+    # 如果完成,计算结果
+    if result.get("is_completed"):
+        assessment_result = calculate_result(session, user_id=user_id)
+        data_store.save_assessment(assessment_result)
+        result["assessment_result"] = assessment_result
+        # 发放XP奖励
+        from .gamification_service import get_gamification_service
+        svc = get_gamification_service()
+        svc.award_xp(user_id, 20, f"完成水平检测: {session.path_type}")
+        if assessment_result.score >= 100:
+            svc.award_xp(user_id, 30, "满分通关奖励")
+            svc.check_perfect_score(user_id)
+        svc.check_speed_demon(user_id)
+
+    return result
+
+
+def complete_assessment(session_id: str, user_id: str = "default") -> Optional[AssessmentResult]:
+    """强制完成评估(跳过剩余题目)"""
+    session = get_session(session_id)
+    if not session:
+        return None
+
+    result = calculate_result(session, user_id=user_id)
+    data_store.save_assessment(result)
+    delete_session(session_id)
+
+    # 发放XP奖励
+    from .gamification_service import get_gamification_service
+    svc = get_gamification_service()
+    svc.award_xp(user_id, 20, f"完成水平检测: {session.path_type}")
+    if result.score >= 100:
+        svc.award_xp(user_id, 30, "满分通关奖励")
+        svc.check_perfect_score(user_id)
+    # 检查速度徽章也在这里
+    svc.check_speed_demon(user_id)
+
+    return result

+ 440 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/code_executor.py

@@ -0,0 +1,440 @@
+"""代码执行服务 - 安全执行用户代码(多语言支持)"""
+
+import subprocess
+import tempfile
+import os
+import re
+import platform
+from abc import ABC, abstractmethod
+from typing import Dict
+
+
+# ---------------------------------------------------------------------------
+# Python 安全检查常量
+# ---------------------------------------------------------------------------
+
+BLOCKED_MODULES = {
+    'os', 'sys', 'subprocess', 'shutil', 'pathlib',
+    'socket', 'http', 'urllib', 'requests',
+    'ctypes', 'importlib',
+}
+
+BLOCKED_PATTERNS = [
+    r'\bexec\s*\(',
+    r'\beval\s*\(',
+    r'\b__import__\s*\(',
+    r'\bglobals\s*\(',
+    r'\blocals\s*\(',
+    r'\bcompile\s*\(',
+]
+
+# ---------------------------------------------------------------------------
+# JavaScript / TypeScript 安全检查常量
+# ---------------------------------------------------------------------------
+
+JS_BLOCKED_PATTERNS = [
+    r"require\s*\(\s*['\"]child_process['\"]\s*\)",
+    r"require\s*\(\s*['\"]fs['\"]\s*\)",
+    r"\bprocess\.exit\s*\(",
+    r"\bprocess\.kill\s*\(",
+    r"import\s+.*\s+from\s+['\"]fs['\"]",
+    r"import\s+.*\s+from\s+['\"]child_process['\"]",
+]
+
+# ---------------------------------------------------------------------------
+# Bash 安全检查常量
+# ---------------------------------------------------------------------------
+
+BASH_SAFE_COMMANDS = {
+    'echo', 'ls', 'cat', 'pwd', 'env', 'printf', 'date', 'whoami', 'uname',
+}
+
+# Operators: always dangerous via substring match
+BASH_BLOCKED_OPERATORS = ['|', '>', '<', '$(', '`', ';', '&&', '||']
+
+# Commands: only dangerous as whole words (avoids false positives like "sh" in "show")
+BASH_BLOCKED_COMMANDS = [
+    r'\bsh\b', r'\bbash\b', r'\bpython\b',
+    r'\bsudo\b', r'\bchmod\b', r'\brm\b', r'\bmv\b', r'\bcp\b', r'\bdd\b',
+    r'\bcurl\b', r'\bwget\b', r'\bnc\b',
+]
+
+
+# ---------------------------------------------------------------------------
+# Abstract base
+# ---------------------------------------------------------------------------
+
+class BaseExecutor(ABC):
+    """All language executors inherit from this."""
+
+    def __init__(self, timeout: int = 10, max_output: int = 5000):
+        self.timeout = timeout
+        self.max_output = max_output
+
+    @abstractmethod
+    def execute(self, code: str) -> Dict:
+        ...
+
+    def _truncate(self, text: str | None) -> str:
+        if not text:
+            return ""
+        if len(text) > self.max_output:
+            return text[:self.max_output] + "\n... (输出过长,已截断)"
+        return text
+
+    def _timeout_result(self) -> Dict:
+        return {
+            "success": False,
+            "output": "",
+            "error": f"执行超时(超过{self.timeout}秒)",
+            "exit_code": -1,
+        }
+
+    def _exception_result(self, exc: Exception) -> Dict:
+        return {
+            "success": False,
+            "output": "",
+            "error": f"执行失败: {str(exc)}",
+            "exit_code": -1,
+        }
+
+    def _safety_result(self, msg: str) -> Dict:
+        return {
+            "success": False,
+            "output": "",
+            "error": f"安全检查失败: {msg}",
+            "exit_code": -1,
+        }
+
+    def _run_subprocess(self, cmd: list, temp_file: str) -> Dict:
+        """Run a subprocess, handle timeout / error, clean up temp file."""
+        try:
+            result = subprocess.run(
+                cmd,
+                capture_output=True,
+                text=True,
+                encoding='utf-8',
+                errors='replace',
+                timeout=self.timeout,
+                cwd=tempfile.gettempdir(),
+            )
+            output = self._truncate(result.stdout)
+            error = self._truncate(result.stderr)
+            return {
+                "success": result.returncode == 0,
+                "output": output,
+                "error": error,
+                "exit_code": result.returncode,
+            }
+        except subprocess.TimeoutExpired:
+            return self._timeout_result()
+        except Exception as e:
+            return self._exception_result(e)
+        finally:
+            try:
+                os.unlink(temp_file)
+            except OSError:
+                pass
+
+
+# ---------------------------------------------------------------------------
+# Python executor
+# ---------------------------------------------------------------------------
+
+class PythonExecutor(BaseExecutor):
+    """Executes Python code inside a safety-wrapped temp file."""
+
+    def _check_safety(self, code: str) -> str:
+        for pattern in BLOCKED_PATTERNS:
+            if re.search(pattern, code):
+                raise ValueError(f"代码包含不允许的操作: {pattern}")
+
+        import_pattern = r'(?:from|import)\s+(\w+)'
+        imports = re.findall(import_pattern, code)
+        for module in imports:
+            if module in BLOCKED_MODULES:
+                raise ValueError(f"不允许导入模块: {module}")
+
+        wrapper = '''
+import sys
+import io
+
+# 重定向stdout/stderr
+_old_stdout = sys.stdout
+_old_stderr = sys.stderr
+sys.stdout = io.StringIO()
+sys.stderr = io.StringIO()
+
+try:
+    # 用户代码开始
+{_code}
+    # 用户代码结束
+finally:
+    # 恢复stdout/stderr并获取输出
+    _stdout_output = sys.stdout.getvalue()
+    _stderr_output = sys.stderr.getvalue()
+    sys.stdout = _old_stdout
+    sys.stderr = _old_stderr
+    
+    # 输出结果
+    if _stdout_output:
+        print(_stdout_output, end='')
+    if _stderr_output:
+        print(_stderr_output, end='', file=sys.stderr)
+'''
+        indented = '\n'.join(f'    {line}' for line in code.split('\n'))
+        return wrapper.replace('{_code}', indented)
+
+    def execute(self, code: str) -> Dict:
+        try:
+            safe_code = self._check_safety(code)
+        except ValueError as e:
+            return self._safety_result(str(e))
+
+        with tempfile.NamedTemporaryFile(
+            mode='w', suffix='.py', delete=False, encoding='utf-8',
+        ) as f:
+            f.write(safe_code)
+            temp_file = f.name
+
+        return self._run_subprocess(['python', temp_file], temp_file)
+
+
+# ---------------------------------------------------------------------------
+# JavaScript executor
+# ---------------------------------------------------------------------------
+
+class JavaScriptExecutor(BaseExecutor):
+    """Executes JavaScript via Node.js."""
+
+    def _check_safety(self, code: str) -> None:
+        for pattern in JS_BLOCKED_PATTERNS:
+            if re.search(pattern, code):
+                raise ValueError(f"代码包含不允许的操作: {pattern}")
+
+    def execute(self, code: str) -> Dict:
+        try:
+            self._check_safety(code)
+        except ValueError as e:
+            return self._safety_result(str(e))
+
+        with tempfile.NamedTemporaryFile(
+            mode='w', suffix='.js', delete=False, encoding='utf-8',
+        ) as f:
+            f.write(code)
+            temp_file = f.name
+
+        return self._run_subprocess(['node', temp_file], temp_file)
+
+
+# ---------------------------------------------------------------------------
+# TypeScript executor
+# ---------------------------------------------------------------------------
+
+class TypeScriptExecutor(BaseExecutor):
+    """Executes TypeScript via node --experimental-strip-types (Node 22+), 
+    falls back to npx tsx for advanced features (enums, decorators, etc.)."""
+
+    def __init__(self):
+        # npx first-run download can be slow → 30s timeout
+        super().__init__(timeout=30)
+
+    def _check_safety(self, code: str) -> None:
+        for pattern in JS_BLOCKED_PATTERNS:
+            if re.search(pattern, code):
+                raise ValueError(f"代码包含不允许的操作: {pattern}")
+
+    def _resolve_npx(self) -> str:
+        """Return the correct npx command for the current platform."""
+        return 'npx.cmd' if platform.system() == 'Windows' else 'npx'
+
+    def _try_cmd(self, cmd: list, code: str, temp_file: str) -> Dict:
+        """Run a subprocess, re-creating temp_file (since _run_subprocess cleans it up)."""
+        # Re-create file (may have been deleted by a previous _run_subprocess)
+        try:
+            with open(temp_file, 'w', encoding='utf-8') as f:
+                f.write(code)
+        except OSError:
+            pass
+        return self._run_subprocess(cmd, temp_file)
+
+    def execute(self, code: str) -> Dict:
+        try:
+            self._check_safety(code)
+        except ValueError as e:
+            return self._safety_result(str(e))
+
+        with tempfile.NamedTemporaryFile(
+            mode='w', suffix='.ts', delete=False, encoding='utf-8',
+        ) as f:
+            f.write(code)
+            temp_file = f.name
+
+        # Primary: node --experimental-strip-types (fast, no download needed)
+        # --no-warnings suppresses ExperimentalWarning from stderr
+        result = self._try_cmd(['node', '--no-warnings', '--experimental-strip-types', temp_file], code, temp_file)
+        if result['success']:
+            return result
+
+        # Fallback: npx tsx — handles TS features strip-types doesn't support
+        npx_cmd = self._resolve_npx()
+        result = self._try_cmd([npx_cmd, '--yes', 'tsx', temp_file], code, temp_file)
+        return result
+
+
+# ---------------------------------------------------------------------------
+# Bash executor
+# ---------------------------------------------------------------------------
+
+class BashExecutor(BaseExecutor):
+    """Executes Shell commands.
+
+    On Linux/Mac: uses bash.
+    On Windows: uses sh (Git Bash) if available, falls back to PowerShell.
+    """
+
+    @staticmethod
+    def _find_shell() -> str | None:
+        """Locate a usable Unix-compatible shell."""
+        if platform.system() != 'Windows':
+            return 'bash'
+
+        # On Windows, try 'sh' (Git Bash etc.)
+        import shutil
+        sh_path = shutil.which('sh')
+        if sh_path:
+            return sh_path
+
+        # Check common Git Bash install paths
+        common_paths = [
+            r'C:\Program Files\Git\bin\sh.exe',
+            r'C:\Program Files (x86)\Git\bin\sh.exe',
+        ]
+        for p in common_paths:
+            if os.path.exists(p):
+                return p
+
+        return None
+
+    def _check_safety(self, code: str, use_powershell: bool = False) -> None:
+        if use_powershell:
+            # PowerShell: block dangerous operators (substring match)
+            blocked_ops = ['$(', '`', ';']
+            # PowerShell: block dangerous cmdlets/commands (word-boundary regex)
+            blocked_cmds = [
+                r'\brm\b', r'\bRemove-Item\b', r'\bsudo\b', r'\bchmod\b',
+                r'\bcurl\b', r'\bwget\b', r'\bInvoke-WebRequest\b', r'\biwr\b',
+            ]
+            safe_commands = {
+                'echo', 'Write-Output', 'Get-ChildItem', 'ls', 'dir',
+                'Get-Content', 'cat', 'pwd', 'Get-Location',
+                'Get-Date', 'date', 'whoami', 'Get-Command', 'Write-Host',
+                'Get-EnvironmentVariable', 'env',
+            }
+        else:
+            blocked_ops = BASH_BLOCKED_OPERATORS
+            blocked_cmds = BASH_BLOCKED_COMMANDS
+            safe_commands = BASH_SAFE_COMMANDS
+
+        # Check operators (plain substring — dangerous anywhere)
+        for op in blocked_ops:
+            if op in code:
+                raise ValueError(f"代码包含不允许的操作符: {repr(op)}")
+
+        # Check blocked commands (word boundary regex — no false positives)
+        for pattern in blocked_cmds:
+            if re.search(pattern, code):
+                raise ValueError(f"代码包含不允许的命令: {pattern}")
+
+        # Verify every line starts with an allowed command
+        for line in code.splitlines():
+            stripped = line.strip()
+            if not stripped or stripped.startswith('#'):
+                continue
+            first_word = stripped.split()[0]
+            if first_word not in safe_commands:
+                raise ValueError(f"不允许的命令: {first_word}")
+
+    def execute(self, code: str) -> Dict:
+        shell = self._find_shell()
+        use_powershell = shell is None
+
+        try:
+            self._check_safety(code, use_powershell=use_powershell)
+        except ValueError as e:
+            return self._safety_result(str(e))
+
+        if use_powershell:
+            # Execute via PowerShell with encoded command
+            try:
+                import base64
+                encoded = base64.b64encode(code.encode('utf-16le')).decode()
+                result = subprocess.run(
+                    ['powershell.exe', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
+                    capture_output=True, text=True, encoding='utf-8', errors='replace',
+                    timeout=self.timeout,
+                )
+                output = self._truncate(result.stdout)
+                error = self._truncate(result.stderr)
+                return {
+                    "success": result.returncode == 0,
+                    "output": output,
+                    "error": error,
+                    "exit_code": result.returncode,
+                }
+            except subprocess.TimeoutExpired:
+                return self._timeout_result()
+            except Exception as e:
+                return self._exception_result(e)
+        else:
+            with tempfile.NamedTemporaryFile(
+                mode='w', suffix='.sh', delete=False, encoding='utf-8',
+            ) as f:
+                f.write(code)
+                temp_file = f.name
+            return self._run_subprocess([shell, temp_file], temp_file)
+
+
+# ---------------------------------------------------------------------------
+# Registry
+# ---------------------------------------------------------------------------
+
+class CodeExecutorRegistry:
+    """Routes code to the appropriate language executor."""
+
+    def __init__(self):
+        self._executors = {
+            "python": PythonExecutor(),
+            "javascript": JavaScriptExecutor(),
+            "typescript": TypeScriptExecutor(),
+            "bash": BashExecutor(),
+        }
+
+    def execute(self, code: str, language: str) -> Dict:
+        executor = self._executors.get(language)
+        if not executor:
+            return {
+                "success": False,
+                "output": "",
+                "error": f"不支持的语言: {language}",
+                "exit_code": -1,
+            }
+        return executor.execute(code)
+
+    def supported_languages(self) -> list:
+        return list(self._executors.keys())
+
+
+# ---------------------------------------------------------------------------
+# Singleton
+# ---------------------------------------------------------------------------
+
+_executor = None
+
+
+def get_executor() -> CodeExecutorRegistry:
+    """获取代码执行器实例(单例)"""
+    global _executor
+    if _executor is None:
+        _executor = CodeExecutorRegistry()
+    return _executor

+ 307 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/data_store.py

@@ -0,0 +1,307 @@
+"""
+JSON持久化存储服务
+将用户进度和测试结果保存到本地JSON文件
+"""
+import json
+import os
+from pathlib import Path
+from typing import Optional, List, Dict
+from datetime import datetime
+
+from ..models.learning import (
+    UserProgress, AssessmentResult, LearningPath, UserLevel, GamificationProfile,
+    CodeSubmission
+)
+
+# 数据存储目录
+DATA_DIR = Path(__file__).parent.parent.parent / "data"
+USER_PROGRESS_FILE = DATA_DIR / "user_progress.json"
+ASSESSMENTS_FILE = DATA_DIR / "assessments.json"
+GAMIFICATION_FILE = DATA_DIR / "gamification.json"
+SUBMISSIONS_FILE = DATA_DIR / "submissions.json"
+
+
+class DataStore:
+    """JSON文件存储服务"""
+
+    def __init__(self):
+        # 确保数据目录存在
+        DATA_DIR.mkdir(parents=True, exist_ok=True)
+
+        # 初始化进度存储
+        self._progress: Dict[str, UserProgress] = {}
+        self._load_progress()
+
+        # 初始化测试结果存储
+        self._assessments: Dict[str, List[AssessmentResult]] = {}
+        self._load_assessments()
+
+        # 初始化游戏化存储
+        self._gamifications: Dict[str, GamificationProfile] = {}
+        self._load_gamifications()
+
+        # 初始化代码提交存储
+        self._submissions: Dict[str, List[CodeSubmission]] = {}
+        self._load_submissions()
+
+    def _load_progress(self):
+        """从文件加载用户进度"""
+        if USER_PROGRESS_FILE.exists():
+            try:
+                with open(USER_PROGRESS_FILE, "r", encoding="utf-8") as f:
+                    data = json.load(f)
+                for user_id, progress_data in data.items():
+                    # 处理 datetime 字段
+                    for field in ["started_at", "last_activity_at"]:
+                        if progress_data.get(field):
+                            progress_data[field] = datetime.fromisoformat(
+                                progress_data[field]
+                            )
+                    # 处理 assessments 中的 datetime
+                    for assessment in progress_data.get("assessments", []):
+                        if assessment.get("completed_at"):
+                            assessment["completed_at"] = datetime.fromisoformat(
+                                assessment["completed_at"]
+                            )
+                    self._progress[user_id] = UserProgress(**progress_data)
+                print(f"[OK] 已加载 {len(self._progress)} 个用户进度")
+            except Exception as e:
+                print(f"[WARN] 加载用户进度失败: {e}")
+                self._progress = {}
+
+    def _save_progress(self):
+        """保存用户进度到文件"""
+        try:
+            data = {}
+            for user_id, progress in self._progress.items():
+                progress_dict = progress.model_dump()
+                # 处理 datetime 序列化
+                for field in ["started_at", "last_activity_at"]:
+                    if progress_dict.get(field):
+                        progress_dict[field] = progress_dict[field].isoformat()
+                for assessment in progress_dict.get("assessments", []):
+                    if assessment.get("completed_at"):
+                        assessment["completed_at"] = assessment["completed_at"].isoformat()
+                data[user_id] = progress_dict
+
+            with open(USER_PROGRESS_FILE, "w", encoding="utf-8") as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+        except Exception as e:
+            print(f"[ERROR] 保存用户进度失败: {e}")
+
+    def _load_assessments(self):
+        """从文件加载测试结果"""
+        if ASSESSMENTS_FILE.exists():
+            try:
+                with open(ASSESSMENTS_FILE, "r", encoding="utf-8") as f:
+                    data = json.load(f)
+                for user_id, assessments_data in data.items():
+                    self._assessments[user_id] = []
+                    for assessment in assessments_data:
+                        if assessment.get("completed_at"):
+                            assessment["completed_at"] = datetime.fromisoformat(
+                                assessment["completed_at"]
+                            )
+                        self._assessments[user_id].append(
+                            AssessmentResult(**assessment)
+                        )
+                print(f"[OK] 已加载 {len(self._assessments)} 个用户测试记录")
+            except Exception as e:
+                print(f"[WARN] 加载测试记录失败: {e}")
+                self._assessments = {}
+
+    def _save_assessments(self):
+        """保存测试结果到文件"""
+        try:
+            data = {}
+            for user_id, assessments in self._assessments.items():
+                data[user_id] = []
+                for assessment in assessments:
+                    assessment_dict = assessment.model_dump()
+                    if assessment_dict.get("completed_at"):
+                        assessment_dict["completed_at"] = (
+                            assessment_dict["completed_at"].isoformat()
+                        )
+                    data[user_id].append(assessment_dict)
+
+            with open(ASSESSMENTS_FILE, "w", encoding="utf-8") as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+        except Exception as e:
+            print(f"[ERROR] 保存测试记录失败: {e}")
+
+    # ==================== 游戏化存储 ====================
+
+    def _load_gamifications(self):
+        """从文件加载游戏化数据"""
+        if GAMIFICATION_FILE.exists():
+            try:
+                with open(GAMIFICATION_FILE, "r", encoding="utf-8") as f:
+                    data = json.load(f)
+                for user_id, profile_data in data.items():
+                    self._gamifications[user_id] = GamificationProfile(**profile_data)
+                print(f"[OK] 已加载 {len(self._gamifications)} 个游戏化档案")
+            except Exception as e:
+                print(f"[WARN] 加载游戏化数据失败: {e}")
+                self._gamifications = {}
+
+    def _save_gamifications(self):
+        """保存游戏化数据到文件"""
+        try:
+            data = {}
+            for user_id, profile in self._gamifications.items():
+                data[user_id] = profile.model_dump()
+            with open(GAMIFICATION_FILE, "w", encoding="utf-8") as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+        except Exception as e:
+            print(f"[ERROR] 保存游戏化数据失败: {e}")
+
+    def get_gamification(self, user_id: str = "default") -> Optional[GamificationProfile]:
+        """获取用户游戏化档案"""
+        return self._gamifications.get(user_id)
+
+    def save_gamification(self, profile: GamificationProfile):
+        """保存用户游戏化档案"""
+        self._gamifications[profile.user_id] = profile
+        self._save_gamifications()
+
+    # ==================== 用户进度 API ====================
+
+    def get_user_progress(
+        self, user_id: str = "default"
+    ) -> Optional[UserProgress]:
+        """获取用户进度"""
+        return self._progress.get(user_id)
+
+    def save_user_progress(self, progress: UserProgress):
+        """保存用户进度"""
+        self._progress[progress.user_id] = progress
+        self._save_progress()
+
+    def update_user_progress(
+        self,
+        user_id: str = "default",
+        **kwargs
+    ) -> UserProgress:
+        """更新用户进度"""
+        progress = self._progress.get(user_id)
+        if not progress:
+            progress = UserProgress(user_id=user_id)
+
+        for key, value in kwargs.items():
+            if hasattr(progress, key):
+                setattr(progress, key, value)
+
+        progress.last_activity_at = datetime.now()
+        self._progress[user_id] = progress
+        self._save_progress()
+        return progress
+
+    # ==================== 测试结果 API ====================
+
+    def get_user_assessments(
+        self, user_id: str = "default", path_type: str = None
+    ) -> List[AssessmentResult]:
+        """获取用户测试结果"""
+        assessments = self._assessments.get(user_id, [])
+        if path_type:
+            assessments = [a for a in assessments if a.path_type == path_type]
+        return assessments
+
+    def get_current_assessment(
+        self, user_id: str = "default", path_type: str = None
+    ) -> Optional[AssessmentResult]:
+        """获取用户当前有效的测试结果"""
+        assessments = self.get_user_assessments(user_id, path_type)
+        # 返回最新的有效结果
+        for assessment in reversed(assessments):
+            if assessment.is_current:
+                return assessment
+        return assessments[-1] if assessments else None
+
+    def save_assessment(self, assessment: AssessmentResult):
+        """保存测试结果"""
+        user_id = assessment.user_id
+        if user_id not in self._assessments:
+            self._assessments[user_id] = []
+
+        # 如果是"学习前重测",将旧结果标记为非当前
+        if assessment.is_current:
+            for existing in self._assessments[user_id]:
+                if existing.path_type == assessment.path_type:
+                    existing.is_current = False
+
+        self._assessments[user_id].append(assessment)
+        self._save_assessments()
+        self._save_user_assessment_to_progress(assessment)
+
+    def _save_user_assessment_to_progress(self, assessment: AssessmentResult):
+        """同步测试结果到用户进度"""
+        progress = self._progress.get(assessment.user_id)
+        if progress:
+            progress.assessments = self.get_user_assessments(
+                assessment.user_id, assessment.path_type
+            )
+            progress.skill_levels = assessment.category_scores
+            self._save_progress()
+
+    def has_assessment(
+        self, user_id: str = "default", path_type: str = None
+    ) -> bool:
+        """检查用户是否已测试"""
+        if path_type:
+            return len(self.get_user_assessments(user_id, path_type)) > 0
+        return len(self._assessments.get(user_id, [])) > 0
+
+    # ==================== 代码提交流持久化 ====================
+
+    def _load_submissions(self):
+        """从文件加载代码提交记录"""
+        if SUBMISSIONS_FILE.exists():
+            try:
+                with open(SUBMISSIONS_FILE, "r", encoding="utf-8") as f:
+                    data = json.load(f)
+                for user_id, submissions_data in data.items():
+                    self._submissions[user_id] = []
+                    for sub in submissions_data:
+                        if sub.get("created_at"):
+                            sub["created_at"] = datetime.fromisoformat(sub["created_at"])
+                        self._submissions[user_id].append(CodeSubmission(**sub))
+                print(f"[OK] 已加载 {sum(len(v) for v in self._submissions.values())} 条代码提交记录")
+            except Exception as e:
+                print(f"[WARN] 加载代码提交记录失败: {e}")
+                self._submissions = {}
+
+    def _save_submissions(self):
+        """保存代码提交记录到文件"""
+        try:
+            data = {}
+            for user_id, submissions in self._submissions.items():
+                data[user_id] = []
+                for sub in submissions:
+                    sub_dict = sub.model_dump()
+                    if sub_dict.get("created_at"):
+                        sub_dict["created_at"] = sub_dict["created_at"].isoformat()
+                    data[user_id].append(sub_dict)
+            with open(SUBMISSIONS_FILE, "w", encoding="utf-8") as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+        except Exception as e:
+            print(f"[ERROR] 保存代码提交记录失败: {e}")
+
+    def save_submission(self, submission: CodeSubmission):
+        """保存代码提交记录"""
+        user_id = submission.user_id
+        if user_id not in self._submissions:
+            self._submissions[user_id] = []
+        self._submissions[user_id].append(submission)
+        self._save_submissions()
+
+    def get_user_submissions(
+        self, user_id: str = "default", limit: int = 20
+    ) -> List[CodeSubmission]:
+        """获取用户最近提交记录"""
+        submissions = self._submissions.get(user_id, [])
+        return sorted(submissions, key=lambda s: s.created_at, reverse=True)[:limit]
+
+
+# 全局单例
+data_store = DataStore()

+ 208 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/gamification_service.py

@@ -0,0 +1,208 @@
+"""
+游戏化服务 - XP、等级、徽章、连击系统
+"""
+from datetime import date, datetime
+from typing import List, Optional, Dict, Tuple
+from ..models.learning import GamificationProfile
+from .data_store import data_store
+
+# ========== 配置 ==========
+XP_PER_LEVEL = 100
+
+# 徽章定义
+BADGE_DEFINITIONS: Dict[str, dict] = {
+    "first_lesson": {
+        "id": "first_lesson",
+        "name": "第一步",
+        "description": "完成第一节课",
+        "icon": "🌱",
+        "condition": "完成1节课",
+    },
+    "ten_lessons": {
+        "id": "ten_lessons",
+        "name": "勤学苦练",
+        "description": "累计完成10节课",
+        "icon": "📚",
+        "condition": "完成10节课",
+    },
+    "twenty_lessons": {
+        "id": "twenty_lessons",
+        "name": "学富五车",
+        "description": "累计完成20节课",
+        "icon": "🧠",
+        "condition": "完成20节课",
+    },
+    "first_assessment": {
+        "id": "first_assessment",
+        "name": "自我认知",
+        "description": "完成第一次水平检测",
+        "icon": "📊",
+        "condition": "完成1次测试",
+    },
+    "perfect_score": {
+        "id": "perfect_score",
+        "name": "完美主义者",
+        "description": "水平检测获得满分",
+        "icon": "💯",
+        "condition": "测试得分100",
+    },
+    "speed_demon": {
+        "id": "speed_demon",
+        "name": "神速",
+        "description": "同一天完成5节课",
+        "icon": "⚡",
+        "condition": "单日5节课",
+    },
+    "first_path": {
+        "id": "first_path",
+        "name": "选择方向",
+        "description": "选择一条学习路径",
+        "icon": "🛤️",
+        "condition": "选择路径",
+    },
+    "all_modules": {
+        "id": "all_modules",
+        "name": "开拓者",
+        "description": "完成一个路径的所有模块",
+        "icon": "🏆",
+        "condition": "完成全部模块",
+    },
+    "week_streak": {
+        "id": "week_streak",
+        "name": "坚持不懈",
+        "description": "连续学习7天",
+        "icon": "🔥",
+        "condition": "连续7天",
+    },
+    "month_streak": {
+        "id": "month_streak",
+        "name": "铁杆学员",
+        "description": "连续学习30天",
+        "icon": "💎",
+        "condition": "连续30天",
+    },
+}
+
+
+class GamificationService:
+    """游戏化服务"""
+
+    def get_profile(self, user_id: str) -> GamificationProfile:
+        """获取用户游戏化档案"""
+        profile = data_store.get_gamification(user_id)
+        if not profile:
+            profile = GamificationProfile(user_id=user_id)
+            data_store.save_gamification(profile)
+        return profile
+
+    def award_xp(self, user_id: str, amount: int, reason: str) -> GamificationProfile:
+        """给用户增加XP"""
+        profile = self.get_profile(user_id)
+        profile.total_xp += amount
+        profile.level = max(1, profile.total_xp // XP_PER_LEVEL + 1)
+
+        if len(profile.xp_log) > 500:
+            profile.xp_log = profile.xp_log[-500:]
+        profile.xp_log.append({
+            "amount": amount,
+            "reason": reason,
+            "timestamp": datetime.now().isoformat(),
+        })
+
+        # 更新连击
+        today = date.today().isoformat()
+        if profile.last_active_date == today:
+            pass  # 今天已经活跃过
+        elif profile.last_active_date == _yesterday():
+            profile.streak += 1
+        else:
+            profile.streak = 1
+        profile.last_active_date = today
+
+        # 检查新徽章
+        new_badges = self._check_new_badges(profile, user_id)
+        data_store.save_gamification(profile)
+        return profile, new_badges
+
+    def _check_new_badges(self, profile: GamificationProfile, user_id: str) -> List[dict]:
+        """检查是否有新徽章获得"""
+        progress = data_store.get_user_progress(user_id)
+        if not progress:
+            return []
+
+        earned = set(profile.badges)
+        new_badges = []
+
+        # 按条件检查
+        checks = [
+            ("first_lesson", lambda: len(progress.completed_lessons) >= 1),
+            ("ten_lessons", lambda: len(progress.completed_lessons) >= 10),
+            ("twenty_lessons", lambda: len(progress.completed_lessons) >= 20),
+            ("first_assessment", lambda: len(data_store.get_user_assessments(user_id)) >= 1),
+            ("first_path", lambda: progress.current_path is not None),
+            ("all_modules", lambda: progress.current_path is not None and
+             _all_modules_completed(progress)),
+            ("week_streak", lambda: profile.streak >= 7),
+            ("month_streak", lambda: profile.streak >= 30),
+            ("speed_demon", lambda: False),  # 由外部触发
+            ("perfect_score", lambda: False),  # 由外部触发
+        ]
+
+        for badge_id, check_fn in checks:
+            if badge_id not in earned and check_fn():
+                badge = dict(BADGE_DEFINITIONS[badge_id])
+                badge["awarded_at"] = datetime.now().isoformat()
+                profile.badges.append(badge_id)
+                new_badges.append(badge)
+
+        return new_badges
+
+    def check_perfect_score(self, user_id: str):
+        """检查是否获得完美得分徽章"""
+        profile = self.get_profile(user_id)
+        if "perfect_score" in profile.badges:
+            return
+        assessments = data_store.get_user_assessments(user_id)
+        if any(a.score >= 100 for a in assessments):
+            profile.badges.append("perfect_score")
+            data_store.save_gamification(profile)
+
+    def check_speed_demon(self, user_id: str):
+        """检查单日5课成就"""
+        profile = self.get_profile(user_id)
+        if "speed_demon" in profile.badges:
+            return
+        progress = data_store.get_user_progress(user_id)
+        if not progress:
+            return
+        # 完成5节课即授予
+        if len(progress.completed_lessons) >= 5:
+            profile.badges.append("speed_demon")
+            data_store.save_gamification(profile)
+
+
+def _yesterday() -> str:
+    from datetime import timedelta
+    return (date.today() - timedelta(days=1)).isoformat()
+
+
+def _all_modules_completed(progress) -> bool:
+    """检查一个路径的所有模块是否完成"""
+    if not progress.current_path:
+        return False
+    from .learning_content import get_learning_path
+    path_data = get_learning_path(progress.current_path)
+    if not path_data:
+        return False
+    return all(m.id in progress.completed_modules for m in path_data.modules)
+
+
+# 全局单例
+_gamification_service: Optional[GamificationService] = None
+
+
+def get_gamification_service() -> GamificationService:
+    global _gamification_service
+    if _gamification_service is None:
+        _gamification_service = GamificationService()
+    return _gamification_service

+ 510 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/learning_content.py

@@ -0,0 +1,510 @@
+from typing import List, Dict, Optional
+from ..models.learning import (
+    LearningPath, LearningPathData, LearningModule, LearningLesson, ModuleStatus
+)
+
+
+def get_frontend_path() -> LearningPathData:
+    return LearningPathData(
+        path=LearningPath.FRONTEND,
+        title="前端开发",
+        description="从HTML/CSS基础到现代前端框架",
+        icon="🎨",
+        modules=[
+            LearningModule(
+                id="fe-html-css",
+                title="HTML & CSS 基础",
+                description="网页结构与样式入门",
+                icon="📝",
+                order=1,
+                status=ModuleStatus.NOT_STARTED,
+                lessons=[
+                    LearningLesson(
+                        id="fe-html-01",
+                        title="HTML文档结构",
+                        description="DOCTYPE、head、body标签",
+                        type="theory",
+                        duration_minutes=15
+                    ),
+                    LearningLesson(
+                        id="fe-html-02",
+                        title="常用HTML标签",
+                        description="标题、段落、链接、图片、列表",
+                        type="theory",
+                        duration_minutes=20
+                    ),
+                    LearningLesson(
+                        id="fe-css-01",
+                        title="CSS选择器",
+                        description="元素、类、ID、属性选择器",
+                        type="theory",
+                        duration_minutes=20
+                    ),
+                    LearningLesson(
+                        id="fe-css-02",
+                        title="盒模型与布局",
+                        description="margin、padding、border、display",
+                        type="practice",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fe-css-03",
+                        title="Flexbox布局",
+                        description="弹性盒子布局完全指南",
+                        type="practice",
+                        duration_minutes=40
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="fe-javascript",
+                title="JavaScript 核心",
+                description="JavaScript语言基础",
+                icon="⚡",
+                order=2,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="fe-js-01",
+                        title="变量与数据类型",
+                        description="let、const、基本类型与引用类型",
+                        type="theory",
+                        duration_minutes=25
+                    ),
+                    LearningLesson(
+                        id="fe-js-02",
+                        title="函数与作用域",
+                        description="函数声明、箭头函数、闭包",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fe-js-03",
+                        title="DOM操作",
+                        description="查询、修改、创建DOM元素",
+                        type="practice",
+                        duration_minutes=40
+                    ),
+                    LearningLesson(
+                        id="fe-js-04",
+                        title="事件处理",
+                        description="事件监听、事件委托、事件对象",
+                        type="practice",
+                        duration_minutes=35
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="fe-vue",
+                title="Vue.js 框架",
+                description="现代Vue 3开发",
+                icon="💚",
+                order=3,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="fe-vue-01",
+                        title="Vue 3 基础",
+                        description="模板语法、响应式数据、computed",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fe-vue-02",
+                        title="组件系统",
+                        description="组件创建、props、events、slots",
+                        type="theory",
+                        duration_minutes=35
+                    ),
+                    LearningLesson(
+                        id="fe-vue-03",
+                        title="组合式API",
+                        description="setup、ref、reactive、生命周期",
+                        type="practice",
+                        duration_minutes=40
+                    ),
+                    LearningLesson(
+                        id="fe-vue-04",
+                        title="状态管理",
+                        description="Pinia状态管理实践",
+                        type="practice",
+                        duration_minutes=30
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="fe-project",
+                title="实战项目",
+                description="构建完整的前端应用",
+                icon="🚀",
+                order=4,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="fe-proj-01",
+                        title="项目规划",
+                        description="需求分析、技术选型、架构设计",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fe-proj-02",
+                        title="核心功能开发",
+                        description="实现主要功能模块",
+                        type="project",
+                        duration_minutes=120
+                    ),
+                    LearningLesson(
+                        id="fe-proj-03",
+                        title="优化与部署",
+                        description="性能优化、构建部署",
+                        type="project",
+                        duration_minutes=60
+                    ),
+                ]
+            ),
+        ]
+    )
+
+
+def get_backend_path() -> LearningPathData:
+    return LearningPathData(
+        path=LearningPath.BACKEND,
+        title="后端开发",
+        description="Python后端开发与API设计",
+        icon="⚙️",
+        modules=[
+            LearningModule(
+                id="be-python",
+                title="Python 基础",
+                description="Python语言核心",
+                icon="🐍",
+                order=1,
+                status=ModuleStatus.NOT_STARTED,
+                lessons=[
+                    LearningLesson(
+                        id="be-py-01",
+                        title="Python语法基础",
+                        description="变量、类型、运算符",
+                        type="theory",
+                        duration_minutes=20
+                    ),
+                    LearningLesson(
+                        id="be-py-02",
+                        title="控制流与函数",
+                        description="条件、循环、函数定义",
+                        type="theory",
+                        duration_minutes=25
+                    ),
+                    LearningLesson(
+                        id="be-py-03",
+                        title="面向对象编程",
+                        description="类、继承、多态",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="be-py-04",
+                        title="异常处理与文件操作",
+                        description="try/except、文件读写",
+                        type="practice",
+                        duration_minutes=25
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="be-api",
+                title="REST API 设计",
+                description="FastAPI构建RESTful服务",
+                icon="🔌",
+                order=2,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="be-api-01",
+                        title="HTTP协议基础",
+                        description="请求方法、状态码、头部",
+                        type="theory",
+                        duration_minutes=20
+                    ),
+                    LearningLesson(
+                        id="be-api-02",
+                        title="FastAPI入门",
+                        description="路由、请求、响应",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="be-api-03",
+                        title="数据验证",
+                        description="Pydantic模型、请求验证",
+                        type="practice",
+                        duration_minutes=35
+                    ),
+                    LearningLesson(
+                        id="be-api-04",
+                        title="数据库集成",
+                        description="SQLAlchemy、数据库操作",
+                        type="practice",
+                        duration_minutes=45
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="be-system",
+                title="系统设计",
+                description="架构设计与最佳实践",
+                icon="🏗️",
+                order=3,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="be-sys-01",
+                        title="设计模式",
+                        description="常用设计模式与应用场景",
+                        type="theory",
+                        duration_minutes=40
+                    ),
+                    LearningLesson(
+                        id="be-sys-02",
+                        title="性能优化",
+                        description="缓存、异步、并发",
+                        type="theory",
+                        duration_minutes=35
+                    ),
+                    LearningLesson(
+                        id="be-sys-03",
+                        title="安全实践",
+                        description="认证、授权、数据安全",
+                        type="practice",
+                        duration_minutes=30
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="be-project",
+                title="实战项目",
+                description="构建完整的后端服务",
+                icon="🚀",
+                order=4,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="be-proj-01",
+                        title="项目架构",
+                        description="目录结构、配置管理",
+                        type="theory",
+                        duration_minutes=25
+                    ),
+                    LearningLesson(
+                        id="be-proj-02",
+                        title="核心功能实现",
+                        description="业务逻辑开发",
+                        type="project",
+                        duration_minutes=120
+                    ),
+                    LearningLesson(
+                        id="be-proj-03",
+                        title="测试与部署",
+                        description="单元测试、集成测试、部署",
+                        type="project",
+                        duration_minutes=60
+                    ),
+                ]
+            ),
+        ]
+    )
+
+
+def get_fullstack_path() -> LearningPathData:
+    return LearningPathData(
+        path=LearningPath.FULLSTACK,
+        title="全栈开发",
+        description="前端+后端全栈技能",
+        icon="🌐",
+        modules=[
+            LearningModule(
+                id="fs-web-basics",
+                title="Web基础",
+                description="HTML/CSS/JS核心",
+                icon="🌍",
+                order=1,
+                status=ModuleStatus.NOT_STARTED,
+                lessons=[
+                    LearningLesson(
+                        id="fs-web-01",
+                        title="HTML/CSS基础",
+                        description="网页结构与样式",
+                        type="theory",
+                        duration_minutes=25
+                    ),
+                    LearningLesson(
+                        id="fs-web-02",
+                        title="JavaScript基础",
+                        description="JS核心概念",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fs-web-03",
+                        title="DOM与事件",
+                        description="页面交互实现",
+                        type="practice",
+                        duration_minutes=35
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="fs-frontend",
+                title="前端框架",
+                description="Vue.js开发",
+                icon="💚",
+                order=2,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="fs-fe-01",
+                        title="Vue 3基础",
+                        description="组件、响应式、生命周期",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fs-fe-02",
+                        title="路由与状态",
+                        description="Vue Router、Pinia",
+                        type="practice",
+                        duration_minutes=40
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="fs-backend",
+                title="后端开发",
+                description="Python + FastAPI",
+                icon="⚙️",
+                order=3,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="fs-be-01",
+                        title="Python基础",
+                        description="语法、OOP、异常处理",
+                        type="theory",
+                        duration_minutes=30
+                    ),
+                    LearningLesson(
+                        id="fs-be-02",
+                        title="FastAPI开发",
+                        description="路由、验证、数据库",
+                        type="practice",
+                        duration_minutes=45
+                    ),
+                ]
+            ),
+            LearningModule(
+                id="fs-fullstack",
+                title="全栈实战",
+                description="构建完整应用",
+                icon="🚀",
+                order=4,
+                status=ModuleStatus.LOCKED,
+                lessons=[
+                    LearningLesson(
+                        id="fs-fs-01",
+                        title="前后端联调",
+                        description="API对接、数据流",
+                        type="practice",
+                        duration_minutes=60
+                    ),
+                    LearningLesson(
+                        id="fs-fs-02",
+                        title="部署上线",
+                        description="构建、部署、监控",
+                        type="project",
+                        duration_minutes=60
+                    ),
+                ]
+            ),
+        ]
+    )
+
+
+def _inject_lesson_content(path_data: LearningPathData):
+    """为路径中的所有课程注入Markdown内容"""
+    from .lesson_content import get_lesson_content
+    for module in path_data.modules:
+        for lesson in module.lessons:
+            if not lesson.content_markdown:
+                lesson.content_markdown = get_lesson_content(
+                    lesson.id, lesson.title, lesson.type, lesson.description
+                )
+
+
+def get_learning_path(path: LearningPath) -> LearningPathData:
+    if path == LearningPath.FRONTEND:
+        data = get_frontend_path()
+    elif path == LearningPath.BACKEND:
+        data = get_backend_path()
+    else:
+        data = get_fullstack_path()
+    _inject_lesson_content(data)
+    return data
+
+
+def get_all_paths() -> List[LearningPathData]:
+    paths = [
+        get_frontend_path(),
+        get_backend_path(),
+        get_fullstack_path()
+    ]
+    for p in paths:
+        _inject_lesson_content(p)
+    return paths
+
+
+def find_next_lesson(path_type: str, current_lesson_id: str, completed_lessons: List[str]) -> Optional[Dict]:
+    """根据当前课程,找到路径中下一个未完成的课程
+    
+    Args:
+        path_type: 路径类型 ('frontend' / 'backend' / 'fullstack')
+        current_lesson_id: 当前课程的 lesson_id
+        completed_lessons: 已完成的 lesson_id 列表
+    
+    Returns:
+        下一个课程的 {title, description, id} 或 None
+    """
+    try:
+        path_enum = LearningPath(path_type)
+        path_data = get_learning_path(path_enum)
+    except Exception:
+        return None
+
+    # 扁平化所有课程(保持顺序)
+    all_lessons: List[Dict] = []
+    for module in path_data.modules:
+        for lesson in module.lessons:
+            all_lessons.append({
+                "id": lesson.id,
+                "title": lesson.title,
+                "description": lesson.description,
+                "module_title": module.title,
+                "module_id": module.id,
+            })
+
+    # 找到当前课程在列表中的位置
+    current_idx = None
+    for i, l in enumerate(all_lessons):
+        if l["id"] == current_lesson_id:
+            current_idx = i
+            break
+
+    if current_idx is None:
+        return None
+
+    # 从当前课程之后找第一个未完成的
+    for i in range(current_idx + 1, len(all_lessons)):
+        if all_lessons[i]["id"] not in completed_lessons:
+            return all_lessons[i]
+
+    return None

+ 1259 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/lesson_content.py

@@ -0,0 +1,1259 @@
+"""
+课程内容 - 为每个课程提供丰富的Markdown内容
+"""
+from typing import Optional
+
+
+def get_lesson_content(lesson_id: str, title: str, lesson_type: str, description: str) -> str:
+    """根据课程ID返回对应的Markdown内容"""
+    
+    # 优先查找特定课程的内容
+    custom = _get_custom_content(lesson_id, title)
+    if custom:
+        return custom
+    
+    # 通用模板
+    return _get_generic_content(title, description, lesson_type)
+
+
+def _get_custom_content(lesson_id: str, title: str) -> Optional[str]:
+    """为特定课程ID返回个性化内容"""
+    
+    content_map = {
+        # ======== 前端 - HTML/CSS ========
+        "fe-html-01": """## 学习目标
+
+- 理解 HTML 文档的基本结构
+- 掌握 DOCTYPE、html、head、body 等核心标签
+- 了解 meta 标签的常见用法
+
+## HTML 文档的基本结构
+
+每个 HTML 文档都遵循一个基本的骨架结构:
+
+```html
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>文档标题</title>
+</head>
+<body>
+    <!-- 页面可见内容 -->
+    <h1>Hello, World!</h1>
+    <p>这是我的第一个网页。</p>
+</body>
+</html>
+```
+
+### 各部分作用
+
+| 标签 | 作用 |
+|------|------|
+| `<!DOCTYPE html>` | 声明文档类型为 HTML5 |
+| `<html>` | 文档的根元素,所有内容包含在内 |
+| `<head>` | 文档的元数据区(不可见) |
+| `<body>` | 文档的内容区(用户可见) |
+
+### `<head>` 中的常用标签
+
+- **`<title>`** — 浏览器标签页显示的标题,对 SEO 也很重要
+- **`<meta charset="UTF-8">`** — 设置字符编码,避免乱码
+- **`<meta name="viewport">`** — 移动端适配声明
+- **`<link>`** — 引入外部 CSS 文件
+- **`<style>`** — 内嵌 CSS 样式
+
+## 练习
+
+创建一个包含以下内容的 HTML 文件:
+1. 正确的文档结构声明
+2. 设置标题为"我的第一个网页"
+3. 在 body 中添加一个一级标题和一个段落
+
+> 💡 **提示**:使用 VS Code 新建 `.html` 文件后,输入 `!` 并按 Tab 可以快速生成 HTML5 骨架。
+""",
+        
+        "fe-html-02": """## 学习目标
+
+- 掌握常用 HTML 标签的语义和用法
+- 理解块级元素与行内元素的区别
+- 学会使用列表、图片、链接等构建内容
+
+## 常用标签
+
+### 文本标签
+
+```html
+<!-- 标题 -->
+<h1>一级标题</h1>
+<h2>二级标题</h2>
+<h3>三级标题</h3>
+
+<!-- 段落与文本 -->
+<p>这是一个段落。</p>
+<strong>加重要内容</strong>
+<em>强调文本</em>
+<br> <!-- 换行 -->
+```
+
+### 列表
+
+```html
+<!-- 无序列表 -->
+<ul>
+    <li>苹果</li>
+    <li>香蕉</li>
+    <li>橘子</li>
+</ul>
+
+<!-- 有序列表 -->
+<ol>
+    <li>第一步:打开编辑器</li>
+    <li>第二步:编写代码</li>
+    <li>第三步:保存文件</li>
+</ol>
+```
+
+### 链接与图片
+
+```html
+<!-- 链接 -->
+<a href="https://example.com" target="_blank">打开示例网站</a>
+
+<!-- 图片 -->
+<img src="logo.png" alt="网站Logo" width="200">
+```
+
+> ⚠️ **注意**:`<a>` 的 `target="_blank"` 会在新标签页打开链接。`<img>` 的 `alt` 属性用于图片加载失败时的替代文本,对无障碍访问很重要。
+
+## 块级 vs 行内元素
+
+| 类别 | 特点 | 例子 |
+|------|------|------|
+| **块级元素** | 独占一行,可设置宽高 | `div`, `h1`-`h6`, `p`, `ul`, `ol` |
+| **行内元素** | 不换行,宽高由内容决定 | `span`, `a`, `strong`, `em`, `img` |
+
+## 练习
+
+创建一个"我的兴趣爱好"页面,包含:
+1. 一个二级标题
+2. 一段自我介绍
+3. 一个无序列表列出你的兴趣(至少3项)
+4. 一张图片和指向你最喜欢网站的外部链接
+""",
+        
+        "fe-css-01": """## 学习目标
+
+- 理解 CSS 的作用和基本语法
+- 掌握三种选择器:元素、类、ID
+- 了解选择器的优先级规则
+
+## CSS 基本语法
+
+```css
+选择器 {
+    属性名: 属性值;
+    属性名: 属性值;
+}
+```
+
+### 引入 CSS 的方式
+
+**1. 外部样式表(推荐)**
+
+```html
+<link rel="stylesheet" href="style.css">
+```
+
+**2. 内部样式表**
+
+```html
+<style>
+    p {{ color: red; }}
+</style>
+```
+
+**3. 行内样式(不推荐)**
+
+```html
+<p style="color: red;">这段文字是红色</p>
+```
+
+## 三种基本选择器
+
+### 元素选择器
+
+选中所有该类型的标签:
+
+```css
+p {{ color: #333; }}
+h1 {{ font-size: 24px; }}
+```
+
+### 类选择器(`.`)
+
+选中所有带有该 class 的元素,可重复使用:
+
+```css
+.highlight {{ background-color: yellow; }}
+.card {{ border: 1px solid #ccc; }}
+```
+
+```html
+<p class="highlight">这段有高亮背景</p>
+<div class="card">这是一个卡片</div>
+```
+
+### ID 选择器(`#`)
+
+**唯一**,一个页面中每个 ID 只能使用一次:
+
+```css
+#header {{ height: 60px; }}
+#main-content {{ padding: 20px; }}
+```
+
+```html
+<div id="header">页面头部</div>
+```
+
+## 优先级(权重)
+
+当多个选择器冲突时,按权重决定:
+
+| 选择器 | 权重 | 示例 |
+|--------|------|------|
+| 元素选择器 | 最低 | `p`、`h1` |
+| 类选择器 | 中等 | `.card`、`.highlight` |
+| ID选择器 | 最高 | `#header` |
+| 行内样式 | 更高 | `style="..."` |
+| `!important` | 最高(慎用) | `color: red !important` |
+
+> 🧪 **实验**:给同一个元素同时设置类和 ID 样式,观察哪个生效。
+
+## 练习
+
+创建一个 HTML 页面并添加 CSS:
+1. 用元素选择器设置全局字体
+2. 用类选择器创建两个不同颜色的卡片
+3. 用 ID 选择器设置页面标题的样式
+""",
+
+        "fe-css-02": """## 学习目标
+
+- 理解盒模型的四个组成部分
+- 掌握 width/height、padding、border、margin 的用法
+- 学会使用 `box-sizing` 控制盒模型行为
+
+## 盒模型
+
+每个 HTML 元素都可以看作一个"盒子",从内到外包含:
+
+```
+┌─────────────────────────────────┐
+│          Margin (外边距)         │
+│  ┌───────────────────────────┐  │
+│  │      Border (边框)        │  │
+│  │  ┌─────────────────────┐  │  │
+│  │  │   Padding (内边距)   │  │  │
+│  │  │  ┌───────────────┐  │  │  │
+│  │  │  │   Content     │  │  │  │
+│  │  │  │   (内容区域)   │  │  │  │
+│  │  │  └───────────────┘  │  │  │
+│  │  └─────────────────────┘  │  │
+│  └───────────────────────────┘  │
+└─────────────────────────────────┘
+```
+
+### 代码示例
+
+```css
+.box {{
+    width: 200px;
+    padding: 20px;       /* 内容与边框之间的距离 */
+    border: 2px solid #333;  /* 边框 */
+    margin: 10px;        /* 盒子与其他元素的距离 */
+}}
+```
+
+### 盒模型的计算
+
+默认情况下(`box-sizing: content-box`):
+
+**实际宽度 = width + padding × 2 + border × 2**
+
+这意味着 `width: 200px` 加上 `padding: 20px` 和 `border: 2px` 后,实际占用的宽度是 **244px**!
+
+### `box-sizing: border-box`
+
+推荐做法——让 width 包含 padding 和 border:
+
+```css
+* {{
+    box-sizing: border-box;
+}}
+```
+
+这样 `width: 200px` 就是最终渲染宽度,padding 和 border 向内压缩。
+
+## display 属性
+
+| 值 | 行为 |
+|----|------|
+| `block` | 块级,独占一行 |
+| `inline` | 行内,不换行 |
+| `inline-block` | 行内但可设宽高 |
+| `none` | 隐藏元素,不占空间 |
+
+## 练习
+
+用 HTML + CSS 实现下图效果(描述:三个卡片并排,每个卡片有标题、文字、边框和间距):
+1. 三个 `div` 卡片横向排列
+2. 每个卡片有 1px 边框、16px 内边距
+3. 卡片之间用 margin 隔开
+4. 使用 `box-sizing: border-box`
+""",
+        
+        "fe-css-03": """## 学习目标
+
+- 理解 Flexbox 的核心概念:主轴与交叉轴
+- 掌握容器属性和项目属性
+- 能使用 Flexbox 实现常见布局
+
+## Flexbox 核心概念
+
+Flexbox 是一种一维布局模型,适合在**一行或一列**中排列元素。
+
+```css
+.container {{
+    display: flex;    /* 开启 Flexbox */
+}}
+```
+
+### 主轴与交叉轴
+
+- **主轴(main axis)** — `flex-direction` 决定的方向
+- **交叉轴(cross axis)** — 与主轴垂直的方向
+
+```
+flex-direction: row;       → 主轴水平,从左到右
+flex-direction: column;    → 主轴垂直,从上到下
+```
+
+## 容器属性
+
+```css
+.container {{
+    display: flex;
+    flex-direction: row;        /* row | column | row-reverse | column-reverse */
+    justify-content: center;    /* 主轴对齐方式 */
+    align-items: center;        /* 交叉轴对齐方式 */
+    flex-wrap: wrap;            /* 是否换行 */
+    gap: 16px;                  /* 项目间距(推荐) */
+}}
+```
+
+### justify-content
+
+```
+flex-start   ┃ [项目1][项目2][项目3]
+center       ┃    [项目1][项目2][项目3]
+space-between ┃ [项目1]        [项目2]        [项目3]
+space-around ┃  [项目1]    [项目2]    [项目3]
+```
+
+### align-items
+
+```
+stretch  ┃ 项目高度拉伸填满容器(默认)
+center   ┃ 项目在交叉轴居中
+flex-start ┃ 项目在交叉轴起始位置
+```
+
+## 项目属性
+
+```css
+.item {{
+    flex: 1;              /* 分配剩余空间的比例 */
+    align-self: center;   /* 单独对齐 */
+    order: 2;             /* 排列顺序(越小越前) */
+}}
+```
+
+## 常见布局示例
+
+### 水平居中
+
+```css
+.parent {{
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}}
+```
+
+### 两端对齐导航
+
+```css
+.nav {{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}}
+```
+
+### 响应式卡片网格
+
+```css
+.grid {{
+    display: flex;
+    flex-wrap: wrap;
+    gap: 20px;
+}}
+.card {{
+    flex: 1 1 300px;  /* 最小300px,自动换行 */
+}}
+```
+
+## 练习
+
+用 Flexbox 实现一个导航栏:
+- 左侧是 Logo
+- 中间是导航链接(首页、关于、服务、联系)
+- 右侧是登录/注册按钮
+- 垂直居中
+""",
+
+        # ======== 前端 - JavaScript ========
+        "fe-js-01": """## 学习目标
+
+- 理解 `let`、`const`、`var` 的区别
+- 掌握 JavaScript 的基本数据类型
+- 了解引用类型与基本类型的差异
+
+## 变量声明
+
+### `let` vs `const` vs `var`
+
+| 关键字 | 可修改 | 作用域 | 推荐 |
+|--------|--------|--------|------|
+| `const` | ❌ 不可重新赋值 | 块级作用域 | **默认选择** |
+| `let` | ✅ 可重新赋值 | 块级作用域 | 需要改变时使用 |
+| `var` | ✅ 可重新赋值 | 函数作用域 | ❌ 避免使用 |
+
+```javascript
+const PI = 3.14159;
+let count = 0;
+count = 1;  // ✅ 可以
+
+// 常见错误
+const user = {{ name: 'Alice' }};
+user.name = 'Bob';  // ✅ const 对象的内容可以修改
+user = {{ name: 'Bob' }};  // ❌ 不能重新赋值
+```
+
+## 基本数据类型
+
+| 类型 | 示例 | 说明 |
+|------|------|------|
+| `number` | `42`, `3.14` | 整数和浮点数 |
+| `string` | `'hello'`, `"world"` | 文本 |
+| `boolean` | `true`, `false` | 布尔值 |
+| `null` | `null` | 空值 |
+| `undefined` | `undefined` | 未定义 |
+| `symbol` | `Symbol()` | 唯一标识符 |
+
+```javascript
+const name = 'Alice';
+const age = 25;
+const isStudent = true;
+const score = null;
+let grade;  // undefined
+
+// typeof 运算符
+console.log(typeof name);   // "string"
+console.log(typeof age);    // "number"
+```
+
+### 动态类型
+
+JavaScript 是动态类型语言,变量可以随时改变类型:
+
+```javascript
+let value = 'hello';
+value = 42;       // 变成了 number
+value = true;     // 变成了 boolean
+```
+
+### 类型转换
+
+```javascript
+// 隐式转换
+const result = '5' - 2;    // 3 (字符串转数字)
+const msg = '结果是: ' + 42;  // "结果是: 42"
+
+// 显式转换
+Number('42')     // 42
+String(42)       // "42"
+Boolean(0)       // false
+Boolean('')      // false
+Boolean('hello') // true
+```
+
+> ⚠️ **常见陷阱**:`'5' + 2` 结果是 `'52'`(字符串拼接),而 `'5' - 2` 结果是 `3`(数值减法)。
+
+## 引用类型
+
+对象和数组是引用类型,赋值传递的是引用:
+
+```javascript
+const a = {{ name: 'Alice' }};
+const b = a;        // b 引用同一个对象
+b.name = 'Bob';
+console.log(a.name); // "Bob" — a 也被改了!
+```
+
+## 练习
+
+1. 用 `const` 定义一个对象,包含你的姓名、年龄和爱好
+2. 用 `let` 定义一个计数器,从 0 递增到 3
+3. 分别使用 `typeof` 检查 `null`、`[]`、`{{}}` 的类型
+""",
+
+        "fe-js-02": """## 学习目标
+
+- 掌握函数定义的多种方式
+- 理解作用域和闭包的概念
+- 了解箭头函数的特性
+
+## 函数定义
+
+### 函数声明 vs 函数表达式
+
+```javascript
+// 函数声明(会被提升)
+function add(a, b) {{
+    return a + b;
+}}
+
+// 函数表达式(不会被提升)
+const multiply = function(a, b) {{
+    return a * b;
+}};
+```
+
+### 箭头函数
+
+```javascript
+// 基本语法
+const add = (a, b) => a + b;
+const square = x => x * x;  // 一个参数可省略括号
+const greet = () => 'Hello!';
+
+// 多行需要 {{}} 和 return
+const sum = (a, b) => {{
+    const result = a + b;
+    return result;
+}};
+```
+
+### 箭头函数 vs 普通函数
+
+| 区别 | 普通函数 | 箭头函数 |
+|------|----------|----------|
+| `this` | 动态绑定 | 继承外层作用域 |
+| `arguments` | 有 | 没有 |
+| 作为构造函数 | ✅ | ❌ |
+
+## 作用域
+
+```javascript
+const global = '全局变量';
+
+function outer() {{
+    const outerVar = '外部函数变量';
+    
+    function inner() {{
+        const innerVar = '内部函数变量';
+        console.log(global);   // ✅ 可访问
+        console.log(outerVar); // ✅ 可访问
+    }}
+    
+    console.log(innerVar); // ❌ 不可访问
+}}
+```
+
+## 闭包
+
+函数 + 其被创建时所在的作用域环境的组合:
+
+```javascript
+function createCounter() {{
+    let count = 0;
+    return function() {{
+        count++;
+        return count;
+    }};
+}}
+
+const counter = createCounter();
+console.log(counter()); // 1
+console.log(counter()); // 2
+console.log(counter()); // 3
+```
+
+> 💡 **用途**:数据私有化、函数工厂、模块模式
+
+## 练习
+
+1. 写一个箭头函数 `isEven(n)` 判断数字是否为偶数
+2. 用闭包实现一个 `makeMultiplier(x)`,返回一个乘以 `x` 的函数
+3. 比较普通函数和箭头函数中 `this` 的行为差异
+""",
+
+        # ======== 前端 - Vue ========
+        "fe-vue-01": """## 学习目标
+
+- 理解 Vue 3 的核心概念
+- 掌握 `ref` 和 `reactive` 响应式 API
+- 学会使用模板语法
+
+## 什么是 Vue?
+
+Vue 是一个用于构建用户界面的**渐进式框架**。核心特性:
+
+- **声明式渲染** — 通过模板语法将数据绑定到 DOM
+- **响应式系统** — 数据变化自动更新视图
+- **组件化** — UI 拆分为独立的可复用组件
+
+## 创建 Vue 应用
+
+```javascript
+import {{ createApp, ref }} from 'vue'
+
+createApp({{
+    setup() {{
+        const count = ref(0)
+        const increment = () => count.value++
+        
+        return {{ count, increment }}
+    }}
+}}).mount('#app')
+```
+
+```html
+<div id="app">
+    <p>计数: {{ "{{ count }}" }}</p>
+    <button @click="increment">+1</button>
+</div>
+```
+
+## 响应式 API
+
+### `ref` — 基本响应式
+
+```javascript
+import {{ ref }} from 'vue'
+
+const count = ref(0)
+console.log(count.value) // 0
+
+count.value = 1
+```
+
+> 在模板中使用时自动解包,不需要 `.value`
+
+### `reactive` — 对象响应式
+
+```javascript
+import {{ reactive }} from 'vue'
+
+const user = reactive({{
+    name: 'Alice',
+    age: 25
+}})
+
+user.age = 26  // 直接修改,无需 .value
+```
+
+### `computed` — 计算属性
+
+```javascript
+import {{ ref, computed }} from 'vue'
+
+const price = ref(100)
+const quantity = ref(2)
+const total = computed(() => price.value * quantity.value)
+```
+
+## 模板语法
+
+```html
+<!-- 文本插值 -->
+<p>{{ "{{ message }}" }}</p>
+
+<!-- 属性绑定 -->
+<img :src="imageUrl">
+
+<!-- 事件绑定 -->
+<button @click="handleClick">点击</button>
+
+<!-- 条件渲染 -->
+<p v-if="isVisible">可见</p>
+
+<!-- 列表渲染 -->
+<li v-for="(item, index) in items" :key="index">{{ "{{ item }}" }}</li>
+
+<!-- 双向绑定 -->
+<input v-model="username">
+```
+
+## 练习
+
+创建一个简单的 Vue 应用:
+1. 用 `ref` 定义用户名和年龄
+2. 用 `computed` 计算是否成年(>=18)
+3. 在模板中展示这些数据
+""",
+
+        # ======== 后端 - Python ========
+        "be-py-01": """## 学习目标
+
+- 回顾 Python 基础语法
+- 理解列表推导式、装饰器等进阶用法
+- 掌握 Python 常见最佳实践
+
+## Python 基础回顾
+
+```python
+# 变量与类型
+name: str = "Python"
+version: float = 3.11
+
+# 列表推导式
+squares = [x**2 for x in range(10) if x % 2 == 0]
+
+# 字典操作
+user = {{"name": "Alice", "age": 25}}
+print(user.get("name", "Unknown"))
+```
+
+## 装饰器
+
+装饰器是一种在不修改原函数代码的情况下扩展其功能的方式:
+
+```python
+from functools import wraps
+
+def log_calls(func):
+    @wraps(func)
+    def wrapper(*args, **kwargs):
+        print(f"[LOG] 调用 {{func.__name__}}")
+        result = func(*args, **kwargs)
+        print(f"[LOG] {{func.__name__}} 返回 {{result}}")
+        return result
+    return wrapper
+
+@log_calls
+def add(a, b):
+    return a + b
+
+add(3, 5)
+# [LOG] 调用 add
+# [LOG] add 返回 8
+```
+
+## 上下文管理器
+
+```python
+# 使用 with 语句
+with open("file.txt", "r") as f:
+    content = f.read()
+
+# 自定义上下文管理器
+from contextlib import contextmanager
+
+@contextmanager
+def timer():
+    import time
+    start = time.time()
+    try:
+        yield
+    finally:
+        elapsed = time.time() - start
+        print(f"耗时: {{elapsed:.2f}}秒")
+
+with timer():
+    sum(range(1000000))
+```
+
+## 类型提示
+
+```python
+from typing import List, Optional, Dict
+
+def process_users(users: List[Dict[str, str]]) -> List[str]:
+    \"\"\"提取所有用户名\"\"\"
+    return [u.get("name", "") for u in users]
+
+def find_user(id: int) -> Optional[Dict[str, str]]:
+    \"\"\"根据 ID 查找用户\"\"\"
+    return None  # 未找到
+```
+
+## 练习
+
+1. 编写一个装饰器 `@retry(max_attempts=3)`,让函数在抛出异常时自动重试
+2. 写一个上下文管理器,用于测量代码块的执行时间
+3. 使用类型提示定义一个函数签名,处理用户列表数据
+""",
+
+        "be-py-02": """## 学习目标
+
+- 理解文件读写的基本模式
+- 掌握异常处理的正确方式
+- 学会使用标准库处理常见任务
+
+## 文件操作
+
+```python
+# 推荐方式:使用 with 语句
+with open("data.txt", "r", encoding="utf-8") as f:
+    content = f.read()
+
+# 逐行读取
+with open("data.txt", "r") as f:
+    for line in f:
+        print(line.strip())
+
+# 写入文件
+with open("output.txt", "w") as f:
+    f.write("Hello, World!\\n")
+```
+
+## 异常处理
+
+```python
+try:
+    result = 10 / 0
+except ZeroDivisionError as e:
+    print(f"不能除以零: {{e}}")
+except ValueError as e:
+    print(f"值错误: {{e}}")
+except Exception as e:
+    print(f"未知错误: {{e}}")
+else:
+    print("没有发生异常")
+finally:
+    print("总是执行")
+```
+
+### 自定义异常
+
+```python
+class ValidationError(Exception):
+    \"\"\"数据验证失败\"\"\"
+    pass
+
+def validate_age(age: int):
+    if age < 0 or age > 150:
+        raise ValidationError(f"无效年龄: {{age}}")
+```
+
+## 标准库常用模块
+
+```python
+import json
+import os
+import sys
+from datetime import datetime, timedelta
+
+# JSON 处理
+data = {{"name": "Alice", "age": 25}}
+json_str = json.dumps(data, ensure_ascii=False)
+parsed = json.loads(json_str)
+
+# 路径操作
+path = os.path.join("data", "subdir", "file.txt")
+print(os.path.exists(path))
+
+# 日期时间
+now = datetime.now()
+tomorrow = now + timedelta(days=1)
+print(now.strftime("%Y-%m-%d %H:%M:%S"))
+```
+
+## 练习
+
+1. 读取一个 JSON 配置文件,解析其中的设置
+2. 编写一个函数,安全地将字符串转换为整数,转换失败时返回 None
+3. 使用 `os` 模块遍历一个目录下的所有 Python 文件
+4. 使用 `datetime` 计算距离下个生日还有多少天
+""",
+
+        # ======== 后端 - API ========
+        "be-api-01": """## 学习目标
+
+- 理解 REST API 的核心原则
+- 掌握 HTTP 方法、状态码的正确使用
+- 学会设计资源导向的 URL
+
+## REST 核心原则
+
+REST(Representational State Transfer)是一种 API 设计风格:
+
+1. **资源导向** — URL 表示资源(名词),而不是操作(动词)
+2. **HTTP 方法表示操作** — GET/ POST/ PUT/ DELETE
+3. **无状态** — 每个请求包含所有必要信息
+4. **统一接口** — 一致的 URL 模式和响应格式
+
+## HTTP 方法
+
+| 方法 | 作用 | 幂等 | 请求体 |
+|------|------|------|--------|
+| GET | 获取资源 | ✅ | 通常无 |
+| POST | 创建资源 | ❌ | 有 |
+| PUT | 完整更新资源 | ✅ | 有 |
+| PATCH | 部分更新资源 | ❌ | 有 |
+| DELETE | 删除资源 | ✅ | 通常无 |
+
+## 状态码
+
+| 状态码 | 含义 | 使用场景 |
+|--------|------|----------|
+| 200 OK | 请求成功 | GET、PUT 成功 |
+| 201 Created | 创建成功 | POST 创建资源 |
+| 204 No Content | 成功无返回体 | DELETE 成功 |
+| 400 Bad Request | 客户端请求错误 | 参数校验失败 |
+| 404 Not Found | 资源不存在 | 查询不存在的 ID |
+| 409 Conflict | 资源冲突 | 创建重复资源 |
+| 500 Internal Server Error | 服务器错误 | 未预期的异常 |
+
+## URL 设计
+
+```python
+# ✅ 好的设计(资源导向)
+GET    /api/users              # 获取用户列表
+GET    /api/users/{{id}}        # 获取单个用户
+POST   /api/users              # 创建用户
+PUT    /api/users/{{id}}        # 更新用户
+DELETE /api/users/{{id}}        # 删除用户
+
+# ✅ 查询参数用于过滤/排序/分页
+GET /api/tasks?status=done&page=2&sort=created_at
+
+# ✅ 子资源
+GET /api/users/{{id}}/posts      # 用户的文章列表
+
+# ❌ 不好的设计(动词在 URL 中)
+GET    /api/getUser             # ❌
+POST   /api/createUser          # ❌
+POST   /api/deleteUser          # ❌
+```
+
+## FastAPI 快速开始
+
+```python
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+class Item(BaseModel):
+    name: str
+    price: float
+
+@app.get("/items")
+async def list_items():
+    return [{{"id": 1, "name": "Item 1", "price": 9.99}}]
+
+@app.post("/items")
+async def create_item(item: Item):
+    return {{"id": 2, **item.model_dump()}}
+```
+
+## 练习
+
+1. 为一个"博客系统"设计 RESTful API 端点(文章、评论、标签)
+2. 说明每个端点的 HTTP 方法、URL、请求参数和响应状态码
+""",
+
+        "be-api-02": """## 学习目标
+
+- 掌握 FastAPI 路由和参数处理
+- 理解请求体验证和响应模型
+- 学会使用依赖注入
+
+## FastAPI 路由进阶
+
+### 路径参数与查询参数
+
+```python
+from fastapi import FastAPI, Query, Path
+
+app = FastAPI()
+
+@app.get("/users/{{user_id}}")
+async def get_user(
+    user_id: int = Path(..., title="用户ID"),
+    include_details: bool = Query(False, title="是否包含详情")
+):
+    return {{"user_id": user_id, "details": include_details}}
+```
+
+### 请求体验证
+
+```python
+from pydantic import BaseModel, Field
+
+class CreateUserRequest(BaseModel):
+    username: str = Field(..., min_length=3, max_length=50)
+    email: str = Field(..., pattern=r"^[\\w.-]+@[\\w.-]+\\.\\w{{2,}}$")
+    age: int = Field(ge=0, le=150)
+
+@app.post("/users")
+async def create_user(user: CreateUserRequest):
+    return {{"message": "用户创建成功", "user": user}}
+```
+
+### 响应模型
+
+```python
+from typing import List
+from pydantic import BaseModel
+
+class UserResponse(BaseModel):
+    id: int
+    username: str
+    email: str
+
+@app.get("/users", response_model=List[UserResponse])
+async def list_users():
+    return [
+        UserResponse(id=1, username="alice", email="alice@example.com")
+    ]
+```
+
+## 依赖注入
+
+```python
+from fastapi import Depends, HTTPException
+
+def get_current_user(token: str = Query(...)):
+    if token != "secret":
+        raise HTTPException(status_code=401, detail="未授权")
+    return {{"id": 1, "username": "alice"}}
+
+@app.get("/profile")
+async def get_profile(user: dict = Depends(get_current_user)):
+    return user
+```
+
+## 错误处理
+
+```python
+from fastapi import HTTPException
+from fastapi.responses import JSONResponse
+
+@app.get("/items/{{item_id}}")
+async def get_item(item_id: int):
+    if item_id <= 0:
+        raise HTTPException(
+            status_code=400,
+            detail="无效的项目ID"
+        )
+    # ... 查找项目
+    return {{"id": item_id, "name": "Sample Item"}}
+```
+
+## 练习
+
+1. 创建一个 Todo 的 CRUD API(使用内存列表存储)
+2. 添加参数校验(标题非空、状态只能是 pending/done)
+3. 使用响应模型控制返回字段
+""",
+        
+        # ======== 全栈 - Web基础 ========
+        "fs-web-01": """## 学习目标
+
+- 理解 HTTP 协议的基本原理
+- 掌握请求-响应模型
+- 了解浏览器如何加载网页
+
+## HTTP 协议
+
+HTTP(超文本传输协议)是 Web 的基础通信协议。
+
+### 请求-响应模型
+
+```
+浏览器                         服务器
+  │                              │
+  ├── GET /index.html ──────────►│
+  │                              ├── 查找文件
+  │◄── 200 OK + HTML 内容 ──────┤
+  │                              │
+  ├── GET /style.css ───────────►│
+  │◄── 200 OK + CSS 内容 ───────┤
+  │                              │
+  ├── GET /app.js ──────────────►│
+  │◄── 200 OK + JS 内容 ────────┤
+```
+
+### HTTP 请求结构
+
+```
+GET /api/users HTTP/1.1
+Host: example.com
+Authorization: Bearer token123
+Content-Type: application/json
+
+{{"name": "Alice"}}
+```
+
+### HTTP 响应结构
+
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+{{"id": 1, "name": "Alice"}}
+```
+
+## 常见请求头
+
+| 请求头 | 作用 |
+|--------|------|
+| `Authorization` | 认证信息 |
+| `Content-Type` | 请求体格式 |
+| `Accept` | 期望的响应格式 |
+| `User-Agent` | 客户端标识 |
+
+## 常见响应头
+
+| 响应头 | 作用 |
+|--------|------|
+| `Content-Type` | 响应体格式 |
+| `Set-Cookie` | 设置 Cookie |
+| `Cache-Control` | 缓存策略 |
+| `Access-Control-Allow-Origin` | CORS 设置 |
+
+## 浏览器加载流程
+
+1. 解析 HTML → 构建 DOM 树
+2. 加载 CSS → 构建 CSSOM 树
+3. 合并为渲染树(Render Tree)
+4. 布局(Layout)→ 计算位置和大小
+5. 绘制(Paint)→ 渲染到屏幕
+
+> 💡 **关键**:CSS 会阻塞渲染,JavaScript 会阻塞解析。所以 `<script>` 标签通常放在 `</body>` 前。
+
+## 练习
+
+1. 用浏览器的开发者工具(F12)打开 Network 面板,访问一个网站,观察所有请求
+2. 识别每个请求的类型(文档、样式、脚本、图片)
+3. 查看一个 API 请求的请求头、响应头和响应体
+""",
+    }
+
+    # 检查是否有匹配的内容
+    if lesson_id in content_map:
+        return content_map[lesson_id]
+    
+    return None
+
+
+def _get_generic_content(title: str, description: str, lesson_type: str) -> str:
+    """为没有个性化内容的课程生成通用内容"""
+    
+    type_headers = {
+        "theory": "📖 理论学习",
+        "practice": "💻 实践练习",
+        "quiz": "❓ 知识测验",
+        "project": "🚀 项目实战",
+    }
+    
+    type_content = {
+        "theory": """
+## 概述
+
+**{title}** — {description}
+
+### 学习要点
+
+请跟随导师在对话中学习本课程的核心概念。导师将为你提供:
+
+1. 详细的概念讲解和原理解析
+2. 实际代码示例和最佳实践
+3. 常见陷阱和注意事项
+
+### 学习建议
+
+- 在 Chat 中发送"开始学习 {title}",导师会引导你学习
+- 遇到不理解的概念,随时追问
+- 完成学习后点击"完成课程"标记进度
+""",
+        "practice": """
+## 概述
+
+**{title}** — {description}
+
+### 练习内容
+
+这是一个实践课程,请在 Chat 中向导师请求练习任务。
+
+### 完成标准
+
+- [ ] 理解练习要求
+- [ ] 独立完成代码实现
+- [ ] 验证代码正确运行
+- [ ] 与导师讨论你的实现方案
+
+### 提示
+
+> 完成练习后,可以让导师 review 你的代码并提供改进建议。
+""",
+        "quiz": """
+## 概述
+
+**{title}** — {description}
+
+### 测验说明
+
+本课程包含知识测验,检验你对前面所学内容的理解。
+
+请在 Chat 中让导师为你生成测验题目。
+
+### 准备
+
+- 复习相关课程内容
+- 准备好回答概念题和代码题
+""",
+        "project": """
+## 概述
+
+**{title}** — {description}
+
+### 项目要求
+
+这是一个实战项目,综合运用所学知识完成一个完整的功能模块。
+
+请在 Chat 中让导师为你分配项目任务并提供指导。
+
+### 交付标准
+
+- [ ] 功能完整可用
+- [ ] 代码结构清晰
+- [ ] 包含错误处理
+- [ ] 有必要的注释
+""",
+    }
+    
+    header = type_headers.get(lesson_type, "📖 学习内容")
+    body = type_content.get(lesson_type, type_content["theory"])
+    
+    return f"""## {header}
+
+{body}
+"""

+ 116 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/app/services/llm_service.py

@@ -0,0 +1,116 @@
+"""LLM服务"""
+
+import json
+import os
+from pathlib import Path
+from hello_agents import HelloAgentsLLM
+from ..config import get_settings
+
+
+_llm_instance = None
+_llm_config_override = None
+LLM_CONFIG_FILE = Path(__file__).parent.parent.parent / "data" / "llm_config.json"
+
+
+def _load_llm_config():
+    """从文件加载运行时LLM配置(优先于.env)"""
+    global _llm_config_override
+    if LLM_CONFIG_FILE.exists():
+        try:
+            with open(LLM_CONFIG_FILE, "r", encoding="utf-8") as f:
+                _llm_config_override = json.load(f)
+            print(f"[LLM] 加载运行时配置: {_llm_config_override.get('base_url', '')}")
+        except Exception as e:
+            print(f"[LLM] 加载运行时配置失败: {e}")
+            _llm_config_override = None
+    else:
+        _llm_config_override = None
+
+
+def _build_llm():
+    """根据配置创建LLM实例(运行时配置优先,回退到.env)"""
+    settings = get_settings()
+    
+    # 优先使用运行时配置,空字符串回退到.env
+    if _llm_config_override:
+        model = _llm_config_override.get("model_id") or settings.deepseek_model_id
+        api_key = _llm_config_override.get("api_key") or settings.deepseek_api_key
+        base_url = _llm_config_override.get("base_url") or settings.deepseek_base_url
+    else:
+        model = settings.deepseek_model_id
+        api_key = settings.deepseek_api_key
+        base_url = settings.deepseek_base_url
+    
+    return HelloAgentsLLM(
+        model=model,
+        api_key=api_key,
+        base_url=base_url,
+    )
+
+
+def get_llm() -> HelloAgentsLLM:
+    """获取LLM实例"""
+    global _llm_instance
+    if _llm_instance is None:
+        _load_llm_config()
+        _llm_instance = _build_llm()
+        print(f"LLM服务初始化成功: {_llm_instance.model}")
+    return _llm_instance
+
+
+def reload_llm(config: dict = None) -> HelloAgentsLLM:
+    """重新配置并刷新LLM实例"""
+    global _llm_instance, _llm_config_override
+    
+    if config:
+        # 清洗空值:去掉空字符串的字段,保留有效值
+        clean = {}
+        for key in ("base_url", "model_id", "api_key"):
+            val = (config.get(key) or "").strip()
+            if val:
+                clean[key] = val
+        # 保存运行时配置
+        LLM_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
+        with open(LLM_CONFIG_FILE, "w", encoding="utf-8") as f:
+            json.dump(clean, f, ensure_ascii=False, indent=2)
+        _load_llm_config()
+    
+    # 重置实例
+    _llm_instance = None
+    new_llm = get_llm()
+    print(f"LLM服务已重新配置: {new_llm.model}")
+    return new_llm
+
+
+def reset_llm_config() -> HelloAgentsLLM:
+    """清除运行时配置,回退到.env默认值"""
+    global _llm_instance, _llm_config_override
+    
+    _llm_config_override = None
+    if LLM_CONFIG_FILE.exists():
+        LLM_CONFIG_FILE.unlink()
+        print("[LLM] 已清除运行时配置文件,回退到.env默认值")
+    
+    _llm_instance = None
+    return get_llm()
+
+
+def get_llm_config() -> dict:
+    """获取当前LLM配置(API密钥脱敏)"""
+    settings = get_settings()
+    
+    if _llm_config_override:
+        config = _llm_config_override.copy()
+    else:
+        config = {
+            "base_url": settings.deepseek_base_url,
+            "model_id": settings.deepseek_model_id,
+            "api_key": settings.deepseek_api_key,
+        }
+    
+    # API密钥脱敏
+    if config.get("api_key"):
+        key = config["api_key"]
+        config["api_key"] = key[:4] + "****" + key[-4:] if len(key) > 8 else "****"
+    
+    return config

+ 20 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/requirements.txt

@@ -0,0 +1,20 @@
+# FastAPI
+fastapi
+uvicorn[standard]
+pydantic
+pydantic-settings
+
+# AI Framework
+hello-agents
+
+# Environment
+python-dotenv
+
+# HTTP Client
+httpx
+
+# Logging
+loguru
+
+# CORS
+python-multipart

+ 19 - 0
Co-creation-projects/Max3753-Way_to_Engineer/backend/run.py

@@ -0,0 +1,19 @@
+"""启动脚本"""
+
+import os
+import uvicorn
+from app.config import get_settings
+
+if __name__ == '__main__':
+    settings = get_settings()
+    
+    # 若端口被 Windows 保留导致 WSAEACCES,设为 false 可绕过:$env:UVICORN_RELOAD='false'
+    reload_enabled = os.getenv("UVICORN_RELOAD", "").lower() not in ("false", "0", "no")
+    
+    uvicorn.run(
+        "app.api.main:app",
+        host=settings.host,
+        port=settings.port,
+        reload=reload_enabled,
+        log_level="info",
+    )

+ 24 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 13 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Way_to_Engineer</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 2703 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/package-lock.json

@@ -0,0 +1,2703 @@
+{
+  "name": "frontend",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "frontend",
+      "version": "0.0.0",
+      "dependencies": {
+        "@monaco-editor/loader": "^1.7.0",
+        "@types/dompurify": "^3.0.5",
+        "@vue-flow/background": "^1.3.2",
+        "@vue-flow/controls": "^1.1.3",
+        "@vue-flow/core": "^1.48.2",
+        "ant-design-vue": "^4.2.6",
+        "axios": "^1.18.1",
+        "chart.js": "^4.5.1",
+        "dompurify": "^3.4.12",
+        "highlight.js": "^11.11.1",
+        "marked": "^18.0.6",
+        "pinia": "^3.0.4",
+        "vue-chartjs": "^5.3.4",
+        "vue-router": "^5.1.0"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^6.0.7",
+        "typescript": "~6.0.2",
+        "vite": "^8.1.1"
+      }
+    },
+    "node_modules/@ant-design/colors": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
+      "integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^3.4.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz",
+      "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/icons-vue": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
+      "integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-svg": "^4.2.1"
+      },
+      "peerDependencies": {
+        "vue": ">=3.0.3"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "8.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.4.tgz",
+      "integrity": "sha512-YZ+FuIgkj7KrIb2a2X1XiY0QYgDxAbVbYP64SjwJzOK3euCsUerzenh2oqdsmKuPSlhzmFOOklnxzHAzXagvpw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^8.0.0-rc.4",
+        "@babel/types": "^8.0.0-rc.4",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "@types/jsesc": "^2.5.0",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
+      "version": "8.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.4.tgz",
+      "integrity": "sha512-dluR3v287dp6YPF57kyKKrHPKffUeuxH1zQcF1WD30TeFzWXhDiVi1U6PkqaDB0++H1PeCwRhmYl4DvoerlPIw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
+      "version": "8.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.4.tgz",
+      "integrity": "sha512-HTD3bskipk5MSm08twTW6832jzIXUhxMddy4NPPzIMuyMEsrs0ZgwAaMj5ubB5+6hMlUjDu17vNconEmwsmpYg==",
+      "license": "MIT",
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@babel/generator/node_modules/@babel/parser": {
+      "version": "8.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.4.tgz",
+      "integrity": "sha512-0S/1yefMa15N4i2v3t8Fw9pgMHhf2gF6Lc1UEXI96Ls6FNAjqvHHZouZ2ZS/deqLhbMFtmfVeFac6iTsvFbLwA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^8.0.0-rc.4"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@babel/generator/node_modules/@babel/types": {
+      "version": "8.0.0-rc.4",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.4.tgz",
+      "integrity": "sha512-bw30DV880P/VYtsjWWdoWmJpb9S2Vn1/PqayyccTELzRQ/HslIO7+BD9rNoZ4AAFOAjC1vrNeBCkAsyh6Ibfww==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^8.0.0-rc.4",
+        "@babel/helper-validator-identifier": "^8.0.0-rc.4"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+      "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+      "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.7"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+      "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+      "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.29.7",
+        "@babel/helper-validator-identifier": "^7.29.7"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "3.6.1",
+      "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+      "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@emnapi/core": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+      "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/wasi-threads": "1.2.2",
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/runtime": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+      "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emnapi/wasi-threads": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+      "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+      "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+      "license": "MIT"
+    },
+    "node_modules/@monaco-editor/loader": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+      "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
+      "license": "MIT",
+      "dependencies": {
+        "state-local": "^1.0.6"
+      }
+    },
+    "node_modules/@napi-rs/wasm-runtime": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+      "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@tybys/wasm-util": "^0.10.3"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/Brooooooklyn"
+      },
+      "peerDependencies": {
+        "@emnapi/core": "^1.7.1",
+        "@emnapi/runtime": "^1.7.1"
+      }
+    },
+    "node_modules/@oxc-project/types": {
+      "version": "0.139.0",
+      "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+      "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
+      "devOptional": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/Boshen"
+      }
+    },
+    "node_modules/@rolldown/binding-android-arm64": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+      "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-arm64": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+      "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-darwin-x64": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+      "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-freebsd-x64": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+      "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+      "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-gnu": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+      "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-arm64-musl": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+      "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+      "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-s390x-gnu": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+      "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-gnu": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+      "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-linux-x64-musl": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+      "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-openharmony-arm64": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+      "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-wasm32-wasi": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+      "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
+      "cpu": [
+        "wasm32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@emnapi/core": "1.11.1",
+        "@emnapi/runtime": "1.11.1",
+        "@napi-rs/wasm-runtime": "^1.1.6"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-arm64-msvc": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+      "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/binding-win32-x64-msvc": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+      "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+      "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+      "devOptional": true,
+      "license": "MIT"
+    },
+    "node_modules/@simonwep/pickr": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.8.2.tgz",
+      "integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^3.15.1",
+        "nanopop": "^2.1.0"
+      }
+    },
+    "node_modules/@tybys/wasm-util": {
+      "version": "0.10.3",
+      "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+      "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
+    "node_modules/@types/dompurify": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
+      "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/trusted-types": "*"
+      }
+    },
+    "node_modules/@types/jsesc": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
+      "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/trusted-types": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.20",
+      "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+      "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "6.0.7",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
+      "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rolldown/pluginutils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue-flow/background": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/@vue-flow/background/-/background-1.3.2.tgz",
+      "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@vue-flow/core": "^1.23.0",
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/@vue-flow/controls": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz",
+      "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@vue-flow/core": "^1.23.0",
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/@vue-flow/core": {
+      "version": "1.48.2",
+      "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.2.tgz",
+      "integrity": "sha512-raxhgKWE+G/mcEvXJjGFUDYW9rAI3GOtiHR3ZkNpwBWuIaCC1EYiBmKGwJOoNzVFgwO7COgErnK7i08i287AFA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vueuse/core": "^10.5.0",
+        "d3-drag": "^3.0.0",
+        "d3-interpolate": "^3.0.1",
+        "d3-selection": "^3.0.0",
+        "d3-zoom": "^3.0.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.0"
+      }
+    },
+    "node_modules/@vue-macros/common": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.3.tgz",
+      "integrity": "sha512-pphnexn8CDKugcA4TYSKlg1XanBYPbILST+eZK9ZGqG8sVbNR5L0kXEpRqs8+iSznosHt/Jo2k1FGl0tnWIpyg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-sfc": "^3.5.22",
+        "ast-kit": "^2.1.2",
+        "local-pkg": "^1.1.2",
+        "magic-string-ast": "^1.0.2",
+        "unplugin-utils": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/vue-macros"
+      },
+      "peerDependencies": {
+        "vue": "^2.7.0 || ^3.2.25"
+      },
+      "peerDependenciesMeta": {
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
+      "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.7",
+        "@vue/shared": "3.5.39",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
+      "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.39",
+        "@vue/shared": "3.5.39"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
+      "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.7",
+        "@vue/compiler-core": "3.5.39",
+        "@vue/compiler-dom": "3.5.39",
+        "@vue/compiler-ssr": "3.5.39",
+        "@vue/shared": "3.5.39",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.15",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
+      "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.39",
+        "@vue/shared": "3.5.39"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "8.1.5",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz",
+      "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^8.1.5"
+      }
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "8.1.5",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz",
+      "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^8.1.5",
+        "birpc": "^2.6.1",
+        "hookable": "^5.5.3",
+        "perfect-debounce": "^2.0.0"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "8.1.5",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz",
+      "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
+      "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/shared": "3.5.39"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
+      "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.5.39",
+        "@vue/shared": "3.5.39"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
+      "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/reactivity": "3.5.39",
+        "@vue/runtime-core": "3.5.39",
+        "@vue/shared": "3.5.39",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
+      "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.39",
+        "@vue/shared": "3.5.39"
+      },
+      "peerDependencies": {
+        "vue": "3.5.39"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
+      "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
+      "license": "MIT"
+    },
+    "node_modules/@vueuse/core": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+      "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.20",
+        "@vueuse/metadata": "10.11.1",
+        "@vueuse/shared": "10.11.1",
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/core/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+      "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "10.11.1",
+      "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+      "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+      "license": "MIT",
+      "dependencies": {
+        "vue-demi": ">=0.14.8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.17.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+      "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/ant-design-vue": {
+      "version": "4.2.6",
+      "resolved": "https://registry.npmjs.org/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
+      "integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^6.0.0",
+        "@ant-design/icons-vue": "^7.0.0",
+        "@babel/runtime": "^7.10.5",
+        "@ctrl/tinycolor": "^3.5.0",
+        "@emotion/hash": "^0.9.0",
+        "@emotion/unitless": "^0.8.0",
+        "@simonwep/pickr": "~1.8.0",
+        "array-tree-filter": "^2.1.0",
+        "async-validator": "^4.0.0",
+        "csstype": "^3.1.1",
+        "dayjs": "^1.10.5",
+        "dom-align": "^1.12.1",
+        "dom-scroll-into-view": "^2.0.0",
+        "lodash": "^4.17.21",
+        "lodash-es": "^4.17.15",
+        "resize-observer-polyfill": "^1.5.1",
+        "scroll-into-view-if-needed": "^2.2.25",
+        "shallow-equal": "^1.0.0",
+        "stylis": "^4.1.3",
+        "throttle-debounce": "^5.0.0",
+        "vue-types": "^3.0.0",
+        "warning": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design-vue"
+      },
+      "peerDependencies": {
+        "vue": ">=3.2.0"
+      }
+    },
+    "node_modules/array-tree-filter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
+      "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
+      "license": "MIT"
+    },
+    "node_modules/ast-kit": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
+      "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.28.5",
+        "pathe": "^2.0.3"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/ast-walker-scope": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz",
+      "integrity": "sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.2",
+        "@babel/types": "^7.29.0",
+        "ast-kit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
+      "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/chart.js": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+      "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+      "license": "MIT",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/compute-scroll-into-view": {
+      "version": "1.0.20",
+      "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
+      "integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
+      "license": "MIT"
+    },
+    "node_modules/confbox": {
+      "version": "0.2.4",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+      "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+      "license": "MIT"
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/core-js": {
+      "version": "3.49.0",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz",
+      "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+      "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-dispatch": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+      "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-drag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+      "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-selection": "3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+      "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+      "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-selection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+      "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+      "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-transition": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+      "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3",
+        "d3-dispatch": "1 - 3",
+        "d3-ease": "1 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-timer": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "peerDependencies": {
+        "d3-selection": "2 - 3"
+      }
+    },
+    "node_modules/d3-zoom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+      "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-dispatch": "1 - 3",
+        "d3-drag": "2 - 3",
+        "d3-interpolate": "1 - 3",
+        "d3-selection": "2 - 3",
+        "d3-transition": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.21",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
+      "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dom-align": {
+      "version": "1.12.4",
+      "resolved": "https://registry.npmjs.org/dom-align/-/dom-align-1.12.4.tgz",
+      "integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
+      "license": "MIT"
+    },
+    "node_modules/dom-scroll-into-view": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
+      "integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
+      "license": "MIT"
+    },
+    "node_modules/dompurify": {
+      "version": "3.4.12",
+      "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
+      "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
+      "license": "(MPL-2.0 OR Apache-2.0)",
+      "optionalDependencies": {
+        "@types/trusted-types": "^2.0.7"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/exsolve": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz",
+      "integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+      "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.4",
+        "mime-types": "^2.1.35"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/highlight.js": {
+      "version": "11.11.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+      "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.1.tgz",
+      "integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/lightningcss": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+      "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "devOptional": true,
+      "license": "MPL-2.0",
+      "dependencies": {
+        "detect-libc": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "lightningcss-android-arm64": "1.32.0",
+        "lightningcss-darwin-arm64": "1.32.0",
+        "lightningcss-darwin-x64": "1.32.0",
+        "lightningcss-freebsd-x64": "1.32.0",
+        "lightningcss-linux-arm-gnueabihf": "1.32.0",
+        "lightningcss-linux-arm64-gnu": "1.32.0",
+        "lightningcss-linux-arm64-musl": "1.32.0",
+        "lightningcss-linux-x64-gnu": "1.32.0",
+        "lightningcss-linux-x64-musl": "1.32.0",
+        "lightningcss-win32-arm64-msvc": "1.32.0",
+        "lightningcss-win32-x64-msvc": "1.32.0"
+      }
+    },
+    "node_modules/lightningcss-android-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+      "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-arm64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+      "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-darwin-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+      "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-freebsd-x64": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+      "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm-gnueabihf": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+      "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+      "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-arm64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+      "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-gnu": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+      "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-linux-x64-musl": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+      "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-arm64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+      "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/lightningcss-win32-x64-msvc": {
+      "version": "1.32.0",
+      "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+      "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MPL-2.0",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 12.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/local-pkg": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz",
+      "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "mlly": "^1.7.4",
+        "pkg-types": "^2.3.0",
+        "quansync": "^0.2.11"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/magic-string-ast": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
+      "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
+      "license": "MIT",
+      "dependencies": {
+        "magic-string": "^0.30.19"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/marked": {
+      "version": "18.0.6",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.6.tgz",
+      "integrity": "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/mlly": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+      "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.16.0",
+        "pathe": "^2.0.3",
+        "pkg-types": "^1.3.1",
+        "ufo": "^1.6.3"
+      }
+    },
+    "node_modules/mlly/node_modules/confbox": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+      "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+      "license": "MIT"
+    },
+    "node_modules/mlly/node_modules/pkg-types": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+      "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.1.8",
+        "mlly": "^1.7.4",
+        "pathe": "^2.0.1"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.16",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+      "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/nanopop": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.4.2.tgz",
+      "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
+      "license": "MIT"
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+      "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/@vue/devtools-api": {
+      "version": "7.7.10",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz",
+      "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.10"
+      }
+    },
+    "node_modules/pinia/node_modules/@vue/devtools-kit": {
+      "version": "7.7.10",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz",
+      "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.10",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/pinia/node_modules/@vue/devtools-shared": {
+      "version": "7.7.10",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz",
+      "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/pinia/node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/pkg-types": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
+      "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
+      "license": "MIT",
+      "dependencies": {
+        "confbox": "^0.2.4",
+        "exsolve": "^1.0.8",
+        "pathe": "^2.0.3"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.19",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
+      "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.12",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/quansync": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+      "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/antfu"
+        },
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/sxzz"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rolldown": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+      "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "@oxc-project/types": "=0.139.0",
+        "@rolldown/pluginutils": "^1.0.0"
+      },
+      "bin": {
+        "rolldown": "bin/cli.mjs"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "optionalDependencies": {
+        "@rolldown/binding-android-arm64": "1.1.5",
+        "@rolldown/binding-darwin-arm64": "1.1.5",
+        "@rolldown/binding-darwin-x64": "1.1.5",
+        "@rolldown/binding-freebsd-x64": "1.1.5",
+        "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+        "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+        "@rolldown/binding-linux-arm64-musl": "1.1.5",
+        "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+        "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+        "@rolldown/binding-linux-x64-gnu": "1.1.5",
+        "@rolldown/binding-linux-x64-musl": "1.1.5",
+        "@rolldown/binding-openharmony-arm64": "1.1.5",
+        "@rolldown/binding-wasm32-wasi": "1.1.5",
+        "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+        "@rolldown/binding-win32-x64-msvc": "1.1.5"
+      }
+    },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "2.2.31",
+      "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
+      "integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^1.0.20"
+      }
+    },
+    "node_modules/scule": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+      "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+      "license": "MIT"
+    },
+    "node_modules/shallow-equal": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
+      "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
+      "license": "MIT"
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/state-local": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+      "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
+      "license": "MIT"
+    },
+    "node_modules/stylis": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
+      "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
+      "license": "MIT"
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD",
+      "optional": true
+    },
+    "node_modules/typescript": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+      "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/ufo": {
+      "version": "1.6.4",
+      "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+      "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
+      "license": "MIT"
+    },
+    "node_modules/unplugin": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
+      "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/remapping": "^2.3.5",
+        "picomatch": "^4.0.4",
+        "webpack-virtual-modules": "^0.6.2"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "peerDependencies": {
+        "@farmfe/core": "*",
+        "@rspack/core": "*",
+        "bun-types-no-globals": "*",
+        "esbuild": "*",
+        "rolldown": "*",
+        "rollup": "*",
+        "unloader": "*",
+        "vite": "*",
+        "webpack": "*"
+      },
+      "peerDependenciesMeta": {
+        "@farmfe/core": {
+          "optional": true
+        },
+        "@rspack/core": {
+          "optional": true
+        },
+        "bun-types-no-globals": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "rolldown": {
+          "optional": true
+        },
+        "rollup": {
+          "optional": true
+        },
+        "unloader": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/unplugin-utils": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz",
+      "integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sxzz"
+      }
+    },
+    "node_modules/vite": {
+      "version": "8.1.4",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
+      "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
+      "devOptional": true,
+      "license": "MIT",
+      "dependencies": {
+        "lightningcss": "^1.32.0",
+        "picomatch": "^4.0.5",
+        "postcss": "^8.5.16",
+        "rolldown": "~1.1.4",
+        "tinyglobby": "^0.2.17"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "@vitejs/devtools": "^0.3.0",
+        "esbuild": "^0.27.0 || ^0.28.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "@vitejs/devtools": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.39",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
+      "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.39",
+        "@vue/compiler-sfc": "3.5.39",
+        "@vue/runtime-dom": "3.5.39",
+        "@vue/server-renderer": "3.5.39",
+        "@vue/shared": "3.5.39"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-chartjs": {
+      "version": "5.3.4",
+      "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz",
+      "integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==",
+      "license": "MIT",
+      "peerDependencies": {
+        "chart.js": "^4.1.1",
+        "vue": "^3.0.0-0 || ^2.7.0"
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
+      "integrity": "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/generator": "^8.0.0-rc.4",
+        "@vue-macros/common": "^3.1.1",
+        "@vue/devtools-api": "^8.1.2",
+        "ast-walker-scope": "^0.9.0",
+        "chokidar": "^5.0.0",
+        "json5": "^2.2.3",
+        "local-pkg": "^1.1.2",
+        "magic-string": "^0.30.21",
+        "mlly": "^1.8.2",
+        "muggle-string": "^0.4.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "scule": "^1.3.0",
+        "tinyglobby": "^0.2.16",
+        "unplugin": "^3.0.0",
+        "unplugin-utils": "^0.3.1",
+        "yaml": "^2.9.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@pinia/colada": ">=0.21.2",
+        "@vue/compiler-sfc": "^3.5.34",
+        "pinia": "^3.0.4",
+        "vite": "^7.0.0 || ^8.0.0",
+        "vue": "^3.5.34"
+      },
+      "peerDependenciesMeta": {
+        "@pinia/colada": {
+          "optional": true
+        },
+        "@vue/compiler-sfc": {
+          "optional": true
+        },
+        "pinia": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz",
+      "integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "3.0.1"
+      },
+      "engines": {
+        "node": ">=10.15.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/warning": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
+      "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.0.0"
+      }
+    },
+    "node_modules/webpack-virtual-modules": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+      "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+      "license": "MIT"
+    },
+    "node_modules/yaml": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+      "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/eemeli"
+      }
+    }
+  }
+}

+ 32 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/package.json

@@ -0,0 +1,32 @@
+{
+  "name": "frontend",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc && vite build",
+    "preview": "vite preview"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^6.0.7",
+    "typescript": "~6.0.2",
+    "vite": "^8.1.1"
+  },
+  "dependencies": {
+    "@monaco-editor/loader": "^1.7.0",
+    "@types/dompurify": "^3.0.5",
+    "@vue-flow/background": "^1.3.2",
+    "@vue-flow/controls": "^1.1.3",
+    "@vue-flow/core": "^1.48.2",
+    "ant-design-vue": "^4.2.6",
+    "axios": "^1.18.1",
+    "chart.js": "^4.5.1",
+    "dompurify": "^3.4.12",
+    "highlight.js": "^11.11.1",
+    "marked": "^18.0.6",
+    "pinia": "^3.0.4",
+    "vue-chartjs": "^5.3.4",
+    "vue-router": "^5.1.0"
+  }
+}

File diff suppressed because it is too large
+ 0 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/public/favicon.svg


+ 24 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/public/icons.svg

@@ -0,0 +1,24 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+  <symbol id="bluesky-icon" viewBox="0 0 16 17">
+    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
+    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
+  </symbol>
+  <symbol id="discord-icon" viewBox="0 0 20 19">
+    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
+  </symbol>
+  <symbol id="documentation-icon" viewBox="0 0 21 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
+  </symbol>
+  <symbol id="github-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
+  </symbol>
+  <symbol id="social-icon" viewBox="0 0 20 20">
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
+    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
+  </symbol>
+  <symbol id="x-icon" viewBox="0 0 19 19">
+    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
+  </symbol>
+</svg>

+ 208 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/App.vue

@@ -0,0 +1,208 @@
+<template>
+  <div v-if="!authStore.isLoggedIn" class="login-view">
+    <router-view />
+  </div>
+  <div v-else class="app-layout" :data-theme="themeStore.theme">
+    <nav class="app-nav">
+      <div class="nav-brand">{{ langStore.t('nav.brand') }}</div>
+      <div class="nav-links">
+        <router-link to="/" class="nav-link">{{ langStore.t('nav.chat') }}</router-link>
+        <router-link to="/learning" class="nav-link">{{ langStore.t('nav.learning') }}</router-link>
+        <router-link to="/dashboard" class="nav-link">{{ langStore.t('nav.dashboard') }}</router-link>
+        <router-link to="/orchestration" class="nav-link">{{ langStore.t('nav.orchestration') }}</router-link>
+      </div>
+      <div class="nav-user">
+        <button class="icon-btn" @click="showSettings = true" title="LLM 配置">
+          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <circle cx="12" cy="12" r="3"/>
+            <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
+          </svg>
+        </button>
+        <button class="theme-toggle" @click="themeStore.toggleTheme" :title="themeStore.theme === 'dark' ? langStore.t('theme.light') : langStore.t('theme.dark')">
+          {{ themeStore.theme === 'dark' ? '☀️' : '🌙' }}
+        </button>
+        <button class="lang-toggle" @click="langStore.toggleLang" :title="langStore.t('nav.switchLang')">
+          {{ langStore.lang === 'zh' ? 'EN' : '中' }}
+        </button>
+        <span class="user-id">{{ authStore.userId }}</span>
+        <button class="logout-btn" @click="handleLogout" :title="langStore.t('nav.logout')">
+          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
+            <polyline points="16 17 21 12 16 7"/>
+            <line x1="21" y1="12" x2="9" y2="12"/>
+          </svg>
+        </button>
+      </div>
+    </nav>
+    <router-view v-slot="{ Component }">
+      <keep-alive>
+        <component :is="Component" />
+      </keep-alive>
+    </router-view>
+    <SettingsModal :visible="showSettings" @close="showSettings = false" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useAuthStore } from './stores/authStore'
+import { useThemeStore } from './stores/themeStore'
+import { useLangStore } from './stores/langStore'
+import { useRouter } from 'vue-router'
+import SettingsModal from './components/SettingsModal.vue'
+
+const showSettings = ref(false)
+const authStore = useAuthStore()
+const themeStore = useThemeStore()
+const langStore = useLangStore()
+const router = useRouter()
+
+// Initialize theme on app start
+themeStore.init()
+
+const handleLogout = () => {
+  authStore.logout()
+  router.push('/login')
+}
+</script>
+
+<style>
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.app-layout {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+}
+
+.app-nav {
+  display: flex;
+  align-items: center;
+  padding: 0 24px;
+  height: 56px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  z-index: 100;
+}
+
+.nav-brand {
+  font-size: 18px;
+  font-weight: 600;
+  margin-right: 32px;
+}
+
+.nav-links {
+  display: flex;
+  gap: 8px;
+}
+
+.nav-link {
+  padding: 8px 16px;
+  color: rgba(255, 255, 255, 0.8);
+  text-decoration: none;
+  border-radius: 8px;
+  transition: all 0.2s;
+  font-size: 14px;
+}
+
+.nav-link:hover {
+  background: rgba(255, 255, 255, 0.2);
+  color: white;
+}
+
+.nav-link.router-link-active {
+  background: rgba(255, 255, 255, 0.25);
+  color: white;
+  font-weight: 500;
+}
+
+.nav-user {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-left: auto;
+}
+
+.user-id {
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.8);
+  background: rgba(255, 255, 255, 0.15);
+  padding: 4px 12px;
+  border-radius: 12px;
+}
+
+.logout-btn {
+  background: none;
+  border: none;
+  font-size: 16px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 6px;
+  transition: background 0.2s;
+}
+
+.logout-btn:hover {
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.lang-toggle {
+  background: rgba(255, 255, 255, 0.15);
+  border: 1px solid rgba(255, 255, 255, 0.3);
+  color: white;
+  font-size: 13px;
+  font-weight: 600;
+  cursor: pointer;
+  padding: 4px 10px;
+  border-radius: 6px;
+  transition: background 0.2s;
+  letter-spacing: 0.5px;
+}
+
+.lang-toggle:hover {
+  background: rgba(255, 255, 255, 0.3);
+}
+
+.icon-btn {
+  background: none;
+  border: none;
+  font-size: 16px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 6px;
+  transition: background 0.2s;
+  color: rgba(255, 255, 255, 0.85);
+  display: flex;
+  align-items: center;
+}
+
+.icon-btn:hover {
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.theme-toggle {
+  background: none;
+  border: none;
+  font-size: 16px;
+  cursor: pointer;
+  padding: 4px 8px;
+  border-radius: 6px;
+  transition: background 0.2s;
+}
+
+.theme-toggle:hover {
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.login-view {
+  height: 100vh;
+}
+</style>

BIN
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/assets/hero.png


+ 454 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/AgentConfigPanel.vue

@@ -0,0 +1,454 @@
+<template>
+  <Teleport to="body">
+    <Transition name="panel-overlay">
+      <div v-if="visible" class="config-overlay" @click.self="$emit('close')"></div>
+    </Transition>
+    <Transition name="panel-slide">
+      <div v-if="visible && agent" class="config-panel">
+        <div class="panel-header" :class="agent.type">
+          <div class="header-top">
+            <div class="header-identity">
+              <span class="panel-icon">{{ agent.icon }}</span>
+              <div class="header-text">
+                <h2>{{ agent.name }}</h2>
+                <span class="panel-type">{{ agent.type }}</span>
+              </div>
+            </div>
+            <button class="close-btn" @click="$emit('close')" title="Close">
+              <span class="close-icon">&times;</span>
+            </button>
+          </div>
+          <div class="status-row">
+            <span class="status-badge" :class="agent.status">
+              <span class="status-dot"></span>
+              {{ statusLabel }}
+            </span>
+            <span class="message-count">{{ agent.messageCount }} messages</span>
+          </div>
+        </div>
+
+        <div class="panel-body">
+          <section class="config-section">
+            <h3 class="section-title">Description</h3>
+            <p class="section-text">{{ agent.description }}</p>
+          </section>
+
+          <section class="config-section">
+            <h3 class="section-title">System Prompt</h3>
+            <div class="prompt-block">
+              <pre class="prompt-text">{{ systemPrompt }}</pre>
+            </div>
+          </section>
+
+          <section class="config-section">
+            <h3 class="section-title">Activity</h3>
+            <div class="activity-grid">
+              <div class="activity-card">
+                <span class="activity-value">{{ agent.messageCount }}</span>
+                <span class="activity-label">Messages</span>
+              </div>
+              <div class="activity-card">
+                <span class="activity-value">{{ lastUsedDisplay }}</span>
+                <span class="activity-label">Last Active</span>
+              </div>
+            </div>
+          </section>
+
+          <section v-if="agent.lastMessage" class="config-section">
+            <h3 class="section-title">Last Conversation</h3>
+            <div class="last-message">
+              <span class="msg-preview">{{ agent.lastMessage }}</span>
+            </div>
+          </section>
+        </div>
+
+        <div class="panel-footer">
+          <button class="action-btn primary" @click="startConversation">
+            <span class="btn-icon">💬</span>
+            Start Conversation
+          </button>
+          <button class="action-btn secondary" @click="$emit('close')">
+            Close
+          </button>
+        </div>
+      </div>
+    </Transition>
+  </Teleport>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useRouter } from 'vue-router'
+import type { AgentState } from '../stores/agentStore'
+
+const props = defineProps<{
+  visible: boolean
+  agent: AgentState | null
+}>()
+
+const emit = defineEmits<{
+  (e: 'close'): void
+  (e: 'start-chat', type: string): void
+}>()
+
+const router = useRouter()
+
+const SYSTEM_PROMPTS: Record<string, string> = {
+  orchestrator: 'You are the orchestrator agent. Your role is to analyze user requests and route them to the most appropriate specialist agent. You coordinate the workflow between tutors, debuggers, reviewers, architects, and coaches to provide comprehensive assistance.',
+  tutor: 'You are a programming tutor. Your role is to explain programming concepts clearly and concisely. Use examples, analogies, and step-by-step explanations. Adapt your teaching style to the student\'s level. Encourage understanding over memorization.',
+  debug: 'You are a debugging specialist. Your role is to analyze error messages, trace bugs, and provide clear fixes. Always explain the root cause before the solution. Help users understand why the bug occurred and how to prevent similar issues.',
+  review: 'You are a code reviewer. Your role is to analyze code for quality, best practices, security issues, and potential improvements. Be constructive and specific in your feedback. Prioritize issues by severity.',
+  arch: 'You are a software architect. Your role is to help design system architecture, make technology choices, and plan scalable solutions. Consider trade-offs and explain the reasoning behind architectural decisions.',
+  coach: 'You are a learning coach. Your role is to guide users through their learning journey. Create personalized learning paths, track progress, and provide encouragement. Focus on practical skills and real-world application.',
+}
+
+const statusLabel = computed(() => {
+  const labels: Record<string, string> = {
+    active: 'Active',
+    processing: 'Processing',
+    idle: 'Idle',
+    error: 'Error',
+  }
+  return labels[props.agent?.status || 'idle'] || 'Idle'
+})
+
+const systemPrompt = computed(() => {
+  if (!props.agent) return ''
+  return SYSTEM_PROMPTS[props.agent.type] || 'No system prompt configured.'
+})
+
+const lastUsedDisplay = computed(() => {
+  if (!props.agent?.lastUsedAt) return 'Never'
+  const diff = Date.now() - props.agent.lastUsedAt
+  if (diff < 60000) return 'Just now'
+  if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`
+  if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`
+  return `${Math.floor(diff / 86400000)}d ago`
+})
+
+const startConversation = () => {
+  if (props.agent) {
+    emit('start-chat', props.agent.type)
+    router.push('/')
+  }
+}
+</script>
+
+<style scoped>
+/* ─── Overlay ─── */
+.config-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.35);
+  z-index: 900;
+  backdrop-filter: blur(2px);
+}
+
+/* ─── Panel ─── */
+.config-panel {
+  position: fixed;
+  top: 56px;
+  right: 0;
+  bottom: 0;
+  width: 380px;
+  background: var(--bg-secondary, #ffffff);
+  box-shadow: -4px 0 24px rgba(0, 0, 0, 0.12);
+  z-index: 910;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+/* ─── Header ─── */
+.panel-header {
+  padding: 24px 24px 16px;
+  color: white;
+  position: relative;
+}
+
+.panel-header.orchestrator { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
+.panel-header.tutor { background: linear-gradient(135deg, #764ba2 0%, #9b59b6 100%); }
+.panel-header.debug { background: linear-gradient(135deg, #f5222d 0%, #cf1322 100%); }
+.panel-header.review { background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); }
+.panel-header.arch { background: linear-gradient(135deg, #fa8c16 0%, #d46b08 100%); }
+.panel-header.coach { background: linear-gradient(135deg, #722ed1 0%, #531dab 100%); }
+
+.header-top {
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.header-identity {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+}
+
+.panel-icon {
+  font-size: 32px;
+  width: 52px;
+  height: 52px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255, 255, 255, 0.2);
+  border-radius: 14px;
+}
+
+.header-text h2 {
+  margin: 0;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.panel-type {
+  font-size: 12px;
+  opacity: 0.75;
+  text-transform: uppercase;
+  letter-spacing: 1px;
+}
+
+.close-btn {
+  background: rgba(255, 255, 255, 0.2);
+  border: none;
+  color: white;
+  width: 32px;
+  height: 32px;
+  border-radius: 8px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: background 0.2s;
+  flex-shrink: 0;
+}
+
+.close-btn:hover {
+  background: rgba(255, 255, 255, 0.35);
+}
+
+.close-icon {
+  font-size: 20px;
+  line-height: 1;
+}
+
+.status-row {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.status-badge {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 12px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 500;
+  background: rgba(255, 255, 255, 0.2);
+}
+
+.status-badge .status-dot {
+  width: 7px;
+  height: 7px;
+  border-radius: 50%;
+  background: rgba(255, 255, 255, 0.6);
+}
+
+.status-badge.active .status-dot {
+  background: #b7eb8f;
+  animation: dot-pulse 1.5s infinite;
+}
+
+.status-badge.processing .status-dot {
+  background: #91d5ff;
+  animation: dot-pulse 1s infinite;
+}
+
+.status-badge.idle .status-dot {
+  background: rgba(255, 255, 255, 0.4);
+}
+
+.status-badge.error .status-dot {
+  background: #ffa39e;
+}
+
+.message-count {
+  font-size: 12px;
+  opacity: 0.8;
+}
+
+@keyframes dot-pulse {
+  0%, 100% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(1.4); }
+}
+
+/* ─── Body ─── */
+.panel-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px 24px;
+}
+
+.config-section {
+  margin-bottom: 24px;
+}
+
+.config-section:last-child {
+  margin-bottom: 0;
+}
+
+.section-title {
+  margin: 0 0 10px 0;
+  font-size: 11px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 1.2px;
+  color: var(--text-muted, #999);
+}
+
+.section-text {
+  margin: 0;
+  font-size: 14px;
+  line-height: 1.6;
+  color: var(--text-primary, #333);
+}
+
+.prompt-block {
+  background: var(--bg-tertiary, #f0f2f5);
+  border-radius: 10px;
+  padding: 14px 16px;
+  border: 1px solid var(--border-color, #e8e8e8);
+}
+
+.prompt-text {
+  margin: 0;
+  font-size: 12px;
+  line-height: 1.7;
+  color: var(--text-secondary, #666);
+  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.activity-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 10px;
+}
+
+.activity-card {
+  background: var(--bg-tertiary, #f0f2f5);
+  border-radius: 10px;
+  padding: 14px 16px;
+  text-align: center;
+  border: 1px solid var(--border-color, #e8e8e8);
+}
+
+.activity-value {
+  display: block;
+  font-size: 20px;
+  font-weight: 700;
+  color: var(--text-primary, #333);
+  margin-bottom: 2px;
+}
+
+.activity-label {
+  display: block;
+  font-size: 11px;
+  color: var(--text-muted, #999);
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.last-message {
+  background: var(--bg-tertiary, #f0f2f5);
+  border-radius: 10px;
+  padding: 12px 16px;
+  border: 1px solid var(--border-color, #e8e8e8);
+}
+
+.msg-preview {
+  font-size: 13px;
+  color: var(--text-secondary, #666);
+  line-height: 1.5;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+/* ─── Footer ─── */
+.panel-footer {
+  padding: 16px 24px;
+  border-top: 1px solid var(--border-color, #e8e8e8);
+  display: flex;
+  gap: 10px;
+}
+
+.action-btn {
+  flex: 1;
+  padding: 10px 16px;
+  border-radius: 10px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  border: none;
+  transition: all 0.2s;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+}
+
+.action-btn.primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.action-btn.primary:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.action-btn.secondary {
+  background: var(--bg-tertiary, #f0f2f5);
+  color: var(--text-secondary, #666);
+  border: 1px solid var(--border-color, #e8e8e8);
+}
+
+.action-btn.secondary:hover {
+  background: var(--border-color, #e8e8e8);
+}
+
+.btn-icon {
+  font-size: 16px;
+}
+
+/* ─── Transitions ─── */
+.panel-overlay-enter-active,
+.panel-overlay-leave-active {
+  transition: opacity 0.3s ease;
+}
+
+.panel-overlay-enter-from,
+.panel-overlay-leave-to {
+  opacity: 0;
+}
+
+.panel-slide-enter-active {
+  transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+.panel-slide-leave-active {
+  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.panel-slide-enter-from,
+.panel-slide-leave-to {
+  transform: translateX(100%);
+}
+</style>

+ 269 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/AgentNode.vue

@@ -0,0 +1,269 @@
+<template>
+  <div :class="['agent-node', type, { active: status === 'active' || status === 'processing' }]" @click="$emit('node-click', type)">
+    <div class="node-header">
+      <span class="node-icon">{{ icon }}</span>
+      <span class="node-title">{{ label }}</span>
+      <span v-if="status === 'active' || status === 'processing'" class="header-pulse"></span>
+    </div>
+    <div class="node-body">
+      <p class="node-desc">{{ description }}</p>
+      <div class="node-status">
+        <span class="status-dot" :class="status"></span>
+        <span class="status-text">{{ statusText }}</span>
+      </div>
+      <div class="node-stats">
+        <span class="stat">
+          <span class="stat-icon">💬</span>
+          <span class="stat-value">{{ messageCount }}</span>
+        </span>
+        <span class="stat">
+          <span class="stat-icon">🕐</span>
+          <span class="stat-value">{{ lastUsedText }}</span>
+        </span>
+      </div>
+    </div>
+    <div class="node-handles">
+      <div class="handle handle-left"></div>
+      <div class="handle handle-right"></div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from 'vue'
+
+defineEmits<{
+  (e: 'node-click', type: string): void
+}>()
+
+const props = withDefaults(defineProps<{
+  type: 'orchestrator' | 'tutor' | 'debug' | 'review' | 'arch' | 'coach'
+  label: string
+  description?: string
+  status?: 'active' | 'idle' | 'error' | 'processing'
+  messageCount?: number
+  lastUsed?: number | null
+}>(), {
+  status: 'idle',
+  messageCount: 0,
+  lastUsed: null
+})
+
+const icon = computed(() => {
+  const icons: Record<string, string> = {
+    orchestrator: '🧠',
+    tutor: '👨‍🏫',
+    debug: '🐛',
+    review: '🔍',
+    arch: '🏗️',
+    coach: '🎯',
+  }
+  return icons[props.type] || '🤖'
+})
+
+const description = computed(() => {
+  const descs: Record<string, string> = {
+    orchestrator: '路由和协调',
+    tutor: '概念讲解答疑',
+    debug: '错误分析修复',
+    review: '代码质量审查',
+    arch: '架构设计咨询',
+    coach: '学习路径规划',
+  }
+  return props.description || descs[props.type] || ''
+})
+
+const statusText = computed(() => {
+  const texts: Record<string, string> = {
+    active: '运行中',
+    processing: '处理中',
+    idle: '空闲',
+    error: '错误',
+  }
+  return texts[props.status || 'idle'] || '空闲'
+})
+
+const lastUsedText = computed(() => {
+  if (!props.lastUsed) return '-'
+  const diff = Date.now() - props.lastUsed
+  if (diff < 60000) return '刚刚'
+  if (diff < 3600000) return `${Math.floor(diff / 60000)}m`
+  if (diff < 86400000) return `${Math.floor(diff / 3600000)}h`
+  return `${Math.floor(diff / 86400000)}d`
+})
+</script>
+
+<style scoped>
+.agent-node {
+  min-width: 180px;
+  border-radius: 12px;
+  background: white;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  overflow: hidden;
+  border: 2px solid transparent;
+  transition: all 0.3s;
+  cursor: pointer;
+}
+
+.agent-node:hover {
+  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
+  transform: translateY(-2px);
+}
+
+.agent-node.active {
+  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2);
+}
+
+.agent-node.orchestrator { border-color: #667eea; }
+.agent-node.orchestrator.active { box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2), 0 6px 24px rgba(102, 126, 234, 0.3); }
+.agent-node.tutor { border-color: #764ba2; }
+.agent-node.tutor.active { box-shadow: 0 0 0 3px rgba(118, 75, 162, 0.2), 0 6px 24px rgba(118, 75, 162, 0.3); }
+.agent-node.debug { border-color: #f5222d; }
+.agent-node.debug.active { box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.2), 0 6px 24px rgba(245, 34, 45, 0.3); }
+.agent-node.review { border-color: #52c41a; }
+.agent-node.review.active { box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.2), 0 6px 24px rgba(82, 196, 26, 0.3); }
+.agent-node.arch { border-color: #fa8c16; }
+.agent-node.arch.active { box-shadow: 0 0 0 3px rgba(250, 140, 22, 0.2), 0 6px 24px rgba(250, 140, 22, 0.3); }
+.agent-node.coach { border-color: #722ed1; }
+.agent-node.coach.active { box-shadow: 0 0 0 3px rgba(114, 46, 209, 0.2), 0 6px 24px rgba(114, 46, 209, 0.3); }
+
+.node-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 12px 16px;
+  color: white;
+  position: relative;
+  overflow: hidden;
+}
+
+.orchestrator .node-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
+.tutor .node-header { background: linear-gradient(135deg, #764ba2 0%, #9b59b6 100%); }
+.debug .node-header { background: linear-gradient(135deg, #f5222d 0%, #cf1322 100%); }
+.review .node-header { background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); }
+.arch .node-header { background: linear-gradient(135deg, #fa8c16 0%, #d46b08 100%); }
+.coach .node-header { background: linear-gradient(135deg, #722ed1 0%, #531dab 100%); }
+
+.header-pulse {
+  position: absolute;
+  right: 12px;
+  width: 10px;
+  height: 10px;
+  background: white;
+  border-radius: 50%;
+  animation: header-pulse 1.5s infinite;
+}
+
+@keyframes header-pulse {
+  0%, 100% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.6; transform: scale(1.2); }
+}
+
+.node-icon {
+  font-size: 20px;
+}
+
+.node-title {
+  font-size: 14px;
+  font-weight: 600;
+}
+
+.node-body {
+  padding: 12px 16px;
+}
+
+.node-desc {
+  margin: 0 0 8px 0;
+  font-size: 12px;
+  color: #666;
+}
+
+.node-status {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-bottom: 8px;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #d9d9d9;
+}
+
+.status-dot.active {
+  background: #52c41a;
+  animation: dot-pulse 1.5s infinite;
+}
+
+.status-dot.processing {
+  background: #1890ff;
+  animation: dot-pulse 1s infinite;
+}
+
+.status-dot.idle {
+  background: #d9d9d9;
+}
+
+.status-dot.error {
+  background: #f5222d;
+}
+
+@keyframes dot-pulse {
+  0%, 100% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(1.3); }
+}
+
+.status-text {
+  font-size: 12px;
+  font-weight: 500;
+  color: #666;
+}
+
+.node-stats {
+  display: flex;
+  gap: 12px;
+  padding-top: 8px;
+  border-top: 1px solid #f0f0f0;
+}
+
+.stat {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 11px;
+  color: #999;
+}
+
+.stat-icon {
+  font-size: 12px;
+}
+
+.stat-value {
+  font-weight: 500;
+}
+
+.node-handles {
+  position: relative;
+}
+
+.handle {
+  position: absolute;
+  width: 12px;
+  height: 12px;
+  background: #667eea;
+  border: 2px solid white;
+  border-radius: 50%;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.handle-left {
+  left: -6px;
+}
+
+.handle-right {
+  right: -6px;
+}
+</style>

+ 639 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/CodeEditor.vue

@@ -0,0 +1,639 @@
+<template>
+  <div class="code-editor" :class="{ 'exercise-mode': exerciseData !== null }">
+    <div class="editor-header">
+      <div class="tabs">
+        <select v-model="selectedLanguage" class="lang-select" @change="onLanguageChange" :disabled="exerciseData !== null">
+          <option v-for="lang in languages" :key="lang.id" :value="lang.id">
+            {{ lang.label }}
+          </option>
+        </select>
+        <span v-if="exerciseData" class="exercise-indicator">📝 练习模式</span>
+        <span v-else class="tab active">{{ currentTab }}</span>
+      </div>
+      <div class="header-actions">
+        <button class="submit-btn" @click="submitForReview" :disabled="submitting">
+          {{ submitting ? (exerciseData ? '提交中...' : langStore.t('codeEditor.submitting')) : (exerciseData ? '📝 提交练习反馈' : langStore.t('codeEditor.submitFeedback')) }}
+        </button>
+        <button class="run-btn" @click="runCode" :disabled="running">
+          <span v-if="!running">{{ currentExecutor === 'iframe' ? 'Preview' : langStore.t('codeEditor.run') }}</span>
+          <span v-else>{{ currentExecutor === 'iframe' ? 'Loading...' : langStore.t('codeEditor.running') }}</span>
+        </button>
+        <button v-if="exerciseData" class="close-exercise-btn" @click="closeExercise">✕</button>
+      </div>
+    </div>
+    
+    <div class="editor-container" ref="editorContainer" @mousedown="focusEditor"></div>
+    
+    <div class="output-panel" :class="{ 'iframe-mode': currentExecutor === 'iframe' }">
+      <div class="output-header">
+        <span class="output-title">{{ hasError ? langStore.t('codeEditor.error') : (currentExecutor === 'iframe' ? 'Preview' : langStore.t('codeEditor.output')) }}</span>
+        <button class="clear-btn" @click="clearOutput" v-if="output || error || previewSrc">{{ langStore.t('codeEditor.clear') }}</button>
+      </div>
+
+      <!-- iframe preview for HTML/CSS -->
+      <div v-if="previewSrc" class="output-content preview-content">
+        <iframe :srcdoc="previewSrc" sandbox="allow-scripts" class="preview-iframe" />
+      </div>
+
+      <!-- normal output panel -->
+      <div v-else class="output-content">
+        <pre v-if="output" class="output-text">{{ output }}</pre>
+        <pre v-if="error" class="error-text">{{ error }}</pre>
+        <div v-if="!output && !error && !feedback" class="output-placeholder">
+          {{ currentExecutor === 'iframe' ? 'Click "Preview" to render your code.' : langStore.t('codeEditor.placeholder') }}
+        </div>
+      </div>
+      
+      <div v-if="feedback" class="feedback-section">
+        <div class="feedback-header">
+          <span class="feedback-title">{{ langStore.t('codeEditor.feedbackTitle') }}</span>
+        </div>
+        <div class="feedback-body">
+          <pre class="feedback-text">{{ feedback }}</pre>
+        </div>
+      </div>
+      <div v-if="feedbackError" class="feedback-section feedback-error">
+        <div class="feedback-body">
+          <pre class="feedback-text error-text">{{ feedbackError }}</pre>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
+import axios from 'axios'
+import loader from '@monaco-editor/loader'
+import { useLangStore } from '../stores/langStore'
+import { useAuthStore } from '../stores/authStore'
+
+// ── Types ──────────────────────────────────────────────────────────────
+
+interface LangConfig {
+  id: string
+  label: string
+  monaco: string
+  tab: string
+  executor: 'backend' | 'iframe'
+  defaultCode: string
+}
+
+// ── Language definitions ───────────────────────────────────────────────
+
+const languages: LangConfig[] = [
+  {
+    id: 'python',
+    label: 'Python',
+    monaco: 'python',
+    tab: 'main.py',
+    executor: 'backend',
+    defaultCode: '# Write Python code here\nprint("Hello World!")',
+  },
+  {
+    id: 'javascript',
+    label: 'JavaScript',
+    monaco: 'javascript',
+    tab: 'script.js',
+    executor: 'backend',
+    defaultCode: '// Write JavaScript code here\nconsole.log("Hello World!");',
+  },
+  {
+    id: 'typescript',
+    label: 'TypeScript',
+    monaco: 'typescript',
+    tab: 'index.ts',
+    executor: 'backend',
+    defaultCode:
+      '// Write TypeScript code here\nconst msg: string = "Hello World!";\nconsole.log(msg);',
+  },
+  {
+    id: 'html',
+    label: 'HTML',
+    monaco: 'html',
+    tab: 'index.html',
+    executor: 'iframe',
+    defaultCode:
+      '<!DOCTYPE html>\n<html>\n<head>\n  <title>My Page</title>\n  <style>\n    body { font-family: sans-serif; padding: 20px; }\n  </style>\n</head>\n<body>\n  <h1>Hello World!</h1>\n  <p>Write your HTML here.</p>\n</body>\n</html>',
+  },
+  {
+    id: 'css',
+    label: 'CSS',
+    monaco: 'css',
+    tab: 'style.css',
+    executor: 'iframe',
+    defaultCode:
+      '/* Write CSS code here */\nbody {\n  font-family: sans-serif;\n  background: #f0f0f0;\n  margin: 20px;\n}',
+  },
+  {
+    id: 'bash',
+    label: 'Bash',
+    monaco: 'shell',
+    tab: 'script.sh',
+    executor: 'backend',
+    defaultCode: '# Write shell commands here\necho "Hello World!"',
+  },
+]
+
+// ── Emits ─────────────────────────────────────────────────────────────
+
+const emit = defineEmits<{
+  exerciseSubmitted: [result: { feedback: string }]
+}>()
+
+// ── Props ──────────────────────────────────────────────────────────────
+
+const props = withDefaults(defineProps<{
+  exerciseData?: { code: string; language: string; lessonId?: string | null } | null
+}>(), {
+  exerciseData: null,
+})
+
+// ── State ──────────────────────────────────────────────────────────────
+
+const editorContainer = ref<HTMLElement>()
+const output = ref('')
+const error = ref('')
+const previewSrc = ref('')
+const running = ref(false)
+const submitting = ref(false)
+const feedback = ref('')
+const feedbackError = ref('')
+const selectedLanguage = ref('python')
+const langStore = useLangStore()
+const authStore = useAuthStore()
+
+let editor: any = null
+let monaco: any = null
+
+// ── Computed ───────────────────────────────────────────────────────────
+
+const currentLangConfig = computed((): LangConfig => {
+  return languages.find((l) => l.id === selectedLanguage.value) || languages[0]
+})
+
+const currentTab = computed(() => currentLangConfig.value.tab)
+const currentExecutor = computed(() => currentLangConfig.value.executor)
+const hasError = computed(() => !!error.value)
+
+// ── Lifecycle ──────────────────────────────────────────────────────────
+
+onMounted(async () => {
+  monaco = await loader.init()
+
+  editor = monaco.editor.create(editorContainer.value!, {
+    value: currentLangConfig.value.defaultCode,
+    language: currentLangConfig.value.monaco,
+    theme: 'vs-dark',
+    automaticLayout: true,
+    fontSize: 14,
+    fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace",
+    minimap: { enabled: false },
+    scrollBeyondLastLine: false,
+    lineNumbers: 'on',
+    roundedSelection: false,
+    readOnly: false,
+    cursorStyle: 'line',
+    padding: { top: 12, bottom: 12 },
+    suggest: {
+      showKeywords: true,
+      showSnippets: true,
+    },
+    tabSize: 4,
+    insertSpaces: true,
+  })
+
+  // Ctrl+Enter execute
+  editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
+    runCode()
+  })
+
+  // 自动聚焦编辑器,否则空格键等键盘输入不会生效
+  editor.focus()
+})
+
+onBeforeUnmount(() => {
+  if (editor) {
+    editor.dispose()
+  }
+})
+
+// ── Language switching ─────────────────────────────────────────────────
+
+function onLanguageChange() {
+  const cfg = currentLangConfig.value
+
+  if (editor && monaco) {
+    const model = editor.getModel()
+    if (model) {
+      monaco.editor.setModelLanguage(model, cfg.monaco)
+    }
+    editor.setValue(cfg.defaultCode)
+  }
+
+  clearOutput()
+}
+
+// ── Exercise mode ──────────────────────────────────────────────────────
+
+watch(() => props.exerciseData, (data) => {
+  if (data && editor && monaco) {
+    // Switch to exercise language
+    const lang = languages.find(l => l.id === data.language)
+    if (lang) {
+      selectedLanguage.value = lang.id
+      const model = editor.getModel()
+      if (model) {
+        monaco.editor.setModelLanguage(model, lang.monaco)
+      }
+    }
+    editor.setValue(data.code)
+    editor.focus()
+    clearOutput()
+  }
+}, { immediate: true })
+
+function focusEditor() {
+  if (editor) {
+    editor.focus()
+  }
+}
+
+function closeExercise() {
+  // Restore default code for current language
+  const cfg = currentLangConfig.value
+  if (editor) {
+    editor.setValue(cfg.defaultCode)
+  }
+  clearOutput()
+}
+
+// ── Run / Preview ──────────────────────────────────────────────────────
+
+const runCode = async () => {
+  if (running.value || !editor) return
+
+  const code = editor.getValue()
+  if (!code.trim()) return
+
+  running.value = true
+  output.value = ''
+  error.value = ''
+  feedback.value = ''
+  feedbackError.value = ''
+  previewSrc.value = ''
+
+  try {
+    if (currentExecutor.value === 'iframe') {
+      // HTML: render directly; CSS: wrap in a minimal HTML shell
+      if (selectedLanguage.value === 'css') {
+        previewSrc.value =
+          "<html><head><style>" + code + "</style></head><body><div class='preview-content'>Preview your styles here</div></body></html>"
+      } else {
+        previewSrc.value = code
+      }
+    } else {
+      const response = await axios.post('/api/code/execute', {
+        code: code,
+        language: selectedLanguage.value,
+      })
+
+      const result = response.data
+      output.value = result.output
+      error.value = result.error
+    }
+  } catch (err: any) {
+    error.value = err.response?.data?.detail || '请求失败'
+  } finally {
+    running.value = false
+  }
+}
+
+// ── Submit for review ──────────────────────────────────────────────────
+
+const submitForReview = async () => {
+  if (submitting.value || !editor) return
+
+  const code = editor.getValue()
+  if (!code.trim()) return
+
+  submitting.value = true
+  feedback.value = ''
+  feedbackError.value = ''
+
+  try {
+    const response = await axios.post('/api/code/submit', {
+      code: code,
+      language: selectedLanguage.value,
+      output: output.value,
+      error: error.value,
+      success: !error.value,
+      exit_code: error.value ? 1 : 0,
+      user_id: authStore.userId || 'default',
+      lesson_id: props.exerciseData?.lessonId || null,
+    })
+
+    feedback.value = response.data.feedback
+    // 通知父组件练习已提交
+    emit('exerciseSubmitted', { feedback: response.data.feedback })
+  } catch (err: any) {
+    feedbackError.value =
+      err.response?.data?.detail || langStore.t('codeEditor.feedbackError')
+  } finally {
+    submitting.value = false
+  }
+}
+
+// ── Utilities ──────────────────────────────────────────────────────────
+
+const clearOutput = () => {
+  output.value = ''
+  error.value = ''
+  previewSrc.value = ''
+  feedback.value = ''
+  feedbackError.value = ''
+}
+</script>
+
+<style scoped>
+.code-editor {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  border: 1px solid #3c3c3c;
+  border-radius: 8px;
+  background: #1e1e1e;
+}
+
+.editor-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 12px;
+  height: 40px;
+  background: #252526;
+  border-bottom: 1px solid #3c3c3c;
+  border-radius: 8px 8px 0 0;
+  overflow: hidden;
+}
+
+.header-actions {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.tabs {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.lang-select {
+  background: #3c3c3c;
+  color: #fff;
+  border: none;
+  padding: 4px 8px;
+  border-radius: 4px;
+  font-size: 13px;
+  cursor: pointer;
+  outline: none;
+}
+
+.lang-select:hover {
+  background: #4a4a4a;
+}
+
+.lang-select option {
+  background: #3c3c3c;
+  color: #fff;
+}
+
+.tab {
+  padding: 8px 16px;
+  font-size: 13px;
+  color: #969696;
+  cursor: pointer;
+  border-bottom: 2px solid transparent;
+}
+
+.tab.active {
+  color: #fff;
+  border-bottom-color: #007acc;
+  background: #1e1e1e;
+}
+
+.run-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 16px;
+  background: #0e639c;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  font-size: 13px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.run-btn:hover:not(:disabled) {
+  background: #1177bb;
+}
+
+.run-btn:disabled {
+  background: #3c3c3c;
+  color: #6c6c6c;
+  cursor: not-allowed;
+}
+
+.submit-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 16px;
+  background: #2d7d46;
+  color: #fff;
+  border: none;
+  border-radius: 4px;
+  font-size: 13px;
+  cursor: pointer;
+  transition: background 0.2s;
+  white-space: nowrap;
+}
+
+.submit-btn:hover:not(:disabled) {
+  background: #389e54;
+}
+
+.submit-btn:disabled {
+  background: #3c3c3c;
+  color: #6c6c6c;
+  cursor: not-allowed;
+}
+
+.exercise-indicator {
+  padding: 4px 10px;
+  font-size: 12px;
+  font-weight: 500;
+  color: #fff;
+  background: #667eea;
+  border-radius: 4px;
+  margin-left: 8px;
+}
+
+.close-exercise-btn {
+  width: 28px;
+  height: 28px;
+  border: none;
+  background: transparent;
+  color: #969696;
+  border-radius: 4px;
+  font-size: 14px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.close-exercise-btn:hover {
+  background: rgba(255,255,255,0.1);
+  color: #fff;
+}
+
+.exercise-mode .editor-header {
+  border-bottom-color: #667eea;
+}
+
+.editor-container {
+  flex: 1;
+  min-height: 300px;
+}
+
+.output-panel {
+  border-top: 1px solid #3c3c3c;
+  background: #1e1e1e;
+  max-height: 45vh;
+  overflow-y: auto;
+}
+
+.output-panel.iframe-mode {
+  max-height: none;
+}
+
+.output-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  background: #252526;
+  border-bottom: 1px solid #3c3c3c;
+}
+
+.output-title {
+  font-size: 12px;
+  font-weight: 500;
+  color: #ccc;
+  text-transform: uppercase;
+}
+
+.clear-btn {
+  padding: 2px 8px;
+  background: transparent;
+  color: #969696;
+  border: 1px solid #3c3c3c;
+  border-radius: 3px;
+  font-size: 12px;
+  cursor: pointer;
+}
+
+.clear-btn:hover {
+  background: #3c3c3c;
+  color: #fff;
+}
+
+.output-content {
+  max-height: 200px;
+  overflow-y: auto;
+  padding: 12px;
+}
+
+.preview-content {
+  max-height: none;
+  overflow: visible;
+}
+
+.preview-iframe {
+  width: 100%;
+  min-height: 300px;
+  border: none;
+  background: #fff;
+  border-radius: 4px;
+}
+
+.output-text, .error-text {
+  margin: 0;
+  font-family: 'Consolas', 'Monaco', monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+
+.output-text {
+  color: #d4d4d4;
+}
+
+.error-text {
+  color: #f48771;
+}
+
+.output-placeholder {
+  color: #6c6c6c;
+  font-size: 13px;
+  font-style: italic;
+}
+
+.feedback-section {
+  border-top: 1px solid #3c3c3c;
+}
+
+.feedback-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  background: #1a3a2a;
+  border-bottom: 1px solid #3c3c3c;
+}
+
+.feedback-title {
+  font-size: 12px;
+  font-weight: 500;
+  color: #8dd0a8;
+  text-transform: uppercase;
+}
+
+.feedback-body {
+  padding: 12px;
+  background: #1a2a1e;
+}
+
+.feedback-text {
+  margin: 0;
+  font-family: 'Consolas', 'Monaco', monospace;
+  font-size: 13px;
+  line-height: 1.6;
+  white-space: pre-wrap;
+  word-break: break-word;
+  color: #cce8d0;
+}
+
+.feedback-error .feedback-body {
+  background: #2a1a1a;
+}
+
+.feedback-error .feedback-text {
+  color: #f48771;
+}
+</style>

+ 290 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/CodeRunner.vue

@@ -0,0 +1,290 @@
+<template>
+  <div class="code-runner" :class="{ running, hasOutput: output !== null }">
+    <!-- Header -->
+    <div class="cr-header">
+      <span class="cr-lang">{{ language }}</span>
+      <button class="cr-run-btn" :disabled="running" @click="runCode">
+        <span v-if="!running" class="run-icon">▶</span>
+        <span v-else class="spinner"></span>
+        {{ running ? '运行中...' : '运行' }}
+      </button>
+    </div>
+
+    <!-- Code: highlighted read-only -->
+    <pre class="cr-code"><code v-html="highlightedCode"></code></pre>
+
+    <!-- Iframe preview for HTML/CSS -->
+    <Transition name="slide">
+      <div v-if="isHtmlLike && iframeSrc !== null" class="cr-output cr-iframe-output">
+        <div class="output-header">
+          <span class="output-label">✓ 预览</span>
+          <button class="output-close" @click="iframeSrc = null">✕</button>
+        </div>
+        <iframe class="preview-iframe" :srcdoc="iframeSrc" sandbox="allow-scripts"></iframe>
+      </div>
+    </Transition>
+
+    <!-- Output for backend-executed languages -->
+    <Transition name="slide">
+      <div v-if="!isHtmlLike && output !== null" class="cr-output" :class="{ error: !success }">
+        <div class="output-header">
+          <span class="output-label">{{ success ? '✓ 输出' : '✗ 错误' }}</span>
+          <button class="output-close" @click="output = null">✕</button>
+        </div>
+        <pre class="output-content"><code>{{ output }}</code></pre>
+      </div>
+    </Transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import hljs from 'highlight.js'
+import axios from 'axios'
+
+const props = withDefaults(defineProps<{
+  code: string
+  language?: string
+}>(), {
+  language: 'python',
+})
+
+const running = ref(false)
+const output = ref<string | null>(null)
+const success = ref(true)
+const iframeSrc = ref<string | null>(null)
+
+const isHtmlLike = computed(() => props.language === 'html' || props.language === 'css')
+
+const highlightedCode = computed(() => {
+  try {
+    if (props.language && hljs.getLanguage(props.language)) {
+      return hljs.highlight(props.code, { language: props.language }).value
+    }
+    return hljs.highlightAuto(props.code).value
+  } catch {
+    return escapeHtml(props.code)
+  }
+})
+
+function escapeHtml(text: string): string {
+  return text.replace(/[&<>"']/g, (ch: string) => {
+    const map: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
+    return map[ch] || ch
+  })
+}
+
+async function runCode() {
+  if (running.value) return
+  running.value = true
+  output.value = null
+  iframeSrc.value = null
+
+  // HTML/CSS: render in iframe directly (no backend call)
+  if (isHtmlLike.value) {
+    if (props.language === 'html') {
+      iframeSrc.value = props.code
+    } else if (props.language === 'css') {
+      iframeSrc.value = `<!DOCTYPE html>
+<html>
+<head>
+  <style>${props.code}</style>
+</head>
+<body>
+  <div class="preview-content" style="font-family:sans-serif;padding:20px;">
+    <h1>CSS 预览</h1>
+    <p>你的样式已应用到此页面。</p>
+  </div>
+</body>
+</html>`
+    }
+    running.value = false
+    return
+  }
+
+  // Backend execution for other languages
+  try {
+    const res = await axios.post('/api/code/execute', {
+      code: props.code,
+      language: props.language,
+    })
+    success.value = res.data.success
+    output.value = res.data.output || res.data.error || '(无输出)'
+  } catch (err: any) {
+    success.value = false
+    output.value = err.response?.data?.detail || err.message || '请求失败'
+  } finally {
+    running.value = false
+  }
+}
+</script>
+
+<style scoped>
+.code-runner {
+  margin: 12px 0;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+  overflow: hidden;
+  background: #1e1e1e;
+  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Consolas, monospace;
+}
+
+.cr-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 14px;
+  background: #2d2d2d;
+  border-bottom: 1px solid #3a3a3a;
+}
+
+.cr-lang {
+  font-size: 11px;
+  font-weight: 600;
+  color: #999;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.cr-run-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 14px;
+  font-size: 12px;
+  font-weight: 600;
+  font-family: inherit;
+  color: #fff;
+  background: #2ea043;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.cr-run-btn:hover:not(:disabled) {
+  background: #2c974b;
+  transform: translateY(-1px);
+}
+
+.cr-run-btn:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+
+.run-icon {
+  font-size: 10px;
+}
+
+.spinner {
+  width: 12px;
+  height: 12px;
+  border: 2px solid rgba(255,255,255,0.3);
+  border-top-color: white;
+  border-radius: 50%;
+  animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.cr-code {
+  margin: 0;
+  padding: 14px 16px;
+  font-size: 13px;
+  line-height: 1.6;
+  overflow-x: auto;
+  color: #c9d1d9;
+}
+
+.cr-code :deep(code) {
+  font-family: inherit;
+}
+
+/* Output */
+.cr-output {
+  border-top: 1px solid #3a3a3a;
+  background: #0d1117;
+}
+
+.cr-output.error {
+  border-top-color: #5a1d1d;
+}
+
+.output-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 14px;
+  background: #161b22;
+}
+
+.output-label {
+  font-size: 11px;
+  font-weight: 600;
+  color: #7ee787;
+}
+
+.cr-output.error .output-label {
+  color: #ff7b72;
+}
+
+.output-close {
+  background: none;
+  border: none;
+  color: #666;
+  cursor: pointer;
+  font-size: 14px;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+
+.output-close:hover {
+  color: #fff;
+  background: rgba(255,255,255,0.1);
+}
+
+.output-content {
+  margin: 0;
+  padding: 12px 16px;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #c9d1d9;
+  max-height: 200px;
+  overflow-y: auto;
+  white-space: pre-wrap;
+  word-break: break-all;
+}
+
+/* Iframe output */
+.cr-iframe-output {
+  padding: 0;
+}
+
+.preview-iframe {
+  width: 100%;
+  min-height: 300px;
+  border: none;
+  background: #fff;
+  display: block;
+}
+
+/* Transition */
+.slide-enter-active,
+.slide-leave-active {
+  transition: all 0.25s ease;
+}
+
+.slide-enter-from,
+.slide-leave-to {
+  opacity: 0;
+  max-height: 0;
+}
+
+.slide-enter-to,
+.slide-leave-from {
+  opacity: 1;
+  max-height: 300px;
+}
+</style>

+ 409 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/GamificationPanel.vue

@@ -0,0 +1,409 @@
+<template>
+  <div class="gamification-panel">
+    <div class="gp-header">
+      <div class="gp-level-section">
+        <div class="level-badge-circle">
+          <span class="level-number">{{ profile.level }}</span>
+          <span class="level-label">LV</span>
+        </div>
+        <div class="xp-info">
+          <div class="xp-text">
+            <span class="xp-amount">{{ profile.total_xp }} XP</span>
+            <span class="xp-next">/ {{ nextLevelXp }} XP</span>
+          </div>
+          <div class="xp-bar">
+            <div class="xp-fill" :style="{ width: xpPercent + '%' }"></div>
+          </div>
+        </div>
+      </div>
+
+      <div class="gp-stats">
+        <div class="stat-item" title="连续学习天数">
+          <span class="stat-icon">🔥</span>
+          <span class="stat-value">{{ profile.streak }}</span>
+          <span class="stat-label">天</span>
+        </div>
+        <div class="stat-item" title="已获得徽章">
+          <span class="stat-icon">🏅</span>
+          <span class="stat-value">{{ profile.badges?.length || 0 }}</span>
+          <span class="stat-label">徽章</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="gp-badges">
+      <div class="badges-title">
+        <span>🏅 徽章墙</span>
+        <span class="badges-count">{{ earnedCount }}/{{ allBadges.length }}</span>
+      </div>
+      <div class="badges-grid">
+        <div
+          v-for="badge in displayBadges"
+          :key="badge.id"
+          class="badge-item"
+          :class="{ earned: isEarned(badge.id), locked: !isEarned(badge.id) }"
+          :title="badge.description + (isEarned(badge.id) ? '' : ' (未获得)')"
+        >
+          <span class="badge-icon">{{ isEarned(badge.id) ? badge.icon : '🔒' }}</span>
+          <span class="badge-name">{{ badge.name }}</span>
+        </div>
+      </div>
+      <div v-if="allBadges.length > 7" class="badges-toggle" @click="showAllBadges = !showAllBadges">
+        {{ showAllBadges ? '收起' : `查看全部 ${allBadges.length} 个徽章` }}
+      </div>
+    </div>
+
+    <!-- XP获得通知 -->
+    <transition name="xp-notify">
+      <div v-if="xpNotification" class="xp-notification">
+        <span class="xp-n-icon">✨</span>
+        <span class="xp-n-text">+{{ xpNotification }} XP</span>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch } from 'vue'
+import axios from 'axios'
+
+interface Badge {
+  id: string
+  name: string
+  description: string
+  icon: string
+  condition?: string
+}
+
+interface GamificationProfile {
+  user_id: string
+  total_xp: number
+  level: number
+  streak: number
+  last_active_date: string
+  badges: string[]
+  xp_log: Array<{ amount: number; reason: string; timestamp: string }>
+}
+
+const XP_PER_LEVEL = 100
+
+const props = withDefaults(defineProps<{
+  userId?: string
+}>(), {
+  userId: '',
+})
+
+const emit = defineEmits<{
+  (e: 'xp-earned', amount: number): void
+}>()
+
+const profile = ref<GamificationProfile>({
+  user_id: '',
+  total_xp: 0,
+  level: 1,
+  streak: 0,
+  last_active_date: '',
+  badges: [],
+  xp_log: [],
+})
+
+const allBadges = ref<Badge[]>([])
+const showAllBadges = ref(false)
+const xpNotification = ref<number | null>(null)
+
+const nextLevelXp = computed(() => profile.value.level * XP_PER_LEVEL)
+const xpPercent = computed(() => {
+  const currentLevelXp = (profile.value.level - 1) * XP_PER_LEVEL
+  const progressInLevel = profile.value.total_xp - currentLevelXp
+  return Math.min(100, (progressInLevel / XP_PER_LEVEL) * 100)
+})
+
+const earnedCount = computed(() => {
+  return allBadges.value.filter(b => isEarned(b.id)).length
+})
+
+const displayBadges = computed(() => {
+  if (showAllBadges.value || allBadges.value.length <= 7) return allBadges.value
+  const earned = allBadges.value.filter(b => isEarned(b.id))
+  const unearned = allBadges.value.filter(b => !isEarned(b.id))
+  // Show all earned + enough unearned to fill 7
+  const remaining = Math.max(0, 7 - earned.length)
+  return [...earned, ...unearned.slice(0, remaining)]
+})
+
+function isEarned(badgeId: string): boolean {
+  return profile.value.badges?.includes(badgeId) || false
+}
+
+async function loadProfile() {
+  if (!props.userId) return
+  try {
+    const res = await axios.get('/api/gamification/profile', {
+      params: { user_id: props.userId }
+    })
+    profile.value = res.data
+  } catch (e) {
+    console.error('加载游戏化数据失败', e)
+  }
+}
+
+async function loadBadges() {
+  try {
+    const res = await axios.get('/api/gamification/badges')
+    allBadges.value = res.data.badges
+  } catch (e) {
+    console.error('加载徽章数据失败', e)
+  }
+}
+
+// 外部调用来显示XP通知
+function showXpNotification(amount: number) {
+  xpNotification.value = amount
+  setTimeout(() => {
+    xpNotification.value = null
+  }, 2000)
+  // Reload profile after notification
+  setTimeout(() => loadProfile(), 500)
+}
+
+defineExpose({ showXpNotification, loadProfile })
+
+onMounted(() => {
+  loadProfile()
+  loadBadges()
+})
+</script>
+
+<style scoped>
+.gamification-panel {
+  background: var(--color-bg-secondary);
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  padding: 16px 20px;
+  margin-bottom: 20px;
+  position: relative;
+  overflow: hidden;
+}
+
+.gp-header {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+}
+
+.gp-level-section {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  flex: 1;
+}
+
+.level-badge-circle {
+  width: 52px;
+  height: 52px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  flex-shrink: 0;
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.level-number {
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.level-label {
+  font-size: 9px;
+  opacity: 0.85;
+  text-transform: uppercase;
+  letter-spacing: 1px;
+}
+
+.xp-info {
+  flex: 1;
+  min-width: 120px;
+}
+
+.xp-text {
+  font-size: 13px;
+  margin-bottom: 4px;
+}
+
+.xp-amount {
+  font-weight: 600;
+  color: var(--color-text);
+}
+
+.xp-next {
+  color: var(--color-text-secondary);
+  font-size: 12px;
+}
+
+.xp-bar {
+  height: 8px;
+  background: var(--color-bg-tertiary);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.xp-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea, #764ba2);
+  border-radius: 4px;
+  transition: width 0.6s ease;
+}
+
+.gp-stats {
+  display: flex;
+  gap: 16px;
+  flex-shrink: 0;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 13px;
+  background: var(--color-bg-tertiary);
+  padding: 4px 10px;
+  border-radius: 8px;
+  white-space: nowrap;
+}
+
+.stat-icon {
+  font-size: 15px;
+}
+
+.stat-value {
+  font-weight: 700;
+  color: var(--color-text);
+}
+
+.stat-label {
+  color: var(--color-text-secondary);
+  font-size: 12px;
+}
+
+.gp-badges {
+  margin-top: 12px;
+  padding-top: 12px;
+  border-top: 1px solid var(--color-border);
+}
+
+.badges-title {
+  font-size: 13px;
+  font-weight: 600;
+  margin-bottom: 8px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.badges-count {
+  font-size: 12px;
+  color: var(--color-text-secondary);
+  font-weight: 400;
+}
+
+.badges-grid {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.badge-item {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  padding: 4px 10px;
+  border-radius: 8px;
+  font-size: 12px;
+  background: var(--color-bg-tertiary);
+  transition: all 0.2s;
+  cursor: default;
+}
+
+.badge-item.earned {
+  opacity: 1;
+}
+
+.badge-item.locked {
+  opacity: 0.5;
+  filter: grayscale(1);
+}
+
+.badge-item:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+}
+
+.badge-icon {
+  font-size: 16px;
+}
+
+.badge-name {
+  font-weight: 500;
+}
+
+.badges-toggle {
+  text-align: center;
+  font-size: 12px;
+  color: var(--color-primary);
+  cursor: pointer;
+  margin-top: 6px;
+  padding: 2px 0;
+}
+
+.badges-toggle:hover {
+  opacity: 0.8;
+}
+
+/* XP Notification */
+.xp-notification {
+  position: absolute;
+  top: 50%;
+  right: 20px;
+  transform: translateY(-50%);
+  background: linear-gradient(135deg, #667eea, #764ba2);
+  color: #fff;
+  padding: 6px 16px;
+  border-radius: 20px;
+  font-size: 14px;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+  z-index: 10;
+  pointer-events: none;
+}
+
+.xp-n-icon {
+  font-size: 16px;
+}
+
+.xp-notify-enter-active {
+  animation: xpPulse 0.3s ease-out;
+}
+
+.xp-notify-leave-active {
+  animation: xpFade 0.4s ease-in;
+}
+
+@keyframes xpPulse {
+  0% { transform: translateY(-50%) scale(0.5); opacity: 0; }
+  60% { transform: translateY(-50%) scale(1.1); }
+  100% { transform: translateY(-50%) scale(1); opacity: 1; }
+}
+
+@keyframes xpFade {
+  0% { opacity: 1; transform: translateY(-50%) translateX(0); }
+  100% { opacity: 0; transform: translateY(-50%) translateX(20px); }
+}
+</style>

+ 469 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/MarkdownRenderer.vue

@@ -0,0 +1,469 @@
+<template>
+  <div class="markdown-renderer">
+    <template v-for="(block, i) in contentBlocks" :key="i">
+      <QuizWidget
+        v-if="block.type === 'quiz'"
+        :data="block.data"
+        @answer="(correct: boolean) => onQuizAnswer(i, correct)"
+      />
+      <CodeRunner
+        v-else-if="block.type === 'code'"
+        :code="block.code"
+        :language="block.language"
+      />
+      <div v-else-if="block.type === 'exercise'" class="exercise-card">
+        <div class="ec-header">
+          <span class="ec-lang">{{ block.language }}</span>
+          <span class="ec-badge">📝 代码练习</span>
+        </div>
+        <pre class="ec-preview"><code>{{ block.code }}</code></pre>
+        <div class="ec-actions">
+          <button class="ec-open-btn" @click="$emit('exerciseDetected', { code: block.code, language: block.language })">
+            📂 在编辑器中打开
+          </button>
+        </div>
+      </div>
+      <div v-else class="markdown-body" v-html="block.html"></div>
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, reactive, watch } from 'vue'
+import { marked } from 'marked'
+import hljs from 'highlight.js'
+import DOMPurify from 'dompurify'
+import QuizWidget from './QuizWidget.vue'
+import CodeRunner from './CodeRunner.vue'
+
+const props = withDefaults(defineProps<{
+  content: string
+  autoCollapse?: boolean
+  collapseDepth?: number
+}>(), {
+  autoCollapse: true,
+  collapseDepth: 2,
+})
+
+// ── marked config ──
+marked.setOptions({ gfm: true, breaks: true })
+
+const renderer = new marked.Renderer()
+
+renderer.code = function({ text, lang }: { text: string; lang?: string }) {
+  const code = typeof text === 'object' ? (text as any).text || '' : text
+  const language = typeof text === 'object' ? (text as any).lang || lang : lang
+  if (language && hljs.getLanguage(language)) {
+    try {
+      const h = hljs.highlight(code, { language }).value
+      return `<pre class="hljs"><code class="language-${language}">${h}</code></pre>`
+    } catch (_) { /* fallback */ }
+  }
+  try {
+    const h = hljs.highlightAuto(code).value
+    return `<pre class="hljs"><code>${h}</code></pre>`
+  } catch (_) {
+    return `<pre class="hljs"><code>${code}</code></pre>`
+  }
+}
+
+const origHtml = renderer.html.bind(renderer)
+renderer.html = function(token: { text: string }) {
+  const html = typeof token === 'string' ? token : token.text
+  if (html.includes('<details')) return html
+  return origHtml(token)
+}
+
+marked.use({ renderer })
+
+// ── types ──
+interface QuizBlock { type: 'quiz'; data: { question: string; options: string[]; correct: number; explanation: string } }
+interface CodeBlock { type: 'code'; code: string; language: string }
+interface ExerciseBlock { type: 'exercise'; code: string; language: string }
+interface HtmlBlock { type: 'html'; html: string }
+type ContentBlock = HtmlBlock | QuizBlock | CodeBlock | ExerciseBlock
+
+// ── auto-wrap ## headings into <details> ──
+function autoWrapDetails(md: string): string {
+  if (!props.autoCollapse || !md) return md
+  const pat = new RegExp(`^#{${props.collapseDepth}} +`, 'm')
+  const lines = md.split('\n')
+  const secs: { h?: string; body: string[] }[] = []
+  let cur: { h?: string; body: string[] } = { body: [] }
+
+  for (const line of lines) {
+    if (pat.test(line)) {
+      if (cur.body.length > 0 || cur.h !== undefined) secs.push(cur)
+      cur = { h: line.replace(/^#{2,4} +/, '').trim(), body: [] }
+    } else {
+      cur.body.push(line)
+    }
+  }
+  if (cur.body.length > 0 || cur.h) secs.push(cur)
+
+  return secs.map((s, i) => {
+    const b = s.body.join('\n').trim()
+    if (!b) return s.h ? `## ${s.h}` : ''
+    if (!s.h) return b
+    // 第一个折叠块默认展开,其余收起
+    const openAttr = i === 0 ? ' open' : ''
+    return `<details class="md-collapse"${openAttr}>\n<summary>${s.h}</summary>\n\n${b}\n\n</details>`
+  }).filter(Boolean).join('\n\n')
+}
+
+// ── extract ```quiz / ```exercise:lang / ```lang blocks ──
+const QUIZ_RE = /```quiz\s*\n([\s\S]*?)```/g
+const EXERCISE_RE = /```exercise:(\w+)\s*\n([\s\S]*?)```/g
+const CODE_RE = /```(python|javascript|typescript|html|css|bash|sh)\s*\n([\s\S]*?)```/g
+
+interface ExtractedCode { index: number; code: string; language: string; fullMatch: string }
+
+function sanitizeHtml(raw: string): string {
+  return DOMPurify.sanitize(raw, {
+    ADD_TAGS: ['pre', 'code', 'details', 'summary'],
+    ADD_ATTR: ['class', 'open'],
+  })
+}
+
+/**
+ * Heuristic: infer correct answer index from quiz explanation text.
+ * LLM often outputs correct=0 due to training data bias, even when
+ * the explanation clearly identifies a different option as correct.
+ * This detects contradictions and explicit mentions to fix the index.
+ */
+function inferCorrectFromExplanation(explanation: string, options: string[], llmCorrect: number): number | null {
+  const letters = ['A', 'B', 'C', 'D']
+  const optCount = options.length
+  const exp = explanation.trim()
+
+  // 1) Explicit positive mention: "X 是正确答案" / "X 正确" / "正确: X" / "选项X"
+  for (let i = 0; i < optCount; i++) {
+    const letter = letters[i]
+    const posPatterns = [
+      new RegExp(`${letter}\\s*(?:是正确答案|正确选项|符合题意|符合|可以|应该选|正确)`),
+      new RegExp(`(?:正确答案|正确选项|应选|选择|推荐)\\s*[::]?\\s*[((]?${letter}[))]?`),
+      new RegExp(`[((]${letter}[))]\\s*(?:正确|符合)`),
+    ]
+    for (const pat of posPatterns) {
+      if (pat.test(exp)) return i
+    }
+  }
+
+  // 2) Contradiction: LLM says correct=0 but explanation says A is wrong
+  if (llmCorrect === 0) {
+    // Broader negative detection: include "描述的是...的行为","是指" etc.
+    const negPattern = /[((]?([A-D])[))]?\s*(?:错误|不对|不符合|缺少|不是|缺少引号|报错|描述的是|的行为|是指)/
+    const negMatch = exp.match(negPattern)
+    if (negMatch) {
+      const wrongLetter = negMatch[1]
+      const wrongIdx = wrongLetter.charCodeAt(0) - 65
+      if (wrongIdx === 0) {
+        // A is explicitly wrong, so correct is not 0. Find right one.
+        // Check if explanation starts with an option's text (strong signal)
+        for (let i = 1; i < optCount; i++) {
+          const optText = options[i].replace(/^[A-D][.、.\s]+/, '').trim()
+          if (optText.length >= 4 && exp.startsWith(optText)) {
+            // Verify this option is NOT marked as wrong
+            const alsoNeg = new RegExp(`[((]?${letters[i]}[))]?\\s*(?:错误|不对|不符合|缺少|不是|缺少引号|报错|描述的是|的行为|是指)`)
+            if (!alsoNeg.test(exp)) return i
+          }
+        }
+        // Fallback: find first option not explicitly negated
+        for (let i = 1; i < optCount; i++) {
+          const alsoNeg = new RegExp(`[((]?${letters[i]}[))]?\\s*(?:错误|不对|不符合|缺少|不是|缺少引号|报错|描述的是|的行为|是指)`)
+          if (!alsoNeg.test(exp)) return i
+        }
+        return 1
+      }
+    }
+  }
+
+  return null // no override
+}
+
+const contentBlocks = computed(() => {
+  const blocks: ContentBlock[] = []
+  let md = props.content || ''
+  if (!md) return blocks
+
+  // ---- Step 1: Extract quiz blocks ----
+  const quizBlocks: { start: number; end: number; data: QuizBlock['data'] }[] = []
+  QUIZ_RE.lastIndex = 0
+  let m: RegExpExecArray | null
+  while ((m = QUIZ_RE.exec(md)) !== null) {
+    try {
+      const raw = m[1].trim()
+      const d = JSON.parse(raw)
+      // validate + fallback: accept quizes even with partial data
+      const hasQuestion = typeof d.question === 'string' && d.question.trim().length > 0
+      const hasOptions = Array.isArray(d.options) && d.options.length >= 2
+      const hasExplanation = typeof d.explanation === 'string' && d.explanation.trim().length > 0
+      if (!hasQuestion || !hasOptions) {
+        // missing essential fields -> skip, renders as plain text
+        continue
+      }
+      // coerce correct to number (AI sometimes outputs string like "2" instead of 2)
+      let rawCorrect = d.correct
+      if (typeof rawCorrect === 'string') {
+        rawCorrect = Number(rawCorrect)
+      }
+      const hasCorrect = typeof rawCorrect === 'number' && !Number.isNaN(rawCorrect)
+      // clamp correct index to valid range
+      let correctIdx = hasCorrect ? Math.round(rawCorrect) : 0
+      if (correctIdx < 0 || correctIdx >= d.options.length) {
+        correctIdx = 0
+      }
+      // ── heuristic: validate correctIdx against explanation ──
+      // LLM often defaults to correct=0 even when answer is elsewhere.
+      // Cross-check: if explanation mentions an option letter as "wrong" but
+      // correctIdx points to it, OR explanation explicitly names the right option, override.
+      if (hasExplanation && d.options.length >= 2) {
+        const inferred = inferCorrectFromExplanation(d.explanation, d.options, correctIdx)
+        if (inferred !== null && inferred !== correctIdx) {
+          correctIdx = inferred
+        }
+      }
+      quizBlocks.push({
+        start: m.index,
+        end: m.index + m[0].length,
+        data: {
+          question: d.question.trim(),
+          options: d.options,
+          correct: correctIdx,
+          explanation: hasExplanation ? d.explanation.trim() : '',
+          code: typeof d.code === 'string' ? d.code.trim() : undefined,
+        },
+      })
+    } catch {
+      // parse failed, leave as-is (renders as plain markdown)
+    }
+  }
+
+  // ---- Step 2: Extract exercise code blocks (editable + submit) ----
+  const exerciseBlocks: { start: number; end: number; code: string; language: string }[] = []
+  EXERCISE_RE.lastIndex = 0
+  while ((m = EXERCISE_RE.exec(md)) !== null) {
+    const insideQuiz = quizBlocks.some(q => m!.index >= q.start && m!.index < q.end)
+    if (!insideQuiz) {
+      exerciseBlocks.push({
+        start: m.index,
+        end: m.index + m[0].length,
+        code: m[2].trim(),
+        language: m[1],
+      })
+    }
+  }
+
+  // ---- Step 3: Extract runnable code blocks ----
+  const codeBlocks: { start: number; end: number; code: string; language: string; exercise?: boolean }[] = []
+  CODE_RE.lastIndex = 0
+  while ((m = CODE_RE.exec(md)) !== null) {
+    // Skip if inside a quiz or exercise block
+    const insideQuiz = quizBlocks.some(q => m!.index >= q.start && m!.index < q.end)
+    const insideExercise = exerciseBlocks.some(e => m!.index >= e.start && m!.index < e.end)
+    if (!insideQuiz && !insideExercise) {
+      codeBlocks.push({
+        start: m.index,
+        end: m.index + m[0].length,
+        code: m[2].trim(),
+        language: m[1],
+      })
+    }
+  }
+
+  // ---- Step 4: Merge all regions and split into segments ----
+  const regions: { start: number; end: number; type: string; data?: any }[] = [
+    ...quizBlocks.map(q => ({ start: q.start, end: q.end, type: 'quiz', data: q.data })),
+    ...exerciseBlocks.map(c => ({ start: c.start, end: c.end, type: 'exercise', data: { code: c.code, language: c.language } })),
+    ...codeBlocks.map(c => ({ start: c.start, end: c.end, type: 'code', data: { code: c.code, language: c.language } })),
+  ].sort((a, b) => a.start - b.start)
+
+  let cursor = 0
+  for (const region of regions) {
+    // Text before this region
+    if (region.start > cursor) {
+      const before = md.slice(cursor, region.start)
+      if (before.trim()) {
+        const html = marked.parse(autoWrapDetails(before)) as string
+        blocks.push({ type: 'html', html: sanitizeHtml(html) })
+      }
+    }
+    // The region itself
+    if (region.type === 'quiz') {
+      blocks.push({ type: 'quiz', data: region.data })
+    } else if (region.type === 'code') {
+      blocks.push({ type: 'code', code: region.data.code, language: region.data.language })
+    } else if (region.type === 'exercise') {
+      blocks.push({ type: 'exercise', code: region.data.code, language: region.data.language })
+    }
+    cursor = region.end
+  }
+
+  // Remaining text
+  if (cursor < md.length) {
+    const rest = md.slice(cursor)
+    if (rest.trim()) {
+      const html = marked.parse(autoWrapDetails(rest)) as string
+      blocks.push({ type: 'html', html: sanitizeHtml(html) })
+    }
+  }
+
+  return blocks
+})
+
+const emit = defineEmits<{
+  quizResult: [result: { total: number; correct: number }]
+  exerciseDetected: [data: { code: string; language: string }]
+  contentMeta: [meta: { quizCount: number; exerciseCount: number }]
+}>()
+
+const quizResults = reactive<Record<number, boolean>>({})
+
+// Reset tracking when content changes (new agent response)
+watch(() => props.content, (newContent) => {
+  for (const key of Object.keys(quizResults)) {
+    delete quizResults[key]
+  }
+  
+  // Emit content meta for quiz/exercise tracking
+  if (newContent) {
+    const quizCount = (newContent.match(/```quiz\s*\n/g) || []).length
+    const exerciseCount = (newContent.match(/```exercise:\w+\s*\n/g) || []).length
+    emit('contentMeta', { quizCount, exerciseCount })
+  }
+}, { immediate: true })
+
+function onQuizAnswer(index: number, correct: boolean) {
+  quizResults[index] = correct
+
+  const total = contentBlocks.value.filter(b => b.type === 'quiz').length
+  const answered = Object.keys(quizResults).length
+  if (answered >= total && total > 0) {
+    const correctCount = Object.values(quizResults).filter(Boolean).length
+    emit('quizResult', { total, correct: correctCount })
+  }
+}
+</script>
+
+<style scoped>
+.markdown-renderer { width: 100%; }
+.markdown-body {
+  font-size: 14px;
+  line-height: 1.7;
+  word-wrap: break-word;
+}
+.markdown-body :deep(h1),
+.markdown-body :deep(h2),
+.markdown-body :deep(h3),
+.markdown-body :deep(h4),
+.markdown-body :deep(h5),
+.markdown-body :deep(h6) {
+  margin-top: 16px; margin-bottom: 8px;
+  font-weight: 600; line-height: 1.25;
+}
+.markdown-body :deep(h1) { font-size: 1.5em; }
+.markdown-body :deep(h2) { font-size: 1.3em; }
+.markdown-body :deep(h3) { font-size: 1.15em; }
+.markdown-body :deep(h4) { font-size: 1em; }
+.markdown-body :deep(p) { margin-top: 0; margin-bottom: 12px; }
+.markdown-body :deep(ul), .markdown-body :deep(ol) { margin-top: 0; margin-bottom: 12px; padding-left: 24px; }
+.markdown-body :deep(li) { margin-bottom: 4px; }
+.markdown-body :deep(li + li) { margin-top: 4px; }
+.markdown-body :deep(pre) { margin: 0 0 12px; padding: 12px 16px; font-size: 12px; line-height: 1.5; background: #1e1e1e; border-radius: 8px; overflow-x: auto; }
+.markdown-body :deep(code) { font-family: 'SF Mono','Fira Code','Cascadia Code',Consolas,monospace; }
+.markdown-body :deep(:not(pre) > code) { padding: 2px 6px; font-size: .9em; background: rgba(0,0,0,.06); border-radius: 4px; color: #e83e8c; }
+.markdown-body :deep(blockquote) { margin: 0 0 12px; padding: 8px 16px; border-left: 4px solid #667eea; background: rgba(102,126,234,.05); border-radius: 0 8px 8px 0; }
+.markdown-body :deep(blockquote p) { margin-bottom: 0; }
+.markdown-body :deep(table) { width: 100%; margin: 0 0 12px; border-collapse: collapse; border: 1px solid #e1e4e8; border-radius: 8px; overflow: hidden; }
+.markdown-body :deep(th), .markdown-body :deep(td) { padding: 8px 12px; border: 1px solid #e1e4e8; text-align: left; }
+.markdown-body :deep(th) { font-weight: 600; background: #f6f8fa; }
+.markdown-body :deep(tr:nth-child(even)) { background: #f6f8fa; }
+.markdown-body :deep(a) { color: #667eea; text-decoration: none; }
+.markdown-body :deep(a:hover) { text-decoration: underline; }
+.markdown-body :deep(strong) { font-weight: 600; }
+.markdown-body :deep(em) { font-style: italic; }
+.markdown-body :deep(hr) { margin: 16px 0; border: 0; border-top: 1px solid #e1e4e8; }
+.markdown-body :deep(img) { max-width: 100%; height: auto; border-radius: 8px; }
+
+/* Details collapsible */
+.markdown-body :deep(details.md-collapse) { margin: 8px 0; border: 1px solid #e4e7ed; border-radius: 10px; overflow: hidden; background: #fafbfc; transition: all .2s; }
+.markdown-body :deep(details.md-collapse[open]) { background: #fff; border-color: #d0d5e0; box-shadow: 0 1px 4px rgba(0,0,0,.04); }
+.markdown-body :deep(details.md-collapse summary) { display: flex; align-items: center; gap: 8px; padding: 10px 14px; font-weight: 600; font-size: 14px; color: #1a1a1a; cursor: pointer; user-select: none; list-style: none; }
+.markdown-body :deep(details.md-collapse summary::before) { content: '▶'; font-size: 10px; color: #999; transition: transform .2s; flex-shrink: 0; }
+.markdown-body :deep(details.md-collapse[open] summary::before) { transform: rotate(90deg); }
+.markdown-body :deep(details.md-collapse summary::-webkit-details-marker) { display: none; }
+.markdown-body :deep(details.md-collapse summary::marker) { display: none; content: ''; }
+.markdown-body :deep(details.md-collapse summary:hover) { background: rgba(0,0,0,.02); }
+.markdown-body :deep(details.md-collapse > :not(summary)) { padding: 0 14px 10px 30px; animation: fade-in .25s ease; }
+@keyframes fade-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
+.markdown-body :deep(details.md-collapse:first-of-type) { border-color: #d0d5e0; background: #fff; }
+
+/* Exercise Card */
+.exercise-card {
+  margin: 12px 0;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+  overflow: hidden;
+  background: #fafbfc;
+}
+.ec-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px 14px;
+  background: #f0f2f5;
+  border-bottom: 1px solid #e4e7ed;
+}
+.ec-lang {
+  font-size: 11px;
+  font-weight: 600;
+  color: #666;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+.ec-badge {
+  font-size: 12px;
+  color: #667eea;
+  font-weight: 500;
+}
+.ec-preview {
+  margin: 0;
+  padding: 12px 14px;
+  font-size: 13px;
+  line-height: 1.5;
+  background: #1e1e1e;
+  color: #c9d1d9;
+  max-height: 180px;
+  overflow-y: auto;
+  font-family: 'SF Mono','Fira Code','Cascadia Code',Consolas,monospace;
+}
+.ec-preview code {
+  font-family: inherit;
+  white-space: pre;
+}
+.ec-actions {
+  padding: 8px 14px;
+  background: #fafbfc;
+  border-top: 1px solid #e4e7ed;
+  display: flex;
+  gap: 8px;
+}
+.ec-open-btn {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 16px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #fff;
+  background: #667eea;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+.ec-open-btn:hover {
+  background: #5a6fd6;
+  transform: translateY(-1px);
+}
+</style>

+ 297 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/QuizWidget.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="quiz-widget" :class="{ answered, correct: isCorrect, wrong: !isCorrect && answered }">
+    <div class="quiz-header">
+      <span class="quiz-badge">📝 测验</span>
+      <span v-if="answered" class="quiz-result" :class="isCorrect ? 'correct' : 'wrong'">
+        {{ isCorrect ? '✓ 正确!' : '✗ 错误' }}
+      </span>
+    </div>
+
+    <div v-if="data.code" class="quiz-code-block">
+      <pre><code>{{ data.code }}</code></pre>
+    </div>
+    <p class="quiz-question">{{ data.question }}</p>
+
+    <div class="quiz-options">
+      <div
+        v-for="(option, index) in data.options"
+        :key="index"
+        :class="[
+          'quiz-option',
+          {
+            selected: selectedIndex === index,
+            correct: answered && index === data.correct,
+            wrong: answered && selectedIndex === index && index !== data.correct,
+            disabled: answered,
+          }
+        ]"
+        @click="selectOption(index)"
+      >
+        <span class="option-radio">
+          <span v-if="answered && index === data.correct" class="check">✓</span>
+          <span v-else-if="answered && selectedIndex === index && index !== data.correct" class="cross">✗</span>
+          <span v-else class="dot"></span>
+        </span>
+        <span class="option-text">{{ option.replace(/^[A-D]\.\s*/, '') }}</span>
+      </div>
+    </div>
+
+    <Transition name="explain-fade">
+      <div v-if="answered" class="quiz-explanation">
+        <div class="explain-icon">{{ isCorrect ? '🎉' : '💡' }}</div>
+        <div class="explain-text">{{ data.explanation }}</div>
+      </div>
+    </Transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+export interface QuizData {
+  question: string
+  options: string[]
+  correct: number
+  explanation: string
+  code?: string
+}
+
+const props = defineProps<{
+  data: QuizData
+}>()
+
+const emit = defineEmits<{
+  answer: [correct: boolean]
+}>()
+
+const selectedIndex = ref<number | null>(null)
+const answered = ref(false)
+
+const isCorrect = ref(false)
+
+const selectOption = (index: number) => {
+  if (answered.value) return
+  selectedIndex.value = index
+  answered.value = true
+  // coerce correct to number (AI sometimes outputs string)
+  const correctIndex = typeof props.data.correct === 'number' ? props.data.correct : Number(props.data.correct)
+  isCorrect.value = index === correctIndex
+  emit('answer', isCorrect.value)
+}
+</script>
+
+<style scoped>
+.quiz-widget {
+  margin: 12px 0;
+  background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
+  border: 1px solid #e0e4f0;
+  border-radius: 12px;
+  padding: 16px;
+  transition: all 0.3s ease;
+}
+
+.quiz-widget.answered {
+  background: #fff;
+}
+
+.quiz-widget.correct {
+  border-color: #b7eb8f;
+  box-shadow: 0 0 0 1px rgba(82, 196, 26, 0.1);
+}
+
+.quiz-widget.wrong {
+  border-color: #ffccc7;
+  box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.1);
+}
+
+.quiz-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.quiz-badge {
+  font-size: 12px;
+  font-weight: 600;
+  color: #667eea;
+  padding: 2px 10px;
+  background: rgba(102, 126, 234, 0.1);
+  border-radius: 10px;
+}
+
+.quiz-result {
+  font-size: 13px;
+  font-weight: 600;
+  padding: 2px 10px;
+  border-radius: 10px;
+}
+
+.quiz-result.correct {
+  color: #52c41a;
+  background: rgba(82, 196, 26, 0.1);
+}
+
+.quiz-result.wrong {
+  color: #ff4d4f;
+  background: rgba(255, 77, 79, 0.1);
+}
+
+.quiz-code-block {
+  margin: 0 0 12px 0;
+  background: #1e1e1e;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.quiz-code-block pre {
+  margin: 0;
+  padding: 14px 16px;
+  overflow-x: auto;
+}
+
+.quiz-code-block code {
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  color: #d4d4d4;
+  white-space: pre;
+}
+
+.quiz-question {
+  margin: 0 0 12px 0;
+  font-size: 14px;
+  font-weight: 500;
+  color: #1a1a1a;
+  line-height: 1.6;
+}
+
+.quiz-options {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.quiz-option {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 10px 14px;
+  background: white;
+  border: 1.5px solid #e8e8e8;
+  border-radius: 10px;
+  cursor: pointer;
+  transition: all 0.2s;
+  font-size: 14px;
+  color: #333;
+}
+
+.quiz-option:hover:not(.disabled) {
+  border-color: #667eea;
+  background: #f8f9ff;
+}
+
+.quiz-option.selected:not(.answered) {
+  border-color: #667eea;
+  background: #f0f2ff;
+}
+
+.quiz-option.correct {
+  border-color: #52c41a;
+  background: #f6ffed;
+  color: #135200;
+}
+
+.quiz-option.wrong {
+  border-color: #ff4d4f;
+  background: #fff2f0;
+  color: #820014;
+}
+
+.quiz-option.disabled {
+  cursor: default;
+  opacity: 0.85;
+}
+
+.option-radio {
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  border: 2px solid #d9d9d9;
+  transition: all 0.2s;
+}
+
+.quiz-option.selected .option-radio {
+  border-color: #667eea;
+  background: #667eea;
+}
+
+.quiz-option.correct .option-radio {
+  border-color: #52c41a;
+  background: #52c41a;
+}
+
+.quiz-option.wrong .option-radio {
+  border-color: #ff4d4f;
+  background: #ff4d4f;
+}
+
+.option-radio .dot {
+  width: 8px;
+  height: 8px;
+  background: transparent;
+  border-radius: 50%;
+}
+
+.quiz-option.selected .option-radio .dot {
+  background: white;
+}
+
+.option-radio .check,
+.option-radio .cross {
+  color: white;
+  font-size: 11px;
+  font-weight: bold;
+}
+
+.option-text {
+  flex: 1;
+  line-height: 1.4;
+}
+
+.quiz-explanation {
+  margin-top: 12px;
+  padding: 12px 14px;
+  background: #f8f9fa;
+  border-radius: 10px;
+  display: flex;
+  gap: 10px;
+  align-items: flex-start;
+}
+
+.explain-icon {
+  font-size: 18px;
+  flex-shrink: 0;
+  margin-top: 1px;
+}
+
+.explain-text {
+  font-size: 13px;
+  color: #666;
+  line-height: 1.5;
+}
+
+/* Transition */
+.explain-fade-enter-active {
+  transition: all 0.3s ease;
+}
+
+.explain-fade-enter-from {
+  opacity: 0;
+  transform: translateY(-6px);
+}
+</style>

+ 424 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/components/SettingsModal.vue

@@ -0,0 +1,424 @@
+<template>
+  <div class="modal-overlay" v-if="visible" @click.self="$emit('close')">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h3>LLM 配置</h3>
+        <button class="close-btn" @click="$emit('close')">&times;</button>
+      </div>
+      <div class="modal-body">
+        <div class="form-group" :class="{ 'has-error': fieldErrors.base_url }">
+          <label>API Base URL</label>
+          <input
+            v-model="form.base_url"
+            type="text"
+            placeholder="https://api.deepseek.com/v1"
+            class="form-input"
+          />
+          <span class="field-error" v-if="fieldErrors.base_url">{{ fieldErrors.base_url }}</span>
+          <span class="field-hint" v-else>例如 https://api.deepseek.com/v1</span>
+        </div>
+        <div class="form-group" :class="{ 'has-error': fieldErrors.model_id }">
+          <label>Model ID</label>
+          <input
+            v-model="form.model_id"
+            type="text"
+            placeholder="deepseek-chat"
+            class="form-input"
+          />
+          <span class="field-error" v-if="fieldErrors.model_id">{{ fieldErrors.model_id }}</span>
+        </div>
+        <div class="form-group" :class="{ 'has-error': fieldErrors.api_key }">
+          <label>API Key</label>
+          <input
+            v-model="form.api_key"
+            type="password"
+            placeholder="sk-xxxxxxxxxxxxxxxx"
+            class="form-input"
+          />
+          <span class="field-error" v-if="fieldErrors.api_key">{{ fieldErrors.api_key }}</span>
+          <span class="field-hint" v-else>必填,修改后将替换现有密钥</span>
+        </div>
+        <div v-if="message" :class="['message', messageType]">{{ message }}</div>
+      </div>
+      <div class="modal-footer">
+        <button class="btn-reset" @click="resetToDefaults">恢复默认</button>
+        <div class="footer-right">
+          <button class="btn-cancel" @click="$emit('close')">取消</button>
+          <button class="btn-save" @click="save" :disabled="saving">
+            {{ saving ? '保存中...' : '保存并应用' }}
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch } from 'vue'
+import axios from 'axios'
+
+const props = defineProps<{ visible: boolean }>()
+const emit = defineEmits<{ close: [] }>()
+
+const form = reactive({
+  base_url: '',
+  model_id: '',
+  api_key: '',
+})
+const fieldErrors = reactive({
+  base_url: '',
+  model_id: '',
+  api_key: '',
+})
+const saving = ref(false)
+const message = ref('')
+const messageType = ref<'success' | 'error'>('success')
+
+function clearFieldErrors() {
+  fieldErrors.base_url = ''
+  fieldErrors.model_id = ''
+  fieldErrors.api_key = ''
+}
+
+function validateForm(): boolean {
+  clearFieldErrors()
+  let valid = true
+
+  if (!form.base_url.trim()) {
+    fieldErrors.base_url = 'Base URL 不能为空'
+    valid = false
+  } else if (!/^https?:\/\/.+/.test(form.base_url.trim())) {
+    fieldErrors.base_url = '请输入有效的 URL(以 http:// 或 https:// 开头)'
+    valid = false
+  }
+
+  if (!form.model_id.trim()) {
+    fieldErrors.model_id = 'Model ID 不能为空'
+    valid = false
+  }
+
+  if (!form.api_key.trim()) {
+    fieldErrors.api_key = 'API Key 不能为空'
+    valid = false
+  }
+
+  return valid
+}
+
+// 打开时加载当前配置
+watch(() => props.visible, async (show) => {
+  if (!show) return
+  message.value = ''
+  clearFieldErrors()
+  try {
+    const res = await axios.get('/api/settings/llm')
+    form.base_url = res.data.base_url
+    form.model_id = res.data.model_id
+    form.api_key = ''  // 不回显密钥,让用户重新输入
+  } catch {
+    message.value = '获取当前配置失败'
+    messageType.value = 'error'
+  }
+})
+
+const save = async () => {
+  if (!validateForm()) {
+    message.value = '请修正标红的字段后重试'
+    messageType.value = 'error'
+    return
+  }
+
+  saving.value = true
+  message.value = ''
+
+  try {
+    const res = await axios.post('/api/settings/llm', {
+      base_url: form.base_url.trim(),
+      model_id: form.model_id.trim(),
+      api_key: form.api_key.trim(),
+    })
+    if (res.data.success) {
+      message.value = '✅ 配置已更新并生效'
+      messageType.value = 'success'
+    } else {
+      message.value = '❌ ' + res.data.message
+      messageType.value = 'error'
+    }
+  } catch (err: any) {
+    message.value = '❌ 保存失败: ' + (err.response?.data?.detail || err.message)
+    messageType.value = 'error'
+  } finally {
+    saving.value = false
+  }
+}
+
+const resetToDefaults = async () => {
+  saving.value = true
+  message.value = ''
+  clearFieldErrors()
+
+  try {
+    const res = await axios.post('/api/settings/llm/reset')
+    if (res.data.success) {
+      // 刷新表单显示默认值
+      form.base_url = res.data.config.base_url
+      form.model_id = res.data.config.model_id
+      form.api_key = ''
+      message.value = '✅ 已恢复为 .env 默认配置'
+      messageType.value = 'success'
+    } else {
+      message.value = '❌ ' + res.data.message
+      messageType.value = 'error'
+    }
+  } catch (err: any) {
+    message.value = '❌ 重置失败: ' + (err.response?.data?.detail || err.message)
+    messageType.value = 'error'
+  } finally {
+    saving.value = false
+  }
+}
+</script>
+
+<style scoped>
+.modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+}
+
+.modal-content {
+  background: #fff;
+  border-radius: 12px;
+  width: 480px;
+  max-width: 90vw;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+}
+
+[data-theme="dark"] .modal-content {
+  background: #2d2d2d;
+  color: #e0e0e0;
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 24px 0;
+}
+
+.modal-header h3 {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  font-size: 24px;
+  cursor: pointer;
+  color: #999;
+  padding: 0 4px;
+  line-height: 1;
+}
+
+.close-btn:hover {
+  color: #333;
+}
+
+[data-theme="dark"] .close-btn:hover {
+  color: #fff;
+}
+
+.modal-body {
+  padding: 20px 24px;
+}
+
+.form-group {
+  margin-bottom: 16px;
+}
+
+.form-group.has-error label {
+  color: #cf1322;
+}
+
+[data-theme="dark"] .form-group.has-error label {
+  color: #f48771;
+}
+
+.form-group label {
+  display: block;
+  font-size: 13px;
+  font-weight: 500;
+  margin-bottom: 6px;
+  color: #555;
+}
+
+[data-theme="dark"] .form-group label {
+  color: #aaa;
+}
+
+.form-input {
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid #d9d9d9;
+  border-radius: 6px;
+  font-size: 14px;
+  outline: none;
+  transition: border-color 0.2s, box-shadow 0.2s;
+  box-sizing: border-box;
+}
+
+.form-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
+}
+
+.has-error .form-input {
+  border-color: #ff4d4f;
+}
+
+.has-error .form-input:focus {
+  border-color: #ff4d4f;
+  box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.15);
+}
+
+[data-theme="dark"] .has-error .form-input {
+  border-color: #f48771;
+}
+
+[data-theme="dark"] .form-input {
+  background: #3c3c3c;
+  border-color: #555;
+  color: #e0e0e0;
+}
+
+[data-theme="dark"] .form-input:focus {
+  border-color: #667eea;
+}
+
+.field-error {
+  display: block;
+  font-size: 12px;
+  color: #ff4d4f;
+  margin-top: 4px;
+}
+
+.field-hint {
+  display: block;
+  font-size: 12px;
+  color: #999;
+  margin-top: 4px;
+}
+
+.message {
+  padding: 10px 14px;
+  border-radius: 6px;
+  font-size: 14px;
+  margin-top: 8px;
+}
+
+.message.success {
+  background: #f6ffed;
+  border: 1px solid #b7eb8f;
+  color: #389e0d;
+}
+
+.message.error {
+  background: #fff2f0;
+  border: 1px solid #ffccc7;
+  color: #cf1322;
+}
+
+[data-theme="dark"] .message.success {
+  background: #1a3a1a;
+  border-color: #2d7d2d;
+  color: #8dd0a8;
+}
+
+[data-theme="dark"] .message.error {
+  background: #3a1a1a;
+  border-color: #7d2d2d;
+  color: #f48771;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 24px 20px;
+}
+
+.footer-right {
+  display: flex;
+  gap: 8px;
+}
+
+.btn-reset {
+  padding: 8px 16px;
+  border: 1px solid #d9d9d9;
+  background: transparent;
+  border-radius: 6px;
+  font-size: 13px;
+  cursor: pointer;
+  color: #999;
+  transition: all 0.2s;
+}
+
+.btn-reset:hover {
+  border-color: #ff4d4f;
+  color: #ff4d4f;
+}
+
+[data-theme="dark"] .btn-reset {
+  border-color: #555;
+  color: #888;
+}
+
+[data-theme="dark"] .btn-reset:hover {
+  border-color: #f48771;
+  color: #f48771;
+}
+
+.btn-cancel {
+  padding: 8px 20px;
+  border: 1px solid #d9d9d9;
+  background: #fff;
+  border-radius: 6px;
+  font-size: 14px;
+  cursor: pointer;
+}
+
+[data-theme="dark"] .btn-cancel {
+  background: #3c3c3c;
+  border-color: #555;
+  color: #e0e0e0;
+}
+
+.btn-save {
+  padding: 8px 20px;
+  border: none;
+  background: #667eea;
+  color: #fff;
+  border-radius: 6px;
+  font-size: 14px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.btn-save:hover:not(:disabled) {
+  background: #5a6fd6;
+}
+
+.btn-save:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+}
+</style>

+ 9 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/counter.ts

@@ -0,0 +1,9 @@
+export function setupCounter(element: HTMLButtonElement) {
+  let counter = 0
+  const setCounter = (count: number) => {
+    counter = count
+    element.innerHTML = `Count is ${counter}`
+  }
+  element.addEventListener('click', () => setCounter(counter + 1))
+  setCounter(0)
+}

+ 639 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/dark-theme-overrides.css

@@ -0,0 +1,639 @@
+/* ═══════════════════════════════════════════════════════════
+   暗色主题全局覆盖规则
+   使用 !important 覆盖 scoped 样式中的硬编码颜色
+   ═══════════════════════════════════════════════════════════ */
+
+/* ─── 全局基础 ─── */
+[data-theme="dark"] body {
+  background: #1a1a2e !important;
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] * {
+  scrollbar-color: #444 #1a1a2e;
+}
+
+[data-theme="dark"] ::-webkit-scrollbar-thumb {
+  background: #444 !important;
+}
+
+[data-theme="dark"] ::-webkit-scrollbar-track {
+  background: #1a1a2e !important;
+}
+
+/* ─── Login.vue ─── */
+[data-theme="dark"] .login-container {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .login-card {
+  background: #16213e !important;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important;
+}
+
+[data-theme="dark"] .login-card h1 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .login-card p,
+[data-theme="dark"] .input-label {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .username-input {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .username-input::placeholder {
+  color: #808080 !important;
+}
+
+[data-theme="dark"] .login-footer p {
+  color: #808080 !important;
+}
+
+/* ─── Chat.vue ─── */
+[data-theme="dark"] .chat-section {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .chat-header {
+  background: #16213e !important;
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .chat-header .subtitle {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .welcome-card {
+  background: linear-gradient(135deg, #16213e 0%, #0f3460 100%) !important;
+}
+
+[data-theme="dark"] .welcome-card h3 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .welcome-card p {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .feature-item {
+  background: #16213e !important;
+  color: #e0e0e0 !important;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .message.assistant .bubble {
+  background: #16213e !important;
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .input-area {
+  background: #16213e !important;
+  border-top-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .input-wrapper {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .input-wrapper:focus-within {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .input-wrapper textarea {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .input-wrapper textarea::placeholder {
+  color: #808080 !important;
+}
+
+[data-theme="dark"] .input-hint {
+  color: #808080 !important;
+}
+
+[data-theme="dark"] .code-section {
+  background: #16213e !important;
+  border-left-color: #2a2a4a !important;
+}
+
+/* ─── Learning.vue ─── */
+[data-theme="dark"] .learning-view {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .learning-view .view-header {
+  background: #16213e !important;
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .learning-view .view-header h1 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .learning-view .view-header p {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .path-card {
+  background: #16213e !important;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .path-card:hover {
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5) !important;
+}
+
+[data-theme="dark"] .path-card .path-title {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .path-card .path-desc {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .path-card .stat {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .module-card {
+  background: #16213e !important;
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .module-card .module-title {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .module-card .module-desc {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .lesson-item {
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .lesson-item .lesson-title {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .lesson-item .lesson-meta {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .lesson-modal {
+  background: #16213e !important;
+}
+
+[data-theme="dark"] .modal-header {
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .modal-header .modal-title-area h2 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .modal-header .modal-module {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .modal-body .lesson-description {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .modal-body .detail-item .detail-label {
+  color: #808080 !important;
+}
+
+[data-theme="dark"] .modal-body .detail-item .detail-value {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .modal-body .code-placeholder {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .coach-panel {
+  background: #16213e !important;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .coach-panel .coach-greeting {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .coach-panel .rec-title {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .coach-panel .rec-desc {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .coach-panel .coach-encouragement {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .assessment-card {
+  background: #16213e !important;
+}
+
+[data-theme="dark"] .assessment-prompt {
+  background: #0f3460 !important;
+}
+
+/* ─── Assessment.vue ─── */
+[data-theme="dark"] .assessment-view {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .assessment-header {
+  background: #16213e !important;
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .assessment-header .header-left h1 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .assessment-header .path-label {
+  color: #7c8cf0 !important;
+}
+
+[data-theme="dark"] .assessment-header .question-count {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .assessment-header .progress-bar {
+  background: #2a2a4a !important;
+}
+
+[data-theme="dark"] .question-card {
+  background: #16213e !important;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .question-content {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .code-block {
+  background: #0f3460 !important;
+  color: #e0e0e0 !important;
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .option-item {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .option-item:hover {
+  background: #16213e !important;
+  border-color: #7c8cf0 !important;
+}
+
+[data-theme="dark"] .option-item.selected {
+  background: #16213e !important;
+  border-color: #7c8cf0 !important;
+}
+
+[data-theme="dark"] .option-item.correct {
+  background: rgba(82, 196, 26, 0.15) !important;
+  border-color: #52c41a !important;
+}
+
+[data-theme="dark"] .option-item.incorrect {
+  background: rgba(255, 77, 79, 0.15) !important;
+  border-color: #ff4d4f !important;
+}
+
+[data-theme="dark"] .explanation-box {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .explanation-title {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .explanation-text {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .result-card {
+  background: #16213e !important;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .result-header h2 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .score-circle {
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .score-value {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .level-text {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .category-name {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .category-score {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .score-bar {
+  background: #2a2a4a !important;
+}
+
+[data-theme="dark"] .recommendation-box {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .recommendation-box p {
+  color: #b0b0b0 !important;
+}
+
+/* ─── Dashboard.vue ─── */
+[data-theme="dark"] .dashboard-view {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .dashboard-view .view-header {
+  background: #16213e !important;
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .dashboard-view .view-header h1 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .dashboard-view .view-header p {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .path-selector {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .path-tab {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .path-tab.active {
+  background: #16213e !important;
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .summary-card {
+  background: #16213e !important;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .summary-card .card-value {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .summary-card .card-label {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .chart-card {
+  background: #16213e !important;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .chart-header h3 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .empty-chart {
+  color: #808080 !important;
+}
+
+[data-theme="dark"] .recent-card {
+  background: #16213e !important;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .recent-card h3 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .history-item {
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .history-info .history-date {
+  color: #808080 !important;
+}
+
+[data-theme="dark"] .history-categories .cat-tag {
+  background: #0f3460 !important;
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .empty-history {
+  color: #808080 !important;
+}
+
+/* ─── Orchestration.vue ─── */
+[data-theme="dark"] .orchestration-view {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .orchestration-view .view-header {
+  background: #16213e !important;
+  border-bottom-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .orchestration-view .view-header h1 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .orchestration-view .view-header p {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .stat-chip {
+  background: #0f3460 !important;
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .info-panel {
+  background: #16213e !important;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3) !important;
+}
+
+[data-theme="dark"] .info-panel h3 {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .agent-item {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .agent-item:hover {
+  background: #16213e !important;
+}
+
+[data-theme="dark"] .agent-item.active {
+  border-color: #7c8cf0 !important;
+}
+
+[data-theme="dark"] .agent-name {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .agent-desc {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .meta-item {
+  color: #808080 !important;
+}
+
+/* ─── Vue Flow (Orchestration) ─── */
+[data-theme="dark"] .vue-flow {
+  background: #1a1a2e !important;
+}
+
+[data-theme="dark"] .vue-flow__edge-path {
+  stroke: #4a4a6a !important;
+}
+
+[data-theme="dark"] .vue-flow__node {
+  background: #16213e !important;
+  border-color: #2a2a4a !important;
+}
+
+/* ─── 通用按钮 ─── */
+[data-theme="dark"] .btn-primary {
+  background: linear-gradient(135deg, #7c8cf0 0%, #9a6dd7 100%) !important;
+}
+
+[data-theme="dark"] .btn-outline {
+  border-color: #4a4a6a !important;
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .btn-outline:hover {
+  border-color: #7c8cf0 !important;
+  color: #e0e0e0 !important;
+}
+
+/* ─── 进度条 ─── */
+[data-theme="dark"] .progress-bar {
+  background: #2a2a4a !important;
+}
+
+[data-theme="dark"] .progress-text {
+  color: #b0b0b0 !important;
+}
+
+/* ─── 徽章/标签 ─── */
+[data-theme="dark"] .status-badge {
+  background: #0f3460 !important;
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .category-badge {
+  background: #0f3460 !important;
+  color: #7c8cf0 !important;
+}
+
+[data-theme="dark"] .difficulty-badge {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .type-badge {
+  background: #0f3460 !important;
+}
+
+[data-theme="dark"] .level-badge {
+  background: #0f3460 !important;
+  color: #e0e0e0 !important;
+}
+
+/* ─── 模态框覆盖层 ─── */
+[data-theme="dark"] .lesson-modal-overlay {
+  background: rgba(0, 0, 0, 0.7) !important;
+}
+
+/* ─── 加载状态 ─── */
+[data-theme="dark"] .spinner {
+  border-color: #2a2a4a !important;
+  border-top-color: #7c8cf0 !important;
+}
+
+[data-theme="dark"] .loading-state p {
+  color: #b0b0b0 !important;
+}
+
+/* ─── AgentConfigPanel ─── */
+[data-theme="dark"] .config-overlay {
+  background: rgba(0, 0, 0, 0.6) !important;
+}
+
+[data-theme="dark"] .config-panel {
+  background: #16213e !important;
+  box-shadow: -4px 0 24px rgba(0, 0, 0, 0.5) !important;
+}
+
+[data-theme="dark"] .panel-body .section-text {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .prompt-block {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .prompt-text {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .activity-card {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .activity-value {
+  color: #e0e0e0 !important;
+}
+
+[data-theme="dark"] .last-message {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .msg-preview {
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .panel-footer {
+  border-top-color: #2a2a4a !important;
+}
+
+[data-theme="dark"] .action-btn.secondary {
+  background: #0f3460 !important;
+  border-color: #2a2a4a !important;
+  color: #b0b0b0 !important;
+}
+
+[data-theme="dark"] .action-btn.secondary:hover {
+  background: #16213e !important;
+}

+ 230 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/locales/en.ts

@@ -0,0 +1,230 @@
+export default {
+  // ===== App.vue =====
+  nav: {
+    brand: 'Way_to_Engineer',
+    chat: '💬 Chat',
+    learning: '📚 Learning Paths',
+    dashboard: '📊 Dashboard',
+    orchestration: '🔗 Agent Orchestration',
+    logout: 'Logout',
+    switchLang: '切换为中文',
+  },
+  theme: {
+    light: 'Switch to light theme',
+    dark: 'Switch to dark theme',
+  },
+
+  // ===== Login.vue =====
+  login: {
+    subtitle: 'AI-Powered Programming Learning Platform',
+    label: 'Enter your username',
+    placeholder: 'e.g. zhangsan',
+    btn: 'Start Learning',
+    footer: 'Username is stored locally to isolate your learning data',
+  },
+
+  // ===== Chat.vue =====
+  chat: {
+    subtitle: 'AI Programming Assistant',
+    online: 'Online',
+    welcomeTitle: 'Welcome to Way_to_Engineer',
+    welcomeDesc: 'I\'m your AI programming team, here to help with:',
+    features: {
+      tutor: 'Programming Tutor',
+      debug: 'Debug Helper',
+      review: 'Code Review',
+      arch: 'Architecture',
+    },
+    placeholder: 'Type your programming question...',
+    send: 'Send',
+    hint: 'Enter to send, Shift+Enter for new line',
+    continueSession: 'Welcome back! You were learning {lesson}. Continue?',
+    continueBtn: 'Continue',
+    masteredBtn: '📖 I\'ve mastered this',
+    quizPass: '🎉 {correct}/{total} correct! Great job!',
+    quizFail: '💪 {correct}/{total} correct. Keep trying!',
+    lessonCompleted: '✅ Lesson marked as completed!',
+    nextLesson: 'Next Lesson →',
+    allComplete: 'All lessons complete! Check your learning path.',
+  },
+
+  // ===== Learning.vue =====
+  learning: {
+    title: 'Learning Paths',
+    subtitle: 'Choose a direction and start your engineer journey',
+    modules: 'Modules',
+    lessons: 'Lessons',
+    selected: '✓ Selected',
+    assessmentResult: 'Assessment Result',
+    currentLevel: 'Current Level: ',
+    score: 'Score',
+    unit: 'pts',
+    retake: '🔄 Retake Test',
+    assessmentPrompt: 'Before starting, we recommend a skill assessment',
+    assessmentPromptDesc: 'AI will recommend a learning starting point based on your results',
+    startTest: 'Start Test',
+    courseDetail: 'Course Details',
+    completePercent: '% Complete',
+    minutes: 'min',
+    type: 'Type',
+    duration: 'Duration',
+    status: 'Status',
+    completed: '✓ Completed',
+    notCompleted: 'Not Completed',
+    learningContent: 'Learning Content',
+    learningContentDesc: 'Course content, code examples, and exercises will appear here.',
+    codePlaceholder: '// Course content area',
+    startLearning: '📚 Start Learning',
+    markComplete: '✓ Mark Complete',
+    close: 'Close',
+    coachTitle: 'Coach Recommendations',
+  },
+
+  // ===== Assessment.vue =====
+  assessment: {
+    loading: 'AI is generating questions...',
+    title: 'Skill Assessment',
+    questionProgress: 'Question {current} / {total}',
+    confirmAnswer: 'Confirm',
+    viewResult: 'View Results',
+    nextQuestion: 'Next',
+    testComplete: 'Test Complete!',
+    yourLevel: 'Your Level: ',
+    correctCount: '{correct} / {total} correct',
+    unit: 'pts',
+    categoryScores: 'Category Scores',
+    learningSuggestion: 'Learning Suggestion',
+    suggestionDesc: 'Based on your results, we suggest starting with {module}.',
+    startLearning: '📚 Start Learning',
+    retake: '🔄 Retake Test',
+    goBack: '← Back',
+    answerCorrect: 'Correct!',
+    answerWrong: 'Incorrect — see explanation',
+  },
+
+  // ===== Dashboard.vue =====
+  dashboard: {
+    title: 'Learning Dashboard',
+    subtitle: 'Track your learning progress and skill growth',
+    totalTests: 'Total Tests',
+    avgScore: 'Average Score',
+    currentLevel: 'Current Level',
+    bestSkill: 'Best Skill',
+    categoryLevels: 'Category Levels',
+    recentTest: 'Recent Test: ',
+    noTestData: 'No test data yet. Complete an assessment first.',
+    startTest: 'Start Test',
+    scoreTrend: 'Score Trend',
+    totalTestsCount: '{count} tests taken',
+    trendDesc: 'Score trends will appear after multiple tests',
+    recentRecords: 'Recent Records',
+    retake: '🔄 Retake Test',
+    noRecords: 'No test records yet',
+    firstTest: 'Take First Test',
+  },
+
+  // ===== Orchestration.vue =====
+  orchestration: {
+    title: 'Agent Orchestration',
+    subtitle: 'View and manage AI agent collaboration',
+    active: 'Active',
+    messages: 'Messages',
+    agents: {
+      orchestrator: 'Orchestrator',
+      tutor: 'Tutor',
+      debug: 'Debugger',
+      review: 'Reviewer',
+      arch: 'Architect',
+      coach: 'Coach',
+    },
+    messagesCount: '{count} messages',
+  },
+
+  // ===== CodeEditor.vue =====
+  codeEditor: {
+    run: '▶ Run',
+    running: 'Running...',
+    error: '❌ Error',
+    output: '📤 Output',
+    clear: 'Clear',
+    placeholder: 'Click "Run" to execute code...',
+    defaultCode: '# Write Python code here\n# Press Ctrl+Enter or click "Run"\n\ndef greet(name):\n    """Greeting function"""\n    return f"Hello, {name}!"\n\n# Call the function\nmessage = greet("World")\nprint(message)\n\n# List comprehension\nsquares = [x**2 for x in range(10)]\nprint(f"Squares: {squares}")',
+    submitFeedback: 'Submit for Review',
+    submitting: 'Submitting...',
+    feedbackTitle: 'AI Review Feedback',
+    feedbackError: 'Failed to get feedback. Please try again.',
+    languageLabel: 'Language:',
+    preview: 'Preview',
+    previewing: 'Rendering...',
+  },
+
+  // ===== helpers.ts (runtime translations) =====
+  helpers: {
+    levels: {
+      beginner: 'Beginner',
+      intermediate: 'Intermediate',
+      advanced: 'Advanced',
+    },
+    statuses: {
+      not_started: 'Not Started',
+      in_progress: 'In Progress',
+      completed: 'Completed',
+      locked: 'Locked',
+    },
+    lessonTypes: {
+      theory: '📖 Theory',
+      practice: '💻 Practice',
+      project: '🛠️ Project',
+      quiz: '📝 Quiz',
+    },
+    recIcons: {
+      start: '🚀',
+      continue: '➡️',
+      practice: '💪',
+      review: '🔄',
+    },
+    categories: {
+      html_css: 'HTML/CSS',
+      javascript: 'JavaScript',
+      vue: 'Vue.js',
+      browser_apis: 'Browser APIs',
+      python: 'Python',
+      api_design: 'API Design',
+      database: 'Database',
+      system_design: 'System Design',
+    },
+    difficulty: {
+      1: 'Easy',
+      2: 'Moderate',
+      3: 'Medium',
+      4: 'Hard',
+      5: 'Expert',
+    },
+    questionTypes: {
+      choice: 'Multiple Choice',
+      code_output: 'Predict Output',
+      code_fill: 'Fill in Code',
+      bug_fix: 'Find Bug',
+    },
+    agentStatus: {
+      active: 'Active',
+      processing: 'Processing',
+      idle: 'Idle',
+      error: 'Error',
+    },
+    modules: {
+      'fe-html-css': 'HTML/CSS Basics',
+      'fe-javascript': 'JavaScript Core',
+      'fe-vue': 'Vue.js Framework',
+      'fe-project': 'Frontend Project',
+      'be-python': 'Python Basics',
+      'be-api': 'REST API Design',
+      'be-system': 'System Design & DB',
+      'be-project': 'Backend Project',
+      'fs-web-basics': 'Web Basics',
+      'fs-frontend': 'Frontend Dev',
+      'fs-backend': 'Backend Dev',
+      'fs-fullstack': 'Fullstack Project',
+    },
+  },
+}

+ 230 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/locales/zh.ts

@@ -0,0 +1,230 @@
+export default {
+  // ===== App.vue =====
+  nav: {
+    brand: 'Way_to_Engineer',
+    chat: '💬 对话',
+    learning: '📚 学习路径',
+    dashboard: '📊 学习仪表盘',
+    orchestration: '🔗 Agent编排',
+    logout: '退出登录',
+    switchLang: 'Switch to English',
+  },
+  theme: {
+    light: '切换亮色主题',
+    dark: '切换暗色主题',
+  },
+
+  // ===== Login.vue =====
+  login: {
+    subtitle: 'AI辅助编程学习平台',
+    label: '输入你的用户名',
+    placeholder: '例如:zhangsan',
+    btn: '进入学习',
+    footer: '用户名将保存在本地,用于隔离你的学习数据',
+  },
+
+  // ===== Chat.vue =====
+  chat: {
+    subtitle: 'AI 编程助手',
+    online: '在线',
+    welcomeTitle: '欢迎使用 Way_to_Engineer',
+    welcomeDesc: '我是你的AI编程团队,可以帮你:',
+    features: {
+      tutor: '编程导师',
+      debug: '调试助手',
+      review: '代码审查',
+      arch: '架构设计',
+    },
+    placeholder: '输入你的编程问题...',
+    send: '发送',
+    hint: '按 Enter 发送,Shift + Enter 换行',
+    continueSession: '欢迎回来!上次你在学习 {lesson},要继续吗?',
+    continueBtn: '继续学习',
+    masteredBtn: '📖 我已掌握',
+    quizPass: '🎉 答对了 {correct}/{total} 题,太棒了!',
+    quizFail: '💪 答对了 {correct}/{total} 题,再接再厉!可以重新学习或尝试通过。',
+    lessonCompleted: '✅ 恭喜!课程已标记完成!',
+    nextLesson: '学习下一课 →',
+    allComplete: '全部课程已完成!去学习路径看看吧。',
+  },
+
+  // ===== Learning.vue =====
+  learning: {
+    title: '学习路径',
+    subtitle: '选择一个方向,开始你的工程师进阶之路',
+    modules: '模块',
+    lessons: '课程',
+    selected: '✓ 已选择',
+    assessmentResult: '水平检测结果',
+    currentLevel: '当前水平:',
+    score: '得分',
+    unit: '分',
+    retake: '🔄 重新测试',
+    assessmentPrompt: '开始学习前,建议先进行水平检测',
+    assessmentPromptDesc: 'AI将根据你的测试结果,为你推荐合适的学习起点',
+    startTest: '开始测试',
+    courseDetail: '课程详情',
+    completePercent: '% 完成',
+    minutes: '分钟',
+    type: '类型',
+    duration: '时长',
+    status: '状态',
+    completed: '✓ 已完成',
+    notCompleted: '未完成',
+    learningContent: '学习内容',
+    learningContentDesc: '在这里可以放置课程的具体内容、代码示例、练习题等。',
+    codePlaceholder: '// 课程内容区域',
+    startLearning: '📚 开始学习',
+    markComplete: '✓ 标记完成',
+    close: '关闭',
+    coachTitle: '学习教练建议',
+  },
+
+  // ===== Assessment.vue =====
+  assessment: {
+    loading: 'AI正在生成测试题目...',
+    title: '水平检测',
+    questionProgress: '第 {current} / {total} 题',
+    confirmAnswer: '确认答案',
+    viewResult: '查看结果',
+    nextQuestion: '下一题',
+    testComplete: '测试完成!',
+    yourLevel: '你的水平:',
+    correctCount: '答对 {correct} / {total} 题',
+    unit: '分',
+    categoryScores: '各分类得分',
+    learningSuggestion: '学习建议',
+    suggestionDesc: '根据你的测试结果,建议从 {module} 开始学习。',
+    startLearning: '📚 开始学习',
+    retake: '🔄 重新测试',
+    goBack: '← 返回',
+    answerCorrect: '回答正确!',
+    answerWrong: '答错了,看看解析',
+  },
+
+  // ===== Dashboard.vue =====
+  dashboard: {
+    title: '学习仪表盘',
+    subtitle: '追踪你的学习进度和技能成长',
+    totalTests: '测试总数',
+    avgScore: '平均得分',
+    currentLevel: '当前水平',
+    bestSkill: '最强技能',
+    categoryLevels: '各分类水平',
+    recentTest: '最近测试:',
+    noTestData: '暂无测试数据,请先完成水平检测',
+    startTest: '开始测试',
+    scoreTrend: '测试成绩趋势',
+    totalTestsCount: '共 {count} 次测试',
+    trendDesc: '完成多次测试后,这里将显示你的成绩变化趋势',
+    recentRecords: '最近评估记录',
+    retake: '🔄 重新测试',
+    noRecords: '暂无测试记录',
+    firstTest: '完成首次测试',
+  },
+
+  // ===== Orchestration.vue =====
+  orchestration: {
+    title: 'Agent 编排可视化',
+    subtitle: '查看和管理AI Agent之间的协作关系',
+    active: '活跃',
+    messages: '消息',
+    agents: {
+      orchestrator: '编排器',
+      tutor: '编程导师',
+      debug: '调试助手',
+      review: '代码审查员',
+      arch: '架构师',
+      coach: '学习教练',
+    },
+    messagesCount: '{count} 条消息',
+  },
+
+    // ===== CodeEditor.vue =====
+  codeEditor: {
+    run: '▶ 运行',
+    running: '执行中...',
+    error: '❌ 错误',
+    output: '📤 输出',
+    clear: '清除',
+    placeholder: '点击"运行"按钮执行代码...',
+    defaultCode: '# 在这里写Python代码\n# 按 Ctrl+Enter 或点击"运行"按钮执行\n\ndef greet(name):\n    """打招呼函数"""\n    return f"Hello, {name}!"\n\n# 调用函数\nmessage = greet("World")\nprint(message)\n\n# 列表推导式\nsquares = [x**2 for x in range(10)]\nprint(f"平方数: {squares}")',
+    submitFeedback: '提交反馈',
+    submitting: '反馈中...',
+    feedbackTitle: 'AI 练习反馈',
+    feedbackError: '获取反馈失败,请重试',
+    languageLabel: '语言:',
+    preview: '预览',
+    previewing: '渲染中...',
+  },
+
+  // ===== helpers.ts (runtime translations) =====
+  helpers: {
+    levels: {
+      beginner: '入门',
+      intermediate: '中级',
+      advanced: '高级',
+    },
+    statuses: {
+      not_started: '未开始',
+      in_progress: '进行中',
+      completed: '已完成',
+      locked: '未解锁',
+    },
+    lessonTypes: {
+      theory: '📖 理论',
+      practice: '💻 实践',
+      project: '🛠️ 项目',
+      quiz: '📝 测验',
+    },
+    recIcons: {
+      start: '🚀',
+      continue: '➡️',
+      practice: '💪',
+      review: '🔄',
+    },
+    categories: {
+      html_css: 'HTML/CSS',
+      javascript: 'JavaScript',
+      vue: 'Vue.js',
+      browser_apis: '浏览器API',
+      python: 'Python',
+      api_design: 'API设计',
+      database: '数据库',
+      system_design: '系统设计',
+    },
+    difficulty: {
+      1: '简单',
+      2: '较简单',
+      3: '中等',
+      4: '较难',
+      5: '困难',
+    },
+    questionTypes: {
+      choice: '选择题',
+      code_output: '预测输出',
+      code_fill: '代码填空',
+      bug_fix: '找Bug',
+    },
+    agentStatus: {
+      active: '运行中',
+      processing: '处理中',
+      idle: '空闲',
+      error: '错误',
+    },
+    modules: {
+      'fe-html-css': 'HTML/CSS基础',
+      'fe-javascript': 'JavaScript核心',
+      'fe-vue': 'Vue.js框架',
+      'fe-project': '前端项目实战',
+      'be-python': 'Python基础',
+      'be-api': 'REST API设计',
+      'be-system': '系统设计与数据库',
+      'be-project': '后端项目实战',
+      'fs-web-basics': 'Web基础',
+      'fs-frontend': '前端开发',
+      'fs-backend': '后端开发',
+      'fs-fullstack': '全栈项目实战',
+    },
+  },
+}

+ 26 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/main.ts

@@ -0,0 +1,26 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import '@vue-flow/core/dist/style.css'
+import '@vue-flow/core/dist/theme-default.css'
+import '@vue-flow/controls/dist/style.css'
+import App from './App.vue'
+import router from './router'
+import './style.css'
+import './dark-theme-overrides.css'
+import './styles/highlight-theme.css'
+
+// Apply saved theme immediately to prevent flash of wrong theme
+const savedTheme = localStorage.getItem('theme') || 'light'
+document.documentElement.setAttribute('data-theme', savedTheme)
+
+const app = createApp(App)
+const pinia = createPinia()
+app.use(pinia)
+app.use(router)
+
+// Initialize language preference from localStorage
+import { useLangStore } from './stores/langStore'
+const langStore = useLangStore()
+langStore.init()
+
+app.mount('#app')

+ 30 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/router/index.ts

@@ -0,0 +1,30 @@
+import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
+import Chat from '../views/Chat.vue'
+import Orchestration from '../views/Orchestration.vue'
+import Learning from '../views/Learning.vue'
+import Assessment from '../views/Assessment.vue'
+import Dashboard from '../views/Dashboard.vue'
+import Login from '../views/Login.vue'
+
+const routes: RouteRecordRaw[] = [
+  { path: '/login', name: 'Login', component: Login },
+  { path: '/', name: 'Chat', component: Chat },
+  { path: '/orchestration', name: 'Orchestration', component: Orchestration },
+  { path: '/learning', name: 'Learning', component: Learning },
+  { path: '/assessment', name: 'Assessment', component: Assessment },
+  { path: '/dashboard', name: 'Dashboard', component: Dashboard }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+})
+
+router.beforeEach((to) => {
+  const userId = localStorage.getItem('currentUserId')
+  if (to.name !== 'Login' && !userId) {
+    return { name: 'Login' }
+  }
+})
+
+export default router

+ 144 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/agentStore.ts

@@ -0,0 +1,144 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+export interface AgentState {
+  type: string
+  name: string
+  icon: string
+  description: string
+  status: 'active' | 'idle' | 'error' | 'processing'
+  messageCount: number
+  lastUsedAt: number | null
+  lastMessage: string
+}
+
+export const useAgentStore = defineStore('agents', () => {
+  const agents = ref<AgentState[]>([
+    {
+      type: 'orchestrator',
+      name: '编排器',
+      icon: '🧠',
+      description: '路由和协调',
+      status: 'idle',
+      messageCount: 0,
+      lastUsedAt: null,
+      lastMessage: ''
+    },
+    {
+      type: 'tutor',
+      name: '编程导师',
+      icon: '👨‍🏫',
+      description: '概念讲解答疑',
+      status: 'idle',
+      messageCount: 0,
+      lastUsedAt: null,
+      lastMessage: ''
+    },
+    {
+      type: 'debug',
+      name: '调试助手',
+      icon: '🐛',
+      description: '错误分析修复',
+      status: 'idle',
+      messageCount: 0,
+      lastUsedAt: null,
+      lastMessage: ''
+    },
+    {
+      type: 'review',
+      name: '代码审查员',
+      icon: '🔍',
+      description: '代码质量审查',
+      status: 'idle',
+      messageCount: 0,
+      lastUsedAt: null,
+      lastMessage: ''
+    },
+    {
+      type: 'arch',
+      name: '架构师',
+      icon: '🏗️',
+      description: '架构设计咨询',
+      status: 'idle',
+      messageCount: 0,
+      lastUsedAt: null,
+      lastMessage: ''
+    },
+    {
+      type: 'coach',
+      name: '学习教练',
+      icon: '🎯',
+      description: '学习路径规划',
+      status: 'idle',
+      messageCount: 0,
+      lastUsedAt: null,
+      lastMessage: ''
+    }
+  ])
+
+  const activeCount = computed(() =>
+    agents.value.filter(a => a.status === 'active' || a.status === 'processing').length
+  )
+
+  const totalMessages = computed(() =>
+    agents.value.reduce((sum, a) => sum + a.messageCount, 0)
+  )
+
+  const getAgent = (type: string) =>
+    agents.value.find(a => a.type === type)
+
+  const setAgentStatus = (type: string, status: AgentState['status']) => {
+    const agent = getAgent(type)
+    if (agent) {
+      agent.status = status
+      if (status === 'active' || status === 'processing') {
+        agent.lastUsedAt = Date.now()
+      }
+    }
+  }
+
+  const recordMessage = (type: string, message: string) => {
+    const agent = getAgent(type)
+    if (agent) {
+      agent.messageCount++
+      agent.lastMessage = message.slice(0, 50)
+      agent.lastUsedAt = Date.now()
+    }
+  }
+
+  const setActiveAgent = (type: string) => {
+    // Reset all to idle first
+    agents.value.forEach(a => {
+      if (a.status === 'active') a.status = 'idle'
+    })
+    // Set target as active
+    setAgentStatus(type, 'active')
+  }
+
+  const resetAllStatus = () => {
+    agents.value.forEach(a => {
+      if (a.status !== 'error') a.status = 'idle'
+    })
+  }
+
+  const formatLastUsed = (timestamp: number | null): string => {
+    if (!timestamp) return '从未使用'
+    const diff = Date.now() - timestamp
+    if (diff < 60000) return '刚刚'
+    if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
+    if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
+    return `${Math.floor(diff / 86400000)}天前`
+  }
+
+  return {
+    agents,
+    activeCount,
+    totalMessages,
+    getAgent,
+    setAgentStatus,
+    recordMessage,
+    setActiveAgent,
+    resetAllStatus,
+    formatLastUsed
+  }
+})

+ 30 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/authStore.ts

@@ -0,0 +1,30 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+const STORAGE_KEY = 'currentUserId'
+
+export const useAuthStore = defineStore('auth', () => {
+  const userId = ref<string>(localStorage.getItem(STORAGE_KEY) || '')
+
+  const isLoggedIn = computed(() => userId.value.length > 0)
+
+  const login = (id: string) => {
+    userId.value = id
+    localStorage.setItem(STORAGE_KEY, id)
+  }
+
+  const logout = () => {
+    userId.value = ''
+    localStorage.removeItem(STORAGE_KEY)
+  }
+
+  const getUserId = () => userId.value
+
+  return {
+    userId,
+    isLoggedIn,
+    login,
+    logout,
+    getUserId
+  }
+})

+ 54 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/langStore.ts

@@ -0,0 +1,54 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import zh from '../locales/zh'
+import en from '../locales/en'
+
+type Lang = 'zh' | 'en'
+type Messages = typeof zh
+
+const STORAGE_KEY = 'lang'
+
+const messages: Record<Lang, Messages> = { zh, en }
+
+function getNestedValue(obj: Record<string, any>, path: string): string {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  let current: any = obj
+  for (const key of path.split('.')) {
+    if (current == null) return path
+    current = current[key]
+  }
+  return current ?? path
+}
+
+export const useLangStore = defineStore('lang', () => {
+  const lang = ref<Lang>((localStorage.getItem(STORAGE_KEY) as Lang) || 'zh')
+
+  function init() {
+    const stored = localStorage.getItem(STORAGE_KEY) as Lang | null
+    if (stored === 'zh' || stored === 'en') {
+      lang.value = stored
+    }
+  }
+
+  function toggleLang() {
+    lang.value = lang.value === 'zh' ? 'en' : 'zh'
+    localStorage.setItem(STORAGE_KEY, lang.value)
+  }
+
+  function t(key: string, params?: Record<string, string | number>): string {
+    let value = getNestedValue(messages[lang.value] as Record<string, any>, key)
+    if (params) {
+      for (const [k, v] of Object.entries(params)) {
+        value = value.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v))
+      }
+    }
+    return value
+  }
+
+  return {
+    lang,
+    init,
+    toggleLang,
+    t,
+  }
+})

+ 30 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/stores/themeStore.ts

@@ -0,0 +1,30 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+const STORAGE_KEY = 'theme'
+
+export type Theme = 'light' | 'dark'
+
+export const useThemeStore = defineStore('theme', () => {
+  const theme = ref<Theme>((localStorage.getItem(STORAGE_KEY) as Theme) || 'light')
+
+  const applyTheme = (value: Theme) => {
+    document.documentElement.setAttribute('data-theme', value)
+  }
+
+  const init = () => {
+    applyTheme(theme.value)
+  }
+
+  const toggleTheme = () => {
+    theme.value = theme.value === 'light' ? 'dark' : 'light'
+    localStorage.setItem(STORAGE_KEY, theme.value)
+    applyTheme(theme.value)
+  }
+
+  return {
+    theme,
+    init,
+    toggleTheme,
+  }
+})

+ 77 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/style.css

@@ -0,0 +1,77 @@
+/* ─── Light Theme (Default) ─── */
+:root {
+  --bg-primary: #f8f9fa;
+  --bg-secondary: #ffffff;
+  --bg-tertiary: #f0f2f5;
+  --text-primary: #333333;
+  --text-secondary: #666666;
+  --text-muted: #999999;
+  --border-color: #e8e8e8;
+  --shadow-color: rgba(0, 0, 0, 0.08);
+  --accent-color: #667eea;
+  --accent-secondary: #764ba2;
+  --success-color: #52c41a;
+  --warning-color: #faad14;
+  --error-color: #ff4d4f;
+  --code-bg: #f6f8fa;
+  --scrollbar-thumb: #d0d0d0;
+}
+
+/* ─── Dark Theme ─── */
+[data-theme="dark"] {
+  --bg-primary: #1a1a2e;
+  --bg-secondary: #16213e;
+  --bg-tertiary: #0f3460;
+  --text-primary: #e0e0e0;
+  --text-secondary: #b0b0b0;
+  --text-muted: #808080;
+  --border-color: #2a2a4a;
+  --shadow-color: rgba(0, 0, 0, 0.3);
+  --accent-color: #7c8cf0;
+  --accent-secondary: #9a6dd7;
+  --success-color: #52c41a;
+  --warning-color: #faad14;
+  --error-color: #ff4d4f;
+  --code-bg: #1e1e3f;
+  --scrollbar-thumb: #444;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html, body {
+  height: 100%;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+  font-size: 14px;
+  line-height: 1.5;
+  color: var(--text-primary);
+  background: var(--bg-primary);
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  transition: background-color 0.3s ease, color 0.3s ease;
+}
+
+#app {
+  height: 100%;
+}
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+  background: var(--scrollbar-thumb);
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: var(--text-muted);
+}

+ 92 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/styles/highlight-theme.css

@@ -0,0 +1,92 @@
+/* GitHub Dark theme for highlight.js */
+.hljs {
+  color: #c9d1d9;
+  background: #0d1117;
+}
+
+.hljs-doctag,
+.hljs-keyword,
+.hljs-meta .hljs-keyword,
+.hljs-template-tag,
+.hljs-template-variable,
+.hljs-type,
+.hljs-variable.language_ {
+  color: #ff7b72;
+}
+
+.hljs-title,
+.hljs-title.class_,
+.hljs-title.class_.inherited__,
+.hljs-title.function_ {
+  color: #d2a8ff;
+}
+
+.hljs-attr,
+.hljs-attribute,
+.hljs-literal,
+.hljs-meta,
+.hljs-number,
+.hljs-operator,
+.hljs-variable,
+.hljs-selector-attr,
+.hljs-selector-class,
+.hljs-selector-id {
+  color: #79c0ff;
+}
+
+.hljs-regexp,
+.hljs-string,
+.hljs-meta .hljs-string {
+  color: #a5d6ff;
+}
+
+.hljs-built_in,
+.hljs-symbol {
+  color: #ffa657;
+}
+
+.hljs-comment,
+.hljs-code,
+.hljs-formula {
+  color: #8b949e;
+}
+
+.hljs-name,
+.hljs-quote,
+.hljs-selector-tag,
+.hljs-selector-pseudo {
+  color: #7ee787;
+}
+
+.hljs-subst {
+  color: #c9d1d9;
+}
+
+.hljs-section {
+  color: #1f6feb;
+  font-weight: bold;
+}
+
+.hljs-bullet {
+  color: #f2cc60;
+}
+
+.hljs-emphasis {
+  color: #c9d1d9;
+  font-style: italic;
+}
+
+.hljs-strong {
+  color: #c9d1d9;
+  font-weight: bold;
+}
+
+.hljs-addition {
+  color: #aff5b4;
+  background-color: #033a16;
+}
+
+.hljs-deletion {
+  color: #ffdcd7;
+  background-color: #67060c;
+}

+ 143 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/types/index.ts

@@ -0,0 +1,143 @@
+/**
+ * 共享TypeScript类型定义
+ * 避免在多个组件中重复定义相同的接口
+ */
+
+// ===== 学习路径相关 =====
+
+export interface Lesson {
+  id: string
+  title: string
+  description: string
+  type: string
+  duration_minutes: number
+  is_completed: boolean
+  language?: string
+}
+
+export interface Module {
+  id: string
+  title: string
+  description: string
+  icon: string
+  order: number
+  lessons: Lesson[]
+  status: string
+  progress: number
+}
+
+export interface PathOverview {
+  path: string
+  title: string
+  description: string
+  icon: string
+  total_modules: number
+  total_lessons: number
+}
+
+export interface PathDetail {
+  path: string
+  title: string
+  description: string
+  icon: string
+  modules: Module[]
+  total_lessons: number
+  completed_lessons: number
+  progress: number
+}
+
+// ===== 教练相关 =====
+
+export interface CoachRecommendation {
+  type: string
+  title: string
+  description: string
+  module_id?: string
+  lesson_id?: string
+  priority: number
+}
+
+export interface CoachData {
+  greeting: string
+  recommendations: CoachRecommendation[]
+  encouragement: string
+  stats: Record<string, any>
+}
+
+// ===== 评估相关 =====
+
+export interface AssessmentQuestion {
+  id: string
+  category: string
+  difficulty: number
+  content: string
+  options: string[]
+  correct_answer: string
+  explanation: string
+}
+
+export interface AssessmentResult {
+  user_id: string
+  path_type: string
+  total_questions: number
+  correct_count: number
+  score: number
+  level: string
+  category_scores: Record<string, number>
+  recommended_start_module: string
+  completed_at: string
+  is_current: boolean
+}
+
+export interface AssessmentRecord {
+  id: string
+  path_type: string
+  score: number
+  level: string
+  category_scores: Record<string, number>
+  completed_at: string
+}
+
+// ===== 聊天相关 =====
+
+export interface ChatMessage {
+  role: 'user' | 'assistant'
+  content: string
+  agent_name?: string
+}
+
+export interface LearningContext {
+  message: string
+  path_type: string
+  user_level: string | null
+  skill_levels: Record<string, number> | null
+  lesson_id?: string
+  module_id?: string
+  lesson_title?: string
+  module_title?: string
+}
+
+// ===== 用户进度相关 =====
+
+export interface UserProgress {
+  user_id: string
+  current_path: string | null
+  completed_modules: string[]
+  completed_lessons: string[]
+  current_module: string | null
+  current_lesson: string | null
+  started_at: string | null
+  last_activity_at: string | null
+  total_study_minutes: number
+}
+
+// ===== Agent相关 =====
+
+export interface AgentNode {
+  id: string
+  type: string
+  name: string
+  status: 'idle' | 'active' | 'processing' | 'completed' | 'error'
+  lastUsed?: string
+  messageCount: number
+}

+ 164 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/utils/helpers.ts

@@ -0,0 +1,164 @@
+/**
+ * 共享工具函数
+ * 支持中英文切换,通过 langStore 获取翻译
+ */
+import { useLangStore } from '../stores/langStore'
+
+// 延迟获取 langStore(确保 Pinia 已安装)
+function getLang() {
+  try {
+    return useLangStore()
+  } catch {
+    return null
+  }
+}
+
+function t(key: string): string {
+  const lang = getLang()
+  return lang ? lang.t(key) : key
+}
+
+// ===== 分类名称 =====
+export function getCategoryName(category: string): string {
+  return t(`helpers.categories.${category}`)
+}
+
+// ===== 水平文本 =====
+export function getLevelText(level: string): string {
+  return t(`helpers.levels.${level}`)
+}
+
+// ===== 状态文本 =====
+export function getStatusText(status: string): string {
+  return t(`helpers.statuses.${status}`)
+}
+
+// ===== 课程类型图标(图标不需要翻译) =====
+export const LESSON_TYPE_ICONS: Record<string, string> = {
+  theory: '📖',
+  practice: '💻',
+  quiz: '❓',
+  project: '🚀'
+}
+
+export function getLessonTypeIcon(type: string): string {
+  return LESSON_TYPE_ICONS[type] || '📝'
+}
+
+// ===== 课程类型文本 =====
+export function getLessonTypeText(type: string): string {
+  return t(`helpers.lessonTypes.${type}`)
+}
+
+// ===== 推荐类型图标(图标不需要翻译) =====
+export const REC_ICONS: Record<string, string> = {
+  next_lesson: '➡️',
+  review: '🔄',
+  practice: '💪',
+  challenge: '🏆',
+  select_path: '🎯'
+}
+
+export function getRecIcon(type: string): string {
+  return REC_ICONS[type] || '💡'
+}
+
+// ===== Agent图标(图标不需要翻译) =====
+export const AGENT_ICONS: Record<string, string> = {
+  '编程导师': '👨‍🏫',
+  '调试助手': '🐛',
+  '代码审查员': '🔍',
+  '架构师': '🏗️',
+  '学习教练': '🎯',
+  'Tutor': '👨‍🏫',
+  'Debugger': '🐛',
+  'Reviewer': '🔍',
+  'Architect': '🏗️',
+  'Coach': '🎯',
+}
+
+// ===== Agent类型映射 =====
+export const AGENT_TYPES: Record<string, string> = {
+  '编程导师': 'tutor',
+  '调试助手': 'debug',
+  '代码审查员': 'review',
+  '架构师': 'arch',
+  '学习教练': 'coach',
+  'Tutor': 'tutor',
+  'Debugger': 'debug',
+  'Reviewer': 'review',
+  'Architect': 'arch',
+  'Coach': 'coach',
+}
+
+export function getAgentIcon(agentName?: string): string {
+  return AGENT_ICONS[agentName || ''] || '🤖'
+}
+
+export function getAgentType(agentName?: string): string {
+  return AGENT_TYPES[agentName || ''] || 'default'
+}
+
+// ===== 分数CSS类名(不需要翻译) =====
+export function getScoreClass(score: number): string {
+  if (score >= 80) return 'excellent'
+  if (score >= 60) return 'good'
+  if (score >= 40) return 'fair'
+  return 'poor'
+}
+
+export function getScoreBarClass(score: number): string {
+  return getScoreClass(score)
+}
+
+// ===== 难度文本 =====
+export function getDifficultyText(level: number): string {
+  return t(`helpers.difficulty.${level}`)
+}
+
+// ===== 难度CSS类名(不需要翻译) =====
+export function getDifficultyClass(level: number): string {
+  if (level <= 2) return 'easy'
+  if (level <= 3) return 'medium'
+  return 'hard'
+}
+
+// ===== 模块名称 =====
+export function getModuleName(moduleId: string): string {
+  return t(`helpers.modules.${moduleId}`)
+}
+
+// ===== 路径标题 =====
+export function getPathTitle(path: string): string {
+  const key = `helpers.categories.${path}`
+  const result = t(key)
+  // 如果翻译不存在,回退到硬编码
+  if (result === key) {
+    const fallbacks: Record<string, string> = {
+      frontend: 'Frontend Development / 前端开发',
+      backend: 'Backend Development / 后端开发',
+      fullstack: 'Fullstack Development / 全栈开发'
+    }
+    return fallbacks[path] || path
+  }
+  return result
+}
+
+// ===== 选项工具 =====
+export function getOptionText(option: string): string {
+  return option.replace(/^[A-D]\.\s*/, '')
+}
+
+export function getOptionLetter(index: number): string {
+  return String.fromCharCode(65 + index)
+}
+
+// ===== Agent状态文本 =====
+export function getAgentStatusText(status: string): string {
+  return t(`helpers.agentStatus.${status}`)
+}
+
+// ===== 题目类型文本 =====
+export function getQuestionTypeText(type: string): string {
+  return t(`helpers.questionTypes.${type}`)
+}

+ 824 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Assessment.vue

@@ -0,0 +1,824 @@
+<template>
+  <div class="assessment-view">
+    <!-- 加载中 -->
+    <div v-if="loading" class="loading-state">
+      <div class="spinner"></div>
+      <p>{{ langStore.t('assessment.loading') }}</p>
+    </div>
+
+    <!-- 测试进行中 -->
+    <div v-else-if="currentQuestion && !completed" class="assessment-content">
+      <div class="assessment-header">
+        <div class="header-left">
+          <h1>{{ langStore.t('assessment.title') }}</h1>
+          <p class="path-label">{{ getPathTitle(pathType) }}</p>
+        </div>
+        <div class="header-right">
+          <div class="progress-info">
+            <span class="question-count">{{ langStore.t('assessment.questionProgress', { current: currentIndex + 1, total: totalQuestions }) }}</span>
+            <div class="progress-bar">
+              <div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="question-card">
+        <div class="question-meta">
+          <span class="category-badge">{{ getCategoryName(currentQuestion.category) }}</span>
+          <span class="difficulty-badge" :class="getDifficultyClass(currentQuestion.difficulty)">
+            {{ getDifficultyText(currentQuestion.difficulty) }}
+          </span>
+          <span class="type-badge" :class="currentQuestion.question_type">
+            {{ getQuestionTypeText(currentQuestion.question_type) }}
+          </span>
+        </div>
+
+        <h2 class="question-content">{{ currentQuestion.content }}</h2>
+
+        <!-- 代码片段 -->
+        <div v-if="currentQuestion.code_snippet" class="code-block">
+          <pre><code>{{ currentQuestion.code_snippet }}</code></pre>
+        </div>
+
+        <div class="options-list">
+          <div
+            v-for="(option, index) in currentQuestion.options"
+            :key="index"
+            :class="[
+              'option-item',
+              {
+                selected: selectedAnswer === getOptionLetter(index),
+                correct: showResult && getOptionLetter(index) === currentQuestion.correct_answer,
+                wrong: showResult && selectedAnswer === getOptionLetter(index) && getOptionLetter(index) !== currentQuestion.correct_answer
+              }
+            ]"
+            @click="selectAnswer(getOptionLetter(index))"
+          >
+            <span class="option-letter">{{ getOptionLetter(index) }}</span>
+            <span class="option-text">{{ getOptionText(option) }}</span>
+            <span v-if="showResult && getOptionLetter(index) === currentQuestion.correct_answer" class="result-icon correct">✓</span>
+            <span v-else-if="showResult && selectedAnswer === getOptionLetter(index)" class="result-icon wrong">✗</span>
+          </div>
+        </div>
+
+        <!-- 答案解析 -->
+        <div v-if="showResult" class="explanation-box">
+          <div class="explanation-header">
+            <span class="explanation-icon">{{ lastAnswerCorrect ? '🎉' : '💡' }}</span>
+            <span class="explanation-title">{{ lastAnswerCorrect ? langStore.t('assessment.answerCorrect') : langStore.t('assessment.answerWrong') }}</span>
+          </div>
+          <p class="explanation-text">{{ currentQuestion.explanation }}</p>
+        </div>
+
+        <div class="action-buttons">
+          <button
+            v-if="!showResult"
+            class="btn btn-primary"
+            :disabled="!selectedAnswer"
+            @click="submitAnswer"
+          >
+            {{ langStore.t('assessment.confirmAnswer') }}
+          </button>
+          <button
+            v-else
+            class="btn btn-primary"
+            @click="nextQuestion"
+          >
+            {{ currentIndex >= totalQuestions - 1 ? langStore.t('assessment.viewResult') : langStore.t('assessment.nextQuestion') }}
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 测试结果 -->
+    <div v-else-if="completed && assessmentResult" class="result-content">
+      <div class="result-card">
+        <div class="result-header">
+          <div class="score-circle" :class="getScoreClass(assessmentResult.score)">
+            <span class="score-value">{{ assessmentResult.score }}</span>
+            <span class="score-label">{{ langStore.t('assessment.unit') }}</span>
+          </div>
+          <div class="result-summary">
+            <h1>{{ langStore.t('assessment.testComplete') }}</h1>
+            <p class="level-text">
+              {{ langStore.t('assessment.yourLevel') }}<span :class="['level-badge', assessmentResult.level]">{{ getLevelText(assessmentResult.level) }}</span>
+            </p>
+            <p class="correct-text">{{ langStore.t('assessment.correctCount', { correct: assessmentResult.correct_count, total: assessmentResult.total_questions }) }}</p>
+          </div>
+        </div>
+
+        <!-- 分类得分 -->
+        <div class="category-scores">
+          <h3>{{ langStore.t('assessment.categoryScores') }}</h3>
+          <div class="scores-grid">
+            <div
+              v-for="(score, category) in assessmentResult.category_scores"
+              :key="category"
+              class="score-item"
+            >
+              <div class="score-header">
+                <span class="category-name">{{ getCategoryName(category) }}</span>
+                <span class="category-score">{{ score }}%</span>
+              </div>
+              <div class="score-bar">
+                <div class="score-fill" :style="{ width: score + '%' }" :class="getScoreBarClass(score)"></div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 推荐 -->
+        <div class="recommendation-box">
+          <div class="rec-icon">🎯</div>
+          <div class="rec-content">
+            <h3>{{ langStore.t('assessment.learningSuggestion') }}</h3>
+            <p>{{ langStore.t('assessment.suggestionDesc', { module: getModuleName(assessmentResult.recommended_start_module) }) }}</p>
+          </div>
+        </div>
+
+        <div class="result-actions">
+          <button class="btn btn-primary" @click="startLearning">
+            {{ langStore.t('assessment.startLearning') }}
+          </button>
+          <button class="btn btn-outline" @click="retakeTest">
+            {{ langStore.t('assessment.retake') }}
+          </button>
+          <button class="btn btn-outline" @click="goBack">
+            {{ langStore.t('assessment.goBack') }}
+          </button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import axios from 'axios'
+import { useAuthStore } from '../stores/authStore'
+import { useLangStore } from '../stores/langStore'
+import { getLevelText, getCategoryName, getScoreBarClass, getModuleName, getQuestionTypeText, getDifficultyText, getDifficultyClass } from '../utils/helpers'
+
+const router = useRouter()
+const route = useRoute()
+const authStore = useAuthStore()
+const langStore = useLangStore()
+
+// 状态
+const loading = ref(true)
+const pathType = ref('')
+const sessionId = ref('')
+const currentQuestion = ref<any>(null)
+const currentIndex = ref(0)
+const totalQuestions = ref(10)
+const selectedAnswer = ref('')
+const showResult = ref(false)
+const lastAnswerCorrect = ref(false)
+const completed = ref(false)
+const assessmentResult = ref<any>(null)
+const nextQuestionData = ref<any>(null)
+
+// 计算属性
+const progressPercent = computed(() => {
+  return (currentIndex.value / totalQuestions.value) * 100
+})
+
+// 初始化
+onMounted(async () => {
+  // 从query获取pathType
+  pathType.value = (route.query.path as string) || 'frontend'
+
+  // 开始测试
+  await startTest()
+})
+
+// 开始测试
+const startTest = async () => {
+  loading.value = true
+  try {
+    const response = await axios.post('/api/assessment/start', {
+      path_type: pathType.value,
+      user_id: authStore.userId,
+    })
+    sessionId.value = response.data.session_id
+    currentQuestion.value = response.data.question
+    totalQuestions.value = response.data.total_questions
+    currentIndex.value = response.data.current_index
+    loading.value = false
+  } catch (error) {
+    console.error('Failed to start assessment:', error)
+    loading.value = false
+    alert('启动测试失败,请重试')
+  }
+}
+
+// 选择答案
+const selectAnswer = (letter: string) => {
+  if (!showResult.value) {
+    selectedAnswer.value = letter
+  }
+}
+
+// 提交答案
+const submitAnswer = async () => {
+  if (!selectedAnswer.value) return
+
+  try {
+    const response = await axios.post('/api/assessment/answer', {
+      session_id: sessionId.value,
+      question_id: currentQuestion.value.id,
+      answer: selectedAnswer.value,
+      path_type: pathType.value,
+      user_id: authStore.userId,
+    })
+
+    lastAnswerCorrect.value = response.data.is_correct
+    showResult.value = true
+
+    // 保存下一题信息(不立即切换,等用户点击"下一题")
+    if (response.data.is_completed) {
+      // 测试完成
+      assessmentResult.value = response.data.assessment_result
+      completed.value = true
+    } else {
+      // 暂存下一题
+      nextQuestionData.value = response.data.next_question
+    }
+  } catch (error) {
+    console.error('Failed to submit answer:', error)
+  }
+}
+
+// 下一题
+const nextQuestion = () => {
+  // 切换到下一题
+  if (nextQuestionData.value) {
+    currentQuestion.value = nextQuestionData.value
+    currentIndex.value++
+    nextQuestionData.value = null
+  }
+  showResult.value = false
+  selectedAnswer.value = ''
+  lastAnswerCorrect.value = false
+}
+
+// 开始学习
+const startLearning = () => {
+  // 存储学习上下文到sessionStorage
+  const context = {
+    message: `我已经完成了水平检测,得分${assessmentResult.value.score}分,水平为${getLevelText(assessmentResult.value.level)}。请根据我的测试结果,从"${getModuleName(assessmentResult.value.recommended_start_module)}"模块开始帮我制定学习计划。`,
+    path_type: pathType.value,
+    user_level: assessmentResult.value.level,
+    skill_levels: assessmentResult.value.category_scores,
+    recommended_module: assessmentResult.value.recommended_start_module,
+    is_assessment_result: true
+  }
+  sessionStorage.setItem('pendingLearningContext', JSON.stringify(context))
+
+  // 跳转到聊天页面
+  router.push('/')
+}
+
+// 重新测试
+const retakeTest = async () => {
+  loading.value = true
+  completed.value = false
+  assessmentResult.value = null
+  selectedAnswer.value = ''
+  showResult.value = false
+  await startTest()
+}
+
+// 返回
+const goBack = () => {
+  router.push('/learning')
+}
+
+// 工具函数
+const getOptionLetter = (index: number) => {
+  return String.fromCharCode(65 + index) // A, B, C, D
+}
+
+const getOptionText = (option: string) => {
+  // 移除开头的"A. ", "B. "等
+  return option.replace(/^[A-D]\.\s*/, '')
+}
+
+const getPathTitle = (path: string) => {
+  const titles: Record<string, string> = {
+    frontend: '前端开发',
+    backend: '后端开发',
+    fullstack: '全栈开发'
+  }
+  return titles[path] || path
+}
+
+const getScoreClass = (score: number) => {
+  if (score >= 80) return 'excellent'
+  if (score >= 60) return 'good'
+  if (score >= 40) return 'fair'
+  return 'poor'
+}
+</script>
+
+<style scoped>
+.assessment-view {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  background: #f8f9fa;
+  overflow-y: auto;
+}
+
+/* 加载状态 */
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  gap: 16px;
+}
+
+.spinner {
+  width: 48px;
+  height: 48px;
+  border: 4px solid #e8e8e8;
+  border-top-color: #667eea;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.loading-state p {
+  color: #666;
+  font-size: 16px;
+}
+
+/* 测试头部 */
+.assessment-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24px 32px;
+  background: white;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.header-left h1 {
+  margin: 0 0 4px 0;
+  font-size: 24px;
+  color: #1a1a1a;
+}
+
+.path-label {
+  margin: 0;
+  color: #667eea;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.progress-info {
+  text-align: right;
+}
+
+.question-count {
+  display: block;
+  margin-bottom: 8px;
+  font-size: 14px;
+  color: #666;
+}
+
+.progress-bar {
+  width: 200px;
+  height: 8px;
+  background: #e8e8e8;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+  border-radius: 4px;
+  transition: width 0.3s;
+}
+
+/* 题目卡片 */
+.assessment-content {
+  padding: 32px;
+}
+
+.question-card {
+  background: white;
+  border-radius: 16px;
+  padding: 32px;
+  max-width: 800px;
+  margin: 0 auto;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+}
+
+.question-meta {
+  display: flex;
+  gap: 12px;
+  margin-bottom: 20px;
+}
+
+.category-badge {
+  padding: 4px 12px;
+  background: #e6f7ff;
+  color: #1890ff;
+  border-radius: 12px;
+  font-size: 13px;
+  font-weight: 500;
+}
+
+.difficulty-badge {
+  padding: 4px 12px;
+  border-radius: 12px;
+  font-size: 13px;
+  font-weight: 500;
+}
+
+.difficulty-badge.easy { background: #f6ffed; color: #52c41a; }
+.difficulty-badge.medium { background: #fff7e6; color: #fa8c16; }
+.difficulty-badge.hard { background: #fff2f0; color: #ff4d4f; }
+
+.type-badge {
+  padding: 4px 12px;
+  border-radius: 12px;
+  font-size: 13px;
+  font-weight: 500;
+}
+.type-badge.choice { background: #f0f5ff; color: #2f54eb; }
+.type-badge.code_output { background: #f9f0ff; color: #722ed1; }
+.type-badge.code_fill { background: #e6fffb; color: #13c2c2; }
+.type-badge.bug_fix { background: #fff1f0; color: #cf1322; }
+
+.question-content {
+  margin: 0 0 28px 0;
+  font-size: 18px;
+  color: #1a1a1a;
+  line-height: 1.6;
+}
+
+/* 代码块 */
+.code-block {
+  background: #1e1e1e;
+  border-radius: 8px;
+  padding: 16px 20px;
+  margin-bottom: 24px;
+  overflow-x: auto;
+}
+
+.code-block pre {
+  margin: 0;
+  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+  font-size: 14px;
+  line-height: 1.6;
+  color: #d4d4d4;
+}
+
+.code-block code {
+  white-space: pre;
+}
+
+/* 选项 */
+.options-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-bottom: 24px;
+}
+
+.option-item {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 16px 20px;
+  background: #f8f9fa;
+  border: 2px solid transparent;
+  border-radius: 12px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.option-item:hover:not(.correct):not(.wrong) {
+  background: #e8e8e8;
+  border-color: #d9d9d9;
+}
+
+.option-item.selected {
+  background: #e6f7ff;
+  border-color: #667eea;
+}
+
+.option-item.correct {
+  background: #f6ffed;
+  border-color: #52c41a;
+}
+
+.option-item.wrong {
+  background: #fff2f0;
+  border-color: #ff4d4f;
+}
+
+.option-letter {
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: white;
+  border-radius: 50%;
+  font-weight: 600;
+  font-size: 14px;
+  color: #666;
+  border: 1px solid #d9d9d9;
+  flex-shrink: 0;
+}
+
+.option-item.selected .option-letter {
+  background: #667eea;
+  color: white;
+  border-color: #667eea;
+}
+
+.option-item.correct .option-letter {
+  background: #52c41a;
+  color: white;
+  border-color: #52c41a;
+}
+
+.option-item.wrong .option-letter {
+  background: #ff4d4f;
+  color: white;
+  border-color: #ff4d4f;
+}
+
+.option-text {
+  flex: 1;
+  font-size: 15px;
+  color: #333;
+  line-height: 1.5;
+}
+
+.result-icon {
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.result-icon.correct { color: #52c41a; }
+.result-icon.wrong { color: #ff4d4f; }
+
+/* 答案解析 */
+.explanation-box {
+  background: #f8f9fa;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 24px;
+}
+
+.explanation-header {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
+}
+
+.explanation-icon {
+  font-size: 24px;
+}
+
+.explanation-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1a1a1a;
+}
+
+.explanation-text {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+  line-height: 1.6;
+}
+
+/* 按钮 */
+.action-buttons {
+  display: flex;
+  justify-content: center;
+}
+
+.btn {
+  padding: 12px 32px;
+  border-radius: 8px;
+  font-size: 15px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: none;
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-primary:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.btn-outline {
+  background: white;
+  border: 1px solid #d9d9d9;
+  color: #666;
+}
+
+.btn-outline:hover {
+  border-color: #667eea;
+  color: #667eea;
+}
+
+/* 结果页面 */
+.result-content {
+  padding: 32px;
+  display: flex;
+  justify-content: center;
+}
+
+.result-card {
+  background: white;
+  border-radius: 16px;
+  padding: 40px;
+  max-width: 700px;
+  width: 100%;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+}
+
+.result-header {
+  display: flex;
+  align-items: center;
+  gap: 32px;
+  margin-bottom: 32px;
+}
+
+.score-circle {
+  width: 120px;
+  height: 120px;
+  border-radius: 50%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.score-circle.excellent { background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%); }
+.score-circle.good { background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%); }
+.score-circle.fair { background: linear-gradient(135deg, #fff7e6 0%, #ffd591 100%); }
+.score-circle.poor { background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%); }
+
+.score-value {
+  font-size: 36px;
+  font-weight: 700;
+  color: #1a1a1a;
+}
+
+.score-label {
+  font-size: 14px;
+  color: #666;
+}
+
+.result-summary h1 {
+  margin: 0 0 12px 0;
+  font-size: 28px;
+  color: #1a1a1a;
+}
+
+.level-text {
+  margin: 0 0 8px 0;
+  font-size: 16px;
+  color: #666;
+}
+
+.level-badge {
+  padding: 4px 12px;
+  border-radius: 12px;
+  font-weight: 600;
+}
+
+.level-badge.beginner { background: #f6ffed; color: #52c41a; }
+.level-badge.intermediate { background: #e6f7ff; color: #1890ff; }
+.level-badge.advanced { background: #fff2f0; color: #ff4d4f; }
+
+.correct-text {
+  margin: 0;
+  font-size: 14px;
+  color: #999;
+}
+
+/* 分类得分 */
+.category-scores {
+  margin-bottom: 28px;
+}
+
+.category-scores h3 {
+  margin: 0 0 16px 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.scores-grid {
+  display: flex;
+  flex-direction: column;
+  gap: 14px;
+}
+
+.score-item {
+  background: #f8f9fa;
+  border-radius: 8px;
+  padding: 14px 16px;
+}
+
+.score-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+}
+
+.category-name {
+  font-size: 14px;
+  color: #333;
+  font-weight: 500;
+}
+
+.category-score {
+  font-size: 14px;
+  color: #666;
+  font-weight: 600;
+}
+
+.score-bar {
+  height: 6px;
+  background: #e8e8e8;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.score-fill {
+  height: 100%;
+  border-radius: 3px;
+  transition: width 0.5s;
+}
+
+.score-fill.excellent { background: #52c41a; }
+.score-fill.good { background: #1890ff; }
+.score-fill.fair { background: #fa8c16; }
+.score-fill.poor { background: #ff4d4f; }
+
+/* 推荐 */
+.recommendation-box {
+  display: flex;
+  gap: 16px;
+  padding: 20px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
+  border-radius: 12px;
+  margin-bottom: 28px;
+}
+
+.rec-icon {
+  font-size: 32px;
+  flex-shrink: 0;
+}
+
+.rec-content h3 {
+  margin: 0 0 8px 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.rec-content p {
+  margin: 0;
+  font-size: 14px;
+  color: #666;
+  line-height: 1.6;
+}
+
+/* 结果操作 */
+.result-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+}
+</style>

+ 986 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Chat.vue

@@ -0,0 +1,986 @@
+<template>
+  <div class="app-container">
+    <!-- 左侧对话区 -->
+    <div class="chat-section">
+      <div class="chat-header">
+        <div class="header-left">
+          <div class="avatar">
+            <span>🤖</span>
+          </div>
+          <div class="header-info">
+            <h2>Way_to_Engineer</h2>
+            <span class="subtitle">{{ langStore.t('chat.subtitle') }}</span>
+          </div>
+        </div>
+        <div class="status-badge">
+          <span class="status-dot"></span>
+          <span>{{ langStore.t('chat.online') }}</span>
+        </div>
+        <button class="clear-btn" @click="clearChat" title="清除对话历史">
+          🗑️
+        </button>
+      </div>
+      
+      <div class="messages" ref="messagesContainer">
+        <div class="welcome-card">
+          <div class="welcome-icon">👋</div>
+          <h3>{{ langStore.t('chat.welcomeTitle') }}</h3>
+          <p>{{ langStore.t('chat.welcomeDesc') }}</p>
+          <div class="feature-list">
+            <div class="feature-item">
+              <span class="feature-icon">👨‍🏫</span>
+              <span>{{ langStore.t('chat.features.tutor') }}</span>
+            </div>
+            <div class="feature-item">
+              <span class="feature-icon">🐛</span>
+              <span>{{ langStore.t('chat.features.debug') }}</span>
+            </div>
+            <div class="feature-item">
+              <span class="feature-icon">🔍</span>
+              <span>{{ langStore.t('chat.features.review') }}</span>
+            </div>
+            <div class="feature-item">
+              <span class="feature-icon">🏗️</span>
+              <span>{{ langStore.t('chat.features.arch') }}</span>
+            </div>
+          </div>
+        </div>
+
+        <div v-if="sessionData" class="session-banner">
+          <div class="session-banner-content">
+            <span class="session-banner-icon">👋</span>
+            <div class="session-banner-text">
+              <p>{{ langStore.t('chat.continueSession', { lesson: sessionData.lesson_title || '' }) }}</p>
+            </div>
+            <button class="btn btn-primary btn-sm" @click="continueSession">继续学习 →</button>
+            <button class="session-dismiss" @click="sessionData = null">✕</button>
+          </div>
+        </div>
+        
+        <div 
+          v-for="(msg, index) in messages" 
+          :key="index" 
+          :class="['message', msg.role]"
+        >
+          <div v-if="msg.role === 'assistant'" class="msg-avatar" :class="'avatar-' + getAgentType(msg.agent_name)">
+            {{ getAgentIcon(msg.agent_name) }}
+          </div>
+          <div class="msg-body">
+            <div v-if="msg.agent_name" class="agent-tag" :class="'tag-' + getAgentType(msg.agent_name)">{{ msg.agent_name }}</div>
+            <div v-if="msg.role === 'assistant'" class="bubble assistant-bubble">
+              <MarkdownRenderer
+                :content="msg.content"
+                @quiz-result="(r) => onQuizResult(r, msg)"
+                @exercise-detected="onExerciseDetected"
+                @content-meta="(m) => onContentMeta(m, msg)"
+              />
+            </div>
+            <div v-else class="bubble user-bubble">{{ msg.content }}</div>
+            <div v-if="isLastAssistantMessage(index) && learningContext" class="mastered-section">
+              <button class="mastered-btn" @click="masteredClick" :disabled="masteredLoading">
+                <span v-if="!masteredLoading">{{ langStore.t('chat.masteredBtn') }}</span>
+                <span v-else class="spinner"></span>
+              </button>
+            </div>
+          </div>
+          <div v-if="msg.role === 'user'" class="msg-avatar user-avatar">👤</div>
+        </div>
+
+        <div v-if="loading" class="message assistant">
+          <div class="msg-avatar">🤖</div>
+          <div class="msg-body">
+            <div class="bubble typing-bubble">
+              <span class="dot"></span>
+              <span class="dot"></span>
+              <span class="dot"></span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Completion banner (between messages and input) -->
+      <div v-if="completionData" class="completion-banner" :class="completionData.passed ? 'pass' : 'fail'">
+        <div class="completion-content">
+          <span class="completion-icon">{{ completionData.passed ? '🎉' : '💪' }}</span>
+          <div class="completion-text">
+            <p v-if="completionData.passed">
+              {{ langStore.t('chat.quizPass', { correct: completionData.correct, total: completionData.total }) }}
+            </p>
+            <p v-else>
+              {{ langStore.t('chat.quizFail', { correct: completionData.correct, total: completionData.total }) }}
+            </p>
+            <p v-if="completionData.lessonCompleted" class="lesson-complete">
+              {{ langStore.t('chat.lessonCompleted') }}
+            </p>
+          </div>
+          <button v-if="completionData.lessonCompleted" class="btn btn-primary btn-sm" @click="goNextLesson">
+            {{ langStore.t('chat.nextLesson') }}
+          </button>
+          <button v-else class="btn btn-outline btn-sm" @click="completionData = null">
+            {{ langStore.t('chat.close') }}
+          </button>
+        </div>
+      </div>
+      
+      <div class="input-area">
+        <div class="input-wrapper">
+          <textarea 
+            v-model="input" 
+            rows="1"
+            :placeholder="langStore.t('chat.placeholder')"
+            @keydown.enter.exact.prevent="send"
+            @input="autoResize"
+            ref="textareaRef"
+          ></textarea>
+          <button class="send-btn" @click="send" :disabled="loading || !input.trim()">
+            <span v-if="!loading">{{ langStore.t('chat.send') }}</span>
+            <span v-else class="spinner"></span>
+          </button>
+        </div>
+        <div class="input-hint">{{ langStore.t('chat.hint') }}</div>
+      </div>
+    </div>
+    
+    <!-- 右侧代码区 -->
+    <div class="code-section">
+      <CodeEditor :exerciseData="currentExercise" @exercise-submitted="onExerciseSubmitted" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, nextTick, onMounted, onActivated } from 'vue'
+import axios from 'axios'
+import CodeEditor from '../components/CodeEditor.vue'
+import MarkdownRenderer from '../components/MarkdownRenderer.vue'
+import { useAgentStore } from '../stores/agentStore'
+import { useAuthStore } from '../stores/authStore'
+import { useLangStore } from '../stores/langStore'
+import { getAgentIcon, getAgentType } from '../utils/helpers'
+
+interface Message {
+  role: 'user' | 'assistant'
+  content: string
+  agent_name?: string
+}
+
+const agentStore = useAgentStore()
+const authStore = useAuthStore()
+const langStore = useLangStore()
+const messages = ref<Message[]>([])
+const input = ref('')
+const loading = ref(false)
+const messagesContainer = ref<HTMLElement>()
+const textareaRef = ref<HTMLTextAreaElement>()
+
+const sessionData = ref<any>(null)
+const masteredLoading = ref(false)
+const completionData = ref<{ passed: boolean; correct: number; total: number; lessonCompleted: boolean } | null>(null)
+const nextLessonData = ref<{ id: string; title: string; description: string; module_title: string; module_id: string; path_type?: string } | null>(null)
+const currentExercise = ref<{ code: string; language: string; lessonId?: string | null } | null>(null)
+
+// 练习追踪
+const pendingExerciseCount = ref(0)        // 当前消息中待完成的练习数
+const completedExerciseCount = ref(0)       // 已完成的练习数
+
+const autoResize = () => {
+  const textarea = textareaRef.value
+  if (textarea) {
+    textarea.style.height = 'auto'
+    textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'
+  }
+}
+
+const isLastAssistantMessage = (index: number) => {
+  if (index !== messages.value.length - 1) return false
+  return messages.value[index].role === 'assistant'
+}
+
+const clearChat = () => {
+  if (messages.value.length === 0) return
+  if (confirm('确定清除所有对话记录吗?此操作不可恢复。')) {
+    messages.value = []
+  }
+}
+
+const send = async (context?: Record<string, any>) => {
+  const text = input.value.trim()
+  
+  if (!text || loading.value) return
+
+  // 发送新消息时清理练习模式
+  currentExercise.value = null
+  
+  messages.value.push({ role: 'user', content: text })
+  input.value = ''
+  loading.value = true
+  
+  // 重置textarea高度
+  if (textareaRef.value) {
+    textareaRef.value.style.height = 'auto'
+  }
+  
+  await scrollToBottom()
+  
+  try {
+    const payload: Record<string, any> = {
+      message: text,
+      user_id: authStore.userId,
+    }
+    if (context) {
+      payload.context = context
+    }
+    
+    const response = await axios.post('/api/chat/', payload)
+    
+    const agentName = response.data.agent_name
+    const agentType = getAgentType(agentName)
+    
+    // 更新Agent状态
+    agentStore.setActiveAgent(agentType)
+    
+    messages.value.push({ 
+      role: 'assistant', 
+      content: response.data.reply,
+      agent_name: agentName 
+    })
+    
+    // 记录消息
+    agentStore.recordMessage(agentType, response.data.reply)
+    agentStore.resetAllStatus()
+    
+    // 保存学习会话(带最新回复摘要)
+    if (learningContext.value) {
+      saveSession(response.data.reply)
+    }
+  } catch (error: any) {
+    const errorMsg = error.response?.data?.detail || error.message || '未知错误'
+    messages.value.push({ 
+      role: 'assistant', 
+      content: `抱歉,出现了一些问题:${errorMsg}` 
+    })
+    agentStore.resetAllStatus()
+  } finally {
+    loading.value = false
+    await scrollToBottom()
+  }
+}
+
+const scrollToBottom = async () => {
+  await nextTick()
+  if (messagesContainer.value) {
+    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
+  }
+}
+
+onMounted(async () => {
+  // Load saved session
+  try {
+    const sessionRes = await axios.get('/api/learning/session', {
+      params: { user_id: authStore.userId }
+    })
+    const sess = sessionRes.data.session
+    if (sess && sess.lesson_id) {
+      sessionData.value = sess
+    }
+  } catch (e) {
+    console.error('Failed to load session:', e)
+  }
+
+  checkPendingMessage()
+})
+
+onActivated(() => {
+  checkPendingMessage()
+})
+
+// 学习上下文
+const learningContext = ref<any>(null)
+
+const checkPendingMessage = () => {
+  // 优先读取新的上下文格式
+  const pendingContext = sessionStorage.getItem('pendingLearningContext')
+  if (pendingContext) {
+    sessionStorage.removeItem('pendingLearningContext')
+    try {
+      const context = JSON.parse(pendingContext)
+      learningContext.value = context
+      input.value = context.message
+      saveSession() // 保存学习会话到后端
+      // 延迟确保DOM更新后再发送
+      setTimeout(() => {
+        send(context)
+      }, 100)
+      return
+    } catch (e) {
+      console.error('Failed to parse learning context:', e)
+    }
+  }
+  
+  // 兼容旧格式
+  const pendingMessage = sessionStorage.getItem('pendingLearningMessage')
+  if (pendingMessage) {
+    sessionStorage.removeItem('pendingLearningMessage')
+    input.value = pendingMessage
+    // 延迟确保DOM更新后再发送
+    setTimeout(() => {
+      send()
+    }, 100)
+  }
+}
+
+const saveSession = async (summary?: string) => {
+  if (!learningContext.value) return
+  try {
+    await axios.post('/api/learning/session', {
+      lesson_id: learningContext.value.lesson_id,
+      module_id: learningContext.value.module_id,
+      lesson_title: learningContext.value.lesson_title,
+      module_title: learningContext.value.module_title,
+      path_type: learningContext.value.path_type,
+      last_reply_summary: summary || '',
+    }, { params: { user_id: authStore.userId } })
+  } catch (e) {
+    // 静默失败,不影响用户体验
+  }
+}
+
+const clearSession = async () => {
+  try {
+    await axios.post('/api/learning/session', {}, { params: { user_id: authStore.userId, clear: true } })
+  } catch (e) { /* ignore */ }
+}
+
+const continueSession = async () => {
+  if (!sessionData.value) return
+  const ctx = {
+    message: `我想继续学习"${sessionData.value.lesson_title}"这个课程。请帮我讲解这个主题。`,
+    path_type: sessionData.value.path_type,
+    lesson_id: sessionData.value.lesson_id,
+    module_id: sessionData.value.module_id,
+    lesson_title: sessionData.value.lesson_title,
+    module_title: sessionData.value.module_title,
+  }
+  learningContext.value = ctx
+  input.value = ctx.message
+  sessionData.value = null
+  await saveSession()
+  setTimeout(() => send(ctx), 100)
+}
+
+const masteredClick = async () => {
+  if (!learningContext.value || masteredLoading.value) return
+  masteredLoading.value = true
+  
+  const quizMsg = `我已完成"${learningContext.value.lesson_title || ''}"的学习。作为编程导师,请出 2-3 道测验题检验我的理解,请严格按照以下格式:
+1. 选择题使用 \`\`\`quiz JSON 格式
+2. 代码练习题使用 \`\`\`exercise:python(或对应的语言)格式,题目作为注释写在代码里,留出填空位让用户自己写代码
+3. 题目难度匹配我当前的水平
+4. 对于代码练习题,请给出操作指引告诉用户:「请点击「📂 在编辑器中打开」按钮,在右侧编辑器中补全代码,然后点击「📝 提交练习反馈」获取批改和优化建议」。不要写「告诉我你的答案」或「在代码注释填空」等与实际操作不符的指引`
+  
+  try {
+    const res = await axios.post('/api/chat/', {
+      message: quizMsg,
+      context: learningContext.value,
+    })
+    messages.value.push({
+      role: 'assistant',
+      content: res.data.reply,
+      agent_name: res.data.agent_name,
+    })
+    await scrollToBottom()
+  } catch (err: any) {
+    messages.value.push({
+      role: 'assistant',
+      content: `抱歉,出题失败:${err.message}`
+    })
+  } finally {
+    masteredLoading.value = false
+  }
+}
+
+const checkLessonComplete = (quizResult?: { correct: number; total: number }) => {
+  // 有练习时要求全部提交完成
+  if (pendingExerciseCount.value > 0 && completedExerciseCount.value < pendingExerciseCount.value) {
+    return
+  }
+  const lessonId = learningContext.value?.lesson_id
+  if (!lessonId) return
+  
+  axios.post(`/api/learning/complete-lesson/${lessonId}`, null, {
+    params: { user_id: authStore.userId }
+  }).then((res) => {
+    // 保存下一课程信息(包含 path_type)
+    const next = res.data?.next_lesson
+    if (next) {
+      nextLessonData.value = {
+        ...next,
+        path_type: learningContext.value?.path_type,
+      }
+    } else {
+      nextLessonData.value = null
+    }
+    completionData.value = {
+      passed: true,
+      correct: quizResult?.correct || 0,
+      total: quizResult?.total || 0,
+      lessonCompleted: true,
+    }
+    learningContext.value = null
+    clearSession()
+  }).catch(e => {
+    console.error('Failed to complete lesson:', e)
+    completionData.value = {
+      passed: true,
+      correct: quizResult?.correct || 0,
+      total: quizResult?.total || 0,
+      lessonCompleted: false,
+    }
+  })
+}
+
+const onQuizResult = (result: { total: number; correct: number }) => {
+  if (!learningContext.value) return
+  
+  const passed = result.correct / result.total >= 0.6
+  
+  if (passed && result.total >= 2) {
+    if (pendingExerciseCount.value > 0 && completedExerciseCount.value < pendingExerciseCount.value) {
+      // 有练习未完成,暂存结果但不等完成
+      completionData.value = {
+        passed: true,
+        correct: result.correct,
+        total: result.total,
+        lessonCompleted: false,
+      }
+    } else {
+      // 无练习 或 练习已全部提交 → 直接完成
+      checkLessonComplete(result)
+    }
+  } else if (passed && result.total < 2) {
+    completionData.value = null
+  } else {
+    completionData.value = {
+      passed: false,
+      correct: result.correct,
+      total: result.total,
+      lessonCompleted: false,
+    }
+  }
+}
+
+const onContentMeta = (meta: { quizCount: number; exerciseCount: number }, msg: any) => {
+  // 记录练习数,用于完成判定
+  pendingExerciseCount.value = meta.exerciseCount
+}
+const onExerciseSubmitted = (result: { feedback: string }) => {
+  completedExerciseCount.value++
+  
+  // 所有练习完成 & 选择题已通过 → 完成课程
+  if (completedExerciseCount.value >= pendingExerciseCount.value && learningContext.value) {
+    if (completionData.value?.passed && !completionData.value.lessonCompleted) {
+      checkLessonComplete({
+        correct: completionData.value.correct,
+        total: completionData.value.total,
+      })
+    }
+    const lastMsg = messages.value[messages.value.length - 1]
+    if (lastMsg) saveSession(lastMsg.content)
+  }
+}
+
+const onExerciseDetected = (data: { code: string; language: string }) => {
+  currentExercise.value = {
+    code: data.code,
+    language: data.language,
+    lessonId: learningContext.value?.lesson_id || null,
+  }
+}
+
+const goNextLesson = () => {
+  const next = nextLessonData.value
+  completionData.value = null
+  nextLessonData.value = null
+
+  if (!next || !next.path_type) {
+    // 没有下一课信息 → 回到学习路径页
+    window.location.hash = '#/learning'
+    return
+  }
+
+  // 自动启动下一课:设置学习上下文并发消息给导师
+  const ctx = {
+    message: `请开始讲解"${next.title}"。这是下一节课的内容,请详细讲解。`,
+    path_type: next.path_type,
+    lesson_id: next.id,
+    module_id: next.module_id,
+    lesson_title: next.title,
+    module_title: next.module_title,
+  }
+  learningContext.value = ctx
+  input.value = ctx.message
+  saveSession(ctx.message)
+  // 延迟发送确保 DOM 更新
+  setTimeout(() => send(ctx), 100)
+}
+</script>
+
+<style scoped>
+.app-container {
+  display: flex;
+  flex: 1;
+  background: #f8f9fa;
+  overflow: hidden;
+}
+
+.chat-section {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-right: 1px solid #e5e5e5;
+}
+
+.code-section {
+  width: 550px;
+  padding: 16px;
+  background: #1e1e1e;
+  overflow: hidden;
+}
+
+/* Header */
+.chat-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.avatar {
+  width: 44px;
+  height: 44px;
+  border-radius: 12px;
+  background: rgba(255,255,255,0.2);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22px;
+}
+
+.header-info h2 {
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+}
+
+.subtitle {
+  font-size: 13px;
+  opacity: 0.85;
+}
+
+.status-badge {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  background: rgba(255,255,255,0.2);
+  border-radius: 20px;
+  font-size: 13px;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #52c41a;
+  animation: pulse 2s infinite;
+}
+
+.clear-btn {
+  width: 36px;
+  height: 36px;
+  border: none;
+  background: rgba(255, 255, 255, 0.15);
+  border-radius: 8px;
+  font-size: 16px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.2s;
+  margin-left: 8px;
+  flex-shrink: 0;
+}
+
+.clear-btn:hover {
+  background: rgba(255, 255, 255, 0.3);
+  transform: scale(1.05);
+}
+
+.clear-btn:active {
+  transform: scale(0.95);
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+/* Messages */
+.messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.welcome-card {
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
+  border-radius: 16px;
+  padding: 32px;
+  text-align: center;
+  margin-bottom: 16px;
+}
+
+.welcome-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.welcome-card h3 {
+  margin: 0 0 8px 0;
+  font-size: 20px;
+  color: #1a1a1a;
+}
+
+.welcome-card p {
+  margin: 0 0 16px 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.feature-list {
+  display: flex;
+  justify-content: center;
+  gap: 24px;
+}
+
+.feature-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  background: white;
+  border-radius: 8px;
+  font-size: 13px;
+  color: #333;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+}
+
+.feature-icon {
+  font-size: 16px;
+}
+
+/* Message Bubble */
+.message {
+  display: flex;
+  gap: 10px;
+  max-width: 85%;
+}
+
+.message.user {
+  align-self: flex-end;
+  flex-direction: row-reverse;
+}
+
+.message.assistant {
+  align-self: flex-start;
+}
+
+.msg-avatar {
+  width: 36px;
+  height: 36px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 18px;
+  flex-shrink: 0;
+}
+
+.avatar-tutor {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.avatar-debug {
+  background: linear-gradient(135deg, #f5222d 0%, #cf1322 100%);
+}
+
+.avatar-review {
+  background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
+}
+
+.avatar-arch {
+  background: linear-gradient(135deg, #fa8c16 0%, #d46b08 100%);
+}
+
+.avatar-coach {
+  background: linear-gradient(135deg, #722ed1 0%, #531dab 100%);
+}
+
+.user-avatar {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+}
+
+.msg-body {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  min-width: 0;
+  flex: 1;
+}
+
+.agent-tag {
+  font-size: 11px;
+  color: #999;
+  padding-left: 4px;
+}
+
+.tag-tutor { color: #667eea; }
+.tag-debug { color: #f5222d; }
+.tag-review { color: #52c41a; }
+.tag-arch { color: #fa8c16; }
+.tag-coach { color: #722ed1; }
+
+.bubble {
+  padding: 12px 16px;
+  border-radius: 16px;
+  line-height: 1.6;
+  white-space: pre-wrap;
+  word-break: break-word;
+  font-size: 14px;
+}
+
+.message.user .bubble {
+  background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
+  color: white;
+  border-bottom-right-radius: 4px;
+}
+
+.message.assistant .bubble {
+  background: #f0f2f5;
+  color: #1a1a1a;
+  border-bottom-left-radius: 4px;
+}
+
+.assistant-bubble {
+  width: 100%;
+  min-width: 0;
+}
+
+.user-bubble {
+  white-space: pre-wrap;
+}
+
+/* Typing Animation */
+.typing-bubble {
+  display: flex;
+  gap: 4px;
+  padding: 16px 20px;
+}
+
+.dot {
+  width: 8px;
+  height: 8px;
+  background: #999;
+  border-radius: 50%;
+  animation: bounce 1.4s infinite ease-in-out;
+}
+
+.dot:nth-child(1) { animation-delay: -0.32s; }
+.dot:nth-child(2) { animation-delay: -0.16s; }
+
+@keyframes bounce {
+  0%, 80%, 100% { transform: scale(0); }
+  40% { transform: scale(1); }
+}
+
+/* Input Area */
+.input-area {
+  padding: 16px 24px;
+  background: #fff;
+  border-top: 1px solid #e5e5e5;
+}
+
+.input-wrapper {
+  display: flex;
+  gap: 12px;
+  align-items: flex-end;
+  background: #f5f7fa;
+  border-radius: 12px;
+  padding: 8px 12px;
+  border: 2px solid transparent;
+  transition: all 0.2s;
+}
+
+.input-wrapper:focus-within {
+  border-color: #667eea;
+  background: #fff;
+  box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
+}
+
+.input-wrapper textarea {
+  flex: 1;
+  border: none;
+  background: transparent;
+  resize: none;
+  font-size: 14px;
+  line-height: 1.5;
+  padding: 8px 4px;
+  font-family: inherit;
+}
+
+.input-wrapper textarea:focus {
+  outline: none;
+}
+
+.send-btn {
+  padding: 10px 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  font-size: 14px;
+  font-weight: 500;
+  transition: all 0.2s;
+  min-width: 80px;
+}
+
+.send-btn:hover:not(:disabled) {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.send-btn:disabled {
+  background: #d9d9d9;
+  cursor: not-allowed;
+}
+
+.spinner {
+  display: inline-block;
+  width: 16px;
+  height: 16px;
+  border: 2px solid rgba(255,255,255,0.3);
+  border-radius: 50%;
+  border-top-color: white;
+  animation: spin 0.8s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.input-hint {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #999;
+  text-align: center;
+}
+
+/* Session banner */
+.session-banner {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px;
+  padding: 14px 18px;
+  margin-bottom: 16px;
+  animation: slideUp 0.3s ease;
+}
+
+.session-banner-content {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.session-banner-icon { font-size: 24px; }
+
+.session-banner-text { flex: 1; }
+
+.session-banner-text p { margin: 0; color: white; font-size: 14px; line-height: 1.5; }
+
+.session-dismiss {
+  background: none; border: none; color: rgba(255,255,255,0.6);
+  font-size: 18px; cursor: pointer; padding: 4px;
+}
+
+.session-dismiss:hover { color: white; }
+
+/* Mastered button */
+.mastered-section { margin-top: 8px; text-align: right; }
+
+.mastered-btn {
+  padding: 8px 20px; background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
+  color: white; border: none; border-radius: 20px; cursor: pointer;
+  font-size: 14px; font-weight: 500; transition: all 0.2s;
+}
+
+.mastered-btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(82, 196, 26, 0.4); }
+
+.mastered-btn:disabled { opacity: 0.6; cursor: not-allowed; }
+
+/* Completion banner */
+.completion-banner {
+  border-radius: 12px; padding: 16px 20px; margin: 0 24px 8px; animation: fadeIn 0.3s;
+  flex-shrink: 0;
+}
+
+.completion-banner.pass { background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%); border: 1px solid #b7eb8f; }
+
+.completion-banner.fail { background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%); border: 1px solid #ffccc7; }
+
+.completion-content { display: flex; align-items: center; gap: 12px; }
+
+.completion-icon { font-size: 28px; }
+
+.completion-text { flex: 1; }
+
+.completion-text p { margin: 0; font-size: 14px; color: #333; line-height: 1.5; }
+
+.completion-text .lesson-complete { color: #52c41a; font-weight: 600; margin-top: 4px; }
+
+/* Button utilities */
+.btn { padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: none; display: inline-flex; align-items: center; gap: 6px; }
+.btn-sm { padding: 6px 14px; font-size: 13px; }
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white; border: none; border-radius: 8px; cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }
+
+.btn-outline {
+  background: transparent; color: #667eea; border: 1px solid #667eea;
+  border-radius: 8px; cursor: pointer; transition: all 0.2s;
+}
+
+.btn-outline:hover { background: rgba(102, 126, 234, 0.05); }
+
+/* Animations */
+@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
+
+@keyframes slideUp { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
+</style>

+ 887 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Dashboard.vue

@@ -0,0 +1,887 @@
+<template>
+  <div class="dashboard-view">
+    <div class="view-header">
+      <div class="header-left">
+        <h1>{{ langStore.t('dashboard.title') }}</h1>
+        <p>{{ langStore.t('dashboard.subtitle') }}</p>
+      </div>
+      <div class="path-selector">
+        <button
+          v-for="path in pathOptions"
+          :key="path.value"
+          :class="['path-tab', { active: selectedPath === path.value }]"
+          @click="switchPath(path.value)"
+        >
+          <span class="tab-icon">{{ path.icon }}</span>
+          {{ path.label }}
+        </button>
+      </div>
+    </div>
+
+    <!-- 概览卡片 -->
+    <div class="summary-cards">
+      <div class="summary-card">
+        <div class="card-icon total-tests">📋</div>
+        <div class="card-body">
+          <span class="card-value">{{ totalTests }}</span>
+          <span class="card-label">{{ langStore.t('dashboard.totalTests') }}</span>
+        </div>
+      </div>
+      <div class="summary-card">
+        <div class="card-icon avg-score">📊</div>
+        <div class="card-body">
+          <span class="card-value">{{ averageScore }}</span>
+          <span class="card-label">{{ langStore.t('dashboard.avgScore') }}</span>
+        </div>
+      </div>
+      <div class="summary-card">
+        <div class="card-icon current-level">🏆</div>
+        <div class="card-body">
+          <span class="card-value">{{ currentLevelText }}</span>
+          <span class="card-label">{{ langStore.t('dashboard.currentLevel') }}</span>
+        </div>
+      </div>
+      <div class="summary-card">
+        <div class="card-icon best-skill">⭐</div>
+        <div class="card-body">
+          <span class="card-value">{{ bestSkillName }}</span>
+          <span class="card-label">{{ langStore.t('dashboard.bestSkill') }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 图表区域 -->
+    <div class="charts-row">
+      <!-- 雷达图:分类水平 -->
+      <div class="chart-card">
+        <div class="chart-header">
+          <h3>{{ langStore.t('dashboard.categoryLevels') }}</h3>
+          <span v-if="latestAssessment" class="chart-meta">
+            {{ langStore.t('dashboard.recentTest') }}{{ formatDate(latestAssessment.completed_at) }}
+          </span>
+        </div>
+        <div v-if="latestAssessment && radarData" class="chart-container">
+          <Radar :data="radarData" :options="radarOptions" />
+        </div>
+        <div v-else class="empty-chart">
+          <span class="empty-icon">📡</span>
+          <p>{{ langStore.t('dashboard.noTestData') }}</p>
+          <button class="btn btn-primary btn-sm" @click="goToAssessment">{{ langStore.t('dashboard.startTest') }}</button>
+        </div>
+      </div>
+
+      <!-- 折线图:成绩趋势 -->
+      <div class="chart-card">
+        <div class="chart-header">
+          <h3>{{ langStore.t('dashboard.scoreTrend') }}</h3>
+          <span v-if="assessments.length > 0" class="chart-meta">
+            {{ langStore.t('dashboard.totalTestsCount', { count: assessments.length }) }}
+          </span>
+        </div>
+        <div v-if="assessments.length >= 1" class="chart-container">
+          <Line :data="trendData" :options="trendOptions" />
+        </div>
+        <div v-else class="empty-chart">
+          <span class="empty-icon">📈</span>
+          <p>{{ langStore.t('dashboard.trendDesc') }}</p>
+        </div>
+      </div>
+    </div>
+
+    <!-- 最近测试记录 -->
+    <div class="recent-card">
+      <div class="chart-header">
+        <h3>{{ langStore.t('dashboard.recentRecords') }}</h3>
+        <button
+          v-if="assessments.length > 0"
+          class="btn btn-outline btn-sm"
+          @click="goToAssessment"
+        >
+          {{ langStore.t('dashboard.retake') }}
+        </button>
+      </div>
+      <div v-if="assessments.length > 0" class="history-list">
+        <div
+          v-for="(item, index) in sortedAssessments"
+          :key="index"
+          class="history-item"
+        >
+          <div class="history-index">
+            <span :class="['rank-badge', index === 0 ? 'top' : '']">{{ index + 1 }}</span>
+          </div>
+          <div class="history-info">
+            <span class="history-date">{{ formatDate(item.completed_at) }}</span>
+            <div class="history-categories">
+              <span
+                v-for="(score, cat) in item.category_scores"
+                :key="cat"
+                class="cat-tag"
+              >
+                {{ getCategoryName(String(cat)) }} {{ Math.round(score) }}%
+              </span>
+            </div>
+          </div>
+          <div class="history-score-area">
+            <span :class="['score-pill', getScoreClass(item.score)]">{{ Math.round(item.score) }}</span>
+            <span :class="['level-badge', item.level]">{{ getLevelText(item.level) }}</span>
+          </div>
+        </div>
+      </div>
+      <div v-else class="empty-history">
+        <span class="empty-icon">📭</span>
+        <p>{{ langStore.t('dashboard.noRecords') }}</p>
+        <button class="btn btn-primary btn-sm" @click="goToAssessment">{{ langStore.t('dashboard.firstTest') }}</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, onActivated, watch } from 'vue'
+import { useRouter } from 'vue-router'
+import axios from 'axios'
+import { Radar, Line } from 'vue-chartjs'
+import { useAuthStore } from '../stores/authStore'
+import { useLangStore } from '../stores/langStore'
+import {
+  Chart as ChartJS,
+  RadarController,
+  RadialLinearScale,
+  PointElement,
+  LineElement,
+  Filler,
+  Tooltip,
+  Legend,
+  CategoryScale,
+  LinearScale,
+  LineController
+} from 'chart.js'
+
+ChartJS.register(
+  RadarController,
+  RadialLinearScale,
+  PointElement,
+  LineElement,
+  Filler,
+  Tooltip,
+  Legend,
+  CategoryScale,
+  LinearScale,
+  LineController
+)
+
+const router = useRouter()
+const authStore = useAuthStore()
+const langStore = useLangStore()
+
+// --- Types ---
+interface AssessmentRecord {
+  path_type: string
+  score: number
+  level: string
+  category_scores: Record<string, number>
+  completed_at: string
+}
+
+// --- State ---
+const selectedPath = ref('frontend')
+const assessments = ref<AssessmentRecord[]>([])
+
+const pathOptions = [
+  { value: 'frontend', label: '前端开发', icon: '🎨' },
+  { value: 'backend', label: '后端开发', icon: '⚙️' },
+  { value: 'fullstack', label: '全栈开发', icon: '🚀' }
+]
+
+// --- Computed: Summary ---
+const totalTests = computed(() => assessments.value.length)
+
+const averageScore = computed(() => {
+  if (assessments.value.length === 0) return '—'
+  const sum = assessments.value.reduce((acc, a) => acc + a.score, 0)
+  return Math.round(sum / assessments.value.length)
+})
+
+const currentLevelText = computed(() => {
+  const latest = sortedAssessments.value[0]
+  if (!latest) return '—'
+  return getLevelText(latest.level)
+})
+
+const bestSkillName = computed(() => {
+  const latest = sortedAssessments.value[0]
+  if (!latest || !latest.category_scores) return '—'
+  const entries = Object.entries(latest.category_scores)
+  if (entries.length === 0) return '—'
+  const best = entries.reduce((a, b) => (b[1] > a[1] ? b : a))
+  return getCategoryName(best[0])
+})
+
+const latestAssessment = computed(() => {
+  return sortedAssessments.value[0] || null
+})
+
+const sortedAssessments = computed(() => {
+  return [...assessments.value].sort(
+    (a, b) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime()
+  )
+})
+
+// --- Computed: Radar Chart ---
+const radarData = computed(() => {
+  if (!latestAssessment.value) return null
+  const scores = latestAssessment.value.category_scores
+  const labels = Object.keys(scores).map((k) => getCategoryName(k))
+  const values = Object.values(scores)
+
+  return {
+    labels,
+    datasets: [
+      {
+        label: '技能水平',
+        data: values,
+        backgroundColor: 'rgba(102, 126, 234, 0.18)',
+        borderColor: '#667eea',
+        borderWidth: 2,
+        pointBackgroundColor: '#667eea',
+        pointBorderColor: '#fff',
+        pointBorderWidth: 2,
+        pointRadius: 4,
+        pointHoverRadius: 6,
+        fill: true
+      }
+    ]
+  }
+})
+
+const radarOptions = {
+  responsive: true,
+  maintainAspectRatio: false,
+  scales: {
+    r: {
+      beginAtZero: true,
+      max: 100,
+      ticks: {
+        stepSize: 20,
+        display: true,
+        color: '#999',
+        backdropColor: 'transparent' as const,
+        font: { size: 11 }
+      },
+      grid: {
+        color: 'rgba(0, 0, 0, 0.06)'
+      },
+      angleLines: {
+        color: 'rgba(0, 0, 0, 0.06)'
+      },
+      pointLabels: {
+        color: '#333',
+        font: { size: 13, weight: '500' as const }
+      }
+    }
+  },
+  plugins: {
+    legend: { display: false },
+    tooltip: {
+      backgroundColor: 'rgba(0,0,0,0.75)',
+      titleFont: { size: 13 },
+      bodyFont: { size: 12 },
+      padding: 10,
+      cornerRadius: 8,
+      callbacks: {
+        label: (ctx: any) => `${ctx.label}: ${ctx.raw}%`
+      }
+    }
+  }
+}
+
+// --- Computed: Trend Chart ---
+const trendData = computed(() => {
+  if (assessments.value.length === 0) return { labels: [], datasets: [] }
+  const sorted = sortedAssessments.value
+
+  return {
+    labels: sorted.map((a) => formatDateShort(a.completed_at)),
+    datasets: [
+      {
+        label: '测试得分',
+        data: sorted.map((a) => a.score),
+        borderColor: '#667eea',
+        backgroundColor: (ctx: any) => {
+          const chart = ctx.chart
+          const { ctx: canvasCtx, chartArea } = chart
+          if (!chartArea) return 'rgba(102, 126, 234, 0.1)'
+          const gradient = canvasCtx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom)
+          gradient.addColorStop(0, 'rgba(102, 126, 234, 0.25)')
+          gradient.addColorStop(1, 'rgba(102, 126, 234, 0.02)')
+          return gradient
+        },
+        borderWidth: 2.5,
+        fill: true,
+        tension: 0.35,
+        pointBackgroundColor: '#667eea',
+        pointBorderColor: '#fff',
+        pointBorderWidth: 2,
+        pointRadius: 5,
+        pointHoverRadius: 7
+      }
+    ]
+  }
+})
+
+const trendOptions = {
+  responsive: true,
+  maintainAspectRatio: false,
+  scales: {
+    x: {
+      grid: { display: false },
+      ticks: { color: '#999', font: { size: 12 } }
+    },
+    y: {
+      beginAtZero: true,
+      max: 100,
+      grid: { color: 'rgba(0, 0, 0, 0.04)' },
+      ticks: {
+        color: '#999',
+        stepSize: 20,
+        font: { size: 12 },
+        callback: (val: any) => `${val}分`
+      }
+    }
+  },
+  plugins: {
+    legend: { display: false },
+    tooltip: {
+      backgroundColor: 'rgba(0,0,0,0.75)',
+      titleFont: { size: 13 },
+      bodyFont: { size: 12 },
+      padding: 10,
+      cornerRadius: 8,
+      callbacks: {
+        label: (ctx: any) => `得分: ${ctx.raw}分`
+      }
+    }
+  }
+}
+
+// --- Methods ---
+const fetchAssessments = async () => {
+  try {
+    const response = await axios.get(
+      `/api/assessment/history/${selectedPath.value}`,
+      { params: { user_id: authStore.userId } }
+    )
+    assessments.value = response.data.assessments || []
+  } catch (error) {
+    console.error('Failed to load assessment history:', error)
+    assessments.value = []
+  }
+}
+
+const switchPath = (path: string) => {
+  selectedPath.value = path
+  fetchAssessments()
+}
+
+const goToAssessment = () => {
+  router.push({
+    path: '/assessment',
+    query: { path: selectedPath.value }
+  })
+}
+
+const formatDate = (dateStr: string) => {
+  const d = new Date(dateStr)
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  const h = String(d.getHours()).padStart(2, '0')
+  const min = String(d.getMinutes()).padStart(2, '0')
+  return `${y}-${m}-${day} ${h}:${min}`
+}
+
+const formatDateShort = (dateStr: string) => {
+  const d = new Date(dateStr)
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  return `${m}-${day}`
+}
+
+const getCategoryName = (category: string) => {
+  const names: Record<string, string> = {
+    html_css: 'HTML/CSS',
+    javascript: 'JavaScript',
+    vue: 'Vue.js',
+    browser_apis: '浏览器API',
+    python: 'Python',
+    api_design: 'API设计',
+    database: '数据库',
+    system_design: '系统设计'
+  }
+  return names[category] || category
+}
+
+const getLevelText = (level: string) => {
+  const texts: Record<string, string> = {
+    beginner: '入门',
+    intermediate: '中级',
+    advanced: '高级'
+  }
+  return texts[level] || level
+}
+
+const getScoreClass = (score: number) => {
+  if (score >= 80) return 'excellent'
+  if (score >= 60) return 'good'
+  if (score >= 40) return 'fair'
+  return 'poor'
+}
+
+// --- Lifecycle ---
+onMounted(async () => {
+  // 读取用户已选路径
+  try {
+    const progressRes = await axios.get('/api/learning/progress', {
+      params: { user_id: authStore.userId }
+    })
+    if (progressRes.data.current_path) {
+      selectedPath.value = progressRes.data.current_path
+    }
+  } catch (e) {
+    // ignore, defaults to 'frontend'
+  }
+  fetchAssessments()
+})
+
+// 从keep-alive缓存恢复时刷新数据
+onActivated(() => {
+  fetchAssessments()
+})
+</script>
+
+<style scoped>
+.dashboard-view {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  background: #f8f9fa;
+  overflow-y: auto;
+}
+
+/* Header */
+.view-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24px 32px;
+  background: white;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.view-header h1 {
+  margin: 0 0 4px 0;
+  font-size: 24px;
+  color: #1a1a1a;
+}
+
+.view-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.path-selector {
+  display: flex;
+  gap: 6px;
+  background: #f5f5f5;
+  border-radius: 12px;
+  padding: 4px;
+}
+
+.path-tab {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 8px 16px;
+  border: none;
+  background: transparent;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #666;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.path-tab:hover {
+  color: #333;
+  background: rgba(255, 255, 255, 0.6);
+}
+
+.path-tab.active {
+  background: white;
+  color: #667eea;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+}
+
+.tab-icon {
+  font-size: 15px;
+}
+
+/* Summary Cards */
+.summary-cards {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 16px;
+  padding: 24px 32px;
+}
+
+.summary-card {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+  transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.summary-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
+}
+
+.card-icon {
+  width: 48px;
+  height: 48px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22px;
+  flex-shrink: 0;
+}
+
+.card-icon.total-tests {
+  background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
+}
+
+.card-icon.avg-score {
+  background: linear-gradient(135deg, #f0f5ff 0%, #d6e4ff 100%);
+}
+
+.card-icon.current-level {
+  background: linear-gradient(135deg, #fff7e6 0%, #ffe7ba 100%);
+}
+
+.card-icon.best-skill {
+  background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
+}
+
+.card-body {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.card-value {
+  font-size: 22px;
+  font-weight: 700;
+  color: #1a1a1a;
+  line-height: 1.2;
+}
+
+.card-label {
+  font-size: 13px;
+  color: #999;
+}
+
+/* Charts Row */
+.charts-row {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+  padding: 0 32px;
+  margin-bottom: 16px;
+}
+
+.chart-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.chart-header h3 {
+  margin: 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.chart-meta {
+  font-size: 12px;
+  color: #999;
+}
+
+.chart-container {
+  height: 280px;
+  position: relative;
+}
+
+.empty-chart {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 240px;
+  gap: 8px;
+}
+
+.empty-icon {
+  font-size: 40px;
+  opacity: 0.5;
+}
+
+.empty-chart p {
+  margin: 0;
+  color: #999;
+  font-size: 14px;
+  text-align: center;
+}
+
+/* Recent Assessments */
+.recent-card {
+  margin: 0 32px 24px;
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.history-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.history-item {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 14px 16px;
+  background: #f8f9fa;
+  border-radius: 10px;
+  transition: background 0.15s;
+}
+
+.history-item:hover {
+  background: #f0f0f0;
+}
+
+.rank-badge {
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: 600;
+  color: #999;
+  background: #e8e8e8;
+  flex-shrink: 0;
+}
+
+.rank-badge.top {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.history-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  min-width: 0;
+}
+
+.history-date {
+  font-size: 13px;
+  color: #333;
+  font-weight: 500;
+}
+
+.history-categories {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.cat-tag {
+  padding: 2px 8px;
+  background: #f0f0f0;
+  border-radius: 6px;
+  font-size: 11px;
+  color: #666;
+  white-space: nowrap;
+}
+
+.history-score-area {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
+.score-pill {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 44px;
+  height: 44px;
+  border-radius: 50%;
+  font-size: 16px;
+  font-weight: 700;
+  color: #1a1a1a;
+}
+
+.score-pill.excellent {
+  background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
+  color: #389e0d;
+}
+
+.score-pill.good {
+  background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
+  color: #0958d9;
+}
+
+.score-pill.fair {
+  background: linear-gradient(135deg, #fff7e6 0%, #ffd591 100%);
+  color: #d46b08;
+}
+
+.score-pill.poor {
+  background: linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%);
+  color: #cf1322;
+}
+
+.level-badge {
+  padding: 4px 10px;
+  border-radius: 10px;
+  font-size: 12px;
+  font-weight: 600;
+}
+
+.level-badge.beginner {
+  background: #f6ffed;
+  color: #52c41a;
+}
+
+.level-badge.intermediate {
+  background: #e6f7ff;
+  color: #1890ff;
+}
+
+.level-badge.advanced {
+  background: #fff2f0;
+  color: #ff4d4f;
+}
+
+/* Empty History */
+.empty-history {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px;
+  gap: 8px;
+}
+
+.empty-history p {
+  margin: 0;
+  color: #999;
+  font-size: 14px;
+}
+
+/* Buttons */
+.btn {
+  padding: 10px 20px;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: none;
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.btn-primary:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-outline {
+  background: white;
+  border: 1px solid #d9d9d9;
+  color: #666;
+}
+
+.btn-outline:hover {
+  border-color: #667eea;
+  color: #667eea;
+}
+
+.btn-sm {
+  padding: 6px 14px;
+  font-size: 13px;
+}
+
+/* Responsive */
+@media (max-width: 1024px) {
+  .summary-cards {
+    grid-template-columns: repeat(2, 1fr);
+  }
+
+  .charts-row {
+    grid-template-columns: 1fr;
+  }
+}
+
+@media (max-width: 640px) {
+  .view-header {
+    flex-direction: column;
+    gap: 12px;
+    align-items: flex-start;
+  }
+
+  .summary-cards {
+    grid-template-columns: 1fr;
+    padding: 16px;
+  }
+
+  .charts-row {
+    padding: 0 16px;
+  }
+
+  .recent-card {
+    margin: 0 16px 16px;
+  }
+
+  .history-item {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+}
+</style>

+ 1732 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Learning.vue

@@ -0,0 +1,1732 @@
+<template>
+  <div class="learning-view">
+    <div class="view-header">
+      <h1>{{ langStore.t('learning.title') }}</h1>
+      <p>{{ langStore.t('learning.subtitle') }}</p>
+    </div>
+
+    <!-- 游戏化面板 -->
+    <div class="gamification-wrapper">
+      <GamificationPanel ref="gamificationRef" :userId="authStore.userId" />
+    </div>
+    
+    <div class="paths-container">
+      <div 
+        v-for="path in paths" 
+        :key="path.path"
+        :class="['path-card', path.path, { selected: selectedPath === path.path }]"
+        @click="selectPath(path.path)"
+      >
+        <div class="path-icon">{{ path.icon }}</div>
+        <h3 class="path-title">{{ path.title }}</h3>
+        <p class="path-desc">{{ path.description }}</p>
+        <div class="path-stats">
+          <span class="stat">
+            <span class="stat-icon">📚</span>
+            <span>{{ path.total_modules }} {{ langStore.t('learning.modules') }}</span>
+          </span>
+          <span class="stat">
+            <span class="stat-icon">📝</span>
+            <span>{{ path.total_lessons }} {{ langStore.t('learning.lessons') }}</span>
+          </span>
+        </div>
+        <div v-if="selectedPath === path.path" class="selected-badge">{{ langStore.t('learning.selected') }}</div>
+      </div>
+    </div>
+    
+    <div v-if="selectedPath && pathDetail" class="path-detail">
+      <!-- 水平检测状态卡片 -->
+      <div v-if="assessmentResult" class="assessment-card">
+        <div class="assessment-header">
+          <div class="assessment-info">
+            <span class="assessment-icon">📊</span>
+            <div class="assessment-text">
+              <h3>{{ langStore.t('learning.assessmentResult') }}</h3>
+              <p class="assessment-level">
+                {{ langStore.t('learning.currentLevel') }}<span :class="['level-badge', assessmentResult.level]">{{ getLevelText(assessmentResult.level) }}</span>
+                <span class="assessment-score">{{ langStore.t('learning.score') }} {{ assessmentResult.score }} {{ langStore.t('learning.unit') }}</span>
+              </p>
+            </div>
+          </div>
+          <button class="btn btn-outline btn-sm" @click="retakeTest">
+            🔄 {{ langStore.t('learning.retake') }}
+          </button>
+        </div>
+        <div class="assessment-skills">
+          <div 
+            v-for="(score, skill) in assessmentResult.category_scores" 
+            :key="skill"
+            class="skill-item"
+          >
+            <span class="skill-name">{{ getCategoryName(skill) }}</span>
+            <div class="skill-bar">
+              <div class="skill-fill" :style="{ width: score + '%' }" :class="getScoreBarClass(score)"></div>
+            </div>
+            <span class="skill-score">{{ score }}%</span>
+          </div>
+        </div>
+      </div>
+      
+      <div v-else class="assessment-prompt">
+        <div class="prompt-content">
+          <span class="prompt-icon">📝</span>
+          <div class="prompt-text">
+            <h3>{{ langStore.t('learning.assessmentPrompt') }}</h3>
+            <p>{{ langStore.t('learning.assessmentPromptDesc') }}</p>
+          </div>
+          <button class="btn btn-primary" @click="startTest">
+            {{ langStore.t('learning.startTest') }}
+          </button>
+        </div>
+      </div>
+
+      <div class="detail-header">
+        <h2>{{ pathDetail.title }} - {{ langStore.t('learning.courseDetail') }}</h2>
+        <div class="progress-summary">
+          <div class="progress-bar">
+            <div class="progress-fill" :style="{ width: pathDetail.progress + '%' }"></div>
+          </div>
+          <span class="progress-text">{{ pathDetail.progress.toFixed(0) }}{{ langStore.t('learning.completePercent') }}</span>
+        </div>
+      </div>
+      
+      <div class="modules-list">
+        <div 
+          v-for="module in pathDetail.modules" 
+          :key="module.id"
+          :class="['module-card', module.status]"
+        >
+          <div class="module-header">
+            <span class="module-icon">{{ module.icon }}</span>
+            <div class="module-info">
+              <h3 class="module-title">{{ module.title }}</h3>
+              <p class="module-desc">{{ module.description }}</p>
+            </div>
+            <div class="module-status">
+              <span class="status-badge" :class="module.status">
+                {{ getStatusText(module.status) }}
+              </span>
+            </div>
+          </div>
+          
+          <div class="module-progress">
+            <div class="progress-bar small">
+              <div class="progress-fill" :style="{ width: module.progress + '%' }"></div>
+            </div>
+            <span class="progress-label">{{ module.progress.toFixed(0) }}%</span>
+          </div>
+          
+          <div class="lessons-list">
+            <div 
+              v-for="lesson in module.lessons" 
+              :key="lesson.id"
+              :class="['lesson-item', { 
+                completed: lesson.is_completed,
+                clickable: module.status !== 'locked',
+                locked: module.status === 'locked'
+              }]"
+              @click="openLesson(lesson, module)"
+            >
+              <span class="lesson-type-icon">{{ getLessonTypeIcon(lesson.type) }}</span>
+              <div class="lesson-info">
+                <span class="lesson-title">{{ lesson.title }}</span>
+                <span class="lesson-meta">
+                  <span class="lesson-duration">{{ lesson.duration_minutes }}{{ langStore.t('learning.minutes') }}</span>
+                  <span class="lesson-type">{{ getLessonTypeText(lesson.type) }}</span>
+                </span>
+              </div>
+              <span v-if="lesson.is_completed" class="completed-icon">✓</span>
+              <span v-else-if="module.status === 'locked'" class="lock-icon">🔒</span>
+              <span v-else class="arrow-icon">→</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+    <!-- 课程详情弹窗 -->
+    <div v-if="selectedLesson" class="lesson-modal-overlay" @click.self="closeLesson">
+      <div class="lesson-modal">
+        <div class="modal-header">
+          <span class="modal-icon">{{ getLessonTypeIcon(selectedLesson.type) }}</span>
+          <div class="modal-title-area">
+            <h3>{{ selectedLesson.title }}</h3>
+            <span class="modal-module">{{ selectedModule?.title }}</span>
+          </div>
+          <button class="close-btn" @click="closeLesson">×</button>
+        </div>
+        
+        <div class="modal-body">
+          <p class="lesson-description">{{ selectedLesson.description }}</p>
+          
+          <div class="lesson-details">
+            <div class="detail-item">
+              <span class="detail-label">{{ langStore.t('learning.type') }}</span>
+              <span class="detail-value">{{ getLessonTypeText(selectedLesson.type) }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">{{ langStore.t('learning.duration') }}</span>
+              <span class="detail-value">{{ selectedLesson.duration_minutes }} {{ langStore.t('learning.minutes') }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">{{ langStore.t('learning.status') }}</span>
+              <span class="detail-value" :class="{ completed: selectedLesson.is_completed }">
+                {{ selectedLesson.is_completed ? langStore.t('learning.completed') : langStore.t('learning.notCompleted') }}
+              </span>
+            </div>
+          </div>
+          
+          <div class="lesson-content">
+            <template v-if="selectedLesson && selectedLesson.content_markdown">
+              <MarkdownRenderer :content="selectedLesson.content_markdown" />
+            </template>
+            <template v-else>
+              <div class="no-content-prompt">
+                <span class="no-content-icon">📚</span>
+                <h4>在 Chat 中学习</h4>
+                <p>点击"开始学习"进入对话模式,导师会为你讲解本课程内容</p>
+              </div>
+            </template>
+          </div>
+        </div>
+        
+        <div class="modal-footer">
+          <button 
+            v-if="!selectedLesson.is_completed" 
+            class="btn btn-primary"
+            @click="startLearning"
+          >
+            {{ langStore.t('learning.startLearning') }}
+          </button>
+          <button 
+            v-if="!selectedLesson.is_completed" 
+            class="btn btn-success"
+            @click="completeLesson"
+          >
+            {{ langStore.t('learning.markComplete') }}
+          </button>
+          <button 
+            v-else 
+            class="btn btn-secondary"
+            disabled
+          >
+            {{ langStore.t('learning.completed') }}
+          </button>
+          <button class="btn btn-outline" @click="closeLesson">{{ langStore.t('learning.close') }}</button>
+        </div>
+      </div>
+    </div>
+    
+    <div v-if="coachData" class="coach-panel">
+      <div class="coach-header">
+        <span class="coach-icon">🎯</span>
+        <h3>{{ langStore.t('learning.coachTitle') }}</h3>
+      </div>
+      <p class="coach-greeting">{{ coachData.greeting }}</p>
+      <div class="coach-recommendations">
+        <div 
+          v-for="(rec, index) in coachData.recommendations" 
+          :key="index"
+          :class="['recommendation-item', { clickable: rec.type === 'next_lesson' || rec.type === 'select_path' }]"
+          @click="handleRecClick(rec)"
+        >
+          <span class="rec-icon">{{ getRecIcon(rec.type) }}</span>
+          <div class="rec-content">
+            <span class="rec-title">{{ rec.title }}</span>
+            <span class="rec-desc">{{ rec.description }}</span>
+          </div>
+          <span v-if="rec.type === 'next_lesson'" class="rec-arrow">→</span>
+        </div>
+      </div>
+      <p class="coach-encouragement">{{ coachData.encouragement }}</p>
+      
+      <!-- 个性化学习计划 -->
+      <div v-if="coachData.learning_plan" class="learning-plan-section">
+        <div class="plan-header" @click="planExpanded = !planExpanded">
+          <span class="plan-header-icon">📋</span>
+          <span class="plan-header-title">个性化学习计划</span>
+          <span class="plan-toggle">{{ planExpanded ? '收起' : '展开' }}</span>
+        </div>
+        <div v-if="planExpanded" class="plan-content">
+          <div class="plan-markdown">{{ coachData.learning_plan }}</div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 新徽章获得通知 -->
+    <div v-if="newBadges.length > 0" class="badge-modal-overlay" @click.self="newBadges = []">
+      <div class="badge-modal">
+        <div class="badge-modal-header">
+          <span class="badge-modal-icon">🎉</span>
+          <h2>获得新徽章!</h2>
+        </div>
+        <div class="badge-modal-body">
+          <div v-for="badge in newBadges" :key="badge.id" class="new-badge-item">
+            <span class="new-badge-icon">{{ badge.icon }}</span>
+            <div class="new-badge-info">
+              <span class="new-badge-name">{{ badge.name }}</span>
+              <span class="new-badge-desc">{{ badge.description }}</span>
+            </div>
+          </div>
+        </div>
+        <button class="btn btn-primary" @click="newBadges = []">太棒了!</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onActivated } from 'vue'
+import { useRouter } from 'vue-router'
+import axios from 'axios'
+import { useAuthStore } from '../stores/authStore'
+import { useLangStore } from '../stores/langStore'
+import { 
+  getLevelText, getStatusText, 
+  getLessonTypeIcon, getLessonTypeText, getRecIcon,
+  getCategoryName, getScoreBarClass
+} from '../utils/helpers'
+import GamificationPanel from '../components/GamificationPanel.vue'
+import MarkdownRenderer from '../components/MarkdownRenderer.vue'
+
+const router = useRouter()
+const authStore = useAuthStore()
+const langStore = useLangStore()
+
+// 游戏化相关
+const gamificationRef = ref<InstanceType<typeof GamificationPanel> | null>(null)
+const newBadges = ref<any[]>([])
+
+// 学习计划折叠状态
+const planExpanded = ref(false)
+
+interface PathOverview {
+  path: string
+  title: string
+  description: string
+  icon: string
+  total_modules: number
+  total_lessons: number
+}
+
+interface Lesson {
+  id: string
+  title: string
+  description: string
+  type: string
+  duration_minutes: number
+  is_completed: boolean
+  content_markdown?: string
+}
+
+interface Module {
+  id: string
+  title: string
+  description: string
+  icon: string
+  order: number
+  lessons: Lesson[]
+  status: string
+  progress: number
+}
+
+interface PathDetail {
+  path: string
+  title: string
+  description: string
+  icon: string
+  modules: Module[]
+  total_lessons: number
+  completed_lessons: number
+  progress: number
+}
+
+interface CoachRecommendation {
+  type: string
+  title: string
+  description: string
+  module_id?: string
+  lesson_id?: string
+  priority: number
+}
+
+interface CoachData {
+  greeting: string
+  recommendations: CoachRecommendation[]
+  encouragement: string
+  stats: Record<string, any>
+  learning_plan?: string
+}
+
+const paths = ref<PathOverview[]>([])
+const selectedPath = ref<string | null>(null)
+const pathDetail = ref<PathDetail | null>(null)
+const coachData = ref<CoachData | null>(null)
+const assessmentResult = ref<any>(null)
+
+// 课程弹窗状态
+const selectedLesson = ref<Lesson | null>(null)
+const selectedModule = ref<Module | null>(null)
+
+onMounted(async () => {
+  await loadPaths()
+  
+  // 从后端恢复上次选中的路径
+  try {
+    const progressRes = await axios.get('/api/learning/progress', {
+      params: { user_id: authStore.userId }
+    })
+    if (progressRes.data.current_path) {
+      selectedPath.value = progressRes.data.current_path
+      await loadPathDetail(progressRes.data.current_path)
+      await loadAssessmentResult(progressRes.data.current_path)
+    }
+  } catch (e) {
+    console.error('Failed to restore selected path:', e)
+  }
+  
+  await loadCoachData()
+})
+
+// keep-alive缓存恢复时刷新数据
+onActivated(async () => {
+  await loadCoachData()
+  if (selectedPath.value) {
+    await loadPathDetail(selectedPath.value)
+    await loadAssessmentResult(selectedPath.value)
+  }
+})
+
+const loadPaths = async () => {
+  try {
+    const response = await axios.get('/api/learning/paths', {
+      params: { user_id: authStore.userId }
+    })
+    paths.value = response.data.paths
+  } catch (error) {
+    console.error('Failed to load paths:', error)
+  }
+}
+
+let pathRequestSeq = 0
+
+const selectPath = async (pathType: string) => {
+  selectedPath.value = pathType
+  const seq = ++pathRequestSeq
+  
+  // 持久化路径选择到后端
+  try {
+    await axios.post(`/api/learning/select-path/${pathType}`, null, {
+      params: { user_id: authStore.userId }
+    })
+  } catch (e) {
+    console.error('Failed to save path selection:', e)
+  }
+  
+  if (seq !== pathRequestSeq) return
+  await loadPathDetail(pathType)
+  if (seq !== pathRequestSeq) return
+  await loadAssessmentResult(pathType)
+  if (seq !== pathRequestSeq) return
+  await loadCoachData()
+}
+
+const loadPathDetail = async (pathType: string) => {
+  try {
+    const response = await axios.get(`/api/learning/paths/${pathType}`, {
+      params: { user_id: authStore.userId }
+    })
+    pathDetail.value = response.data
+  } catch (error) {
+    console.error('Failed to load path detail:', error)
+  }
+}
+
+const loadAssessmentResult = async (pathType: string) => {
+  try {
+    const response = await axios.get(`/api/assessment/check/${pathType}`, {
+      params: { user_id: authStore.userId }
+    })
+    if (response.data.has_assessment && response.data.current_result) {
+      assessmentResult.value = response.data.current_result
+    } else {
+      assessmentResult.value = null
+    }
+  } catch (error) {
+    console.error('Failed to load assessment result:', error)
+    assessmentResult.value = null
+  }
+}
+
+const startTest = () => {
+  console.log('startTest called, selectedPath:', selectedPath.value)
+  if (!selectedPath.value) {
+    console.error('No path selected')
+    return
+  }
+  router.push({
+    path: '/assessment',
+    query: { path: selectedPath.value }
+  })
+}
+
+const retakeTest = () => {
+  if (confirm('重新测试将覆盖当前测试结果,确定要继续吗?')) {
+    router.push({
+      path: '/assessment',
+      query: { path: selectedPath.value }
+    })
+  }
+}
+
+const loadCoachData = async () => {
+  try {
+    const response = await axios.get('/api/learning/coach', {
+      params: { user_id: authStore.userId }
+    })
+    coachData.value = response.data
+  } catch (error) {
+    console.error('Failed to load coach data:', error)
+  }
+}
+
+const handleRecClick = (rec: CoachRecommendation) => {
+  if (rec.type === 'select_path') {
+    // 滚动到路径选择区域
+    const el = document.querySelector('.paths-container')
+    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
+    return
+  }
+  
+  if ((rec.type === 'next_lesson' || rec.type === 'review') && rec.lesson_id && rec.module_id && pathDetail.value) {
+    // 查找对应模块
+    const mod = pathDetail.value.modules.find(m => m.id === rec.module_id)
+    if (!mod) return
+    
+    // 查找对应课程
+    const lesson = mod.lessons.find(l => l.id === rec.lesson_id)
+    if (!lesson) return
+    
+    // 检查是否锁定
+    if (mod.status === 'locked') return
+    
+    // 打开课程详情弹窗
+    selectedLesson.value = lesson
+    selectedModule.value = mod
+  }
+}
+
+const openLesson = (lesson: Lesson, module: Module) => {
+  if (module.status === 'locked') {
+    return // 锁定的模块不能打开
+  }
+  selectedLesson.value = lesson
+  selectedModule.value = module
+}
+
+const closeLesson = () => {
+  selectedLesson.value = null
+  selectedModule.value = null
+}
+
+const startLearning = async () => {
+  if (!selectedLesson.value || !selectedModule.value || !selectedPath.value) return
+  
+  // 检查是否已测试
+  try {
+    const checkResponse = await axios.get(`/api/assessment/check/${selectedPath.value}`, {
+      params: { user_id: authStore.userId }
+    })
+    const { has_assessment, current_result } = checkResponse.data
+    
+    if (!has_assessment) {
+      // 未测试过,跳转到测试页面
+      router.push({
+        path: '/assessment',
+        query: { path: selectedPath.value }
+      })
+      return
+    }
+    
+    // 已测试过,构建学习消息(包含水平信息)
+    const lesson = selectedLesson.value
+    const module = selectedModule.value
+    let message = `我想学习"${lesson.title}"这个课程。这是"${module.title}"模块中的内容,类型是${getLessonTypeText(lesson.type)}。请帮我讲解这个主题。`
+    
+    // 如果有测试结果,添加到消息中
+    if (current_result) {
+      message = `我已经完成了水平检测,得分${current_result.score}分,水平为${getLevelText(current_result.level)}。\n\n${message}`
+    }
+    
+    // 存储到sessionStorage,Chat页面会读取
+    const context = {
+      message: message,
+      path_type: selectedPath.value,
+      user_level: current_result?.level || null,
+      skill_levels: current_result?.category_scores || null,
+      lesson_id: lesson.id,
+      module_id: module.id,
+      lesson_title: lesson.title,
+      module_title: module.title
+    }
+    sessionStorage.setItem('pendingLearningContext', JSON.stringify(context))
+    
+    // 跳转到聊天页面
+    router.push('/')
+  } catch (error) {
+    console.error('Failed to check assessment:', error)
+    // 出错时直接跳转到测试
+    router.push({
+      path: '/assessment',
+      query: { path: selectedPath.value }
+    })
+  }
+}
+
+const completeLesson = async () => {
+  if (!selectedLesson.value || !selectedPath.value) return
+  
+  try {
+    const response = await axios.post(`/api/learning/complete-lesson/${selectedLesson.value.id}`, null, {
+      params: { user_id: authStore.userId }
+    })
+    selectedLesson.value.is_completed = true
+    
+    // 处理XP奖励
+    const xp = response.data.xp_awarded
+    if (xp > 0) {
+      gamificationRef.value?.showXpNotification(xp)
+    }
+    
+    // 处理新徽章
+    const badges = response.data.new_badges
+    if (badges && badges.length > 0) {
+      newBadges.value = badges
+    }
+    
+    // 重新加载路径详情以更新进度
+    await loadPathDetail(selectedPath.value)
+    await loadCoachData()
+  } catch (error) {
+    console.error('Failed to complete lesson:', error)
+  }
+}
+</script>
+
+<style scoped>
+.learning-view {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  background: #f8f9fa;
+  overflow-y: auto;
+}
+
+.view-header {
+  padding: 24px 32px;
+  background: white;
+  border-bottom: 1px solid #e5e5e5;
+}
+
+.view-header h1 {
+  margin: 0 0 4px 0;
+  font-size: 24px;
+  color: #1a1a1a;
+}
+
+.view-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.paths-container {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+  gap: 20px;
+  padding: 24px 32px;
+}
+
+.path-card {
+  position: relative;
+  background: white;
+  border-radius: 16px;
+  padding: 24px;
+  cursor: pointer;
+  transition: all 0.3s;
+  border: 2px solid transparent;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.path-card:hover {
+  transform: translateY(-4px);
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+}
+
+.path-card.selected {
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
+}
+
+.path-card.frontend { border-top: 4px solid #764ba2; }
+.path-card.backend { border-top: 4px solid #f5222d; }
+.path-card.fullstack { border-top: 4px solid #52c41a; }
+
+.path-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.path-title {
+  margin: 0 0 8px 0;
+  font-size: 20px;
+  color: #1a1a1a;
+}
+
+.path-desc {
+  margin: 0 0 16px 0;
+  color: #666;
+  font-size: 14px;
+  line-height: 1.5;
+}
+
+.path-stats {
+  display: flex;
+  gap: 16px;
+}
+
+.stat {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  font-size: 13px;
+  color: #999;
+}
+
+.stat-icon {
+  font-size: 14px;
+}
+
+.selected-badge {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  background: #667eea;
+  color: white;
+  padding: 4px 12px;
+  border-radius: 20px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.path-detail {
+  padding: 0 32px 24px;
+}
+
+.detail-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.detail-header h2 {
+  margin: 0;
+  font-size: 20px;
+  color: #1a1a1a;
+}
+
+.progress-summary {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.progress-bar {
+  width: 200px;
+  height: 8px;
+  background: #e8e8e8;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.progress-bar.small {
+  width: 100px;
+  height: 6px;
+}
+
+.progress-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
+  border-radius: 4px;
+  transition: width 0.3s;
+}
+
+.progress-text {
+  font-size: 13px;
+  color: #666;
+  font-weight: 500;
+}
+
+.modules-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.module-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.module-card.locked {
+  opacity: 0.7;
+  background: #fafafa;
+}
+
+.module-card.completed {
+  border-left: 4px solid #52c41a;
+}
+
+.module-card.in_progress {
+  border-left: 4px solid #667eea;
+}
+
+.module-header {
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+  margin-bottom: 12px;
+}
+
+.module-icon {
+  font-size: 32px;
+}
+
+.module-info {
+  flex: 1;
+}
+
+.module-title {
+  margin: 0 0 4px 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.module-desc {
+  margin: 0;
+  font-size: 13px;
+  color: #666;
+}
+
+.module-status {
+  flex-shrink: 0;
+}
+
+.status-badge {
+  display: inline-block;
+  padding: 4px 10px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.status-badge.not_started { background: #f5f5f5; color: #999; }
+.status-badge.in_progress { background: #e6f7ff; color: #1890ff; }
+.status-badge.completed { background: #f6ffed; color: #52c41a; }
+.status-badge.locked { background: #f5f5f5; color: #999; }
+
+.module-progress {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 12px;
+}
+
+.progress-label {
+  font-size: 12px;
+  color: #999;
+}
+
+.lessons-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.lesson-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 12px 16px;
+  background: #f8f9fa;
+  border-radius: 8px;
+  transition: all 0.2s;
+}
+
+.lesson-item.clickable {
+  cursor: pointer;
+}
+
+.lesson-item.clickable:hover {
+  background: #e8e8e8;
+  transform: translateX(4px);
+}
+
+.lesson-item.locked {
+  cursor: not-allowed;
+  opacity: 0.6;
+}
+
+.lesson-item.completed {
+  background: #f6ffed;
+}
+
+.lesson-type-icon {
+  font-size: 18px;
+}
+
+.lesson-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.lesson-title {
+  font-size: 14px;
+  color: #1a1a1a;
+  font-weight: 500;
+}
+
+.lesson-meta {
+  display: flex;
+  gap: 12px;
+  margin-top: 2px;
+}
+
+.lesson-duration, .lesson-type {
+  font-size: 12px;
+  color: #999;
+}
+
+.completed-icon {
+  color: #52c41a;
+  font-weight: 600;
+  font-size: 16px;
+}
+
+.lock-icon {
+  color: #999;
+  font-size: 14px;
+}
+
+.arrow-icon {
+  color: #667eea;
+  font-size: 16px;
+  opacity: 0;
+  transition: opacity 0.2s;
+}
+
+.lesson-item.clickable:hover .arrow-icon {
+  opacity: 1;
+}
+
+/* 弹窗样式 */
+.lesson-modal-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  animation: fadeIn 0.2s;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+.lesson-modal {
+  background: white;
+  border-radius: 16px;
+  width: 95%;
+  max-width: 820px;
+  max-height: 85vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  animation: slideUp 0.3s;
+}
+
+@keyframes slideUp {
+  from { transform: translateY(20px); opacity: 0; }
+  to { transform: translateY(0); opacity: 1; }
+}
+
+.modal-header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 20px 24px;
+  border-bottom: 1px solid #e8e8e8;
+}
+
+.modal-icon {
+  font-size: 36px;
+}
+
+.modal-title-area {
+  flex: 1;
+}
+
+.modal-title-area h3 {
+  margin: 0 0 4px 0;
+  font-size: 18px;
+  color: #1a1a1a;
+}
+
+.modal-module {
+  font-size: 13px;
+  color: #999;
+}
+
+.close-btn {
+  width: 32px;
+  height: 32px;
+  border: none;
+  background: #f5f5f5;
+  border-radius: 50%;
+  font-size: 20px;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: background 0.2s;
+}
+
+.close-btn:hover {
+  background: #e8e8e8;
+}
+
+.modal-body {
+  padding: 24px;
+  overflow-y: auto;
+  flex: 1;
+}
+
+.lesson-description {
+  font-size: 15px;
+  color: #333;
+  line-height: 1.6;
+  margin: 0 0 20px 0;
+}
+
+.lesson-details {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.detail-item {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.detail-label {
+  font-size: 12px;
+  color: #999;
+}
+
+.detail-value {
+  font-size: 14px;
+  color: #333;
+  font-weight: 500;
+}
+
+.detail-value.completed {
+  color: #52c41a;
+}
+
+.lesson-content h4 {
+  margin: 0 0 12px 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.lesson-content p {
+  margin: 0 0 16px 0;
+  color: #666;
+  line-height: 1.6;
+}
+
+.lesson-content :deep(.markdown-renderer) {
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+.lesson-content :deep(.markdown-body) h1,
+.lesson-content :deep(.markdown-body) h2,
+.lesson-content :deep(.markdown-body) h3 {
+  margin-top: 24px;
+  margin-bottom: 12px;
+}
+
+.lesson-content :deep(.markdown-body) h2 {
+  font-size: 20px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #eee;
+}
+
+.lesson-content :deep(.markdown-body) h3 {
+  font-size: 16px;
+}
+
+.lesson-content :deep(.markdown-body) p {
+  margin: 0 0 12px 0;
+  color: #333;
+  line-height: 1.8;
+}
+
+.lesson-content :deep(.markdown-body) code {
+  background: #f0f0f0;
+  padding: 2px 6px;
+  border-radius: 4px;
+  font-size: 13px;
+  font-family: 'Consolas', 'Monaco', monospace;
+}
+
+.lesson-content :deep(.markdown-body) pre {
+  margin: 12px 0 16px;
+  border-radius: 8px;
+  overflow-x: auto;
+}
+
+.lesson-content :deep(.markdown-body) table {
+  border-collapse: collapse;
+  width: 100%;
+  margin: 12px 0 16px;
+  font-size: 13px;
+}
+
+.lesson-content :deep(.markdown-body) th,
+.lesson-content :deep(.markdown-body) td {
+  border: 1px solid #e0e0e0;
+  padding: 8px 12px;
+  text-align: left;
+}
+
+.lesson-content :deep(.markdown-body) th {
+  background: #f8f9fa;
+  font-weight: 600;
+}
+
+.lesson-content :deep(.markdown-body) blockquote {
+  margin: 12px 0;
+  padding: 10px 16px;
+  background: #f0f5ff;
+  border-left: 4px solid #667eea;
+  border-radius: 0 8px 8px 0;
+  color: #555;
+}
+
+.lesson-content :deep(.markdown-body) blockquote p {
+  margin: 0;
+  color: #555;
+}
+
+.lesson-content :deep(.markdown-body) ul,
+.lesson-content :deep(.markdown-body) ol {
+  padding-left: 24px;
+  margin: 8px 0 12px;
+}
+
+.lesson-content :deep(.markdown-body) li {
+  margin: 4px 0;
+}
+
+/* 无内容时的提示 */
+.no-content-prompt {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  text-align: center;
+}
+
+.no-content-icon {
+  font-size: 48px;
+  margin-bottom: 12px;
+}
+
+.no-content-prompt h4 {
+  margin: 0 0 8px 0;
+  font-size: 16px;
+  color: #333;
+}
+
+.no-content-prompt p {
+  margin: 0;
+  color: #999;
+  font-size: 14px;
+}
+
+.modal-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 16px 24px;
+  border-top: 1px solid #e8e8e8;
+}
+
+.btn {
+  padding: 10px 20px;
+  border-radius: 8px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s;
+  border: none;
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.btn-primary:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-success {
+  background: #f6ffed;
+  color: #52c41a;
+  border: 1px solid #b7eb8f;
+}
+
+.btn-success:hover {
+  background: #f6ffed;
+  border-color: #52c41a;
+}
+
+.btn-secondary {
+  background: #f6ffed;
+  color: #52c41a;
+  cursor: not-allowed;
+}
+
+.btn-outline {
+  background: white;
+  border: 1px solid #d9d9d9;
+  color: #666;
+}
+
+.btn-outline:hover {
+  border-color: #667eea;
+  color: #667eea;
+}
+
+/* 教练面板 */
+.coach-panel {
+  margin: 0 32px 24px;
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.coach-header {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 12px;
+}
+
+.coach-icon {
+  font-size: 24px;
+}
+
+.coach-header h3 {
+  margin: 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.coach-greeting {
+  margin: 0 0 16px 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.coach-recommendations {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin-bottom: 16px;
+}
+
+.recommendation-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 16px;
+  background: #f8f9fa;
+  border-radius: 8px;
+}
+
+.rec-icon {
+  font-size: 20px;
+}
+
+.rec-content {
+  display: flex;
+  flex-direction: column;
+}
+
+.rec-title {
+  font-size: 14px;
+  font-weight: 500;
+  color: #1a1a1a;
+}
+
+.rec-desc {
+  font-size: 12px;
+  color: #999;
+}
+
+.recommendation-item.clickable {
+  cursor: pointer;
+  transition: background 0.15s, transform 0.15s, box-shadow 0.15s;
+}
+
+.recommendation-item.clickable:hover {
+  background: #eef0f5;
+  transform: translateX(3px);
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.recommendation-item.clickable:active {
+  transform: translateX(1px);
+}
+
+.rec-arrow {
+  font-size: 14px;
+  color: #667eea;
+  font-weight: 600;
+  opacity: 0;
+  transition: opacity 0.15s, transform 0.15s;
+  flex-shrink: 0;
+}
+
+.recommendation-item.clickable:hover .rec-arrow {
+  opacity: 1;
+  transform: translateX(2px);
+}
+
+@media (prefers-color-scheme: dark) {
+  .recommendation-item.clickable:hover {
+    background: #2a2a2a;
+  }
+}
+
+.coach-encouragement {
+  margin: 0;
+  padding: 12px 16px;
+  background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
+  border-radius: 8px;
+  font-size: 14px;
+  color: #667eea;
+  font-weight: 500;
+  text-align: center;
+}
+
+/* 学习计划卡片 */
+.learning-plan-section {
+  margin-top: 12px;
+  border: 1px solid #e8e8e8;
+  border-radius: 10px;
+  overflow: hidden;
+  background: white;
+}
+
+.plan-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 10px 14px;
+  cursor: pointer;
+  background: #fafafa;
+  transition: background 0.15s;
+  user-select: none;
+}
+
+.plan-header:hover {
+  background: #f0f0f0;
+}
+
+.plan-header-icon {
+  font-size: 16px;
+}
+
+.plan-header-title {
+  flex: 1;
+  font-weight: 600;
+  font-size: 14px;
+  color: #333;
+}
+
+.plan-toggle {
+  font-size: 12px;
+  color: #667eea;
+  font-weight: 500;
+}
+
+.plan-content {
+  border-top: 1px solid #e8e8e8;
+  max-height: 60vh;
+  overflow-y: auto;
+}
+
+.plan-markdown {
+  padding: 14px 16px;
+  font-size: 13px;
+  line-height: 1.7;
+  color: #333;
+  white-space: pre-wrap;
+  word-break: break-word;
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+}
+
+@media (prefers-color-scheme: dark) {
+  .learning-plan-section {
+    border-color: #333;
+    background: #1e1e1e;
+  }
+  .plan-header {
+    background: #252525;
+  }
+  .plan-header:hover {
+    background: #2a2a2a;
+  }
+  .plan-header-title {
+    color: #ccc;
+  }
+  .plan-markdown {
+    color: #ccc;
+  }
+  .plan-content {
+    border-color: #333;
+  }
+}
+
+/* 水平检测卡片 */
+.assessment-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.assessment-card .assessment-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.assessment-card .assessment-info {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.assessment-card .assessment-icon {
+  font-size: 32px;
+}
+
+.assessment-card .assessment-text h3 {
+  margin: 0 0 4px 0;
+  font-size: 16px;
+  color: #1a1a1a;
+}
+
+.assessment-card .assessment-level {
+  margin: 0;
+  font-size: 14px;
+  color: #666;
+}
+
+.assessment-card .assessment-score {
+  margin-left: 12px;
+  color: #999;
+}
+
+.level-badge {
+  padding: 2px 8px;
+  border-radius: 10px;
+  font-weight: 500;
+  font-size: 13px;
+}
+
+.level-badge.beginner { background: #f6ffed; color: #52c41a; }
+.level-badge.intermediate { background: #e6f7ff; color: #1890ff; }
+.level-badge.advanced { background: #fff2f0; color: #ff4d4f; }
+
+.assessment-card .assessment-skills {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 12px;
+}
+
+.assessment-card .skill-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.assessment-card .skill-name {
+  font-size: 13px;
+  color: #666;
+  width: 80px;
+  flex-shrink: 0;
+}
+
+.assessment-card .skill-bar {
+  flex: 1;
+  height: 6px;
+  background: #e8e8e8;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.assessment-card .skill-fill {
+  height: 100%;
+  border-radius: 3px;
+}
+
+.assessment-card .skill-fill.excellent { background: #52c41a; }
+.assessment-card .skill-fill.good { background: #1890ff; }
+.assessment-card .skill-fill.fair { background: #fa8c16; }
+.assessment-card .skill-fill.poor { background: #ff4d4f; }
+
+.assessment-card .skill-score {
+  font-size: 13px;
+  color: #999;
+  width: 40px;
+  text-align: right;
+}
+
+/* 测试提示卡片 */
+.assessment-prompt {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 12px;
+  padding: 24px;
+  margin-bottom: 20px;
+}
+
+.assessment-prompt .prompt-content {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.assessment-prompt .prompt-icon {
+  font-size: 40px;
+}
+
+.assessment-prompt .prompt-text {
+  flex: 1;
+}
+
+.assessment-prompt .prompt-text h3 {
+  margin: 0 0 4px 0;
+  font-size: 16px;
+  color: white;
+}
+
+.assessment-prompt .prompt-text p {
+  margin: 0;
+  font-size: 13px;
+  color: rgba(255, 255, 255, 0.85);
+}
+
+.assessment-prompt .btn-primary {
+  background: white;
+  color: #667eea;
+}
+
+.assessment-prompt .btn-primary:hover {
+  background: #f5f5f5;
+}
+
+.btn-sm {
+  padding: 6px 12px;
+  font-size: 13px;
+}
+
+/* 游戏化面板包裹 */
+.gamification-wrapper {
+  padding: 0 32px;
+  margin-top: 16px;
+}
+
+/* 徽章通知弹窗 */
+.badge-modal-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  animation: fadeIn 0.2s ease;
+}
+
+.badge-modal {
+  background: var(--color-bg-secondary, #fff);
+  border-radius: 16px;
+  padding: 32px;
+  max-width: 400px;
+  width: 90%;
+  text-align: center;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
+  animation: slideUp 0.3s ease;
+}
+
+.badge-modal-header {
+  margin-bottom: 20px;
+}
+
+.badge-modal-icon {
+  font-size: 48px;
+  display: block;
+  margin-bottom: 8px;
+}
+
+.badge-modal-header h2 {
+  margin: 0;
+  font-size: 22px;
+  color: var(--color-text, #1a1a1a);
+}
+
+.badge-modal-body {
+  margin-bottom: 24px;
+}
+
+.new-badge-item {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 12px;
+  background: var(--color-bg-tertiary, #f5f5f5);
+  border-radius: 12px;
+  margin-bottom: 8px;
+}
+
+.new-badge-icon {
+  font-size: 36px;
+}
+
+.new-badge-info {
+  text-align: left;
+}
+
+.new-badge-name {
+  display: block;
+  font-weight: 700;
+  font-size: 16px;
+  color: var(--color-text, #1a1a1a);
+}
+
+.new-badge-desc {
+  display: block;
+  font-size: 13px;
+  color: var(--color-text-secondary, #666);
+  margin-top: 2px;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes slideUp {
+  from { transform: translateY(20px); opacity: 0; }
+  to { transform: translateY(0); opacity: 1; }
+}
+
+@media (prefers-color-scheme: dark) {
+  .lesson-modal {
+    background: #1e1e1e;
+  }
+  .modal-header {
+    border-color: #333;
+  }
+  .modal-title-area h3 {
+    color: #e0e0e0;
+  }
+  .lesson-description {
+    color: #ccc;
+  }
+  .detail-value {
+    color: #ddd;
+  }
+  .lesson-content :deep(.markdown-body) p,
+  .lesson-content :deep(.markdown-body) li {
+    color: #ccc;
+  }
+  .lesson-content :deep(.markdown-body) code {
+    background: #2a2a2a;
+    color: #e0e0e0;
+  }
+  .lesson-content :deep(.markdown-body) th {
+    background: #2a2a2a;
+  }
+  .lesson-content :deep(.markdown-body) th,
+  .lesson-content :deep(.markdown-body) td {
+    border-color: #444;
+    color: #ccc;
+  }
+  .lesson-content :deep(.markdown-body) h2 {
+    border-bottom-color: #333;
+  }
+  .lesson-content :deep(.markdown-body) blockquote {
+    background: #1a1a2e;
+  }
+  .no-content-prompt h4 {
+    color: #ddd;
+  }
+  .modal-footer {
+    border-color: #333;
+  }
+  .close-btn {
+    background: #333;
+    color: #ccc;
+  }
+  .btn-outline {
+    background: #2a2a2a;
+    border-color: #444;
+    color: #aaa;
+  }
+  .detail-label {
+    color: #888;
+  }
+  .badge-modal {
+    background: #1e1e1e;
+  }
+  .new-badge-item {
+    background: #2a2a2a;
+  }
+}
+</style>

+ 346 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Login.vue

@@ -0,0 +1,346 @@
+<template>
+  <div class="login-container">
+    <div class="login-card">
+      <div class="login-header">
+        <div class="logo-icon">🚀</div>
+        <h1>Way_to_Engineer</h1>
+        <p>{{ langStore.t('login.subtitle') }}</p>
+      </div>
+      <div class="login-form">
+        <label class="input-label">{{ langStore.t('login.label') }}</label>
+        <input
+          v-model="username"
+          type="text"
+          class="username-input"
+          :class="{ 'input-error': errorMsg }"
+          :placeholder="langStore.t('login.placeholder')"
+          @keydown.enter="handleLogin"
+          @input="errorMsg = ''"
+          autofocus
+        />
+        <p v-if="errorMsg" class="error-text">{{ errorMsg }}</p>
+        <button
+          class="login-btn"
+          @click="handleLogin"
+          :disabled="!username.trim() || checking"
+        >
+          <span v-if="!checking">{{ langStore.t('login.btn') }}</span>
+          <span v-else class="spinner"></span>
+        </button>
+      </div>
+      <div class="login-footer">
+        <p>{{ langStore.t('login.footer') }}</p>
+      </div>
+    </div>
+
+    <!-- 重名确认对话框 -->
+    <Teleport to="body">
+      <Transition name="fade">
+        <div v-if="showConfirm" class="confirm-overlay" @click.self="cancelConfirm">
+          <div class="confirm-dialog">
+            <div class="confirm-icon">⚠️</div>
+            <h3>用户名已被使用</h3>
+            <p>用户 <strong>{{ confirmUsername }}</strong> 已有学习记录。</p>
+            <p class="confirm-desc">继续使用将加载该用户的学习进度,确定吗?</p>
+            <div class="confirm-actions">
+              <button class="btn btn-cancel" @click="cancelConfirm">换一个用户名</button>
+              <button class="btn btn-confirm" @click="confirmLogin">继续使用</button>
+            </div>
+          </div>
+        </div>
+      </Transition>
+    </Teleport>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import axios from 'axios'
+import { useAuthStore } from '../stores/authStore'
+import { useLangStore } from '../stores/langStore'
+
+const router = useRouter()
+const authStore = useAuthStore()
+const langStore = useLangStore()
+const username = ref('')
+const errorMsg = ref('')
+const checking = ref(false)
+const showConfirm = ref(false)
+const confirmUsername = ref('')
+
+const handleLogin = async () => {
+  const trimmed = username.value.trim()
+  if (!trimmed) return
+
+  checking.value = true
+  errorMsg.value = ''
+
+  try {
+    // 检查用户名是否已存在
+    const checkRes = await axios.post('/api/auth/check', { username: trimmed })
+    const { exists, has_data } = checkRes.data
+
+    if (exists && has_data) {
+      // 用户名已存在且有学习数据 → 需要用户确认
+      confirmUsername.value = trimmed
+      showConfirm.value = true
+      checking.value = false
+      return
+    }
+
+    // 新用户 或 存在但无数据 → 直接登录
+    await doLogin(trimmed)
+  } catch (error: any) {
+    errorMsg.value = error.response?.data?.detail || '登录失败,请重试'
+    checking.value = false
+  }
+}
+
+const doLogin = async (name: string) => {
+  try {
+    const res = await axios.post('/api/auth/login', { username: name })
+    authStore.login(name)
+    router.push('/')
+  } catch (error: any) {
+    errorMsg.value = error.response?.data?.detail || '登录失败,请重试'
+  } finally {
+    checking.value = false
+  }
+}
+
+const confirmLogin = () => {
+  showConfirm.value = false
+  doLogin(confirmUsername.value)
+}
+
+const cancelConfirm = () => {
+  showConfirm.value = false
+  confirmUsername.value = ''
+  checking.value = false
+}
+</script>
+
+<style scoped>
+.login-container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.login-card {
+  background: white;
+  border-radius: 20px;
+  padding: 48px 40px;
+  width: 400px;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
+}
+
+.login-header {
+  text-align: center;
+  margin-bottom: 36px;
+}
+
+.logo-icon {
+  font-size: 56px;
+  margin-bottom: 16px;
+}
+
+.login-header h1 {
+  margin: 0 0 8px 0;
+  font-size: 28px;
+  font-weight: 700;
+  color: #1a1a1a;
+}
+
+.login-header p {
+  margin: 0;
+  color: #666;
+  font-size: 14px;
+}
+
+.login-form {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.input-label {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+}
+
+.username-input {
+  padding: 14px 16px;
+  border: 2px solid #e8e8e8;
+  border-radius: 12px;
+  font-size: 16px;
+  transition: all 0.2s;
+  outline: none;
+}
+
+.username-input:focus {
+  border-color: #667eea;
+  box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
+}
+
+.input-error {
+  border-color: #ff4d4f;
+}
+
+.input-error:focus {
+  border-color: #ff4d4f;
+  box-shadow: 0 0 0 4px rgba(255, 77, 79, 0.1);
+}
+
+.error-text {
+  margin: 0;
+  font-size: 13px;
+  color: #ff4d4f;
+}
+
+.login-btn {
+  padding: 14px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 12px;
+  font-size: 16px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.2s;
+  margin-top: 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 48px;
+}
+
+.login-btn:hover:not(:disabled) {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
+}
+
+.login-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.spinner {
+  width: 20px;
+  height: 20px;
+  border: 2px solid rgba(255, 255, 255, 0.3);
+  border-top-color: white;
+  border-radius: 50%;
+  animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.login-footer {
+  margin-top: 24px;
+  text-align: center;
+}
+
+.login-footer p {
+  margin: 0;
+  font-size: 12px;
+  color: #999;
+}
+
+/* 确认对话框 */
+.confirm-overlay {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.4);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  backdrop-filter: blur(2px);
+}
+
+.confirm-dialog {
+  background: white;
+  border-radius: 20px;
+  padding: 36px 32px;
+  width: 380px;
+  text-align: center;
+  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
+}
+
+.confirm-icon {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+.confirm-dialog h3 {
+  margin: 0 0 12px 0;
+  font-size: 20px;
+  color: #1a1a1a;
+}
+
+.confirm-dialog p {
+  margin: 0 0 6px 0;
+  font-size: 14px;
+  color: #666;
+  line-height: 1.5;
+}
+
+.confirm-desc {
+  margin-bottom: 24px !important;
+  color: #999 !important;
+  font-size: 13px !important;
+}
+
+.confirm-actions {
+  display: flex;
+  gap: 12px;
+}
+
+.btn {
+  flex: 1;
+  padding: 12px 16px;
+  border-radius: 12px;
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  border: none;
+  transition: all 0.2s;
+}
+
+.btn-cancel {
+  background: #f0f2f5;
+  color: #666;
+}
+
+.btn-cancel:hover {
+  background: #e8e8e8;
+}
+
+.btn-confirm {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+}
+
+.btn-confirm:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+/* 过渡动画 */
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.25s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+}
+</style>

+ 592 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/views/Orchestration.vue

@@ -0,0 +1,592 @@
+<template>
+  <div class="orchestration-view">
+    <div class="view-header">
+      <div class="header-left">
+        <h1>{{ langStore.t('orchestration.title') }}</h1>
+        <p>{{ langStore.t('orchestration.subtitle') }}</p>
+      </div>
+      <div class="header-stats">
+        <div class="stat-chip">
+          <span class="stat-dot active"></span>
+          <span>{{ agentStore.activeCount }} {{ langStore.t('orchestration.active') }}</span>
+        </div>
+        <div class="stat-chip">
+          <span class="stat-dot total"></span>
+          <span>{{ agentStore.totalMessages }} {{ langStore.t('orchestration.messages') }}</span>
+        </div>
+      </div>
+    </div>
+    
+    <div class="flow-container">
+      <VueFlow
+        v-model="nodes"
+        :edges="edges"
+        :default-viewport="{ zoom: 1, x: 0, y: 0 }"
+        :min-zoom="0.5"
+        :max-zoom="2"
+        :nodes-draggable="true"
+        :nodes-connectable="false"
+        :elements-selectable="true"
+        fit-view-on-init
+        @node-click="handleNodeClick"
+        @node-drag-stop="handleNodeDragStop"
+      >
+        <template #node-orchestrator="nodeProps">
+          <AgentNode 
+            v-bind="nodeProps" 
+            type="orchestrator" 
+            :label="langStore.t('orchestration.agents.orchestrator')"
+            :status="getAgentStatus('orchestrator')"
+            :message-count="getAgentMessages('orchestrator')"
+            :last-used="getAgentLastUsed('orchestrator')"
+          />
+        </template>
+        
+        <template #node-tutor="nodeProps">
+          <AgentNode 
+            v-bind="nodeProps" 
+            type="tutor" 
+            :label="langStore.t('orchestration.agents.tutor')"
+            :status="getAgentStatus('tutor')"
+            :message-count="getAgentMessages('tutor')"
+            :last-used="getAgentLastUsed('tutor')"
+          />
+        </template>
+        
+        <template #node-debug="nodeProps">
+          <AgentNode 
+            v-bind="nodeProps" 
+            type="debug" 
+            :label="langStore.t('orchestration.agents.debug')"
+            :status="getAgentStatus('debug')"
+            :message-count="getAgentMessages('debug')"
+            :last-used="getAgentLastUsed('debug')"
+          />
+        </template>
+        
+        <template #node-review="nodeProps">
+          <AgentNode 
+            v-bind="nodeProps" 
+            type="review" 
+            :label="langStore.t('orchestration.agents.review')"
+            :status="getAgentStatus('review')"
+            :message-count="getAgentMessages('review')"
+            :last-used="getAgentLastUsed('review')"
+          />
+        </template>
+        
+        <template #node-arch="nodeProps">
+          <AgentNode 
+            v-bind="nodeProps" 
+            type="arch" 
+            :label="langStore.t('orchestration.agents.arch')"
+            :status="getAgentStatus('arch')"
+            :message-count="getAgentMessages('arch')"
+            :last-used="getAgentLastUsed('arch')"
+          />
+        </template>
+        
+        <template #node-coach="nodeProps">
+          <AgentNode 
+            v-bind="nodeProps" 
+            type="coach" 
+            :label="langStore.t('orchestration.agents.coach')"
+            :status="getAgentStatus('coach')"
+            :message-count="getAgentMessages('coach')"
+            :last-used="getAgentLastUsed('coach')"
+          />
+        </template>
+        
+        <Background />
+        <Controls />
+      </VueFlow>
+    </div>
+    
+    <div class="info-panel">
+      <div class="agent-list">
+        <div 
+          v-for="agent in agentStore.agents" 
+          :key="agent.type"
+          :class="['agent-item', agent.type, { active: agent.status === 'active' || agent.status === 'processing' }]"
+          @click="openConfigPanel(agent.type)"
+        >
+          <div class="agent-icon-wrap">
+            <span class="agent-icon">{{ agent.icon }}</span>
+            <span v-if="agent.status === 'active' || agent.status === 'processing'" class="active-ring"></span>
+          </div>
+          <div class="agent-info">
+            <div class="agent-header">
+              <span class="agent-name">{{ agent.name }}</span>
+              <span class="agent-status" :class="agent.status">{{ getAgentStatusText(agent.status) }}</span>
+            </div>
+            <span class="agent-desc">{{ agent.description }}</span>
+            <div class="agent-meta">
+              <span class="meta-item">
+                <span class="meta-icon">💬</span>
+                <span>{{ langStore.t('orchestration.messagesCount', { count: agent.messageCount }) }}</span>
+              </span>
+              <span class="meta-item">
+                <span class="meta-icon">🕐</span>
+                <span>{{ agentStore.formatLastUsed(agent.lastUsedAt) }}</span>
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Agent Configuration Panel -->
+    <AgentConfigPanel
+      :visible="configPanelVisible"
+      :agent="configPanelAgent"
+      @close="closeConfigPanel"
+      @start-chat="handleStartChat"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { VueFlow, type GraphNode } from '@vue-flow/core'
+import { Background } from '@vue-flow/background'
+import { Controls } from '@vue-flow/controls'
+import AgentNode from '../components/AgentNode.vue'
+import AgentConfigPanel from '../components/AgentConfigPanel.vue'
+import { useAgentStore, type AgentState } from '../stores/agentStore'
+import { useLangStore } from '../stores/langStore'
+import { getAgentStatusText } from '../utils/helpers'
+
+const agentStore = useAgentStore()
+const langStore = useLangStore()
+
+// ─── Agent data helpers ───
+
+const getAgentStatus = (type: string) => {
+  const agent = agentStore.getAgent(type)
+  return agent?.status || 'idle'
+}
+
+const getAgentMessages = (type: string) => {
+  const agent = agentStore.getAgent(type)
+  return agent?.messageCount || 0
+}
+
+const getAgentLastUsed = (type: string) => {
+  const agent = agentStore.getAgent(type)
+  return agent?.lastUsedAt || null
+}
+
+// ─── Agent color map (for edge styling) ───
+
+const AGENT_COLORS: Record<string, string> = {
+  orchestrator: '#667eea',
+  tutor: '#764ba2',
+  debug: '#f5222d',
+  review: '#52c41a',
+  arch: '#fa8c16',
+  coach: '#722ed1',
+}
+
+// ─── Nodes (with drag persistence) ───
+
+const DEFAULT_POSITIONS: Record<string, { x: number; y: number }> = {
+  orchestrator: { x: 350, y: 50 },
+  tutor: { x: 100, y: 250 },
+  debug: { x: 280, y: 250 },
+  review: { x: 460, y: 250 },
+  arch: { x: 640, y: 250 },
+  coach: { x: 820, y: 250 },
+}
+
+const loadSavedPositions = (): Record<string, { x: number; y: number }> => {
+  try {
+    const saved = localStorage.getItem('orchestration-node-positions')
+    if (saved) return JSON.parse(saved)
+  } catch { /* ignore */ }
+  return {}
+}
+
+const savedPositions = loadSavedPositions()
+
+const nodes = ref([
+  {
+    id: 'orchestrator',
+    type: 'orchestrator',
+    position: savedPositions['orchestrator'] || DEFAULT_POSITIONS['orchestrator'],
+    data: { label: langStore.t('orchestration.agents.orchestrator') }
+  },
+  {
+    id: 'tutor',
+    type: 'tutor',
+    position: savedPositions['tutor'] || DEFAULT_POSITIONS['tutor'],
+    data: { label: langStore.t('orchestration.agents.tutor') }
+  },
+  {
+    id: 'debug',
+    type: 'debug',
+    position: savedPositions['debug'] || DEFAULT_POSITIONS['debug'],
+    data: { label: langStore.t('orchestration.agents.debug') }
+  },
+  {
+    id: 'review',
+    type: 'review',
+    position: savedPositions['review'] || DEFAULT_POSITIONS['review'],
+    data: { label: langStore.t('orchestration.agents.review') }
+  },
+  {
+    id: 'arch',
+    type: 'arch',
+    position: savedPositions['arch'] || DEFAULT_POSITIONS['arch'],
+    data: { label: langStore.t('orchestration.agents.arch') }
+  },
+  {
+    id: 'coach',
+    type: 'coach',
+    position: savedPositions['coach'] || DEFAULT_POSITIONS['coach'],
+    data: { label: langStore.t('orchestration.agents.coach') }
+  }
+])
+
+// ─── Edges (reactive to active agent) ───
+
+const activeAgentType = computed(() => {
+  const active = agentStore.agents.find(
+    a => a.status === 'active' || a.status === 'processing'
+  )
+  return active?.type || null
+})
+
+const edgeDefinitions = [
+  { id: 'e1', source: 'orchestrator', target: 'tutor' },
+  { id: 'e2', source: 'orchestrator', target: 'debug' },
+  { id: 'e3', source: 'orchestrator', target: 'review' },
+  { id: 'e4', source: 'orchestrator', target: 'arch' },
+  { id: 'e5', source: 'orchestrator', target: 'coach' },
+]
+
+const edges = computed(() => {
+  return edgeDefinitions.map(def => {
+    const isActive = activeAgentType.value === def.target
+    const color = AGENT_COLORS[def.target] || '#b1b1b7'
+
+    if (isActive) {
+      return {
+        ...def,
+        type: 'smoothstep' as const,
+        animated: true,
+        style: {
+          stroke: color,
+          strokeWidth: 3,
+          filter: `drop-shadow(0 0 6px ${color}66)`,
+        },
+        class: 'edge-active',
+      }
+    }
+
+    return {
+      ...def,
+      type: 'smoothstep' as const,
+      animated: false,
+      style: {
+        stroke: '#d1d5db',
+        strokeWidth: 1.5,
+      },
+      class: '',
+    }
+  })
+})
+
+// ─── Drag persistence ───
+
+const handleNodeDragStop = () => {
+  const positions: Record<string, { x: number; y: number }> = {}
+  nodes.value.forEach(node => {
+    positions[node.id] = { ...node.position }
+  })
+  try {
+    localStorage.setItem('orchestration-node-positions', JSON.stringify(positions))
+  } catch { /* quota exceeded - ignore */ }
+}
+
+// ─── Config panel ───
+
+const configPanelVisible = ref(false)
+const configPanelAgentType = ref<string | null>(null)
+
+const configPanelAgent = computed<AgentState | null>(() => {
+  if (!configPanelAgentType.value) return null
+  return agentStore.getAgent(configPanelAgentType.value) || null
+})
+
+const handleNodeClick = ({ node }: { node: GraphNode; event: MouseEvent }) => {
+  configPanelAgentType.value = node.id
+  configPanelVisible.value = true
+}
+
+const openConfigPanel = (type: string) => {
+  configPanelAgentType.value = type
+  configPanelVisible.value = true
+}
+
+const closeConfigPanel = () => {
+  configPanelVisible.value = false
+  configPanelAgentType.value = null
+}
+
+const handleStartChat = (agentType: string) => {
+  // Store the intended agent for Chat.vue to optionally pick up
+  try {
+    sessionStorage.setItem('pendingAgentType', agentType)
+  } catch { /* ignore */ }
+  closeConfigPanel()
+}
+
+// ─── Reset positions on first load if none saved ───
+
+onMounted(() => {
+  if (!savedPositions || Object.keys(savedPositions).length === 0) {
+    // First visit — positions are at defaults, nothing to restore
+  }
+})
+</script>
+
+<style scoped>
+.orchestration-view {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  background: var(--bg-primary, #f8f9fa);
+  overflow: hidden;
+}
+
+.view-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 20px 32px;
+  background: var(--bg-secondary, white);
+  border-bottom: 1px solid var(--border-color, #e5e5e5);
+}
+
+.header-left h1 {
+  margin: 0 0 4px 0;
+  font-size: 22px;
+  color: var(--text-primary, #1a1a1a);
+}
+
+.header-left p {
+  margin: 0;
+  color: var(--text-secondary, #666);
+  font-size: 13px;
+}
+
+.header-stats {
+  display: flex;
+  gap: 12px;
+}
+
+.stat-chip {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 12px;
+  background: var(--bg-tertiary, #f5f7fa);
+  border-radius: 20px;
+  font-size: 13px;
+  color: var(--text-secondary, #666);
+}
+
+.stat-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+}
+
+.stat-dot.active {
+  background: var(--success-color, #52c41a);
+  animation: pulse 2s infinite;
+}
+
+.stat-dot.total {
+  background: var(--accent-color, #667eea);
+}
+
+@keyframes pulse {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0.5; }
+}
+
+.flow-container {
+  flex: 1;
+  background: var(--bg-secondary, white);
+  margin: 16px 32px;
+  border-radius: 12px;
+  box-shadow: 0 2px 8px var(--shadow-color, rgba(0, 0, 0, 0.06));
+  overflow: hidden;
+  position: relative;
+}
+
+/* ─── Active edge glow (global style for VueFlow edges) ─── */
+.flow-container :deep(.edge-active .vue-flow__edge-path) {
+  filter: drop-shadow(0 0 8px currentColor);
+}
+
+.flow-container :deep(.vue-flow__edge.animated .vue-flow__edge-path) {
+  stroke-dasharray: 8 4;
+  animation: edge-flow 0.8s linear infinite;
+}
+
+@keyframes edge-flow {
+  to { stroke-dashoffset: -12; }
+}
+
+.info-panel {
+  padding: 0 32px 20px;
+}
+
+.agent-list {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+  gap: 12px;
+}
+
+.agent-item {
+  display: flex;
+  gap: 12px;
+  padding: 14px 16px;
+  background: var(--bg-secondary, white);
+  border-radius: 12px;
+  box-shadow: 0 2px 8px var(--shadow-color, rgba(0, 0, 0, 0.06));
+  border: 1px solid var(--border-color, #e8e8e8);
+  transition: all 0.2s;
+  cursor: pointer;
+}
+
+.agent-item:hover {
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  transform: translateY(-1px);
+}
+
+.agent-item.active {
+  border-color: var(--accent-color, #667eea);
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.agent-item.orchestrator.active { border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); }
+.agent-item.tutor.active { border-color: #764ba2; box-shadow: 0 0 0 3px rgba(118, 75, 162, 0.1); }
+.agent-item.debug.active { border-color: #f5222d; box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.1); }
+.agent-item.review.active { border-color: #52c41a; box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.1); }
+.agent-item.arch.active { border-color: #fa8c16; box-shadow: 0 0 0 3px rgba(250, 140, 22, 0.1); }
+.agent-item.coach.active { border-color: #722ed1; box-shadow: 0 0 0 3px rgba(114, 46, 209, 0.1); }
+
+.agent-icon-wrap {
+  position: relative;
+  width: 44px;
+  height: 44px;
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.orchestrator .agent-icon-wrap { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
+.tutor .agent-icon-wrap { background: linear-gradient(135deg, #764ba2 0%, #9b59b6 100%); }
+.debug .agent-icon-wrap { background: linear-gradient(135deg, #f5222d 0%, #cf1322 100%); }
+.review .agent-icon-wrap { background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%); }
+.arch .agent-icon-wrap { background: linear-gradient(135deg, #fa8c16 0%, #d46b08 100%); }
+.coach .agent-icon-wrap { background: linear-gradient(135deg, #722ed1 0%, #531dab 100%); }
+
+.agent-icon {
+  font-size: 22px;
+}
+
+.active-ring {
+  position: absolute;
+  inset: -3px;
+  border: 2px solid currentColor;
+  border-radius: 14px;
+  animation: ring-pulse 1.5s infinite;
+}
+
+.orchestrator .active-ring { border-color: #667eea; }
+.tutor .active-ring { border-color: #764ba2; }
+.debug .active-ring { border-color: #f5222d; }
+.review .active-ring { border-color: #52c41a; }
+.arch .active-ring { border-color: #fa8c16; }
+.coach .active-ring { border-color: #722ed1; }
+
+@keyframes ring-pulse {
+  0%, 100% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(1.05); }
+}
+
+.agent-info {
+  flex: 1;
+  min-width: 0;
+}
+
+.agent-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 2px;
+}
+
+.agent-name {
+  font-weight: 600;
+  color: var(--text-primary, #1a1a1a);
+  font-size: 14px;
+}
+
+.agent-status {
+  font-size: 11px;
+  padding: 2px 8px;
+  border-radius: 10px;
+  font-weight: 500;
+}
+
+.agent-status.active {
+  background: #f6ffed;
+  color: #52c41a;
+}
+
+.agent-status.processing {
+  background: #e6f7ff;
+  color: #1890ff;
+}
+
+.agent-status.idle {
+  background: #f5f5f5;
+  color: #999;
+}
+
+.agent-status.error {
+  background: #fff2f0;
+  color: #ff4d4f;
+}
+
+.agent-desc {
+  display: block;
+  color: var(--text-muted, #999);
+  font-size: 12px;
+  margin-bottom: 6px;
+}
+
+.agent-meta {
+  display: flex;
+  gap: 12px;
+}
+
+.meta-item {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 11px;
+  color: var(--text-muted, #999);
+}
+
+.meta-icon {
+  font-size: 12px;
+}
+</style>

+ 7 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/src/vite-env.d.ts

@@ -0,0 +1,7 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 24 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/tsconfig.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "target": "es2023",
+    "module": "esnext",
+    "lib": ["ES2023", "DOM"],
+    "types": ["vite/client"],
+    "allowArbitraryExtensions": true,
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": ["src"]
+}

+ 16 - 0
Co-creation-projects/Max3753-Way_to_Engineer/frontend/vite.config.ts

@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    host: '0.0.0.0',
+    port: 3000,
+    proxy: {
+      '/api': {
+        target: 'http://localhost:12000',
+        changeOrigin: true,
+      },
+    },
+  },
+})

BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/agentsView.png


BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/chatView.png


BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/configView.png


BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/coursePath1.png


BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/coursePath2.png


BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/coursePath3.png


BIN
Co-creation-projects/Max3753-Way_to_Engineer/image/learningRecords.png


+ 49 - 0
Co-creation-projects/Max3753-Way_to_Engineer/start.bat

@@ -0,0 +1,49 @@
+@echo off
+title Way to Engineer - 启动器
+chcp 65001 >nul
+
+echo ========================================
+echo   Way to Engineer - 启动中...
+echo ========================================
+echo.
+
+REM 检查后端虚拟环境
+if not exist "backend\venv\Scripts\python.exe" (
+    echo [!] 后端虚拟环境未找到,请先执行:
+    echo     cd backend
+    echo     python -m venv venv
+    echo     venv\Scripts\activate ^&^& pip install -r requirements.txt
+    echo.
+    pause
+    exit /b 1
+)
+
+REM 检查前端依赖
+if not exist "frontend\node_modules" (
+    echo [!] 前端依赖未安装,请先执行:
+    echo     cd frontend
+    echo     npm install
+    echo.
+    pause
+    exit /b 1
+)
+
+REM 启动后端 (新窗口)
+echo [1/2] 启动后端服务...
+start "Way-to-Engineer-Backend" cmd /c "cd /d %~dp0backend && venv\Scripts\activate && python run.py"
+
+REM 等待后端启动
+timeout /t 3 /nobreak >nul
+
+REM 启动前端 (新窗口)
+echo [2/2] 启动前端服务...
+start "Way-to-Engineer-Frontend" cmd /c "cd /d %~dp0frontend && npm run dev"
+
+echo.
+echo ========================================
+echo   后端: http://localhost:8000
+echo   前端: http://localhost:5173
+echo   按任意键关闭此窗口(服务将继续运行)
+echo ========================================
+echo.
+pause >nul

Some files were not shown because too many files changed in this diff