Spaces:
Sleeping
Sleeping
| 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 }; | |
| } | |
| ('TA Dashboard') | |
| ('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'); | |
| } | |
| } | |
| ({ summary: '[TEST] Tạo dữ liệu Stress Test' }) | |
| ('test/generate-stress-data') | |
| async generateStressData(('spaceId', ParseUUIDPipe) spaceId: string) { | |
| return this.taService.generateStressTestData(spaceId); | |
| } | |
| (JwtAuthGuard) | |
| () | |
| ('scan-at-risk') | |
| async scanAtRisk(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| return await this.taService.scanAtRiskStudents(spaceId); | |
| } | |
| ({ summary: 'Thu nạp tín hiệu rủi ro' }) | |
| ('ingest-signal') | |
| async ingestSignal(() req: RequestWithUser, () dto: IngestSignalDto) { | |
| await this.checkTaPermission(dto.space_id, req.user.id); | |
| return await this.taService.ingestSignal(dto); | |
| } | |
| ({ summary: 'Lấy danh sách học viên có nguy cơ' }) | |
| ('at-risk') | |
| async getAtRisk(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| const data = await this.taService.getAtRiskList(spaceId); | |
| return { success: true, data }; | |
| } | |
| ({ summary: 'Phân tích real-time signals từ chat messages' }) | |
| ('at-risk/analyze') | |
| async analyzeAtRisk(() req: RequestWithUser, () 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; | |
| } | |
| ({ summary: 'Đánh dấu đã giải quyết cảnh báo' }) | |
| ('at-risk/:id/resolve') | |
| async resolveAlert(() req: RequestWithUser, ('id', ParseUUIDPipe) snapshotId: string, ('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 }; | |
| } | |
| ({ summary: 'Lấy hàng chờ duyệt tóm tắt AI' }) | |
| ('summary-queue') | |
| async getSummaryQueue(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| const data = await this.taService.getSummaryQueue(spaceId); | |
| return { success: true, data }; | |
| } | |
| ({ summary: 'Duyệt bản tóm tắt AI' }) | |
| ('summary-queue/:id/approve') | |
| async approveSummary(() req: RequestWithUser, ('id', ParseUUIDPipe) draftId: string, ('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 }; | |
| } | |
| ({ summary: 'Đặt lịch gửi bản tóm tắt AI' }) | |
| ('summary-queue/:id/schedule') | |
| async scheduleSummary(() req: RequestWithUser, ('id', ParseUUIDPipe) draftId: string, ('spaceId', ParseUUIDPipe) spaceId: string, ('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 }; | |
| } | |
| ({ summary: 'Hủy đặt lịch' }) | |
| ('summary-queue/:id/cancel-schedule') | |
| async cancelSchedule(() req: RequestWithUser, ('id', ParseUUIDPipe) draftId: string, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| const data = await this.taService.cancelSchedule(draftId); | |
| return { success: true, data }; | |
| } | |
| ({ summary: 'Cập nhật bản thảo' }) | |
| ('summary-queue/:id') | |
| async updateSummaryDraft(() req: RequestWithUser, ('id', ParseUUIDPipe) draftId: string, ('spaceId', ParseUUIDPipe) spaceId: string, () updates: Record<string, unknown>) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| const data = await this.taService.updateSummaryDraft(draftId, updates); | |
| return { success: true, data }; | |
| } | |
| ({ summary: 'Gửi tin nhắn riêng thông minh' }) | |
| ('send-smart-message') | |
| async sendSmartMessage(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string, () dto: { studentId: string, content: string, snapshotId?: string }) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| return this.taService.sendSmartMessage({ taId: req.user.id, spaceId, ...dto }); | |
| } | |
| ({ summary: 'AI: Tạo bản thảo tóm tắt' }) | |
| ('summary-queue/draft') | |
| async createSummaryDraft(() req: RequestWithUser, () 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 }; | |
| } | |
| ({ summary: 'Lấy nhật ký hành động' }) | |
| ('action-logs') | |
| async getActionLogs(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| const data = await this.taService.getActionLogs(spaceId); | |
| return { success: true, data }; | |
| } | |
| ({ summary: 'Lấy bộ Context cho Agent' }) | |
| ('at-risk-context/:snapshotId') | |
| async getAtRiskContext(() req: RequestWithUser, ('snapshotId', ParseUUIDPipe) snapshotId: string, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| const data = await this.taService.getAtRiskContext(snapshotId); | |
| return { success: true, data }; | |
| } | |
| ({ summary: 'Tải lên slide' }) | |
| ('upload-slide') | |
| (FileInterceptor('file')) | |
| async uploadSlide(() req: RequestWithUser, () file: Express.Multer.File, ('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 }; | |
| } | |
| ({ summary: 'AI: Index PDF vào Qdrant' }) | |
| ('agent/index-pdf') | |
| (FileInterceptor('file')) | |
| async indexPdf(() req: RequestWithUser, () file: Express.Multer.File, () 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); | |
| } | |
| ({ summary: 'AI: Gọi Agent Chat' }) | |
| ('agent/chat') | |
| async callAgentChat(() req: RequestWithUser, () 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); | |
| } | |
| ({ summary: 'AI: Gọi Agent với File' }) | |
| ({ name: 'spaceId', type: String, description: 'ID của không gian' }) | |
| ({ name: 'query', type: String, required: false, description: 'Câu hỏi về file' }) | |
| ({ name: 'conversationId', type: String, required: false, description: 'ID conversation FastAPI' }) | |
| ('agent/chat-with-file') | |
| (FileInterceptor('file')) | |
| async callAgentWithFile( | |
| () req: RequestWithUser, | |
| () file: Express.Multer.File, | |
| ('spaceId', ParseUUIDPipe) spaceId: string, | |
| ('query') query: string, | |
| ('conversationId') conversationId?: string | |
| ) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| return await this.taService.callAgentWithFile(spaceId, query, req.user.id, file, conversationId); | |
| } | |
| ({ summary: 'AI: Tạo bản thảo tin nhắn thông minh' }) | |
| ('agent/smart-message/:snapshotId') | |
| async generateSmartMessage(() req: RequestWithUser, ('snapshotId', ParseUUIDPipe) snapshotId: string, ('tone') tone: string, ('instruction') instruction?: string, ('pronouns') pronouns?: string, ('taName') taName?: string) { | |
| return this.taService.generateSmartMessage(snapshotId, tone, req.user.id, { instruction, pronouns, taName }); | |
| } | |
| ({ summary: 'At-Risk: Lấy settings (weights, thresholds)' }) | |
| ('at-risk/settings') | |
| async getAtRiskSettings(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| return this.taService.getAtRiskSettingsForUi(spaceId); | |
| } | |
| ({ summary: 'At-Risk: Cập nhật settings (weights, thresholds)' }) | |
| ('at-risk/settings') | |
| async updateAtRiskSettings(() req: RequestWithUser, () 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 | |
| // ========================================== | |
| ({ summary: 'AI: Tạo quiz từ bài giảng' }) | |
| ('quiz/generate') | |
| async generateQuiz(() req: RequestWithUser, () dto: GenerateQuizDto) { | |
| if (dto.space_id) await this.checkTaPermission(dto.space_id, req.user.id); | |
| return await this.taService.generateQuiz(dto, req.user.id); | |
| } | |
| ({ summary: 'List quizzes for TA by space' }) | |
| ('quizzes') | |
| async listQuizzes(() req: RequestWithUser, ('spaceId', ParseUUIDPipe) spaceId: string) { | |
| await this.checkTaPermission(spaceId, req.user.id); | |
| return await this.taService.listQuizzes(spaceId); | |
| } | |
| ({ summary: 'Get quiz chi tiết (edit mode)' }) | |
| ('quiz/:id') | |
| async getQuiz(() req: RequestWithUser, ('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); | |
| } | |
| ({ summary: 'Cập nhật quiz' }) | |
| ('quiz/:id') | |
| async updateQuiz(() req: RequestWithUser, ('id', ParseUUIDPipe) quizId: string, () 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); | |
| } | |
| ({ summary: 'Gửi quiz vào chat' }) | |
| ('quiz/:id/send') | |
| async sendQuiz(() req: RequestWithUser, ('id', ParseUUIDPipe) quizId: string, () 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); | |
| } | |
| ({ summary: 'Get quiz cho học viên làm' }) | |
| ('quiz/:id/student') | |
| async getQuizForStudent(() req: RequestWithUser, ('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); | |
| } | |
| ({ summary: 'Submit quiz attempt' }) | |
| ('quiz/:id/submit') | |
| async submitQuizAttempt(() req: RequestWithUser, ('id', ParseUUIDPipe) quizId: string, () 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); | |
| } | |
| ({ summary: 'Get quiz summary (TA)' }) | |
| ('quiz/:id/summary') | |
| async getQuizSummary(() req: RequestWithUser, ('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); | |
| } | |
| } | |