feat: Phase 1-3 全部完成 — 沙盒增强、学情分析、学习路径
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { LearningService } from './learning.service';
|
||||
|
||||
@ApiTags('学习分析')
|
||||
@Controller('learning')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@ApiBearerAuth()
|
||||
export class LearningController {
|
||||
constructor(private learningService: LearningService) {}
|
||||
|
||||
@Get('analytics')
|
||||
async getAnalytics(@Req() req: any) {
|
||||
return this.learningService.getAnalytics(req.user.userId);
|
||||
}
|
||||
|
||||
@Get('path')
|
||||
async getLearningPath(@Req() req: any) {
|
||||
return this.learningService.getLearningPath(req.user.userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LearningController } from './learning.controller';
|
||||
import { LearningService } from './learning.service';
|
||||
|
||||
@Module({
|
||||
controllers: [LearningController],
|
||||
providers: [LearningService],
|
||||
exports: [LearningService],
|
||||
})
|
||||
export class LearningModule {}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
const KNOWLEDGE_DOMAINS = [
|
||||
{ id: 'ai-basics', name: 'AI 基础知识', keywords: ['AI', '人工智能', '大模型', 'chatgpt', 'gpt', '大语言模型', 'llm', '深度学习', '神经网络', 'machine learning', '机器学习'] },
|
||||
{ id: 'prompt-engineering', name: '提示词工程', keywords: ['提示词', 'prompt', 'system prompt', 'role', 'few-shot', 'chain-of-thought', 'cot'] },
|
||||
{ id: 'programming', name: '编程开发', keywords: ['python', 'javascript', 'typescript', 'java', '代码', '函数', '算法', 'debug', 'bug', '编程', '开发', 'react', 'vue', 'node'] },
|
||||
{ id: 'writing', name: '写作创作', keywords: ['写作', '文章', '文案', '润色', '作文', '创作', '故事', '小说', '博客'] },
|
||||
{ id: 'english', name: '英语学习', keywords: ['英语', 'english', '翻译', '语法', 'grammar', 'vocabulary', '口语', '写作', '阅读'] },
|
||||
{ id: 'data-science', name: '数据分析', keywords: ['数据', '分析', '统计', '图表', '可视化', 'sql', 'excel', 'pandas', 'numpy', '数据分析'] },
|
||||
{ id: 'office', name: '办公效率', keywords: ['ppt', 'excel', 'word', '办公', '邮件', '报告', '文档', '会议', '总结'] },
|
||||
{ id: 'career', name: '职业发展', keywords: ['简历', '面试', '求职', '职业', '工作', '升职', '薪资'] },
|
||||
];
|
||||
|
||||
const COURSE_RECOMMENDATIONS: Record<string, { title: string; url: string }[]> = {
|
||||
'ai-basics': [
|
||||
{ title: 'AI 通识:零基础入门', url: '/courses' },
|
||||
{ title: '大模型原理与应用', url: '/courses' },
|
||||
],
|
||||
'prompt-engineering': [
|
||||
{ title: '提示词工程从入门到精通', url: '/courses' },
|
||||
{ title: '高级 Prompt 技巧', url: '/prompts' },
|
||||
],
|
||||
'programming': [
|
||||
{ title: '用 Python 入门 AI 编程', url: '/courses' },
|
||||
{ title: 'AI 辅助编程实战', url: '/sandbox' },
|
||||
],
|
||||
'writing': [
|
||||
{ title: 'AI 写作实战指南', url: '/prompts/workshop' },
|
||||
{ title: '内容创作与润色技巧', url: '/courses' },
|
||||
],
|
||||
'english': [
|
||||
{ title: 'AI 辅助英语学习', url: '/sandbox' },
|
||||
{ title: '英语写作提升课程', url: '/courses' },
|
||||
],
|
||||
'data-science': [
|
||||
{ title: '数据分析入门', url: '/courses' },
|
||||
{ title: 'Python 数据分析', url: '/courses' },
|
||||
],
|
||||
'office': [
|
||||
{ title: '用 AI 提升 10 倍办公效率', url: '/courses' },
|
||||
{ title: 'AI 办公自动化', url: '/courses' },
|
||||
],
|
||||
'career': [
|
||||
{ title: 'AI 时代职业规划', url: '/courses' },
|
||||
{ title: '面试技巧与简历优化', url: '/sandbox' },
|
||||
],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class LearningService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getAnalytics(userId: number) {
|
||||
const sessions = await this.prisma.sandboxSession.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
|
||||
const domainCounts: Record<string, number> = {};
|
||||
const domainDates: Record<string, string> = {};
|
||||
const totalSessions = sessions.length;
|
||||
|
||||
for (const domain of KNOWLEDGE_DOMAINS) {
|
||||
domainCounts[domain.id] = 0;
|
||||
}
|
||||
|
||||
for (const session of sessions) {
|
||||
const searchText = `${session.title} ${session.messages || ''}`.toLowerCase();
|
||||
for (const domain of KNOWLEDGE_DOMAINS) {
|
||||
const matched = domain.keywords.some(kw => searchText.includes(kw));
|
||||
if (matched) {
|
||||
domainCounts[domain.id] = (domainCounts[domain.id] || 0) + 1;
|
||||
if (!domainDates[domain.id] || session.createdAt.toISOString() > domainDates[domain.id]) {
|
||||
domainDates[domain.id] = session.createdAt.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const domains = KNOWLEDGE_DOMAINS.map(d => {
|
||||
const count = domainCounts[d.id] || 0;
|
||||
const totalSessionsForUser = Math.max(totalSessions, 1);
|
||||
const mastery = Math.min(Math.round((count / Math.max(totalSessionsForUser * 0.3, 1)) * 100), 100);
|
||||
return {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
sessionCount: count,
|
||||
mastery,
|
||||
lastActive: domainDates[d.id] || null,
|
||||
weak: mastery < 30,
|
||||
};
|
||||
});
|
||||
|
||||
const weakDomains = domains.filter(d => d.weak);
|
||||
const recommendations = weakDomains.length > 0
|
||||
? weakDomains.slice(0, 3).flatMap(d => (COURSE_RECOMMENDATIONS[d.id] || []).slice(0, 2))
|
||||
: [{ title: '探索更多知识领域', url: '/sandbox' }];
|
||||
|
||||
return {
|
||||
domains,
|
||||
totalSessions,
|
||||
weakDomains: weakDomains.map(d => d.name),
|
||||
recommendations: [...new Map(recommendations.map(r => [r.title, r])).values()],
|
||||
};
|
||||
}
|
||||
|
||||
async getLearningPath(userId: number) {
|
||||
const sessions = await this.prisma.sandboxSession.findMany({
|
||||
where: { userId },
|
||||
select: { title: true, messages: true },
|
||||
});
|
||||
|
||||
const allText = sessions.map(s => `${s.title} ${s.messages || ''}`.toLowerCase()).join(' ');
|
||||
|
||||
const stages = [
|
||||
{
|
||||
id: 'basics',
|
||||
title: '认识大模型',
|
||||
icon: '🤖',
|
||||
description: '了解 AI 和大型语言模型的基本概念,学会如何使用 AI 工具。',
|
||||
tasks: [
|
||||
{ label: '了解 AI 基本概念', action: '向 AI 提问什么是人工智能', keyword: '人工智能' },
|
||||
{ label: '认识大语言模型', action: '向 AI 提问什么是大模型', keyword: '大模型' },
|
||||
{ label: '体验 AI 对话', action: '在沙盒中发起一次对话', keyword: '' },
|
||||
],
|
||||
links: [
|
||||
{ title: 'AI 通识:零基础入门', url: '/courses' },
|
||||
{ title: '打开 AI 沙盒', url: '/sandbox' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'prompt',
|
||||
title: '提示词工程',
|
||||
icon: '✍️',
|
||||
description: '学习如何编写高质量的提示词,掌握与 AI 高效沟通的技巧。',
|
||||
tasks: [
|
||||
{ label: '了解提示词基础', action: '询问提示词编写技巧', keyword: '提示词' },
|
||||
{ label: '练习提示词编写', action: '在提示词工坊中测试', keyword: '' },
|
||||
{ label: '保存优质提示词', action: '将好的提示词保存到库中', keyword: '' },
|
||||
],
|
||||
links: [
|
||||
{ title: '提示词工坊', url: '/prompts/workshop' },
|
||||
{ title: '提示词库', url: '/prompts' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: '模型微调与高级应用',
|
||||
icon: '⚙️',
|
||||
description: '了解模型微调、RAG、Function Calling 等高级技术。',
|
||||
tasks: [
|
||||
{ label: '了解模型微调', action: '询问什么是模型微调', keyword: '微调' },
|
||||
{ label: '了解 RAG', action: '询问什么是 RAG 检索增强', keyword: 'rag' },
|
||||
{ label: '了解 Function Calling', action: '询问 function calling 是什么', keyword: 'function calling' },
|
||||
],
|
||||
links: [
|
||||
{ title: 'AI 沙盒 - 对比模式', url: '/sandbox/compare' },
|
||||
{ title: '代码沙盒', url: '/sandbox/code' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'agent',
|
||||
title: 'Agent 开发',
|
||||
icon: '🚀',
|
||||
description: '学习构建 AI Agent,实现自动化任务和复杂工作流。',
|
||||
tasks: [
|
||||
{ label: '了解 AI Agent', action: '询问什么是 AI Agent', keyword: 'agent' },
|
||||
{ label: '学习工具调用', action: '询问 AI 工具调用机制', keyword: 'tool' },
|
||||
{ label: '实践项目', action: '尝试用 AI 构建一个小项目', keyword: '' },
|
||||
],
|
||||
links: [
|
||||
{ title: '代码沙盒 - 运行项目', url: '/sandbox/code' },
|
||||
{ title: '对比实验室', url: '/sandbox/compare' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return stages.map(stage => {
|
||||
const completedCount = stage.tasks.filter(t => !t.keyword || allText.includes(t.keyword)).length;
|
||||
const progress = stage.tasks.length > 0 ? Math.round((completedCount / stage.tasks.length) * 100) : 0;
|
||||
return {
|
||||
...stage,
|
||||
completedCount,
|
||||
totalTasks: stage.tasks.length,
|
||||
progress,
|
||||
unlocked: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user