Refactor/improve UI - #36
Conversation
Extract quiz preview modal from practice-view into dedicated full-page component (quiz-preview-view.tsx). Replace modal-based state management with URL-based navigation (?preview=true&id=quizId). Add Explanation field to QuizQuestion interface. Simplify header styling in student and lecturer practice views for consistency. This improves component reusability and enables direct sharing of quiz preview links.
Remove the 'Kết quả sinh viên' (student results) tab and associated table displaying quiz completion data. This includes removing the results panel UI, the Tabs.List navigation, and the mockStudentResults data.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughChangesQuiz preview flow
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Lecturer
participant PracticeView
participant usePractice
participant QuizPage
participant QuizPreviewView
participant quizApi
Lecturer->>PracticeView: Select quiz preview
PracticeView->>usePractice: Handle preview click
usePractice->>QuizPage: Navigate with quiz id and preview=true
QuizPage->>QuizPreviewView: Render lecturer preview
QuizPreviewView->>quizApi: Fetch quiz details
quizApi-->>QuizPreviewView: Return quiz data
QuizPreviewView-->>Lecturer: Display questions and publish action
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/app/`[role]/practice/quiz/page.tsx:
- Around line 9-15: Update QuizPageContent so QuizPreviewView renders only when
the authenticated user has the Better Auth lecturer role, rather than relying
solely on the preview query parameter. Preserve the existing preview behavior
for lecturers and keep the getQuizDetail and publishQuiz backend authorization
checks rejecting non-lecturers.
In `@frontend/src/components/lecturer/quiz/quiz-preview-view.tsx`:
- Around line 23-244: Decompose the oversized modules into focused units under
200 lines: in frontend/src/components/lecturer/quiz/quiz-preview-view.tsx (lines
23-244), extract quiz data/publish handling and question rendering into
dedicated hooks/components while preserving existing behavior; in
frontend/src/hooks/lecturer/use-practice.ts (lines 179-181), extract generation
polling and preview navigation; in
frontend/src/components/lecturer/practice/practice-view.tsx (lines 98-103),
extract dashboard sections, lists, and generation-modal rendering. Ensure each
parent remains focused on composition and orchestration.
- Around line 34-50: Update the useEffect fetch flow keyed by quizId so a
missing ID clears quiz details and stops loading immediately. Clear or
invalidate prior details when the ID changes, and guard fetchDetail’s success
and finally updates so stale or failed requests cannot leave another quiz’s
details available to handlePublish or overwrite current loading state. Preserve
the existing detail fetch for the active quizId.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6253586f-445d-44aa-b8ff-b09a299fab50
📒 Files selected for processing (6)
frontend/src/api/quiz.tsfrontend/src/app/[role]/practice/quiz/page.tsxfrontend/src/components/lecturer/practice/practice-view.tsxfrontend/src/components/lecturer/quiz/quiz-preview-view.tsxfrontend/src/components/student/practice/student-practice-view.tsxfrontend/src/hooks/lecturer/use-practice.ts
| function QuizPageContent() { | ||
| const searchParams = useSearchParams(); | ||
| const isPreview = searchParams.get("preview") === "true"; | ||
|
|
||
| if (isPreview) { | ||
| return <QuizPreviewView />; | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
git ls-files | rg '^frontend/src/app/\[role\]/practice/quiz/page\.tsx$|^frontend/src/.+QuizPreviewView|^frontend/lib/auth-client\.ts$|^frontend/src/.+quiz.+(api|route)\.(ts|js)$|^frontend/src/.+publish.+route\.(ts|js)$|^frontend/src/.+quiz.+(service|client)\.(ts|js)$'Repository: UGing265/SWD392_Chatbot_RAG
Length of output: 212
🏁 Script executed:
sed -n '1,220p' frontend/src/app/[role]/practice/quiz/page.tsxRepository: UGing265/SWD392_Chatbot_RAG
Length of output: 1199
🏁 Script executed:
ast-grep outline frontend/src/app/[role]/practice/quiz/page.tsx --view expandedRepository: UGing265/SWD392_Chatbot_RAG
Length of output: 298
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' 'FILES:'
git ls-files | rg '^frontend/src/app/\[role\]/practice/quiz/page\.tsx$|^frontend/lib/auth-client\.ts$|^frontend/src/.+QuizPreviewView|^frontend/src/.+quiz.+(api|route)\.(ts|js)$|^frontend/src/.+publish.+route\.(ts|js)$|^frontend/src/.+quiz.+(service|client)\.(ts|js)$'Repository: UGing265/SWD392_Chatbot_RAG
Length of output: 219
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' 'frontend/src/app/[role]/practice/quiz/page.tsx'Repository: UGing265/SWD392_Chatbot_RAG
Length of output: 1199
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "QuizPreviewView|publish|correct answer|correctAnswers|preview=true|useSearchParams|auth-client|Better Auth|role" frontend/src -g '!**/*.map'Repository: UGing265/SWD392_Chatbot_RAG
Length of output: 24152
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the preview component file(s)
fd -a 'QuizPreviewView.*\.(ts|tsx|js|jsx)$|quiz.*preview.*\.(ts|tsx|js|jsx)$' frontend/srcRepository: UGing265/SWD392_Chatbot_RAG
Length of output: 244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' frontend/src/components/layout/role-shell.tsx
printf '\n---\n'
sed -n '1,220p' frontend/src/app/[role]/layout.tsx
printf '\n---\n'
sed -n '1,260p' frontend/src/components/lecturer/quiz/quiz-preview-view.tsx
printf '\n---\n'
sed -n '1,180p' frontend/src/middleware.tsRepository: UGing265/SWD392_Chatbot_RAG
Length of output: 13658
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,260p' frontend/src/api/quiz.ts
printf '\n---\n'
sed -n '1,240p' frontend/src/api/client.tsRepository: UGing265/SWD392_Chatbot_RAG
Length of output: 5710
Gate quiz preview behind lecturer auth. The route guard only checks the [role] segment, so ?preview=true still swaps in QuizPreviewView on /student/.... Require the Better Auth lecturer role before rendering this view, and keep getQuizDetail/publishQuiz rejecting non-lecturers on the backend.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/app/`[role]/practice/quiz/page.tsx around lines 9 - 15, Update
QuizPageContent so QuizPreviewView renders only when the authenticated user has
the Better Auth lecturer role, rather than relying solely on the preview query
parameter. Preserve the existing preview behavior for lecturers and keep the
getQuizDetail and publishQuiz backend authorization checks rejecting
non-lecturers.
Source: Coding guidelines
| export function QuizPreviewView() { | ||
| const searchParams = useSearchParams(); | ||
| const router = useRouter(); | ||
| const params = useParams(); | ||
| const role = (params?.role as string) || "lecturer"; | ||
|
|
||
| const quizId = searchParams.get("id"); | ||
| const [quizDetail, setQuizDetail] = useState<QuizDetailResponse | null>(null); | ||
| const [loading, setLoading] = useState(true); | ||
| const [publishing, setPublishing] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| if (!quizId) return; | ||
|
|
||
| const fetchDetail = async () => { | ||
| try { | ||
| setLoading(true); | ||
| const data = await quizApi.getQuizDetail(quizId); | ||
| setQuizDetail(data); | ||
| } catch (err) { | ||
| console.error("Failed to fetch quiz detail:", err); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| fetchDetail(); | ||
| }, [quizId]); | ||
|
|
||
| const handleBack = () => { | ||
| router.push(`/${role}/practice`); | ||
| }; | ||
|
|
||
| const handlePublish = async () => { | ||
| if (!quizId) return; | ||
| try { | ||
| setPublishing(true); | ||
| await quizApi.publishQuiz(quizId); | ||
| router.push(`/${role}/practice`); | ||
| } catch (err) { | ||
| console.error("Failed to publish quiz:", err); | ||
| alert("Phát hành Quiz thất bại."); | ||
| } finally { | ||
| setPublishing(false); | ||
| } | ||
| }; | ||
|
|
||
| if (loading) { | ||
| return ( | ||
| <Center className="py-20"> | ||
| <Stack gap="xs" align="center"> | ||
| <Loader size="md" color="indigo" /> | ||
| <Text size="sm" c="dimmed" fw={500}>Đang tải chi tiết bài tập...</Text> | ||
| </Stack> | ||
| </Center> | ||
| ); | ||
| } | ||
|
|
||
| if (!quizDetail) { | ||
| return ( | ||
| <Center className="py-20"> | ||
| <Stack gap="md" align="center"> | ||
| <Text size="sm" c="red" fw={600}>Không thể tải thông tin bài tập.</Text> | ||
| <Button onClick={handleBack} variant="outline" color="gray" radius="lg"> | ||
| Quay lại | ||
| </Button> | ||
| </Stack> | ||
| </Center> | ||
| ); | ||
| } | ||
|
|
||
| const { quiz, questions } = quizDetail; | ||
|
|
||
| return ( | ||
| <div className="w-full bg-transparent"> | ||
| <div className="max-w-3xl mx-auto w-full px-6 py-6 space-y-8 animate-in fade-in duration-300"> | ||
| {/* Back navigation button */} | ||
| <Button | ||
| onClick={handleBack} | ||
| variant="subtle" | ||
| color="gray" | ||
| leftSection={<IconChevronLeft size={16} />} | ||
| radius="lg" | ||
| className="!text-zinc-500 hover:!bg-zinc-100 self-start font-bold" | ||
| > | ||
| Quay lại danh sách | ||
| </Button> | ||
|
|
||
| {/* Quiz Info Bar (lecturer preview style matching student active view) */} | ||
| <div className="p-6 rounded-2xl bg-white border border-zinc-200 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4"> | ||
| <div> | ||
| <span className="text-[10px] font-bold text-indigo-650 uppercase tracking-widest block mb-1"> | ||
| Xem thử bài tập (Giảng viên) | ||
| </span> | ||
| <h2 className="font-extrabold text-lg text-zinc-900 mb-3">{quiz.Title}</h2> | ||
| <Group gap="xs"> | ||
| <Badge color="indigo" variant="light" size="sm" radius="lg"> | ||
| Môn học ID: {quiz.SubjectID.substring(0, 8)}... | ||
| </Badge> | ||
| <Badge color="dark" variant="outline" size="sm" radius="lg" className="!border-zinc-200 !text-zinc-500"> | ||
| {questions.length} câu hỏi | ||
| </Badge> | ||
| <Badge color={quiz.Status === "published" ? "emerald" : "orange"} variant="light" size="sm" radius="lg"> | ||
| {quiz.Status === "published" ? "Đã Phát Hành" : "Bản Nháp"} | ||
| </Badge> | ||
| </Group> | ||
| </div> | ||
|
|
||
| {quiz.Status === "draft" && ( | ||
| <Button | ||
| onClick={handlePublish} | ||
| loading={publishing} | ||
| color="emerald" | ||
| radius="lg" | ||
| size="md" | ||
| leftSection={<IconSend size={16} />} | ||
| className="font-extrabold shadow-sm bg-emerald-600 hover:bg-emerald-500 text-white shrink-0" | ||
| > | ||
| Phát hành Quiz | ||
| </Button> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Questions List */} | ||
| <Stack gap="md"> | ||
| {questions.map((q, index) => { | ||
| const questionId = q.ID || (q as any).id || `q-${index}`; | ||
| const content = q.Content || (q as any).content || "Nội dung câu hỏi đang tải..."; | ||
| const questionType = q.QuestionType || (q as any).questionType || (q as any).question_type || "single_choice"; | ||
| const options = q.Options || (q as any).options || []; | ||
| const explanation = q.Explanation || (q as any).explanation || ""; | ||
|
|
||
| return ( | ||
| <Paper | ||
| key={questionId} | ||
| withBorder | ||
| p="lg" | ||
| radius="2xl" | ||
| className="bg-white border-zinc-200 shadow-sm relative overflow-hidden" | ||
| > | ||
| {/* Question Title & Text */} | ||
| <Text component="div" fw={755} size="md" className="text-zinc-850 leading-relaxed mb-5 font-sans"> | ||
| <span className="text-indigo-600 mr-2 font-bold">Câu {index + 1}:</span> | ||
| {content} | ||
| {questionType === "multiple_choice" && ( | ||
| <Badge size="xs" color="indigo" variant="light" ml="xs" className="align-middle"> | ||
| Chọn nhiều đáp án | ||
| </Badge> | ||
| )} | ||
| {questionType === "true_false" && ( | ||
| <Badge size="xs" color="dark" variant="light" ml="xs" className="align-middle"> | ||
| Đúng / Sai | ||
| </Badge> | ||
| )} | ||
| </Text> | ||
|
|
||
| {/* Options List */} | ||
| {options.length > 0 ? ( | ||
| <Stack gap="sm"> | ||
| {options.map((opt: any, i: number) => { | ||
| const optId = opt.ID || opt.id || `opt-${index}-${i}`; | ||
| const optContent = opt.Content || opt.content || ""; | ||
| const isCorrect = opt.IsCorrect !== undefined | ||
| ? opt.IsCorrect | ||
| : opt.is_correct !== undefined | ||
| ? opt.is_correct | ||
| : opt.isCorrect || false; | ||
|
|
||
| return ( | ||
| <div | ||
| key={optId} | ||
| className={cn( | ||
| "flex items-center p-4 rounded-xl border transition-all select-none", | ||
| isCorrect | ||
| ? "bg-emerald-50/50 border-emerald-500/60 text-emerald-700" | ||
| : "bg-zinc-50/30 border-zinc-150 text-zinc-700" | ||
| )} | ||
| > | ||
| <div | ||
| className={cn( | ||
| "h-5 w-5 border mr-3 flex items-center justify-center shrink-0 transition-all", | ||
| questionType === "multiple_choice" ? "rounded-md" : "rounded-full", | ||
| isCorrect ? "border-emerald-500 bg-emerald-500" : "border-zinc-300 bg-white" | ||
| )} | ||
| > | ||
| {isCorrect && ( | ||
| <span className="text-white text-[10px] font-bold">✓</span> | ||
| )} | ||
| </div> | ||
| <span className="text-sm font-semibold leading-relaxed">{optContent}</span> | ||
| </div> | ||
| ); | ||
| })} | ||
| </Stack> | ||
| ) : ( | ||
| <Text size="xs" c="dimmed" fs="italic">Không có phương án lựa chọn.</Text> | ||
| )} | ||
|
|
||
| {/* AI Explanation */} | ||
| {explanation && ( | ||
| <div className="mt-4 p-4 rounded-xl bg-gradient-to-br from-indigo-50/40 via-purple-50/20 to-zinc-50/40 border border-indigo-100/45 shadow-[inset_0_1px_2.5px_rgba(255,255,255,0.75)] flex gap-3 items-start hover:shadow-sm transition-all duration-300"> | ||
| <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-indigo-50 border border-indigo-100/80 text-indigo-600 shrink-0 mt-0.5 shadow-sm"> | ||
| <IconSparkles size={14} stroke={2} /> | ||
| </div> | ||
| <div className="flex-1"> | ||
| <span className="text-[10px] font-bold text-indigo-700 uppercase tracking-wider block mb-1"> | ||
| Giải Thích Chi Tiết (AI) | ||
| </span> | ||
| <Text size="xs" className="text-zinc-650 font-medium leading-relaxed font-sans"> | ||
| {explanation} | ||
| </Text> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </Paper> | ||
| ); | ||
| })} | ||
| </Stack> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Decompose the oversized quiz-practice modules.
These three files exceed the repository’s 200-line limit and combine multiple responsibilities:
frontend/src/components/lecturer/quiz/quiz-preview-view.tsx#L23-L244: extract data/publish logic and question rendering.frontend/src/hooks/lecturer/use-practice.ts#L179-L181: extract generation polling and preview navigation.frontend/src/components/lecturer/practice/practice-view.tsx#L98-L103: extract dashboard sections, lists, and the generation modal.
As per coding guidelines, “Avoid God files; keep files under 200 lines and focused on a single purpose.”
📍 Affects 3 files
frontend/src/components/lecturer/quiz/quiz-preview-view.tsx#L23-L244(this comment)frontend/src/hooks/lecturer/use-practice.ts#L179-L181frontend/src/components/lecturer/practice/practice-view.tsx#L98-L103
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/lecturer/quiz/quiz-preview-view.tsx` around lines 23
- 244, Decompose the oversized modules into focused units under 200 lines: in
frontend/src/components/lecturer/quiz/quiz-preview-view.tsx (lines 23-244),
extract quiz data/publish handling and question rendering into dedicated
hooks/components while preserving existing behavior; in
frontend/src/hooks/lecturer/use-practice.ts (lines 179-181), extract generation
polling and preview navigation; in
frontend/src/components/lecturer/practice/practice-view.tsx (lines 98-103),
extract dashboard sections, lists, and generation-modal rendering. Ensure each
parent remains focused on composition and orchestration.
Source: Coding guidelines
| useEffect(() => { | ||
| if (!quizId) return; | ||
|
|
||
| const fetchDetail = async () => { | ||
| try { | ||
| setLoading(true); | ||
| const data = await quizApi.getQuizDetail(quizId); | ||
| setQuizDetail(data); | ||
| } catch (err) { | ||
| console.error("Failed to fetch quiz detail:", err); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| fetchDetail(); | ||
| }, [quizId]); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Bind fetched details to the current quizId.
A missing ID leaves the spinner active forever. More importantly, a failed or out-of-order request can retain another quiz’s details while handlePublish publishes the current ID.
Proposed fix
useEffect(() => {
- if (!quizId) return;
+ let active = true;
+
+ if (!quizId) {
+ setQuizDetail(null);
+ setLoading(false);
+ return;
+ }
+
+ setQuizDetail(null);
+ setLoading(true);
const fetchDetail = async () => {
try {
- setLoading(true);
const data = await quizApi.getQuizDetail(quizId);
- setQuizDetail(data);
+ if (active) setQuizDetail(data);
} catch (err) {
console.error("Failed to fetch quiz detail:", err);
+ if (active) setQuizDetail(null);
} finally {
- setLoading(false);
+ if (active) setLoading(false);
}
};
fetchDetail();
+
+ return () => {
+ active = false;
+ };
}, [quizId]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (!quizId) return; | |
| const fetchDetail = async () => { | |
| try { | |
| setLoading(true); | |
| const data = await quizApi.getQuizDetail(quizId); | |
| setQuizDetail(data); | |
| } catch (err) { | |
| console.error("Failed to fetch quiz detail:", err); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| fetchDetail(); | |
| }, [quizId]); | |
| useEffect(() => { | |
| let active = true; | |
| if (!quizId) { | |
| setQuizDetail(null); | |
| setLoading(false); | |
| return; | |
| } | |
| setQuizDetail(null); | |
| setLoading(true); | |
| const fetchDetail = async () => { | |
| try { | |
| const data = await quizApi.getQuizDetail(quizId); | |
| if (active) setQuizDetail(data); | |
| } catch (err) { | |
| console.error("Failed to fetch quiz detail:", err); | |
| if (active) setQuizDetail(null); | |
| } finally { | |
| if (active) setLoading(false); | |
| } | |
| }; | |
| fetchDetail(); | |
| return () => { | |
| active = false; | |
| }; | |
| }, [quizId]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/lecturer/quiz/quiz-preview-view.tsx` around lines 34
- 50, Update the useEffect fetch flow keyed by quizId so a missing ID clears
quiz details and stops loading immediately. Clear or invalidate prior details
when the ID changes, and guard fetchDetail’s success and finally updates so
stale or failed requests cannot leave another quiz’s details available to
handlePublish or overwrite current loading state. Preserve the existing detail
fetch for the active quizId.
Summary by CodeRabbit
New Features
UI Improvements