Conversation
…ed layout components
…stem for lecturers, remove term (WIP)
…components for lecturers and students
…rting backend infrastructure
…repository support
…layout components
…on handling, document filtering, and layout components
…aphy with lecturer design
…ository, and application use cases
…392_Chatbot_RAG into refactor/improve-ui
Disable document reporting functionality by setting canReport to false. Remove moderation page, components, and hooks: - Delete moderation page and view component - Remove moderation hook and API integration - Remove "Báo Cáo Vi Phạm" from admin sidebar - Update admin dashboard header with improved styling (icon box, serif title, subtitle) This simplifies the admin interface by removing the violation reporting workflow.
Introduce RAG-powered quiz features and large UI updates: add frontend/src/api/quiz.ts for quiz client; implement AI quiz generator and preview in lecturer practice (UI redesign, modal, polling job status, publish flow); add student dashboard component and improve student practice view with tabbed layout; refactor TakeQuizView to work with API-driven quizzes, start/submit attempts and history modal; update hooks (use-practice, use-quiz) to call quizApi/ragApi, manage subjects, documents, quizzes, generation state and attempt lifecycle; minor sidebar nav label change and multiple UX/loading improvements.
Add IsCorrect to QuizOption model and update TeacherPracticeView to visually highlight correct answers. Uses cn utility to apply green styles, replaces option letter with a checkmark for correct choices, and displays a "Đáp án đúng" badge. Files changed: frontend/src/api/quiz.ts, frontend/src/components/lecturer/practice/practice-view.tsx.
Enable multi-choice questions and client-side persistence for ongoing quizzes. Frontend: update take-quiz UI to show multi-select badges, toggle selections, new leave-confirmation modal, completed-count submit validation, and small debug views; persist/restore active attempt, quiz detail, answers and subject to localStorage and clear them on submit/back/reset. Hook: change answers type to string[], map questionType from API, handle multi_choice logic and format payload for submission. Backend: minor import reorder and whitespace fix in generate_quiz.go. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor/improve UI
Refactor/improve admin UI
docs: update README with project architecture and setup instructions
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (6)
📒 Files selected for processing (117)
📝 WalkthroughWalkthroughThe pull request adds quiz generation, publishing, attempts, grading, and frontend quiz flows; introduces chat and session-document persistence; removes academic-term support; updates document filtering, comparison export, bookmarking, and upload progress; and applies broad frontend navigation, layout, and design-system changes. ChangesPlatform feature integration
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Lecturer
participant Frontend
participant QuizAPI
participant QuizUsecase
participant Database
participant LLM
Lecturer->>Frontend: configure documents and question counts
Frontend->>QuizAPI: POST quiz generation request
QuizAPI->>QuizUsecase: enqueue generation job
QuizUsecase->>Database: save pending job
QuizUsecase->>LLM: generate quiz from document chunks
LLM-->>QuizUsecase: return quiz JSON
QuizUsecase->>Database: save draft quiz and mark job completed
Frontend->>QuizAPI: poll generation job status
QuizAPI-->>Frontend: return job progress and result quiz
Possibly related PRs
Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Review by Qodo
Context used✅ Compliance rules (platform):
27 rules 1. quiz-usecase imports infrastructure/llm
|
| "github.com/google/uuid" | ||
| "swd392-chatbot-rag/internal/domain/quiz" | ||
| "swd392-chatbot-rag/internal/infrastructure/llm" |
There was a problem hiding this comment.
1. quiz-usecase imports infrastructure/llm 📘 Rule violation ⌂ Architecture
The application-layer quiz-usecase depends directly on internal/infrastructure/llm, violating the required four-layer dependency direction (application must not import infrastructure). This makes the use case layer tightly coupled to a specific infra implementation instead of an abstraction.
Agent Prompt
## Issue description
`backend/go/internal/application/quiz-usecase/generate_quiz.go` is in the application layer but imports `swd392-chatbot-rag/internal/infrastructure/llm`, coupling application to infrastructure.
## Issue Context
Compliance requires a strict four-layer architecture where application depends on domain abstractions, not infrastructure packages.
## Fix Focus Areas
- backend/go/internal/application/quiz-usecase/generate_quiz.go[11-15]
- backend/go/internal/application/quiz-usecase/usecase.go[53-67]
- backend/go/internal/interface/router/router.go[74-99]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| return | ||
| } | ||
|
|
||
| userID := c.MustGet("user_id").(uuid.UUID) | ||
|
|
||
| attempts, err := h.quizUsecase.GetAttemptHistory(c.Request.Context(), quizID, userID) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{"data": attempts}) | ||
| } | ||
|
|
||
| // @Summary Get all subjects that have published quizzes | ||
| // @Description Returns a list of subjects that contain at least one published quiz. | ||
| // @Tags Quiz | ||
| // @Produce json | ||
| // @Success 200 {object} map[string]interface{} | ||
| // @Failure 500 {object} map[string]interface{} | ||
| // @Router /api/quizzes/subjects [get] | ||
| func (h *QuizHandler) GetSubjectsWithQuizzes(c *gin.Context) { | ||
| subjects, err := h.quizUsecase.ListSubjectsWithPublishedQuizzes(c.Request.Context()) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{"data": subjects}) | ||
| } |
There was a problem hiding this comment.
2. quiz-handler.go exceeds 200 lines 📘 Rule violation ⚙ Maintainability
backend/go/internal/interface/handler/quiz-handler.go is 379 lines long, exceeding the 200-line maximum for source files. This violates the file size limit even though it is a single new file.
Agent Prompt
## Issue description
A source file exceeds the 200-line limit.
## Issue Context
The handler is currently implemented as a single large file; compliance requires source files to be 200 lines or fewer.
## Fix Focus Areas
- backend/go/internal/interface/handler/quiz-handler.go[1-379]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| -- ALTER TABLE public.verification OWNER TO postgres; | ||
|
|
||
| -- | ||
| INSERT INTO public.academic_terms (id, name, term_order) VALUES |
There was a problem hiding this comment.
3. swd391_dangerous_malware.sql broken inserts 📘 Rule violation ≡ Correctness
The PR removes the INSERT INTO public.academic_terms ... VALUES statement but leaves the tuple list, producing invalid SQL that will fail to run. This violates the requirement that changed files be free of syntax/compile errors.
Agent Prompt
## Issue description
The SQL file contains orphaned value tuples because the `INSERT INTO ... VALUES` line was removed while the subsequent tuple rows remain.
## Issue Context
This makes the SQL file syntactically invalid and will fail when executed.
## Fix Focus Areas
- backend/database/swd391_dangerous_malware.sql[434-447]
- backend/database/swd391_dangerous_malware.sql[550-556]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| func (u *usecase) GenerateQuizAsync(ctx context.Context, req GenerateQuizReq) (*quiz.GenerationJob, error) { | ||
| // 1. Validate ownership & Subject consistency | ||
| if len(req.DocumentIDs) == 0 { | ||
| return nil, errors.New("at least one document must be selected") | ||
| } | ||
|
|
||
| for _, docID := range req.DocumentIDs { | ||
| doc, err := u.docRepo.FindByID(ctx, docID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to fetch document %s: %v", docID, err) | ||
| } | ||
|
|
||
| // 1. Quyền truy cập tài liệu: Public (school_wide) hoặc là của chính lecturer | ||
| if doc.Visibility != "school_wide" && doc.OwnerUserID != req.LecturerID { | ||
| return nil, fmt.Errorf("document %s is not public or owned by you", docID) | ||
| } | ||
|
|
||
| // 2. Tính đồng nhất môn học | ||
| if doc.SubjectID == nil || *doc.SubjectID != req.SubjectID { | ||
| return nil, errors.New("all selected documents must belong to the specified subject") | ||
| } | ||
| } | ||
|
|
||
| // 2. Create Background Job | ||
| job := &quiz.GenerationJob{ | ||
| ID: uuid.New(), | ||
| SubjectID: req.SubjectID, | ||
| LecturerID: req.LecturerID, | ||
| Status: quiz.JobStatusPending, | ||
| Progress: 0, | ||
| } | ||
|
|
||
| if err := u.quizRepo.CreateGenerationJob(ctx, job); err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // 3. Trigger Async Background Worker (In real app, this should use a queue like asynq/redis) | ||
| go u.runGenerationWorker(req, job.ID) | ||
|
|
||
| return job, nil | ||
| } |
There was a problem hiding this comment.
4. Quiz usecases lack unit tests 📘 Rule violation ▣ Testability
New core business logic was added under internal/application/quiz-usecase (e.g., GenerateQuizAsync) without any corresponding _test.go unit tests in the same package. This reduces confidence in correctness of the use-case layer behavior and regressions.
Agent Prompt
## Issue description
New/modified application-layer use case logic lacks unit tests.
## Issue Context
Compliance requires at least one unit test per new/modified exported domain/use-case function/method in Go backend packages.
## Fix Focus Areas
- backend/go/internal/application/quiz-usecase/generate_quiz.go[16-56]
- backend/go/internal/application/quiz-usecase/manage_quiz.go[12-75]
- backend/go/internal/application/quiz-usecase/take_quiz.go[12-44]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Quiz routes | ||
| quizGroup := protected.Group("/quizzes") | ||
| { | ||
| // Shared | ||
| quizGroup.GET("/subjects", middleware.RequireRoles(2, 3), quizHandler.GetSubjectsWithQuizzes) | ||
| quizGroup.GET("/:quiz_id/detail", middleware.RequireRoles(2, 3), quizHandler.GetQuizDetail) | ||
|
|
||
| // Lecturer | ||
| lecturerQuiz := quizGroup.Group("/lecturer", middleware.RequireRoles(2)) | ||
| lecturerQuiz.POST("/generate", quizHandler.GenerateQuiz) | ||
| lecturerQuiz.GET("/jobs/:job_id", quizHandler.GetJobStatus) | ||
| lecturerQuiz.POST("/:quiz_id/publish", quizHandler.PublishQuiz) | ||
| lecturerQuiz.GET("/subject/:subject_id", quizHandler.ListQuizzesForLecturer) | ||
|
|
||
| // Student | ||
| studentQuiz := quizGroup.Group("/student", middleware.RequireRoles(3)) | ||
| studentQuiz.GET("/subject/:subject_id", quizHandler.ListQuizzesForStudent) | ||
| studentQuiz.POST("/:quiz_id/attempt", quizHandler.StartAttempt) | ||
| studentQuiz.POST("/attempt/submit", quizHandler.SubmitAttempt) | ||
| studentQuiz.GET("/:quiz_id/attempts", quizHandler.GetAttemptHistory) |
There was a problem hiding this comment.
5. Quiz answers exposed 🐞 Bug ⛨ Security
Students can call the quiz detail endpoint and receive options containing IsCorrect, revealing the correct answers and undermining quiz integrity. The shared route also does not enforce published status for student access.
Agent Prompt
## Issue description
`GET /api/quizzes/:quiz_id/detail` is accessible to role 3 (students) and returns `questions[].options[].IsCorrect`, exposing correct answers. The usecase also does not gate access to published-only for students.
## Issue Context
- Route is registered for roles (2,3).
- Handler returns `questions` directly.
- Domain `quiz.Option` includes exported `IsCorrect bool`, so Gin/JSON will serialize it by default.
## Fix Focus Areas
- backend/go/internal/interface/router/router.go[147-167]
- backend/go/internal/interface/handler/quiz-handler.go[203-232]
- backend/go/internal/application/quiz-usecase/manage_quiz.go[70-89]
- backend/go/internal/domain/quiz/entity.go[58-64]
## Suggested fix
1. Split endpoints or responses by role:
- For students: return a DTO that omits `IsCorrect` (and possibly `Explanation`).
- For lecturers: allow full detail (including correct answers) for authoring/preview.
2. Enforce status checks:
- If requester is a student, require `quiz.status == 'published'`.
- If requester is a lecturer, allow access only if `quiz.status == 'published'` OR `quiz.lecturer_id == requester`.
3. Implement role-aware logic in handler (using role from context) or add dedicated usecase methods like `GetQuizDetailForStudent` / `GetQuizDetailForLecturer`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| func (h *QuizHandler) GetJobStatus(c *gin.Context) { | ||
| jobIDStr := c.Param("job_id") | ||
| jobID, err := uuid.Parse(jobIDStr) | ||
| if err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job_id"}) | ||
| return | ||
| } | ||
|
|
||
| job, err := h.quizUsecase.GetGenerationJobStatus(c.Request.Context(), jobID) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, job) | ||
| } |
There was a problem hiding this comment.
7. Job status auth missing 🐞 Bug ⛨ Security
GET /api/quizzes/lecturer/jobs/:job_id returns any generation job by ID without verifying the authenticated lecturer owns the job. This allows one lecturer to query another lecturer’s job status and metadata if job IDs are obtained.
Agent Prompt
## Issue description
The job status endpoint fetches a generation job by `job_id` and returns it without checking `job.LecturerID == requesterID`.
## Issue Context
- Endpoint is lecturer-only, but still must enforce per-lecturer ownership.
- Usecase method is a direct passthrough to repository by ID.
## Fix Focus Areas
- backend/go/internal/interface/handler/quiz-handler.go[99-114]
- backend/go/internal/application/quiz-usecase/generate_quiz.go[58-60]
## Suggested fix
1. In `QuizHandler.GetJobStatus`, read `lecturerID := c.MustGet("user_id").(uuid.UUID)` and compare it with `job.LecturerID`.
2. If mismatch, return 404 (preferable to avoid resource enumeration) or 403.
3. Optionally, move the check into a new usecase method `GetGenerationJobStatusForLecturer(ctx, jobID, lecturerID)` to keep authz in the application layer.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| for i, q := range llmResult.Questions { | ||
| qType := quiz.TypeSingleChoice | ||
| if q.Type == "multiple_choice" { | ||
| qType = quiz.TypeMultipleChoice | ||
| } else if q.Type == "true_false" { | ||
| qType = quiz.TypeTrueFalse | ||
| } | ||
|
|
||
| newQ := &quiz.Question{ | ||
| ID: uuid.New(), | ||
| QuizID: newQuiz.ID, | ||
| QuestionType: qType, | ||
| Content: q.Content, | ||
| OrderIndex: i + 1, | ||
| CreatedAt: time.Now(), | ||
| } | ||
| if q.Explanation != "" { | ||
| exp := q.Explanation | ||
| newQ.Explanation = &exp | ||
| } | ||
| _ = u.quizRepo.CreateQuestion(ctx, newQ) | ||
|
|
||
| for j, opt := range q.Options { | ||
| newOpt := &quiz.Option{ | ||
| ID: uuid.New(), | ||
| QuestionID: newQ.ID, | ||
| Content: opt.Content, | ||
| IsCorrect: opt.IsCorrect, | ||
| OrderIndex: j + 1, | ||
| } | ||
| _ = u.quizRepo.CreateOption(ctx, newOpt) | ||
| } | ||
| } | ||
|
|
||
| // 6. Update Job Status | ||
| job, _ := u.quizRepo.GetGenerationJobByID(ctx, jobID) | ||
| if job != nil { | ||
| job.Status = quiz.JobStatusCompleted | ||
| job.ResultQuizID = &newQuiz.ID | ||
| job.Progress = 100 | ||
| _ = u.quizRepo.UpdateGenerationJob(ctx, job) | ||
| } |
There was a problem hiding this comment.
8. Quiz generation ignores errors 🐞 Bug ☼ Reliability
During quiz generation, errors from creating questions/options are ignored and the job is still marked completed, which can produce incomplete/broken quizzes while reporting success. Writes are also not wrapped in a transaction, so partial persistence is likely under failure.
Agent Prompt
## Issue description
`runGenerationWorker` ignores persistence errors for questions/options and still updates the job to `completed`, potentially leaving corrupted/incomplete quiz data.
## Issue Context
- Question creation and option creation use `_ = ...` (error discarded).
- Job completion update happens regardless of those errors.
## Fix Focus Areas
- backend/go/internal/application/quiz-usecase/generate_quiz.go[168-225]
- backend/go/internal/infrastructure/repository/postgres/quiz_repo.go[22-260]
## Suggested fix
1. Stop discarding errors:
- If `CreateQuestion` or `CreateOption` fails, call `failJob(err)` and return.
2. Make persistence atomic:
- Add transaction support in quiz repository (e.g., methods that accept `pgx.Tx`), or add a `CreateQuizWithQuestionsAndOptions(...)` method that performs all inserts in a single transaction.
3. Update job status lifecycle:
- Set job to `processing` at start and update progress.
- Only set `completed` after *all* DB writes succeed.
4. Consider cleanup on failure:
- If quiz row was created but later writes fail, delete the quiz (or roll back via transaction).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // 5. Semantic search — find top-K similar chunks | ||
| chunks, err := uc.msgRepo.SearchSimilarChunks(ctx, queryEmbedding, session.CourseID, session.DocumentIDs, topKChunks) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to search chunks: %w", err) | ||
| var chunks []*message.SimilarChunk | ||
|
|
||
| if len(session.DocumentIDs) > 1 { | ||
| // Retrieve chunks for each document to ensure balanced representation in comparison | ||
| chunksPerDoc := 5 | ||
| if len(session.DocumentIDs) > 2 { | ||
| chunksPerDoc = 3 | ||
| } | ||
| for _, docID := range session.DocumentIDs { | ||
| docChunks, docErr := uc.msgRepo.SearchSimilarChunks(ctx, queryEmbedding, session.CourseID, []uuid.UUID{docID}, chunksPerDoc) | ||
| if docErr == nil { | ||
| chunks = append(chunks, docChunks...) | ||
| } | ||
| } |
There was a problem hiding this comment.
9. Chat chunk errors hidden 🐞 Bug ☼ Reliability
When a chat session has multiple documents, per-document SearchSimilarChunks errors are silently ignored, which can degrade grounding (missing context) without surfacing retrieval failures. This makes failures hard to detect and can lead to lower-quality or misleading responses.
Agent Prompt
## Issue description
In multi-document sessions, chunk retrieval errors are ignored (`docErr` is dropped). This can cause partial/empty retrieval context without any error/log signal.
## Issue Context
- Code loops documents and appends chunks only when `docErr == nil`.
- No logging/metrics or error aggregation exists.
## Fix Focus Areas
- backend/go/internal/application/chat-usecase/send-message.go[69-89]
- backend/go/internal/application/chat-usecase/stream-message.go[51-72]
## Suggested fix
1. Track retrieval errors:
- Accumulate `docErr`s and (at minimum) log them with docID/sessionID.
2. Decide policy:
- If *any* document retrieval fails, return an error (strict) OR proceed with partial results but include a warning/metric.
3. If all retrieval attempts fail (chunks empty) and there were errors, return a 5xx (or a specific error) rather than silently proceeding as if retrieval succeeded.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| } | ||
| defer rows.Close() | ||
|
|
||
| if err != nil { | ||
| return nil, 0, err | ||
| } | ||
| defer rows.Close() | ||
|
|
There was a problem hiding this comment.
10. Duplicate rows.close defer 🐞 Bug ⚙ Maintainability
FindAllAdmin redundantly checks err twice and defers rows.Close() twice, creating unreachable/duplicated cleanup logic that makes the function harder to maintain. This should be simplified to a single error check and a single defer.
Agent Prompt
## Issue description
After a successful `Query`, the function repeats `if err != nil` and `defer rows.Close()` a second time. This is redundant and confusing.
## Issue Context
The second `if err != nil` is unreachable because the earlier check already returns on error.
## Fix Focus Areas
- backend/go/internal/infrastructure/repository/postgres/document-repository.go[402-412]
## Suggested fix
Delete the second `if err != nil { ... }` block and the second `defer rows.Close()` so the control flow is:
- query
- error check
- single `defer rows.Close()`
- scan loop
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // 1. Quyền truy cập tài liệu: Public (school_wide) hoặc là của chính lecturer | ||
| if doc.Visibility != "school_wide" && doc.OwnerUserID != req.LecturerID { | ||
| return nil, fmt.Errorf("document %s is not public or owned by you", docID) | ||
| } |
There was a problem hiding this comment.
11. Public docs blocked for quiz 🐞 Bug ≡ Correctness
Quiz generation treats only visibility == "school_wide" as public, but the API documents visibility as public|school_wide|private. This will incorrectly reject public documents during quiz generation even though they are non-private.
Agent Prompt
## Issue description
`GenerateQuizAsync` rejects documents with `visibility == "public"` (documented as a valid non-private visibility) unless owned by the lecturer.
## Issue Context
The document upload API explicitly documents `visibility (public, school_wide, private)`.
## Fix Focus Areas
- backend/go/internal/application/quiz-usecase/generate_quiz.go[22-36]
- backend/go/internal/interface/handler/document-handler.go[145-164]
## Suggested fix
Change the visibility check to allow any non-private visibility, e.g.:
- allow `doc.Visibility != "private"` as public/school-wide
OR
- explicitly allow both `public` and `school_wide`.
Keep the ownership allowance for private docs (owner must match).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
PR Summary by QodoAdd AI quiz system, redesign UI, and remove academic term/moderation features
AI Description
Diagram
High-Level Assessment
Files changed (118)
|
Summary by CodeRabbit