092_UI_core / src /modules /ta /ta.controller.ts
anotherath's picture
Avoid duplicate PDF indexing for recap
391859f
Raw
History Blame Contribute Delete
15 kB
import {
Controller,
Get,
Post,
Patch,
Put,
Param,
Query,
Body,
Req,
UseGuards,
ParseUUIDPipe,
ForbiddenException,
NotFoundException,
BadRequestException,
Logger,
UseInterceptors,
UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiTags, ApiBearerAuth, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
import { TaService } from './ta.service';
import { SupabaseService } from '../../database/supabase.service';
import { IngestSignalDto } from './dto/ingest-signal.dto';
import { AnalyzeAtRiskQueryDto } from './dto/analyze-at-risk.dto';
import { GenerateQuizDto, UpdateQuizDto, SendQuizDto, SubmitAttemptDto } from './dto/quiz.dto';
interface RequestWithUser extends Request {
user: { id: string };
}
@ApiTags('TA Dashboard')
@Controller('ta')
export class TaController {
private readonly logger = new Logger(TaController.name);
constructor(
private readonly taService: TaService,
private readonly supabaseService: SupabaseService,
) {}
private async checkTaPermission(spaceId: string, userId: string) {
const { data: member, error } = await this.supabaseService
.from('space_members')
.select('role')
.eq('space_id', spaceId)
.eq('user_id', userId)
.single();
if (error || !member || (member.role !== 'owner' && member.role !== 'admin')) {
const { data: space } = await this.supabaseService
.from('spaces')
.select('owner_id')
.eq('id', spaceId)
.single();
if (space?.owner_id === userId) return;
throw new ForbiddenException('Chỉ Owner hoặc Admin mới có quyền truy cập Dashboard');
}
}
private async checkSpaceMembership(spaceId: string, userId: string) {
const { data: member } = await this.supabaseService
.from('space_members')
.select('role')
.eq('space_id', spaceId)
.eq('user_id', userId)
.single();
if (!member) {
throw new ForbiddenException('Bạn không phải là thành viên của không gian này');
}
}
@ApiOperation({ summary: '[TEST] Tạo dữ liệu Stress Test' })
@Post('test/generate-stress-data')
async generateStressData(@Query('spaceId', ParseUUIDPipe) spaceId: string) {
return this.taService.generateStressTestData(spaceId);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Post('scan-at-risk')
async scanAtRisk(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
return await this.taService.scanAtRiskStudents(spaceId);
}
@ApiOperation({ summary: 'Thu nạp tín hiệu rủi ro' })
@Post('ingest-signal')
async ingestSignal(@Req() req: RequestWithUser, @Body() dto: IngestSignalDto) {
await this.checkTaPermission(dto.space_id, req.user.id);
return await this.taService.ingestSignal(dto);
}
@ApiOperation({ summary: 'Lấy danh sách học viên có nguy cơ' })
@Get('at-risk')
async getAtRisk(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.getAtRiskList(spaceId);
return { success: true, data };
}
@ApiOperation({ summary: 'Phân tích real-time signals từ chat messages' })
@Post('at-risk/analyze')
async analyzeAtRisk(@Req() req: RequestWithUser, @Query() dto: AnalyzeAtRiskQueryDto) {
await this.checkTaPermission(dto.spaceId, req.user.id);
const hours = dto.hours ?? 168;
const data = await this.taService.analyzeAtRiskSignals(dto.spaceId, hours);
return data;
}
@ApiOperation({ summary: 'Đánh dấu đã giải quyết cảnh báo' })
@Post('at-risk/:id/resolve')
async resolveAlert(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) snapshotId: string, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.resolveAlert(snapshotId, req.user.id, spaceId);
return { success: true, data };
}
@ApiOperation({ summary: 'Lấy hàng chờ duyệt tóm tắt AI' })
@Get('summary-queue')
async getSummaryQueue(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.getSummaryQueue(spaceId);
return { success: true, data };
}
@ApiOperation({ summary: 'Duyệt bản tóm tắt AI' })
@Post('summary-queue/:id/approve')
async approveSummary(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) draftId: string, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.approveSummary(draftId, req.user.id, spaceId);
return { success: true, data };
}
@ApiOperation({ summary: 'Đặt lịch gửi bản tóm tắt AI' })
@Post('summary-queue/:id/schedule')
async scheduleSummary(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) draftId: string, @Query('spaceId', ParseUUIDPipe) spaceId: string, @Body('scheduled_at') scheduledAt: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.scheduleSummary(draftId, scheduledAt, req.user.id);
return { success: true, data };
}
@ApiOperation({ summary: 'Hủy đặt lịch' })
@Post('summary-queue/:id/cancel-schedule')
async cancelSchedule(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) draftId: string, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.cancelSchedule(draftId);
return { success: true, data };
}
@ApiOperation({ summary: 'Cập nhật bản thảo' })
@Patch('summary-queue/:id')
async updateSummaryDraft(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) draftId: string, @Query('spaceId', ParseUUIDPipe) spaceId: string, @Body() updates: Record<string, unknown>) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.updateSummaryDraft(draftId, updates);
return { success: true, data };
}
@ApiOperation({ summary: 'Gửi tin nhắn riêng thông minh' })
@Post('send-smart-message')
async sendSmartMessage(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string, @Body() dto: { studentId: string, content: string, snapshotId?: string }) {
await this.checkTaPermission(spaceId, req.user.id);
return this.taService.sendSmartMessage({ taId: req.user.id, spaceId, ...dto });
}
@ApiOperation({ summary: 'AI: Tạo bản thảo tóm tắt' })
@Post('summary-queue/draft')
async createSummaryDraft(@Req() req: RequestWithUser, @Body() dto: { spaceId: string; content: string; draft_type?: string; metadata?: Record<string, unknown> }) {
if (dto.spaceId) await this.checkTaPermission(dto.spaceId, req.user.id);
const data = await this.taService.createSummaryDraft(dto);
return { success: true, data };
}
@ApiOperation({ summary: 'Lấy nhật ký hành động' })
@Get('action-logs')
async getActionLogs(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.getActionLogs(spaceId);
return { success: true, data };
}
@ApiOperation({ summary: 'Lấy bộ Context cho Agent' })
@Get('at-risk-context/:snapshotId')
async getAtRiskContext(@Req() req: RequestWithUser, @Param('snapshotId', ParseUUIDPipe) snapshotId: string, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.getAtRiskContext(snapshotId);
return { success: true, data };
}
@ApiOperation({ summary: 'Tải lên slide' })
@Post('upload-slide')
@UseInterceptors(FileInterceptor('file'))
async uploadSlide(@Req() req: RequestWithUser, @UploadedFile() file: Express.Multer.File, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
const data = await this.taService.saveLectureSlide(spaceId, file, req.user.id);
return { success: true, data };
}
@ApiOperation({ summary: 'AI: Index PDF vào Qdrant' })
@Post('agent/index-pdf')
@UseInterceptors(FileInterceptor('file'))
async indexPdf(@Req() req: RequestWithUser, @UploadedFile() file: Express.Multer.File, @Body() body: { spaceId: string; conversationId: string }) {
await this.checkTaPermission(body.spaceId, req.user.id);
if (!file.mimetype?.includes('pdf')) {
throw new BadRequestException('Chỉ chấp nhận file PDF.');
}
return await this.taService.indexPdfToQdrant(body.spaceId, file, body.conversationId);
}
@ApiOperation({ summary: 'AI: Gọi Agent Chat' })
@Post('agent/chat')
async callAgentChat(@Req() req: RequestWithUser, @Body() body: { spaceId: string; query: string; senderId: string }) {
await this.checkTaPermission(body.spaceId, req.user.id);
return await this.taService.callAgentChat(body.spaceId, body.query, body.senderId);
}
@ApiOperation({ summary: 'AI: Gọi Agent với File' })
@ApiQuery({ name: 'spaceId', type: String, description: 'ID của không gian' })
@ApiQuery({ name: 'query', type: String, required: false, description: 'Câu hỏi về file' })
@ApiQuery({ name: 'conversationId', type: String, required: false, description: 'ID conversation FastAPI' })
@Post('agent/chat-with-file')
@UseInterceptors(FileInterceptor('file'))
async callAgentWithFile(
@Req() req: RequestWithUser,
@UploadedFile() file: Express.Multer.File,
@Query('spaceId', ParseUUIDPipe) spaceId: string,
@Query('query') query: string,
@Query('conversationId') conversationId?: string
) {
await this.checkTaPermission(spaceId, req.user.id);
return await this.taService.callAgentWithFile(spaceId, query, req.user.id, file, conversationId);
}
@ApiOperation({ summary: 'AI: Tạo bản thảo tin nhắn thông minh' })
@Get('agent/smart-message/:snapshotId')
async generateSmartMessage(@Req() req: RequestWithUser, @Param('snapshotId', ParseUUIDPipe) snapshotId: string, @Query('tone') tone: string, @Query('instruction') instruction?: string, @Query('pronouns') pronouns?: string, @Query('taName') taName?: string) {
return this.taService.generateSmartMessage(snapshotId, tone, req.user.id, { instruction, pronouns, taName });
}
@ApiOperation({ summary: 'At-Risk: Lấy settings (weights, thresholds)' })
@Get('at-risk/settings')
async getAtRiskSettings(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
return this.taService.getAtRiskSettingsForUi(spaceId);
}
@ApiOperation({ summary: 'At-Risk: Cập nhật settings (weights, thresholds)' })
@Post('at-risk/settings')
async updateAtRiskSettings(@Req() req: RequestWithUser, @Body() body: { spaceId: string; weights?: { last_seen: number; fastapi_signals: number; quiz_avg_score: number }; thresholds?: { warning: number; critical: number } }) {
await this.checkTaPermission(body.spaceId, req.user.id);
return this.taService.updateAtRiskSettings(body.spaceId, body.weights, body.thresholds, req.user.id);
}
// ==========================================
// QUIZ ENDPOINTS
// ==========================================
@ApiOperation({ summary: 'AI: Tạo quiz từ bài giảng' })
@Post('quiz/generate')
async generateQuiz(@Req() req: RequestWithUser, @Body() dto: GenerateQuizDto) {
if (dto.space_id) await this.checkTaPermission(dto.space_id, req.user.id);
return await this.taService.generateQuiz(dto, req.user.id);
}
@ApiOperation({ summary: 'List quizzes for TA by space' })
@Get('quizzes')
async listQuizzes(@Req() req: RequestWithUser, @Query('spaceId', ParseUUIDPipe) spaceId: string) {
await this.checkTaPermission(spaceId, req.user.id);
return await this.taService.listQuizzes(spaceId);
}
@ApiOperation({ summary: 'Get quiz chi tiết (edit mode)' })
@Get('quiz/:id')
async getQuiz(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) quizId: string) {
const { data: quiz } = await this.supabaseService.from('quizzes').select('space_id').eq('id', quizId).single();
if (quiz) await this.checkTaPermission(quiz.space_id, req.user.id);
return await this.taService.getQuiz(quizId, req.user.id);
}
@ApiOperation({ summary: 'Cập nhật quiz' })
@Put('quiz/:id')
async updateQuiz(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) quizId: string, @Body() dto: UpdateQuizDto) {
const { data: quiz } = await this.supabaseService.from('quizzes').select('space_id').eq('id', quizId).single();
if (quiz) await this.checkTaPermission(quiz.space_id, req.user.id);
return await this.taService.updateQuiz(quizId, dto, req.user.id);
}
@ApiOperation({ summary: 'Gửi quiz vào chat' })
@Post('quiz/:id/send')
async sendQuiz(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) quizId: string, @Body() dto: SendQuizDto) {
if (dto.space_id) await this.checkTaPermission(dto.space_id, req.user.id);
return await this.taService.sendQuizToChat(quizId, dto, req.user.id);
}
@ApiOperation({ summary: 'Get quiz cho học viên làm' })
@Get('quiz/:id/student')
async getQuizForStudent(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) quizId: string) {
const { data: quiz } = await this.supabaseService.from('quizzes').select('space_id').eq('id', quizId).single();
if (!quiz) throw new NotFoundException('Quiz not found');
await this.checkSpaceMembership(quiz.space_id, req.user.id);
return await this.taService.getQuizForStudent(quizId, req.user.id);
}
@ApiOperation({ summary: 'Submit quiz attempt' })
@Post('quiz/:id/submit')
async submitQuizAttempt(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) quizId: string, @Body() dto: SubmitAttemptDto) {
const { data: quiz } = await this.supabaseService.from('quizzes').select('space_id').eq('id', quizId).single();
if (!quiz) throw new NotFoundException('Quiz not found');
await this.checkSpaceMembership(quiz.space_id, req.user.id);
return await this.taService.submitQuizAttempt(quizId, req.user.id, dto);
}
@ApiOperation({ summary: 'Get quiz summary (TA)' })
@Get('quiz/:id/summary')
async getQuizSummary(@Req() req: RequestWithUser, @Param('id', ParseUUIDPipe) quizId: string) {
const { data: quiz } = await this.supabaseService.from('quizzes').select('space_id').eq('id', quizId).single();
if (quiz) await this.checkTaPermission(quiz.space_id, req.user.id);
return await this.taService.getQuizSummary(quizId, req.user.id);
}
}