first commit

This commit is contained in:
caoyuchun 2026-01-13 09:59:40 +08:00
commit 00af1f959f
46 changed files with 11049 additions and 0 deletions

98
.gitignore vendored Normal file
View 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
View 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
View 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
View 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"
}
}

View File

View 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");

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

View 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
View 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 - )(y_i - ȳ) / Σ(x_i - )²
b = ȳ - w *
\\\`\\\`\\\`
### 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
- ****
- ****//
##
01
###
- 10
- 10
- 10
## 线
线[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)\`
##
线使
##
CNNRNN等模型非常重要`,
},
],
},
},
});
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: '深入理解GPTGenerative 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技巧
- ****
- ****
- ****JSONMarkdown等格式
##
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();
});

View 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);
}

View 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' 启动服务器了"

View 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' });
}
};

View 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' });
}
};

View 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' });
}
};

View 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
View 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}`);
});

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

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

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

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

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

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

30
frontend/src/App.tsx Normal file
View 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;

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

View 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
View 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
View 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>,
);

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

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

View 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
View 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" }]
}

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

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View 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
View 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
View 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
View 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"]
}