first commit
This commit is contained in:
commit
00af1f959f
98
.gitignore
vendored
Normal file
98
.gitignore
vendored
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Production
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# Prisma
|
||||||
|
# Keep migrations but ignore generated client and database
|
||||||
|
backend/prisma/dev.db
|
||||||
|
backend/prisma/dev.db-journal
|
||||||
|
# Note: migrations/ directory should be committed
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
frontend/.vite/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/node_modules/.vite/
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.min.js
|
||||||
|
*.min.css
|
||||||
|
# Source maps (keep for debugging, but ignore in node_modules)
|
||||||
|
node_modules/**/*.map
|
||||||
|
|
||||||
|
# Package manager locks (optional - uncomment if you want to ignore)
|
||||||
|
# package-lock.json
|
||||||
|
# yarn.lock
|
||||||
|
# pnpm-lock.yaml
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
71
DEBUG.md
Normal file
71
DEBUG.md
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# 前端调试指南
|
||||||
|
|
||||||
|
## 问题:前端页面无内容展示
|
||||||
|
|
||||||
|
### 检查步骤
|
||||||
|
|
||||||
|
1. **检查浏览器控制台**
|
||||||
|
- 打开开发者工具 (F12)
|
||||||
|
- 查看 Console 标签页是否有错误信息
|
||||||
|
- 查看 Network 标签页,检查 API 请求是否成功
|
||||||
|
|
||||||
|
2. **检查后端是否运行**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/health
|
||||||
|
```
|
||||||
|
应该返回:`{"status":"ok","message":"AI Learning Platform API"}`
|
||||||
|
|
||||||
|
3. **检查 API 连接**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/courses
|
||||||
|
```
|
||||||
|
应该返回课程列表的 JSON 数据
|
||||||
|
|
||||||
|
4. **检查前端是否运行**
|
||||||
|
- 访问 http://localhost:3000
|
||||||
|
- 应该能看到页面内容
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. API 请求失败
|
||||||
|
- **症状**:控制台显示网络错误或 CORS 错误
|
||||||
|
- **解决**:确保后端在 3001 端口运行,且 CORS 已配置
|
||||||
|
|
||||||
|
#### 2. 数据加载失败
|
||||||
|
- **症状**:页面显示"加载中..."但一直不结束
|
||||||
|
- **解决**:检查浏览器控制台的错误信息,查看 API 响应
|
||||||
|
|
||||||
|
#### 3. 样式未加载
|
||||||
|
- **症状**:页面有内容但样式错乱
|
||||||
|
- **解决**:确保 Tailwind CSS 已正确配置和编译
|
||||||
|
|
||||||
|
#### 4. 路由问题
|
||||||
|
- **症状**:页面空白或 404
|
||||||
|
- **解决**:检查 URL 路径是否正确,确保使用 React Router
|
||||||
|
|
||||||
|
### 调试工具
|
||||||
|
|
||||||
|
已添加的调试功能:
|
||||||
|
- API 请求/响应日志(在浏览器控制台)
|
||||||
|
- 错误边界组件(捕获 React 错误)
|
||||||
|
- 详细的错误提示
|
||||||
|
|
||||||
|
### 手动测试 API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试健康检查
|
||||||
|
curl http://localhost:3001/api/health
|
||||||
|
|
||||||
|
# 测试获取课程
|
||||||
|
curl http://localhost:3001/api/courses
|
||||||
|
|
||||||
|
# 测试获取特定课程
|
||||||
|
curl http://localhost:3001/api/courses/{courseId}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 如果仍然无法解决
|
||||||
|
|
||||||
|
1. 清除浏览器缓存
|
||||||
|
2. 重启开发服务器
|
||||||
|
3. 检查 Node.js 版本(需要 16+)
|
||||||
|
4. 查看完整的错误堆栈信息
|
||||||
157
README.md
Normal file
157
README.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# AI学习平台
|
||||||
|
|
||||||
|
面向开发者的AI知识学习平台,提供结构化的课程内容和智能学习路径推荐。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 📚 **结构化课程**:精心设计的课程体系,涵盖机器学习、深度学习、NLP、LLM、AI工具等
|
||||||
|
- 🎯 **个性化路径**:基于兴趣和水平智能推荐学习路径
|
||||||
|
- 📊 **进度跟踪**:实时记录学习进度
|
||||||
|
- 💻 **现代化UI**:使用React + Tailwind CSS构建美观界面
|
||||||
|
- 📝 **Markdown支持**:课程内容支持Markdown格式,代码高亮
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- React 18 + TypeScript
|
||||||
|
- Vite
|
||||||
|
- Tailwind CSS
|
||||||
|
- React Router
|
||||||
|
- React Markdown
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- Node.js + Express + TypeScript
|
||||||
|
- Prisma ORM
|
||||||
|
- SQLite数据库
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run install:all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 初始化数据库
|
||||||
|
|
||||||
|
**方式一:使用自动化脚本(推荐)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run setup:db
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二:手动执行**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run prisma:generate # 生成Prisma Client
|
||||||
|
npm run prisma:migrate # 创建数据库和表结构
|
||||||
|
npm run prisma:seed # 填充初始课程数据
|
||||||
|
```
|
||||||
|
|
||||||
|
数据库文件将创建在 `backend/prisma/dev.db`
|
||||||
|
|
||||||
|
### 3. 启动开发服务器
|
||||||
|
|
||||||
|
在项目根目录运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
这将同时启动:
|
||||||
|
- 前端开发服务器:http://localhost:3000
|
||||||
|
- 后端API服务器:http://localhost:3001
|
||||||
|
|
||||||
|
### 单独启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动前端
|
||||||
|
npm run dev:frontend
|
||||||
|
|
||||||
|
# 启动后端
|
||||||
|
npm run dev:backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
ai-learning-platform/
|
||||||
|
├── frontend/ # React前端应用
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # 可复用组件
|
||||||
|
│ │ ├── pages/ # 页面组件
|
||||||
|
│ │ ├── services/ # API服务
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── package.json
|
||||||
|
├── backend/ # Express后端
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── routes/ # API路由
|
||||||
|
│ │ ├── controllers/# 控制器
|
||||||
|
│ │ ├── services/ # 业务逻辑
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── prisma/ # 数据库Schema和Seed
|
||||||
|
│ └── package.json
|
||||||
|
├── shared/ # 共享TypeScript类型
|
||||||
|
└── package.json # 根package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## API端点
|
||||||
|
|
||||||
|
### 课程相关
|
||||||
|
- `GET /api/courses` - 获取课程列表
|
||||||
|
- `GET /api/courses/:id` - 获取课程详情
|
||||||
|
- `GET /api/courses/:id/chapters` - 获取章节列表
|
||||||
|
- `GET /api/chapters/:id` - 获取章节内容
|
||||||
|
|
||||||
|
### 学习路径相关
|
||||||
|
- `GET /api/paths` - 获取所有学习路径
|
||||||
|
- `POST /api/paths/generate` - 生成个性化路径
|
||||||
|
- `GET /api/paths/:id` - 获取路径详情
|
||||||
|
|
||||||
|
### 进度相关
|
||||||
|
- `GET /api/progress` - 获取用户进度
|
||||||
|
- `POST /api/progress` - 更新学习进度
|
||||||
|
|
||||||
|
## 课程分类
|
||||||
|
|
||||||
|
- **机器学习基础** (ML_BASICS)
|
||||||
|
- **深度学习** (DEEP_LEARNING)
|
||||||
|
- **自然语言处理** (NLP)
|
||||||
|
- **大语言模型** (LLM)
|
||||||
|
- **AI工具** (AI_TOOLS)
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run prisma:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重新生成Prisma Client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run prisma:generate
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重新初始化数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run prisma:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建生产版本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
28
backend/package.json
Normal file
28
backend/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@ai-learning/backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"prisma:generate": "prisma generate",
|
||||||
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"prisma:seed": "tsx prisma/seed.ts",
|
||||||
|
"setup:db": "node scripts/setup-db.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-learning/shared": "*",
|
||||||
|
"@prisma/client": "^5.7.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.18.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.10.6",
|
||||||
|
"prisma": "^5.7.1",
|
||||||
|
"tsx": "^4.7.1",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
backend/prisma/migrations/.gitkeep
Normal file
0
backend/prisma/migrations/.gitkeep
Normal file
63
backend/prisma/migrations/20260110052756_init/migration.sql
Normal file
63
backend/prisma/migrations/20260110052756_init/migration.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Course" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"category" TEXT NOT NULL,
|
||||||
|
"difficulty" TEXT NOT NULL,
|
||||||
|
"estimatedHours" INTEGER NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Chapter" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"courseId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"order" INTEGER NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "Chapter_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LearningPath" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"targetAudience" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "PathItem" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"pathId" TEXT NOT NULL,
|
||||||
|
"courseId" TEXT NOT NULL,
|
||||||
|
"chapterId" TEXT,
|
||||||
|
"order" INTEGER NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "PathItem_pathId_fkey" FOREIGN KEY ("pathId") REFERENCES "LearningPath" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "PathItem_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "PathItem_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserProgress" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"userId" TEXT NOT NULL DEFAULT 'default',
|
||||||
|
"courseId" TEXT NOT NULL,
|
||||||
|
"chapterId" TEXT,
|
||||||
|
"completed" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"lastAccessed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserProgress_userId_courseId_chapterId_key" ON "UserProgress"("userId", "courseId", "chapterId");
|
||||||
3
backend/prisma/migrations/migration_lock.toml
Normal file
3
backend/prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "sqlite"
|
||||||
73
backend/prisma/schema.prisma
Normal file
73
backend/prisma/schema.prisma
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:./dev.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
model Course {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
title String
|
||||||
|
description String
|
||||||
|
category String // ML_BASICS, DEEP_LEARNING, NLP, LLM, AI_TOOLS
|
||||||
|
difficulty String // BEGINNER, INTERMEDIATE, ADVANCED
|
||||||
|
estimatedHours Int
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
chapters Chapter[]
|
||||||
|
pathItems PathItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Chapter {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
courseId String
|
||||||
|
title String
|
||||||
|
order Int
|
||||||
|
content String // Markdown content
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||||
|
pathItems PathItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearningPath {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
targetAudience String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
pathItems PathItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model PathItem {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
pathId String
|
||||||
|
courseId String
|
||||||
|
chapterId String?
|
||||||
|
order Int
|
||||||
|
type String // 'course' or 'chapter'
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
path LearningPath @relation(fields: [pathId], references: [id], onDelete: Cascade)
|
||||||
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
||||||
|
chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserProgress {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
userId String @default("default") // MVP阶段使用默认用户
|
||||||
|
courseId String
|
||||||
|
chapterId String?
|
||||||
|
completed Boolean @default(false)
|
||||||
|
lastAccessed DateTime @default(now())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([userId, courseId, chapterId])
|
||||||
|
}
|
||||||
814
backend/prisma/seed.ts
Normal file
814
backend/prisma/seed.ts
Normal file
@ -0,0 +1,814 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('开始初始化数据库...');
|
||||||
|
|
||||||
|
// 清理现有数据
|
||||||
|
await prisma.pathItem.deleteMany();
|
||||||
|
await prisma.learningPath.deleteMany();
|
||||||
|
await prisma.userProgress.deleteMany();
|
||||||
|
await prisma.chapter.deleteMany();
|
||||||
|
await prisma.course.deleteMany();
|
||||||
|
|
||||||
|
// 创建机器学习基础课程
|
||||||
|
const mlBasics1 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: '线性回归',
|
||||||
|
description: '学习线性回归的基本原理和实现方法,包括最小二乘法、梯度下降等核心概念。',
|
||||||
|
category: 'ML_BASICS',
|
||||||
|
difficulty: 'BEGINNER',
|
||||||
|
estimatedHours: 3,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: '什么是线性回归',
|
||||||
|
order: 1,
|
||||||
|
content: `# 什么是线性回归
|
||||||
|
|
||||||
|
线性回归是机器学习中最基础的算法之一,用于预测连续值。
|
||||||
|
|
||||||
|
## 基本概念
|
||||||
|
|
||||||
|
线性回归试图找到一条直线(或超平面),使得所有数据点到这条直线的距离之和最小。
|
||||||
|
|
||||||
|
### 数学表示
|
||||||
|
|
||||||
|
对于简单线性回归(一个特征),模型可以表示为:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
y = wx + b
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
- \`y\` 是预测值(目标变量)
|
||||||
|
- \`x\` 是输入特征
|
||||||
|
- \`w\` 是权重(斜率)
|
||||||
|
- \`b\` 是偏置(截距)
|
||||||
|
|
||||||
|
### 应用场景
|
||||||
|
|
||||||
|
- 房价预测
|
||||||
|
- 销售额预测
|
||||||
|
- 温度预测
|
||||||
|
- 任何需要预测连续值的场景
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
在下一章中,我们将学习如何使用最小二乘法来求解线性回归的参数。`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最小二乘法',
|
||||||
|
order: 2,
|
||||||
|
content: `# 最小二乘法
|
||||||
|
|
||||||
|
最小二乘法是求解线性回归参数的标准方法。
|
||||||
|
|
||||||
|
## 原理
|
||||||
|
|
||||||
|
最小二乘法的目标是找到参数 \`w\` 和 \`b\`,使得预测值与真实值之间的平方误差最小:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
损失函数 = Σ(y_i - (wx_i + b))²
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 求解方法
|
||||||
|
|
||||||
|
### 1. 解析解(闭式解)
|
||||||
|
|
||||||
|
对于简单线性回归,可以直接计算:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
w = Σ(x_i - x̄)(y_i - ȳ) / Σ(x_i - x̄)²
|
||||||
|
b = ȳ - w * x̄
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
### 2. 矩阵形式
|
||||||
|
|
||||||
|
对于多元线性回归,可以使用矩阵运算:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
θ = (X^T * X)^(-1) * X^T * y
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 代码示例
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def linear_regression(X, y):
|
||||||
|
# 添加偏置项
|
||||||
|
X = np.column_stack([np.ones(len(X)), X])
|
||||||
|
|
||||||
|
# 计算参数
|
||||||
|
theta = np.linalg.inv(X.T @ X) @ X.T @ y
|
||||||
|
|
||||||
|
return theta
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
最小二乘法提供了线性回归的精确解,但在大数据集上计算成本较高。下一章我们将学习更高效的梯度下降方法。`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '梯度下降',
|
||||||
|
order: 3,
|
||||||
|
content: `# 梯度下降
|
||||||
|
|
||||||
|
梯度下降是一种优化算法,用于找到损失函数的最小值。
|
||||||
|
|
||||||
|
## 基本思想
|
||||||
|
|
||||||
|
梯度下降通过迭代更新参数,沿着损失函数梯度的反方向移动,逐步接近最优解。
|
||||||
|
|
||||||
|
## 算法步骤
|
||||||
|
|
||||||
|
1. 初始化参数 \`w\` 和 \`b\`
|
||||||
|
2. 计算损失函数的梯度
|
||||||
|
3. 更新参数:\`θ = θ - α * ∇θ\`
|
||||||
|
4. 重复步骤2-3直到收敛
|
||||||
|
|
||||||
|
其中 \`α\` 是学习率。
|
||||||
|
|
||||||
|
## 代码实现
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def gradient_descent(X, y, learning_rate=0.01, iterations=1000):
|
||||||
|
m = len(y)
|
||||||
|
theta = np.zeros(X.shape[1])
|
||||||
|
|
||||||
|
for i in range(iterations):
|
||||||
|
# 计算预测值
|
||||||
|
predictions = X @ theta
|
||||||
|
|
||||||
|
# 计算梯度
|
||||||
|
gradient = (1/m) * X.T @ (predictions - y)
|
||||||
|
|
||||||
|
# 更新参数
|
||||||
|
theta = theta - learning_rate * gradient
|
||||||
|
|
||||||
|
# 可选:打印损失
|
||||||
|
if i % 100 == 0:
|
||||||
|
loss = np.mean((predictions - y) ** 2)
|
||||||
|
print(f'Iteration {i}, Loss: {loss}')
|
||||||
|
|
||||||
|
return theta
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 学习率选择
|
||||||
|
|
||||||
|
- 学习率太小:收敛慢
|
||||||
|
- 学习率太大:可能无法收敛或震荡
|
||||||
|
- 建议:从0.01开始,根据实际情况调整
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
梯度下降是深度学习的核心优化算法,理解它对于后续学习非常重要。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mlBasics2 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: '逻辑回归',
|
||||||
|
description: '学习逻辑回归用于分类问题的原理和应用,包括sigmoid函数、损失函数等。',
|
||||||
|
category: 'ML_BASICS',
|
||||||
|
difficulty: 'BEGINNER',
|
||||||
|
estimatedHours: 4,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: '分类问题',
|
||||||
|
order: 1,
|
||||||
|
content: `# 分类问题
|
||||||
|
|
||||||
|
逻辑回归是用于解决分类问题的算法。
|
||||||
|
|
||||||
|
## 分类 vs 回归
|
||||||
|
|
||||||
|
- **回归**:预测连续值(如房价、温度)
|
||||||
|
- **分类**:预测离散类别(如垃圾邮件/正常邮件、猫/狗)
|
||||||
|
|
||||||
|
## 二分类问题
|
||||||
|
|
||||||
|
逻辑回归主要用于二分类问题,输出是0或1。
|
||||||
|
|
||||||
|
### 示例场景
|
||||||
|
|
||||||
|
- 邮件分类:垃圾邮件(1)或正常邮件(0)
|
||||||
|
- 疾病诊断:患病(1)或健康(0)
|
||||||
|
- 客户流失:流失(1)或保留(0)
|
||||||
|
|
||||||
|
## 为什么不能用线性回归?
|
||||||
|
|
||||||
|
线性回归的输出可以是任意实数,不适合分类问题。我们需要将输出映射到[0,1]区间。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
下一章我们将学习sigmoid函数,它是逻辑回归的核心。`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Sigmoid函数',
|
||||||
|
order: 2,
|
||||||
|
content: `# Sigmoid函数
|
||||||
|
|
||||||
|
Sigmoid函数将任意实数映射到(0,1)区间,非常适合二分类问题。
|
||||||
|
|
||||||
|
## 函数定义
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
σ(z) = 1 / (1 + e^(-z))
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 输出范围:(0, 1)
|
||||||
|
- 单调递增
|
||||||
|
- S型曲线
|
||||||
|
- 当z=0时,σ(z)=0.5
|
||||||
|
|
||||||
|
## 代码实现
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
def sigmoid(z):
|
||||||
|
return 1 / (1 + np.exp(-z))
|
||||||
|
|
||||||
|
# 示例
|
||||||
|
z = np.linspace(-10, 10, 100)
|
||||||
|
y = sigmoid(z)
|
||||||
|
|
||||||
|
plt.plot(z, y)
|
||||||
|
plt.xlabel('z')
|
||||||
|
plt.ylabel('σ(z)')
|
||||||
|
plt.title('Sigmoid Function')
|
||||||
|
plt.grid(True)
|
||||||
|
plt.show()
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 在逻辑回归中的应用
|
||||||
|
|
||||||
|
逻辑回归模型:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
h(x) = σ(wx + b) = 1 / (1 + e^(-(wx + b)))
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
输出可以解释为概率:P(y=1|x)
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
Sigmoid函数是逻辑回归的基础,理解它对于掌握分类算法至关重要。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建深度学习课程
|
||||||
|
const dl1 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: '神经网络基础',
|
||||||
|
description: '从感知机到多层神经网络,学习神经网络的基本原理和结构。',
|
||||||
|
category: 'DEEP_LEARNING',
|
||||||
|
difficulty: 'INTERMEDIATE',
|
||||||
|
estimatedHours: 5,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: '感知机',
|
||||||
|
order: 1,
|
||||||
|
content: `# 感知机
|
||||||
|
|
||||||
|
感知机是神经网络的基础,是最简单的神经网络模型。
|
||||||
|
|
||||||
|
## 什么是感知机
|
||||||
|
|
||||||
|
感知机是一个二分类的线性分类模型,由输入层和输出层组成。
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
输入 → 权重 → 求和 → 激活函数 → 输出
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 数学表示
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
y = f(Σ(w_i * x_i) + b)
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
其中f是激活函数(如阶跃函数)。
|
||||||
|
|
||||||
|
## 局限性
|
||||||
|
|
||||||
|
单层感知机只能解决线性可分问题,无法处理XOR等非线性问题。
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
多层感知机(MLP)可以解决非线性问题,我们将在下一章学习。`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '多层神经网络',
|
||||||
|
order: 2,
|
||||||
|
content: `# 多层神经网络
|
||||||
|
|
||||||
|
多层神经网络(MLP)通过增加隐藏层来解决非线性问题。
|
||||||
|
|
||||||
|
## 网络结构
|
||||||
|
|
||||||
|
- **输入层**:接收特征
|
||||||
|
- **隐藏层**:一个或多个,进行特征变换
|
||||||
|
- **输出层**:产生最终预测
|
||||||
|
|
||||||
|
## 前向传播
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
# 第一层
|
||||||
|
z1 = W1 @ x + b1
|
||||||
|
a1 = activation(z1)
|
||||||
|
|
||||||
|
# 第二层
|
||||||
|
z2 = W2 @ a1 + b2
|
||||||
|
a2 = activation(z2)
|
||||||
|
|
||||||
|
# 输出层
|
||||||
|
output = softmax(z2) # 对于多分类
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 激活函数
|
||||||
|
|
||||||
|
常用的激活函数:
|
||||||
|
- ReLU: \`f(x) = max(0, x)\`
|
||||||
|
- Sigmoid: \`f(x) = 1/(1+e^(-x))\`
|
||||||
|
- Tanh: \`f(x) = tanh(x)\`
|
||||||
|
|
||||||
|
## 为什么需要激活函数?
|
||||||
|
|
||||||
|
没有激活函数,多层网络等价于单层网络。激活函数引入非线性,使网络能够学习复杂模式。
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
多层神经网络是深度学习的基础,理解其结构对于后续学习CNN、RNN等模型非常重要。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dl2 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: '卷积神经网络(CNN)',
|
||||||
|
description: '学习CNN在图像处理中的应用,包括卷积层、池化层等核心概念。',
|
||||||
|
category: 'DEEP_LEARNING',
|
||||||
|
difficulty: 'INTERMEDIATE',
|
||||||
|
estimatedHours: 6,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: '卷积操作',
|
||||||
|
order: 1,
|
||||||
|
content: `# 卷积操作
|
||||||
|
|
||||||
|
卷积是CNN的核心操作,用于提取图像特征。
|
||||||
|
|
||||||
|
## 什么是卷积
|
||||||
|
|
||||||
|
卷积是一种数学运算,通过滑动窗口(卷积核)在输入上移动,计算局部区域的加权和。
|
||||||
|
|
||||||
|
## 卷积过程
|
||||||
|
|
||||||
|
1. 将卷积核放在输入图像的左上角
|
||||||
|
2. 计算卷积核覆盖区域的元素乘积之和
|
||||||
|
3. 将结果写入输出特征图
|
||||||
|
4. 滑动卷积核,重复步骤2-3
|
||||||
|
|
||||||
|
## 代码示例
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
import numpy as np
|
||||||
|
from scipy import signal
|
||||||
|
|
||||||
|
# 简单的卷积实现
|
||||||
|
def convolve2d(image, kernel):
|
||||||
|
return signal.convolve2d(image, kernel, mode='valid')
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 卷积核的作用
|
||||||
|
|
||||||
|
不同的卷积核可以检测不同的特征:
|
||||||
|
- 边缘检测
|
||||||
|
- 模糊
|
||||||
|
- 锐化
|
||||||
|
- 特征提取
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
理解卷积操作是掌握CNN的关键第一步。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建NLP课程
|
||||||
|
const nlp1 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: '文本预处理',
|
||||||
|
description: '学习NLP中的文本预处理技术,包括分词、去停用词、词干提取等。',
|
||||||
|
category: 'NLP',
|
||||||
|
difficulty: 'BEGINNER',
|
||||||
|
estimatedHours: 3,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: '分词',
|
||||||
|
order: 1,
|
||||||
|
content: `# 分词
|
||||||
|
|
||||||
|
分词是将文本切分成词语的过程,是NLP的基础步骤。
|
||||||
|
|
||||||
|
## 中文分词
|
||||||
|
|
||||||
|
中文没有明显的词边界,需要专门的分词工具:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
import jieba
|
||||||
|
|
||||||
|
text = "自然语言处理是人工智能的重要分支"
|
||||||
|
words = jieba.cut(text)
|
||||||
|
print(list(words))
|
||||||
|
# ['自然语言', '处理', '是', '人工智能', '的', '重要', '分支']
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 英文分词
|
||||||
|
|
||||||
|
英文分词相对简单,通常按空格分割:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
text = "Natural language processing is important"
|
||||||
|
words = text.split()
|
||||||
|
print(words)
|
||||||
|
# ['Natural', 'language', 'processing', 'is', 'important']
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 分词工具
|
||||||
|
|
||||||
|
- 中文:jieba, HanLP
|
||||||
|
- 英文:NLTK, spaCy
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
准确的分词是后续NLP任务的基础。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const nlp2 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: '词向量',
|
||||||
|
description: '学习词嵌入和词向量的表示方法,包括Word2Vec、GloVe等。',
|
||||||
|
category: 'NLP',
|
||||||
|
difficulty: 'INTERMEDIATE',
|
||||||
|
estimatedHours: 5,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: '词嵌入基础',
|
||||||
|
order: 1,
|
||||||
|
content: `# 词嵌入基础
|
||||||
|
|
||||||
|
词嵌入是将词语映射到低维连续向量空间的技术。
|
||||||
|
|
||||||
|
## 为什么需要词向量?
|
||||||
|
|
||||||
|
传统方法(如one-hot编码)的问题:
|
||||||
|
- 维度高(词汇表大小)
|
||||||
|
- 稀疏
|
||||||
|
- 无法表示词语间的语义关系
|
||||||
|
|
||||||
|
## 词向量的优势
|
||||||
|
|
||||||
|
- 低维密集表示
|
||||||
|
- 语义相似的词向量距离近
|
||||||
|
- 可以进行向量运算(如:king - man + woman ≈ queen)
|
||||||
|
|
||||||
|
## Word2Vec
|
||||||
|
|
||||||
|
Word2Vec是经典的词向量训练方法,包括:
|
||||||
|
- Skip-gram:用中心词预测上下文
|
||||||
|
- CBOW:用上下文预测中心词
|
||||||
|
|
||||||
|
## 使用预训练词向量
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
from gensim.models import Word2Vec
|
||||||
|
|
||||||
|
# 加载预训练模型
|
||||||
|
model = Word2Vec.load('word2vec.model')
|
||||||
|
vector = model.wv['人工智能']
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
词向量是现代NLP的基础,理解它对于学习Transformer和LLM非常重要。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建LLM课程
|
||||||
|
const llm1 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: 'GPT原理',
|
||||||
|
description: '深入理解GPT(Generative Pre-trained Transformer)的工作原理和架构。',
|
||||||
|
category: 'LLM',
|
||||||
|
difficulty: 'ADVANCED',
|
||||||
|
estimatedHours: 8,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: 'Transformer架构',
|
||||||
|
order: 1,
|
||||||
|
content: `# Transformer架构
|
||||||
|
|
||||||
|
GPT基于Transformer架构,理解Transformer是理解GPT的基础。
|
||||||
|
|
||||||
|
## Transformer核心组件
|
||||||
|
|
||||||
|
1. **自注意力机制(Self-Attention)**
|
||||||
|
2. **位置编码(Positional Encoding)**
|
||||||
|
3. **前馈神经网络(Feed-Forward)**
|
||||||
|
4. **层归一化(Layer Normalization)**
|
||||||
|
|
||||||
|
## 自注意力机制
|
||||||
|
|
||||||
|
自注意力允许模型关注输入序列的不同位置:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
Attention(Q, K, V) = softmax(QK^T / √d_k) V
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
其中:
|
||||||
|
- Q: Query(查询)
|
||||||
|
- K: Key(键)
|
||||||
|
- V: Value(值)
|
||||||
|
|
||||||
|
## GPT的改进
|
||||||
|
|
||||||
|
GPT使用:
|
||||||
|
- 仅解码器(Decoder-only)架构
|
||||||
|
- 因果掩码(Causal Masking)确保自回归生成
|
||||||
|
- 位置编码
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
Transformer是GPT的基础,理解其架构对于掌握大语言模型至关重要。`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '预训练与微调',
|
||||||
|
order: 2,
|
||||||
|
content: `# 预训练与微调
|
||||||
|
|
||||||
|
GPT采用两阶段训练:预训练和微调。
|
||||||
|
|
||||||
|
## 预训练阶段
|
||||||
|
|
||||||
|
在大规模无标注文本上训练,学习语言的基本规律:
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
# 目标:预测下一个词
|
||||||
|
损失 = -log P(w_t | w_1, ..., w_{t-1})
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 微调阶段
|
||||||
|
|
||||||
|
在特定任务上微调,如:
|
||||||
|
- 文本分类
|
||||||
|
- 问答
|
||||||
|
- 文本生成
|
||||||
|
|
||||||
|
## 代码示例
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
from transformers import GPT2LMHeadModel, GPT2Tokenizer
|
||||||
|
|
||||||
|
# 加载预训练模型
|
||||||
|
model = GPT2LMHeadModel.from_pretrained('gpt2')
|
||||||
|
tokenizer = GPT2Tokenizer.from_pretrained('gpt2')
|
||||||
|
|
||||||
|
# 微调(伪代码)
|
||||||
|
for batch in training_data:
|
||||||
|
outputs = model(batch.input_ids)
|
||||||
|
loss = compute_loss(outputs, batch.labels)
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
预训练+微调是GPT成功的关键,这种范式被广泛应用于大语言模型。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm2 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: 'Prompt工程',
|
||||||
|
description: '学习如何编写有效的Prompt来引导大语言模型生成期望的输出。',
|
||||||
|
category: 'LLM',
|
||||||
|
difficulty: 'INTERMEDIATE',
|
||||||
|
estimatedHours: 4,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: 'Prompt基础',
|
||||||
|
order: 1,
|
||||||
|
content: `# Prompt基础
|
||||||
|
|
||||||
|
Prompt是用户输入给大语言模型的指令或问题。
|
||||||
|
|
||||||
|
## 什么是Prompt
|
||||||
|
|
||||||
|
Prompt是引导模型生成特定输出的文本输入。
|
||||||
|
|
||||||
|
## 好的Prompt的特征
|
||||||
|
|
||||||
|
1. **清晰明确**:明确表达需求
|
||||||
|
2. **提供上下文**:给出必要的背景信息
|
||||||
|
3. **指定格式**:如果需要特定格式,明确说明
|
||||||
|
4. **示例引导**:提供few-shot示例
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### 不好的Prompt
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
翻译
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
### 好的Prompt
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
请将以下英文翻译成中文:
|
||||||
|
"Hello, how are you?"
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## Prompt技巧
|
||||||
|
|
||||||
|
- **角色设定**:让模型扮演特定角色
|
||||||
|
- **思维链**:引导模型逐步思考
|
||||||
|
- **输出格式**:指定JSON、Markdown等格式
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
掌握Prompt工程是有效使用大语言模型的关键技能。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建AI工具课程
|
||||||
|
const tools1 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: 'LangChain入门',
|
||||||
|
description: '学习使用LangChain构建AI应用,包括链式调用、记忆、工具等核心概念。',
|
||||||
|
category: 'AI_TOOLS',
|
||||||
|
difficulty: 'INTERMEDIATE',
|
||||||
|
estimatedHours: 6,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: 'LangChain简介',
|
||||||
|
order: 1,
|
||||||
|
content: `# LangChain简介
|
||||||
|
|
||||||
|
LangChain是一个用于构建LLM应用的框架。
|
||||||
|
|
||||||
|
## 为什么需要LangChain
|
||||||
|
|
||||||
|
直接使用LLM API的问题:
|
||||||
|
- 上下文管理复杂
|
||||||
|
- 难以集成外部数据
|
||||||
|
- 缺乏可复用组件
|
||||||
|
|
||||||
|
LangChain提供了:
|
||||||
|
- 模块化组件
|
||||||
|
- 链式调用
|
||||||
|
- 数据连接
|
||||||
|
- 记忆管理
|
||||||
|
|
||||||
|
## 核心概念
|
||||||
|
|
||||||
|
1. **LLM/聊天模型**:与语言模型交互
|
||||||
|
2. **提示模板**:可复用的Prompt
|
||||||
|
3. **链(Chain)**:组合多个组件
|
||||||
|
4. **代理(Agent)**:使用工具执行任务
|
||||||
|
5. **记忆(Memory)**:管理对话历史
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
from langchain.llms import OpenAI
|
||||||
|
from langchain.prompts import PromptTemplate
|
||||||
|
|
||||||
|
llm = OpenAI(temperature=0.9)
|
||||||
|
prompt = PromptTemplate(
|
||||||
|
input_variables=["topic"],
|
||||||
|
template="写一篇关于{topic}的短文。"
|
||||||
|
)
|
||||||
|
|
||||||
|
chain = prompt | llm
|
||||||
|
result = chain.invoke({"topic": "人工智能"})
|
||||||
|
print(result)
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
LangChain大大简化了LLM应用的开发,是构建AI应用的重要工具。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tools2 = await prisma.course.create({
|
||||||
|
data: {
|
||||||
|
title: 'Hugging Face使用',
|
||||||
|
description: '学习使用Hugging Face Transformers库加载和使用预训练模型。',
|
||||||
|
category: 'AI_TOOLS',
|
||||||
|
difficulty: 'BEGINNER',
|
||||||
|
estimatedHours: 4,
|
||||||
|
chapters: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
title: 'Transformers库',
|
||||||
|
order: 1,
|
||||||
|
content: `# Transformers库
|
||||||
|
|
||||||
|
Hugging Face Transformers提供了大量预训练模型。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
\\\`\\\`\\\`bash
|
||||||
|
pip install transformers torch
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 使用预训练模型
|
||||||
|
|
||||||
|
\\\`\\\`\\\`python
|
||||||
|
from transformers import AutoTokenizer, AutoModelForCausalLM
|
||||||
|
|
||||||
|
# 加载模型和分词器
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained("gpt2")
|
||||||
|
model = AutoModelForCausalLM.from_pretrained("gpt2")
|
||||||
|
|
||||||
|
# 编码输入
|
||||||
|
inputs = tokenizer("人工智能是", return_tensors="pt")
|
||||||
|
|
||||||
|
# 生成文本
|
||||||
|
outputs = model.generate(**inputs, max_length=50)
|
||||||
|
text = tokenizer.decode(outputs[0])
|
||||||
|
print(text)
|
||||||
|
\\\`\\\`\\\`
|
||||||
|
|
||||||
|
## 模型Hub
|
||||||
|
|
||||||
|
Hugging Face Hub提供了数万个预训练模型:
|
||||||
|
- 文本生成:GPT-2, GPT-Neo
|
||||||
|
- 文本分类:BERT, RoBERTa
|
||||||
|
- 翻译:mBART, T5
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
Transformers库让使用预训练模型变得非常简单,是AI开发的重要工具。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('数据库初始化完成!');
|
||||||
|
console.log(`创建了 ${await prisma.course.count()} 门课程`);
|
||||||
|
console.log(`创建了 ${await prisma.chapter.count()} 个章节`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
44
backend/scripts/setup-db.js
Normal file
44
backend/scripts/setup-db.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// 数据库初始化脚本 (Node.js版本)
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
console.log('🚀 开始初始化数据库...\n');
|
||||||
|
|
||||||
|
const backendDir = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 生成Prisma Client
|
||||||
|
console.log('📦 生成Prisma Client...');
|
||||||
|
execSync('npm run prisma:generate', {
|
||||||
|
cwd: backendDir,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 运行数据库迁移
|
||||||
|
console.log('\n🗄️ 创建数据库和表结构...');
|
||||||
|
execSync('npm run prisma:migrate -- --name init', {
|
||||||
|
cwd: backendDir,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 填充初始数据
|
||||||
|
console.log('\n🌱 填充初始课程数据...');
|
||||||
|
execSync('npm run prisma:seed', {
|
||||||
|
cwd: backendDir,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n✅ 数据库初始化完成!');
|
||||||
|
console.log('\n数据库文件位置: backend/prisma/dev.db');
|
||||||
|
console.log('现在可以运行 "npm run dev" 启动服务器了\n');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 数据库初始化失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
25
backend/scripts/setup-db.sh
Normal file
25
backend/scripts/setup-db.sh
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 数据库初始化脚本
|
||||||
|
|
||||||
|
echo "🚀 开始初始化数据库..."
|
||||||
|
|
||||||
|
# 进入backend目录
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# 生成Prisma Client
|
||||||
|
echo "📦 生成Prisma Client..."
|
||||||
|
npm run prisma:generate
|
||||||
|
|
||||||
|
# 运行数据库迁移
|
||||||
|
echo "🗄️ 创建数据库和表结构..."
|
||||||
|
npm run prisma:migrate -- --name init
|
||||||
|
|
||||||
|
# 填充初始数据
|
||||||
|
echo "🌱 填充初始课程数据..."
|
||||||
|
npm run prisma:seed
|
||||||
|
|
||||||
|
echo "✅ 数据库初始化完成!"
|
||||||
|
echo ""
|
||||||
|
echo "数据库文件位置: backend/prisma/dev.db"
|
||||||
|
echo "现在可以运行 'npm run dev' 启动服务器了"
|
||||||
32
backend/src/controllers/chapterController.ts
Normal file
32
backend/src/controllers/chapterController.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export const getChapterById = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const chapter = await prisma.chapter.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
course: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
category: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!chapter) {
|
||||||
|
return res.status(404).json({ error: 'Chapter not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(chapter);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching chapter:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch chapter' });
|
||||||
|
}
|
||||||
|
};
|
||||||
83
backend/src/controllers/courseController.ts
Normal file
83
backend/src/controllers/courseController.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export const getCourses = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { category } = req.query;
|
||||||
|
const where = category ? { category: category as string } : {};
|
||||||
|
|
||||||
|
const courses = await prisma.course.findMany({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
chapters: {
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
order: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(courses);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching courses:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch courses' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCourseById = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const course = await prisma.course.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
chapters: {
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
order: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
return res.status(404).json({ error: 'Course not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(course);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching course:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch course' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCourseChapters = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const chapters = await prisma.chapter.findMany({
|
||||||
|
where: { courseId: id },
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
order: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(chapters);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching chapters:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch chapters' });
|
||||||
|
}
|
||||||
|
};
|
||||||
98
backend/src/controllers/pathController.ts
Normal file
98
backend/src/controllers/pathController.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { generateLearningPath } from '../services/pathService.js';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export const getPaths = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const paths = await prisma.learningPath.findMany({
|
||||||
|
include: {
|
||||||
|
pathItems: {
|
||||||
|
include: {
|
||||||
|
course: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
category: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chapter: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
order: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(paths);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching paths:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch paths' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePath = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { interests, currentLevel } = req.body;
|
||||||
|
|
||||||
|
if (!interests || !Array.isArray(interests) || interests.length === 0) {
|
||||||
|
return res.status(400).json({ error: 'Interests array is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = await generateLearningPath(interests, currentLevel || 'BEGINNER');
|
||||||
|
res.json(path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating path:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to generate learning path' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPathById = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const path = await prisma.learningPath.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
pathItems: {
|
||||||
|
include: {
|
||||||
|
course: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
description: true,
|
||||||
|
category: true,
|
||||||
|
difficulty: true,
|
||||||
|
estimatedHours: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chapter: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
order: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return res.status(404).json({ error: 'Path not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching path:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch path' });
|
||||||
|
}
|
||||||
|
};
|
||||||
78
backend/src/controllers/progressController.ts
Normal file
78
backend/src/controllers/progressController.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const DEFAULT_USER_ID = 'default';
|
||||||
|
|
||||||
|
export const getProgress = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { userId = DEFAULT_USER_ID } = req.query;
|
||||||
|
|
||||||
|
const progress = await prisma.userProgress.findMany({
|
||||||
|
where: { userId: userId as string },
|
||||||
|
include: {
|
||||||
|
course: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
category: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { lastAccessed: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(progress);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching progress:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch progress' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateProgress = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { courseId, chapterId, completed } = req.body;
|
||||||
|
const userId = DEFAULT_USER_ID;
|
||||||
|
|
||||||
|
if (!courseId) {
|
||||||
|
return res.status(400).json({ error: 'courseId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先尝试查找现有记录
|
||||||
|
const existing = await prisma.userProgress.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
courseId,
|
||||||
|
chapterId: chapterId || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let progress;
|
||||||
|
if (existing) {
|
||||||
|
// 更新现有记录
|
||||||
|
progress = await prisma.userProgress.update({
|
||||||
|
where: { id: existing.id },
|
||||||
|
data: {
|
||||||
|
completed: completed !== undefined ? completed : existing.completed,
|
||||||
|
lastAccessed: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 创建新记录
|
||||||
|
progress = await prisma.userProgress.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
courseId,
|
||||||
|
chapterId: chapterId || null,
|
||||||
|
completed: completed || false,
|
||||||
|
lastAccessed: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(progress);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating progress:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update progress' });
|
||||||
|
}
|
||||||
|
};
|
||||||
28
backend/src/index.ts
Normal file
28
backend/src/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import courseRoutes from './routes/courses.js';
|
||||||
|
import chapterRoutes from './routes/chapters.js';
|
||||||
|
import pathRoutes from './routes/paths.js';
|
||||||
|
import progressRoutes from './routes/progress.js';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/api/courses', courseRoutes);
|
||||||
|
app.use('/api/chapters', chapterRoutes);
|
||||||
|
app.use('/api/paths', pathRoutes);
|
||||||
|
app.use('/api/progress', progressRoutes);
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/api/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', message: 'AI Learning Platform API' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
8
backend/src/routes/chapters.ts
Normal file
8
backend/src/routes/chapters.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getChapterById } from '../controllers/chapterController.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/:id', getChapterById);
|
||||||
|
|
||||||
|
export default router;
|
||||||
10
backend/src/routes/courses.ts
Normal file
10
backend/src/routes/courses.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getCourses, getCourseById, getCourseChapters } from '../controllers/courseController.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', getCourses);
|
||||||
|
router.get('/:id', getCourseById);
|
||||||
|
router.get('/:id/chapters', getCourseChapters);
|
||||||
|
|
||||||
|
export default router;
|
||||||
10
backend/src/routes/paths.ts
Normal file
10
backend/src/routes/paths.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getPaths, generatePath, getPathById } from '../controllers/pathController.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', getPaths);
|
||||||
|
router.post('/generate', generatePath);
|
||||||
|
router.get('/:id', getPathById);
|
||||||
|
|
||||||
|
export default router;
|
||||||
9
backend/src/routes/progress.ts
Normal file
9
backend/src/routes/progress.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { getProgress, updateProgress } from '../controllers/progressController.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/', getProgress);
|
||||||
|
router.post('/', updateProgress);
|
||||||
|
|
||||||
|
export default router;
|
||||||
139
backend/src/services/pathService.ts
Normal file
139
backend/src/services/pathService.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { CourseCategory, Difficulty } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// 课程依赖关系映射
|
||||||
|
const courseDependencies: Record<string, string[]> = {
|
||||||
|
'DEEP_LEARNING': ['ML_BASICS'],
|
||||||
|
'NLP': ['ML_BASICS'],
|
||||||
|
'LLM': ['DEEP_LEARNING', 'NLP'],
|
||||||
|
'AI_TOOLS': ['LLM'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 难度排序
|
||||||
|
const difficultyOrder = {
|
||||||
|
'BEGINNER': 1,
|
||||||
|
'INTERMEDIATE': 2,
|
||||||
|
'ADVANCED': 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateLearningPath(
|
||||||
|
interests: string[],
|
||||||
|
currentLevel: string = 'BEGINNER'
|
||||||
|
): Promise<any> {
|
||||||
|
// 获取所有相关课程
|
||||||
|
const allCourses = await prisma.course.findMany({
|
||||||
|
where: {
|
||||||
|
category: {
|
||||||
|
in: interests,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
chapters: {
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ difficulty: 'asc' },
|
||||||
|
{ createdAt: 'asc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 构建课程依赖图
|
||||||
|
const courseMap = new Map(allCourses.map(c => [c.id, c]));
|
||||||
|
const sortedCourses: typeof allCourses = [];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const visiting = new Set<string>();
|
||||||
|
|
||||||
|
// 拓扑排序函数
|
||||||
|
function visit(courseId: string) {
|
||||||
|
if (visiting.has(courseId)) {
|
||||||
|
return; // 避免循环依赖
|
||||||
|
}
|
||||||
|
if (visited.has(courseId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const course = courseMap.get(courseId);
|
||||||
|
if (!course) return;
|
||||||
|
|
||||||
|
visiting.add(courseId);
|
||||||
|
|
||||||
|
// 先访问依赖课程
|
||||||
|
const deps = courseDependencies[course.category] || [];
|
||||||
|
for (const depCategory of deps) {
|
||||||
|
const depCourses = allCourses.filter(c =>
|
||||||
|
c.category === depCategory &&
|
||||||
|
difficultyOrder[c.difficulty as keyof typeof difficultyOrder] <=
|
||||||
|
difficultyOrder[course.difficulty as keyof typeof difficultyOrder]
|
||||||
|
);
|
||||||
|
for (const depCourse of depCourses) {
|
||||||
|
visit(depCourse.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visiting.delete(courseId);
|
||||||
|
visited.add(courseId);
|
||||||
|
sortedCourses.push(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对每个兴趣领域的课程进行排序
|
||||||
|
for (const interest of interests) {
|
||||||
|
const interestCourses = allCourses.filter(c => c.category === interest);
|
||||||
|
for (const course of interestCourses) {
|
||||||
|
visit(course.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤掉不符合当前水平的课程
|
||||||
|
const userLevel = difficultyOrder[currentLevel as keyof typeof difficultyOrder];
|
||||||
|
const filteredCourses = sortedCourses.filter(course => {
|
||||||
|
const courseLevel = difficultyOrder[course.difficulty as keyof typeof difficultyOrder];
|
||||||
|
return courseLevel <= userLevel + 1; // 允许高一级的课程
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建学习路径
|
||||||
|
const pathName = `个性化学习路径 - ${interests.join(', ')}`;
|
||||||
|
const pathDescription = `基于您的兴趣领域 ${interests.join('、')} 和当前水平 ${currentLevel} 生成的学习路径`;
|
||||||
|
|
||||||
|
const path = await prisma.learningPath.create({
|
||||||
|
data: {
|
||||||
|
name: pathName,
|
||||||
|
description: pathDescription,
|
||||||
|
targetAudience: `水平: ${currentLevel}`,
|
||||||
|
pathItems: {
|
||||||
|
create: filteredCourses.flatMap((course, courseIndex) => {
|
||||||
|
const items = [];
|
||||||
|
// 添加课程节点
|
||||||
|
items.push({
|
||||||
|
courseId: course.id,
|
||||||
|
order: courseIndex * 100,
|
||||||
|
type: 'course',
|
||||||
|
});
|
||||||
|
// 添加章节节点
|
||||||
|
course.chapters.forEach((chapter, chapterIndex) => {
|
||||||
|
items.push({
|
||||||
|
courseId: course.id,
|
||||||
|
chapterId: chapter.id,
|
||||||
|
order: courseIndex * 100 + chapterIndex + 1,
|
||||||
|
type: 'chapter',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
pathItems: {
|
||||||
|
include: {
|
||||||
|
course: true,
|
||||||
|
chapter: true,
|
||||||
|
},
|
||||||
|
orderBy: { order: 'asc' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
18
backend/tsconfig.json
Normal file
18
backend/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI学习平台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
38
frontend/package.json
Normal file
38
frontend/package.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "@ai-learning/frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ai-learning/shared": "*",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.21.1",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
|
"rehype-highlight": "^7.0.0",
|
||||||
|
"highlight.js": "^11.9.0",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"zustand": "^4.4.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.43",
|
||||||
|
"@types/react-dom": "^18.2.17",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
30
frontend/src/App.tsx
Normal file
30
frontend/src/App.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
|
import Layout from './components/Layout';
|
||||||
|
import HomePage from './pages/HomePage';
|
||||||
|
import CoursesPage from './pages/CoursesPage';
|
||||||
|
import CourseDetailPage from './pages/CourseDetailPage';
|
||||||
|
import ChapterPage from './pages/ChapterPage';
|
||||||
|
import LearningPathPage from './pages/LearningPathPage';
|
||||||
|
import PathDetailPage from './pages/PathDetailPage';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Router>
|
||||||
|
<Layout>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/courses" element={<CoursesPage />} />
|
||||||
|
<Route path="/courses/:id" element={<CourseDetailPage />} />
|
||||||
|
<Route path="/chapters/:id" element={<ChapterPage />} />
|
||||||
|
<Route path="/paths" element={<LearningPathPage />} />
|
||||||
|
<Route path="/paths/:id" element={<PathDetailPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Layout>
|
||||||
|
</Router>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
48
frontend/src/components/ErrorBoundary.tsx
Normal file
48
frontend/src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('Uncaught error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
|
||||||
|
<h1 className="text-2xl font-bold text-red-600 mb-4">出现错误</h1>
|
||||||
|
<p className="text-gray-700 mb-4">
|
||||||
|
{this.state.error?.message || '未知错误'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded hover:bg-primary-700"
|
||||||
|
>
|
||||||
|
刷新页面
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
frontend/src/components/Layout.tsx
Normal file
78
frontend/src/components/Layout.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Layout({ children }: LayoutProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isActive = (path: string) => {
|
||||||
|
return location.pathname === path || location.pathname.startsWith(path + '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white shadow-sm border-b">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center h-16">
|
||||||
|
<Link to="/" className="flex items-center space-x-2">
|
||||||
|
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
||||||
|
<span className="text-white font-bold text-lg">AI</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xl font-bold text-gray-900">AI学习平台</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<nav className="flex space-x-1">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
isActive('/') && location.pathname === '/'
|
||||||
|
? 'bg-primary-100 text-primary-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
首页
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/courses"
|
||||||
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
isActive('/courses')
|
||||||
|
? 'bg-primary-100 text-primary-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
课程
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/paths"
|
||||||
|
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
isActive('/paths')
|
||||||
|
? 'bg-primary-100 text-primary-700'
|
||||||
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
学习路径
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t mt-auto">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<p className="text-center text-sm text-gray-500">
|
||||||
|
© 2024 AI学习平台. 面向开发者的AI知识学习平台.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
frontend/src/index.css
Normal file
59
frontend/src/index.css
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-gray-50 text-gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.prose {
|
||||||
|
@apply max-w-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h1 {
|
||||||
|
@apply text-3xl font-bold mt-8 mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2 {
|
||||||
|
@apply text-2xl font-semibold mt-6 mb-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3 {
|
||||||
|
@apply text-xl font-semibold mt-4 mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose p {
|
||||||
|
@apply mb-4 leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose code {
|
||||||
|
@apply bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
@apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre code {
|
||||||
|
@apply bg-transparent p-0 text-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose ul, .prose ol {
|
||||||
|
@apply mb-4 ml-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose li {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose a {
|
||||||
|
@apply text-primary-600 hover:text-primary-700 underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose blockquote {
|
||||||
|
@apply border-l-4 border-primary-300 pl-4 italic my-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
114
frontend/src/pages/ChapterPage.tsx
Normal file
114
frontend/src/pages/ChapterPage.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import rehypeHighlight from 'rehype-highlight';
|
||||||
|
import { chapterApi, progressApi } from '../services/api';
|
||||||
|
import type { Chapter } from '@ai-learning/shared';
|
||||||
|
import 'highlight.js/styles/github-dark.css';
|
||||||
|
|
||||||
|
export default function ChapterPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [chapter, setChapter] = useState<Chapter | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [completed, setCompleted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadChapter();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadChapter = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await chapterApi.getById(id);
|
||||||
|
setChapter(response.data);
|
||||||
|
|
||||||
|
// Load progress
|
||||||
|
if (response.data.courseId) {
|
||||||
|
try {
|
||||||
|
const progressResponse = await progressApi.getAll();
|
||||||
|
const progress = progressResponse.data.find(
|
||||||
|
p => p.chapterId === id && p.completed
|
||||||
|
);
|
||||||
|
setCompleted(!!progress);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore progress errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load chapter:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkComplete = async () => {
|
||||||
|
if (!chapter) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await progressApi.update({
|
||||||
|
courseId: chapter.courseId,
|
||||||
|
chapterId: chapter.id,
|
||||||
|
completed: !completed,
|
||||||
|
});
|
||||||
|
setCompleted(!completed);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update progress:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chapter) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">章节不存在</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link
|
||||||
|
to={`/courses/${chapter.courseId}`}
|
||||||
|
className="text-primary-600 hover:text-primary-700 text-sm mb-4 inline-block"
|
||||||
|
>
|
||||||
|
← 返回课程
|
||||||
|
</Link>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900">{chapter.title}</h1>
|
||||||
|
<button
|
||||||
|
onClick={handleMarkComplete}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
completed
|
||||||
|
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{completed ? '✓ 已完成' : '标记为完成'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8 prose prose-lg max-w-none">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeHighlight]}
|
||||||
|
>
|
||||||
|
{chapter.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
frontend/src/pages/CourseDetailPage.tsx
Normal file
105
frontend/src/pages/CourseDetailPage.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
import { courseApi } from '../services/api';
|
||||||
|
import type { Course, Chapter } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
export default function CourseDetailPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [course, setCourse] = useState<Course | null>(null);
|
||||||
|
const [chapters, setChapters] = useState<Chapter[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadCourse();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadCourse = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const [courseResponse, chaptersResponse] = await Promise.all([
|
||||||
|
courseApi.getById(id),
|
||||||
|
courseApi.getChapters(id),
|
||||||
|
]);
|
||||||
|
setCourse(courseResponse.data);
|
||||||
|
setChapters(chaptersResponse.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load course:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">课程不存在</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8 mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{course.title}</h1>
|
||||||
|
<p className="text-lg text-gray-600 mb-6">{course.description}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4 text-sm text-gray-500">
|
||||||
|
<span>分类: {course.category}</span>
|
||||||
|
<span>难度: {course.difficulty}</span>
|
||||||
|
<span>预计时长: {course.estimatedHours} 小时</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-6">课程章节</h2>
|
||||||
|
|
||||||
|
{chapters.length === 0 ? (
|
||||||
|
<p className="text-gray-500">暂无章节</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{chapters.map((chapter) => (
|
||||||
|
<Link
|
||||||
|
key={chapter.id}
|
||||||
|
to={`/chapters/${chapter.id}`}
|
||||||
|
className="block p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">
|
||||||
|
{chapter.order}. {chapter.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
frontend/src/pages/CoursesPage.tsx
Normal file
145
frontend/src/pages/CoursesPage.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import { courseApi } from '../services/api';
|
||||||
|
import type { Course } from '@ai-learning/shared';
|
||||||
|
import { CourseCategory } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
const categoryLabels: Record<CourseCategory, string> = {
|
||||||
|
[CourseCategory.ML_BASICS]: '机器学习基础',
|
||||||
|
[CourseCategory.DEEP_LEARNING]: '深度学习',
|
||||||
|
[CourseCategory.NLP]: '自然语言处理',
|
||||||
|
[CourseCategory.LLM]: '大语言模型',
|
||||||
|
[CourseCategory.AI_TOOLS]: 'AI工具',
|
||||||
|
};
|
||||||
|
|
||||||
|
const difficultyLabels: Record<string, string> = {
|
||||||
|
BEGINNER: '初级',
|
||||||
|
INTERMEDIATE: '中级',
|
||||||
|
ADVANCED: '高级',
|
||||||
|
};
|
||||||
|
|
||||||
|
const difficultyColors: Record<string, string> = {
|
||||||
|
BEGINNER: 'bg-green-100 text-green-800',
|
||||||
|
INTERMEDIATE: 'bg-yellow-100 text-yellow-800',
|
||||||
|
ADVANCED: 'bg-red-100 text-red-800',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CoursesPage() {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [courses, setCourses] = useState<Course[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const category = searchParams.get('category') || '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadCourses();
|
||||||
|
}, [category]);
|
||||||
|
|
||||||
|
const loadCourses = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await courseApi.getAll(category || undefined);
|
||||||
|
console.log('Courses loaded:', response.data);
|
||||||
|
setCourses(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load courses:', error);
|
||||||
|
console.error('Error details:', error.response?.data || error.message);
|
||||||
|
// 显示错误信息给用户
|
||||||
|
alert('加载课程失败: ' + (error.response?.data?.error || error.message));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCategoryChange = (newCategory: string) => {
|
||||||
|
if (newCategory) {
|
||||||
|
setSearchParams({ category: newCategory });
|
||||||
|
} else {
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||||
|
<p className="mt-4 text-gray-600">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">所有课程</h1>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-6">
|
||||||
|
<button
|
||||||
|
onClick={() => handleCategoryChange('')}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
!category
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</button>
|
||||||
|
{Object.entries(categoryLabels).map(([key, label]) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
onClick={() => handleCategoryChange(key)}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
category === key
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{courses.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-gray-500 text-lg">暂无课程</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{courses.map((course) => (
|
||||||
|
<Link
|
||||||
|
key={course.id}
|
||||||
|
to={`/courses/${course.id}`}
|
||||||
|
className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="mb-4">
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{categoryLabels[course.category as CourseCategory]}
|
||||||
|
</span>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mt-2 mb-2">
|
||||||
|
{course.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 text-sm line-clamp-2">
|
||||||
|
{course.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
difficultyColors[course.difficulty] || difficultyColors.BEGINNER
|
||||||
|
}`}>
|
||||||
|
{difficultyLabels[course.difficulty] || course.difficulty}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{course.estimatedHours} 小时
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
frontend/src/pages/HomePage.tsx
Normal file
138
frontend/src/pages/HomePage.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { CourseCategory } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
const categoryInfo = {
|
||||||
|
[CourseCategory.ML_BASICS]: {
|
||||||
|
name: '机器学习基础',
|
||||||
|
description: '从零开始学习机器学习的基本概念和算法',
|
||||||
|
color: 'bg-blue-500',
|
||||||
|
},
|
||||||
|
[CourseCategory.DEEP_LEARNING]: {
|
||||||
|
name: '深度学习',
|
||||||
|
description: '深入理解神经网络和深度学习技术',
|
||||||
|
color: 'bg-purple-500',
|
||||||
|
},
|
||||||
|
[CourseCategory.NLP]: {
|
||||||
|
name: '自然语言处理',
|
||||||
|
description: '掌握文本处理和语言模型的核心技术',
|
||||||
|
color: 'bg-green-500',
|
||||||
|
},
|
||||||
|
[CourseCategory.LLM]: {
|
||||||
|
name: '大语言模型',
|
||||||
|
description: '学习GPT、BERT等大语言模型的原理和应用',
|
||||||
|
color: 'bg-orange-500',
|
||||||
|
},
|
||||||
|
[CourseCategory.AI_TOOLS]: {
|
||||||
|
name: 'AI工具',
|
||||||
|
description: '掌握LangChain、Hugging Face等实用AI工具',
|
||||||
|
color: 'bg-pink-500',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h1 className="text-5xl font-bold text-gray-900 mb-4">
|
||||||
|
欢迎来到 AI 学习平台
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
|
||||||
|
面向开发者的AI知识学习平台,提供结构化的课程内容和智能学习路径推荐
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center space-x-4">
|
||||||
|
<Link
|
||||||
|
to="/courses"
|
||||||
|
className="px-6 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 transition-colors"
|
||||||
|
>
|
||||||
|
浏览课程
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/paths"
|
||||||
|
className="px-6 py-3 bg-white text-primary-600 border-2 border-primary-600 rounded-lg font-medium hover:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
创建学习路径
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
<div className="mb-16">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-8 text-center">
|
||||||
|
学习分类
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{Object.entries(categoryInfo).map(([category, info]) => (
|
||||||
|
<Link
|
||||||
|
key={category}
|
||||||
|
to={`/courses?category=${category}`}
|
||||||
|
className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<div className={`w-12 h-12 ${info.color} rounded-lg mb-4 flex items-center justify-center`}>
|
||||||
|
<span className="text-white font-bold text-xl">
|
||||||
|
{info.name[0]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
{info.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{info.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-8 text-center">
|
||||||
|
平台特色
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
结构化课程
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
精心设计的课程体系,从基础到进阶,循序渐进
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
个性化路径
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
基于您的兴趣和水平,智能推荐最适合的学习路径
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
进度跟踪
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
实时记录学习进度,帮助您更好地规划学习
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
175
frontend/src/pages/LearningPathPage.tsx
Normal file
175
frontend/src/pages/LearningPathPage.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { pathApi } from '../services/api';
|
||||||
|
import type { LearningPath } from '@ai-learning/shared';
|
||||||
|
import { CourseCategory, Difficulty } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
const categoryOptions: { value: CourseCategory; label: string }[] = [
|
||||||
|
{ value: CourseCategory.ML_BASICS, label: '机器学习基础' },
|
||||||
|
{ value: CourseCategory.DEEP_LEARNING, label: '深度学习' },
|
||||||
|
{ value: CourseCategory.NLP, label: '自然语言处理' },
|
||||||
|
{ value: CourseCategory.LLM, label: '大语言模型' },
|
||||||
|
{ value: CourseCategory.AI_TOOLS, label: 'AI工具' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const difficultyOptions: { value: Difficulty; label: string }[] = [
|
||||||
|
{ value: Difficulty.BEGINNER, label: '初级' },
|
||||||
|
{ value: Difficulty.INTERMEDIATE, label: '中级' },
|
||||||
|
{ value: Difficulty.ADVANCED, label: '高级' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LearningPathPage() {
|
||||||
|
const [paths, setPaths] = useState<LearningPath[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedInterests, setSelectedInterests] = useState<CourseCategory[]>([]);
|
||||||
|
const [selectedLevel, setSelectedLevel] = useState<Difficulty>(Difficulty.BEGINNER);
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadPaths();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadPaths = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await pathApi.getAll();
|
||||||
|
setPaths(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load paths:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInterestToggle = (category: CourseCategory) => {
|
||||||
|
setSelectedInterests((prev) =>
|
||||||
|
prev.includes(category)
|
||||||
|
? prev.filter((c) => c !== category)
|
||||||
|
: [...prev, category]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGeneratePath = async () => {
|
||||||
|
if (selectedInterests.length === 0) {
|
||||||
|
alert('请至少选择一个兴趣领域');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setGenerating(true);
|
||||||
|
const response = await pathApi.generate({
|
||||||
|
interests: selectedInterests,
|
||||||
|
currentLevel: selectedLevel,
|
||||||
|
});
|
||||||
|
await loadPaths(); // Reload paths to show the new one
|
||||||
|
// Navigate to the new path
|
||||||
|
window.location.href = `/paths/${response.data.id}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate path:', error);
|
||||||
|
alert('生成学习路径失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-8">学习路径</h1>
|
||||||
|
|
||||||
|
{/* Generate Path Form */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8 mb-8">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-6">创建个性化学习路径</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||||
|
选择兴趣领域(可多选)
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{categoryOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => handleInterestToggle(option.value)}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
selectedInterests.includes(option.value)
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||||
|
当前水平
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{difficultyOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => setSelectedLevel(option.value)}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
selectedLevel === option.value
|
||||||
|
? 'bg-primary-600 text-white'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleGeneratePath}
|
||||||
|
disabled={generating || selectedInterests.length === 0}
|
||||||
|
className="px-6 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{generating ? '生成中...' : '生成学习路径'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Existing Paths */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-6">已有学习路径</h2>
|
||||||
|
|
||||||
|
{paths.length === 0 ? (
|
||||||
|
<div className="text-center py-12 bg-white rounded-lg shadow-md">
|
||||||
|
<p className="text-gray-500 text-lg">暂无学习路径,请创建一个</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{paths.map((path) => (
|
||||||
|
<Link
|
||||||
|
key={path.id}
|
||||||
|
to={`/paths/${path.id}`}
|
||||||
|
className="block bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
{path.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">{path.description}</p>
|
||||||
|
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||||
|
<span>目标受众: {path.targetAudience}</span>
|
||||||
|
<span>{path.pathItems?.length || 0} 个学习项</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
146
frontend/src/pages/PathDetailPage.tsx
Normal file
146
frontend/src/pages/PathDetailPage.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
import { pathApi } from '../services/api';
|
||||||
|
import type { LearningPath } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
export default function PathDetailPage() {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [path, setPath] = useState<LearningPath | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
loadPath();
|
||||||
|
}
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const loadPath = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await pathApi.getById(id);
|
||||||
|
setPath(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load path:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="text-center">学习路径不存在</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<Link
|
||||||
|
to="/paths"
|
||||||
|
className="text-primary-600 hover:text-primary-700 text-sm mb-4 inline-block"
|
||||||
|
>
|
||||||
|
← 返回学习路径
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8 mb-8">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{path.name}</h1>
|
||||||
|
<p className="text-lg text-gray-600 mb-4">{path.description}</p>
|
||||||
|
<p className="text-sm text-gray-500">目标受众: {path.targetAudience}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-8">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-6">学习路径</h2>
|
||||||
|
|
||||||
|
{!path.pathItems || path.pathItems.length === 0 ? (
|
||||||
|
<p className="text-gray-500">路径为空</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{path.pathItems.map((item, index) => (
|
||||||
|
<div key={item.id} className="flex items-start space-x-4">
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 bg-primary-600 text-white rounded-full flex items-center justify-center font-semibold">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 border-l-2 border-primary-200 pl-4 pb-4 last:border-l-0">
|
||||||
|
{item.type === 'course' ? (
|
||||||
|
<Link
|
||||||
|
to={`/courses/${item.courseId}`}
|
||||||
|
className="block p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-gray-500 uppercase">课程</span>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mt-1">
|
||||||
|
{item.course?.title || '未知课程'}
|
||||||
|
</h3>
|
||||||
|
{item.course?.description && (
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
{item.course.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to={`/chapters/${item.chapterId}`}
|
||||||
|
className="block p-4 border border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-gray-500 uppercase">章节</span>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mt-1">
|
||||||
|
{item.chapter?.title || '未知章节'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
来自: {item.course?.title || '未知课程'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
frontend/src/services/api.ts
Normal file
70
frontend/src/services/api.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type { Course, Chapter, LearningPath, UserProgress, GeneratePathRequest } from '@ai-learning/shared';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加请求拦截器用于调试
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
console.log('API Request:', config.method?.toUpperCase(), config.url);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('API Request Error:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加响应拦截器用于调试
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
console.log('API Response:', response.config.url, response.status);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('API Response Error:', error.config?.url, error.response?.status, error.response?.data);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const courseApi = {
|
||||||
|
getAll: (category?: string) =>
|
||||||
|
api.get<Course[]>('/courses', { params: { category } }),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
api.get<Course>(`/courses/${id}`),
|
||||||
|
|
||||||
|
getChapters: (courseId: string) =>
|
||||||
|
api.get<Chapter[]>(`/courses/${courseId}/chapters`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chapterApi = {
|
||||||
|
getById: (id: string) =>
|
||||||
|
api.get<Chapter>(`/chapters/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pathApi = {
|
||||||
|
getAll: () =>
|
||||||
|
api.get<LearningPath[]>('/paths'),
|
||||||
|
|
||||||
|
generate: (data: GeneratePathRequest) =>
|
||||||
|
api.post<LearningPath>('/paths/generate', data),
|
||||||
|
|
||||||
|
getById: (id: string) =>
|
||||||
|
api.get<LearningPath>(`/paths/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressApi = {
|
||||||
|
getAll: (userId?: string) =>
|
||||||
|
api.get<UserProgress[]>('/progress', { params: { userId } }),
|
||||||
|
|
||||||
|
update: (data: { courseId: string; chapterId?: string; completed: boolean }) =>
|
||||||
|
api.post<UserProgress>('/progress', data),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
26
frontend/tailwind.config.js
Normal file
26
frontend/tailwind.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#f0f9ff',
|
||||||
|
100: '#e0f2fe',
|
||||||
|
200: '#bae6fd',
|
||||||
|
300: '#7dd3fc',
|
||||||
|
400: '#38bdf8',
|
||||||
|
500: '#0ea5e9',
|
||||||
|
600: '#0284c7',
|
||||||
|
700: '#0369a1',
|
||||||
|
800: '#075985',
|
||||||
|
900: '#0c4a6e',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
21
frontend/vite.config.ts
Normal file
21
frontend/vite.config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
7777
package-lock.json
generated
Normal file
7777
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-learning-platform",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "AI Learning Platform for Developers",
|
||||||
|
"private": true,
|
||||||
|
"workspaces": [
|
||||||
|
"frontend",
|
||||||
|
"backend",
|
||||||
|
"shared"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
||||||
|
"dev:frontend": "npm run dev --workspace=frontend",
|
||||||
|
"dev:backend": "npm run dev --workspace=backend",
|
||||||
|
"build": "npm run build --workspace=frontend && npm run build --workspace=backend",
|
||||||
|
"install:all": "npm install && npm install --workspace=frontend && npm install --workspace=backend && npm install --workspace=shared"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"concurrently": "^8.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
shared/package.json
Normal file
11
shared/package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@ai-learning/shared",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
77
shared/src/index.ts
Normal file
77
shared/src/index.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// Course types
|
||||||
|
export interface Course {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
category: CourseCategory;
|
||||||
|
difficulty: Difficulty;
|
||||||
|
estimatedHours: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Chapter {
|
||||||
|
id: string;
|
||||||
|
courseId: string;
|
||||||
|
title: string;
|
||||||
|
order: number;
|
||||||
|
content: string; // Markdown content
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LearningPath {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
targetAudience: string;
|
||||||
|
pathItems: PathItem[];
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathItem {
|
||||||
|
id: string;
|
||||||
|
pathId: string;
|
||||||
|
courseId: string;
|
||||||
|
chapterId?: string;
|
||||||
|
order: number;
|
||||||
|
type: 'course' | 'chapter';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProgress {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
courseId: string;
|
||||||
|
chapterId?: string;
|
||||||
|
completed: boolean;
|
||||||
|
lastAccessed: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CourseCategory {
|
||||||
|
ML_BASICS = 'ML_BASICS',
|
||||||
|
DEEP_LEARNING = 'DEEP_LEARNING',
|
||||||
|
NLP = 'NLP',
|
||||||
|
LLM = 'LLM',
|
||||||
|
AI_TOOLS = 'AI_TOOLS'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Difficulty {
|
||||||
|
BEGINNER = 'BEGINNER',
|
||||||
|
INTERMEDIATE = 'INTERMEDIATE',
|
||||||
|
ADVANCED = 'ADVANCED'
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Request/Response types
|
||||||
|
export interface GeneratePathRequest {
|
||||||
|
interests: CourseCategory[];
|
||||||
|
currentLevel: Difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateProgressRequest {
|
||||||
|
courseId: string;
|
||||||
|
chapterId?: string;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
17
shared/tsconfig.json
Normal file
17
shared/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user