Skip to content

Refactor/improve UI - #36

Merged
UGing265 merged 3 commits into
mainfrom
refactor/improve-ui
Jul 17, 2026
Merged

Refactor/improve UI#36
UGing265 merged 3 commits into
mainfrom
refactor/improve-ui

Conversation

@Dkain-icl

@Dkain-icl Dkain-icl commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added a dedicated quiz preview page for lecturers, including question details, answer indicators, AI explanations, quiz status, and draft publishing.
    • Generated and selected quizzes now open through the preview page.
    • Added support for optional question explanations.
  • UI Improvements

    • Simplified the lecturer practice dashboard by removing student results and the previous preview modal.
    • Refreshed lecturer and student practice page headers and layouts.
    • Added loading and error states to quiz previews.

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-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Quiz preview flow

Layer / File(s) Summary
Preview route and rendering
frontend/src/api/quiz.ts, frontend/src/app/[role]/practice/quiz/page.tsx, frontend/src/components/lecturer/quiz/quiz-preview-view.tsx
Quiz questions support optional explanations, and the quiz page selects between student-taking and lecturer-preview views using the preview query parameter.
Lecturer preview navigation
frontend/src/hooks/lecturer/use-practice.ts
Quiz generation completion and preview selection navigate to the routed quiz preview page.
Practice page layouts
frontend/src/components/lecturer/practice/practice-view.tsx, frontend/src/components/student/practice/student-practice-view.tsx
The lecturer results tab and embedded preview modal are removed, while the student practice page receives a sticky header and revised layout.

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
Loading

Possibly related PRs

Suggested reviewers: uging265

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic and does not describe the specific UI changes in this PR. Use a concise title that names the main change, such as adding quiz preview/navigation UI for practice pages.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/improve-ui

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 33db500 and bbdf0c2.

📒 Files selected for processing (6)
  • frontend/src/api/quiz.ts
  • frontend/src/app/[role]/practice/quiz/page.tsx
  • frontend/src/components/lecturer/practice/practice-view.tsx
  • frontend/src/components/lecturer/quiz/quiz-preview-view.tsx
  • frontend/src/components/student/practice/student-practice-view.tsx
  • frontend/src/hooks/lecturer/use-practice.ts

Comment on lines +9 to +15
function QuizPageContent() {
const searchParams = useSearchParams();
const isPreview = searchParams.get("preview") === "true";

if (isPreview) {
return <QuizPreviewView />;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.tsx

Repository: UGing265/SWD392_Chatbot_RAG

Length of output: 1199


🏁 Script executed:

ast-grep outline frontend/src/app/[role]/practice/quiz/page.tsx --view expanded

Repository: 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/src

Repository: 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.ts

Repository: 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.ts

Repository: 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

Comment on lines +23 to +244
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>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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-L181
  • frontend/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

Comment on lines +34 to +50
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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.

@UGing265
UGing265 merged commit 3e38b14 into main Jul 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants