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