Skip to content

feat: interactive quiz UX - single question + navigation + progress (…#482

Open
Tanushreesmallikarjuna wants to merge 1 commit intoAOSSIE-Org:mainfrom
Tanushreesmallikarjuna:feature/quiz-progress-ui
Open

feat: interactive quiz UX - single question + navigation + progress (…#482
Tanushreesmallikarjuna wants to merge 1 commit intoAOSSIE-Org:mainfrom
Tanushreesmallikarjuna:feature/quiz-progress-ui

Conversation

@Tanushreesmallikarjuna
Copy link

@Tanushreesmallikarjuna Tanushreesmallikarjuna commented Feb 25, 2026

fixes #474

Addressed Issues:

Fixes #474

Demo Flow:

  1. Progress indicator shows "Question 1 of 5"
  2. Next/Previous navigation works smoothly
  3. Clean, single-question focus improves readability

Additional Notes:

  • Frontend-only changes (no backend/model modifications)
  • Responsive design works on mobile + desktop
  • Production-ready React components
  • Ready for mentor feedback and iteration

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

AI Usage Disclosure

  • This PR does not contain AI-generated code at all.

Summary by CodeRabbit

  • New Features

    • Added sequential navigation controls (Previous/Next) to browse through questions one at a time.
    • Added question counter display showing current position (e.g., "Question X of N").
  • UI/UX Changes

    • Removed PDF view mode and streamlined interface for consistent question display.
    • Redesigned header layout with simplified navigation and question tracking.
    • Maintained edit/save functionality and Google Form generation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Refactored Output component to display one QA pair at a time using currentIndex state instead of PDF-mode handling. Added question navigation controls, progress indicator (Question X of N), and streamlined data loading from localStorage. UI now shows current question with Edit/Save flow and Previous/Next navigation.

Changes

Cohort / File(s) Summary
Quiz Navigation & UX Refactor
eduaid_web/src/pages/Output.jsx
Replaced pdfMode state with currentIndex-based navigation system. Added Previous/Next handlers to move between QA pairs while blocking navigation during edits. Updated header to display progress indicator (Question X of N). Simplified data loading to build combined QA pairs from localStorage. Streamlined editing flow to operate on current question. Updated shuffleArray logic and removed PDF dropdown UI.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #383: Both PRs modify the same Output.jsx editing and shuffling logic (editing state/handlers, shuffleArray, shuffledOptionsMap, handleShuffleQuestions, and option-edit/save flows).

Poem

🐰 Question by question, we hop along,
Progress glows bright like a quiz-day song!
"Question Three of Ten," the header cries,
Next button waits beneath blue skies.
EduAid's flow now smooth and clear,
One question at a time, my dear! 📚✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements question-by-question navigation and progress indicator but lacks immediate feedback after answering and final summary screen as required by issue #474. Add immediate feedback display (correct/incorrect) after answering and implement a final summary screen showing score and weak areas before merging.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: implementing interactive quiz UX with single question display, navigation, and progress indicator.
Out of Scope Changes check ✅ Passed All changes are limited to the Output.jsx component and focus on UI/interaction layer improvements without modifying backend or model logic, staying within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@eduaid_web/src/pages/Output.jsx`:
- Around line 36-37: The bug is that currentIndex can point past the newly
loaded qaPairs (and shuffledOptionsMap) causing blank renders; update the
component to reset currentIndex to a valid value whenever the loaded QA data
changes (e.g., when qaPairs length changes or new data is assigned) — inside the
effect or handler that sets qaPairs/shuffledOptionsMap, call setCurrentIndex(0)
or clamp it to Math.max(0, Math.min(currentIndex, qaPairs.length - 1)), and
similarly ensure any uses of shuffledOptionsMap[currentIndex] handle the new
index (reinitialize or clamp shuffledOptionsMap indices) so both the references
around qaPairs/currentIndex and shuffledOptionsMap/currentIndex are safe after
reload.
- Around line 157-246: The screen currently only supports navigation and editing
(qaPair, shuffledOptions, currentIndex, qaPairs, isEditing, handleEditQuestion,
handleSaveQuestion, handleCancelEdit, handlePrevious, handleNext,
generateGoogleForm) but lacks answer attempt flow, immediate feedback, scoring,
and retry-incorrect behavior; implement: add local state for selectedAnswer and
per-question result (correct/incorrect) and score summary, render clickable
answer buttons for shuffledOptions that call an attempt handler (e.g.,
handleAttemptAnswer(questionIndex, option)), show immediate feedback UI
(correct/incorrect) and disable further attempts for that question, track score
and list of incorrect question indices, add end-of-quiz summary view showing
total correct, percentage, and a "Retry Incorrect" action that resets only the
incorrect questions (or navigates through them) while preserving edits, and wire
these flows into navigation (handleNext/handlePrevious) so feedback and attempt
state persist when paging between questions.
- Around line 145-147: The progress and nav controls don't handle an empty
qaPairs list or editing state: change the progress header that uses currentIndex
and qaPairs to conditionally render a friendly empty state (e.g., "No questions"
or nothing) when qaPairs.length === 0, and only render "Question {currentIndex +
1} of {qaPairs.length}" when qaPairs.length > 0; additionally, disable the
navigation buttons (Prev/Next) and any handlers like handlePrev/handleNext when
qaPairs.length === 0 or when the editing flag (e.g., isEditing) is true so
navigation is unavailable while editing or when there are no questions.
- Around line 30-33: The shuffledOptionsMap useMemo currently appends qa.answer
unconditionally which can create duplicates when qa.answer already exists in
qa.options; update the logic inside useMemo (where shuffledOptionsMap is
computed from qaPairs and uses shuffleArray) to first build a deduplicated
options array—e.g., take qa.options || [], filter out any entries equal to
qa.answer, then push qa.answer once (or use a Set) before calling
shuffleArray—so shuffledOptionsMap contains unique choices with the correct
answer included exactly once.
- Around line 95-118: The effect reading localStorage in useEffect can throw if
the stored value is missing or malformed and will crash when accessing nested
arrays like stored.output_mcq.questions or stored.output.forEach; wrap the
JSON.parse in a try/catch and guard downstream accesses by verifying stored is
an object and using Array.isArray before iterating (e.g., check
stored.output_mcq && Array.isArray(stored.output_mcq.questions) and
stored.output && Array.isArray(stored.output)), and handle fallback empty arrays
so combined building always iterates safely without throwing (retain existing
fields question/question_statement, answer, options, question_type).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc3bf1a and 658c249.

📒 Files selected for processing (1)
  • eduaid_web/src/pages/Output.jsx

Comment on lines 30 to +33
const shuffledOptionsMap = useMemo(() => {
return qaPairs.map((qaPair) => {
const combinedOptions = qaPair.options
? [...qaPair.options, qaPair.answer]
: [qaPair.answer];
return shuffleArray(combinedOptions);
});
return qaPairs.map((qa) =>
qa.options ? shuffleArray([...qa.options, qa.answer]) : []
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prevent duplicated MCQ options when answer is already in options.

The current merge always appends qa.answer, which can duplicate a choice and skew the quiz UI.

🔧 Proposed fix
 const shuffledOptionsMap = useMemo(() => {
   return qaPairs.map((qa) =>
-    qa.options ? shuffleArray([...qa.options, qa.answer]) : []
+    qa.options
+      ? shuffleArray(
+          [...new Set([...(qa.options || []), qa.answer].filter(Boolean))]
+        )
+      : []
   );
 }, [qaPairs]);
📝 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
const shuffledOptionsMap = useMemo(() => {
return qaPairs.map((qaPair) => {
const combinedOptions = qaPair.options
? [...qaPair.options, qaPair.answer]
: [qaPair.answer];
return shuffleArray(combinedOptions);
});
return qaPairs.map((qa) =>
qa.options ? shuffleArray([...qa.options, qa.answer]) : []
);
const shuffledOptionsMap = useMemo(() => {
return qaPairs.map((qa) =>
qa.options
? shuffleArray(
[...new Set([...(qa.options || []), qa.answer].filter(Boolean))]
)
: []
);
}, [qaPairs]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eduaid_web/src/pages/Output.jsx` around lines 30 - 33, The shuffledOptionsMap
useMemo currently appends qa.answer unconditionally which can create duplicates
when qa.answer already exists in qa.options; update the logic inside useMemo
(where shuffledOptionsMap is computed from qaPairs and uses shuffleArray) to
first build a deduplicated options array—e.g., take qa.options || [], filter out
any entries equal to qa.answer, then push qa.answer once (or use a Set) before
calling shuffleArray—so shuffledOptionsMap contains unique choices with the
correct answer included exactly once.

Comment on lines +36 to +37
const qaPair = qaPairs[currentIndex];
const shuffledOptions = shuffledOptionsMap[currentIndex];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reinitialize currentIndex when reloading QA data.

When the loaded set changes size, index state can point past the new array and render a blank card.

🔧 Proposed fix
-  setQaPairs(combined);
+  setQaPairs(combined);
+  setCurrentIndex(0);
 }, [questionType]);

Also applies to: 121-122

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eduaid_web/src/pages/Output.jsx` around lines 36 - 37, The bug is that
currentIndex can point past the newly loaded qaPairs (and shuffledOptionsMap)
causing blank renders; update the component to reset currentIndex to a valid
value whenever the loaded QA data changes (e.g., when qaPairs length changes or
new data is assigned) — inside the effect or handler that sets
qaPairs/shuffledOptionsMap, call setCurrentIndex(0) or clamp it to Math.max(0,
Math.min(currentIndex, qaPairs.length - 1)), and similarly ensure any uses of
shuffledOptionsMap[currentIndex] handle the new index (reinitialize or clamp
shuffledOptionsMap indices) so both the references around qaPairs/currentIndex
and shuffledOptionsMap/currentIndex are safe after reload.

Comment on lines 95 to +118
useEffect(() => {
const qaPairsFromStorage =
JSON.parse(localStorage.getItem("qaPairs")) || {};
if (qaPairsFromStorage) {
const combinedQaPairs = [];

if (qaPairsFromStorage["output_boolq"]) {
qaPairsFromStorage["output_boolq"]["Boolean_Questions"].forEach(
(question, index) => {
combinedQaPairs.push({
question,
question_type: "Boolean",
context: qaPairsFromStorage["output_boolq"]["Text"],
});
}
);
}

if (qaPairsFromStorage["output_mcq"]) {
qaPairsFromStorage["output_mcq"]["questions"].forEach((qaPair) => {
combinedQaPairs.push({
question: qaPair.question_statement,
question_type: "MCQ",
options: qaPair.options,
answer: qaPair.answer,
context: qaPair.context,
});
});
}

if (qaPairsFromStorage["output_mcq"] || questionType === "get_mcq") {
qaPairsFromStorage["output"].forEach((qaPair) => {
combinedQaPairs.push({
question: qaPair.question_statement,
question_type: "MCQ",
options: qaPair.options,
answer: qaPair.answer,
context: qaPair.context,
});
});
}

if (questionType == "get_boolq") {
qaPairsFromStorage["output"].forEach((qaPair) => {
combinedQaPairs.push({
question: qaPair,
question_type: "Boolean",
});
const stored = JSON.parse(localStorage.getItem("qaPairs")) || {};
const combined = [];

if (stored.output_mcq) {
stored.output_mcq.questions.forEach((q) => {
combined.push({
question: q.question_statement,
answer: q.answer,
options: q.options,
question_type: "MCQ",
});
} else if (qaPairsFromStorage["output"] && questionType !== "get_mcq") {
qaPairsFromStorage["output"].forEach((qaPair) => {
combinedQaPairs.push({
question:
qaPair.question || qaPair.question_statement || qaPair.Question,
options: qaPair.options,
answer: qaPair.answer || qaPair.Answer,
context: qaPair.context,
question_type: "Short",
});
});
}

setQaPairs(combinedQaPairs);
}
}, []);

const generateGoogleForm = async () => {
try {
const result = await apiClient.post("/generate_gform", {
qa_pairs: qaPairs,
question_type: questionType,
});
const formUrl = result.form_link;
window.open(formUrl, "_blank");
} catch (error) {
console.error("Failed to generate Google Form:", error);
}
};

const loadLogoAsBytes = async () => {
try {
const response = await fetch(logoPNG);
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
console.error('Error loading logo:', error);
return null;
if (stored.output && questionType !== "get_mcq") {
stored.output.forEach((q) => {
combined.push({
question: q.question || q.question_statement,
answer: q.answer,
options: q.options,
question_type: "Short",
});
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard storage parsing and nested shape access to avoid hard crashes.

Malformed localStorage content or missing nested arrays will currently throw during render lifecycle.

🛡️ Proposed hardening
 useEffect(() => {
-  const stored = JSON.parse(localStorage.getItem("qaPairs")) || {};
+  let stored = {};
+  try {
+    stored = JSON.parse(localStorage.getItem("qaPairs") || "{}");
+  } catch {
+    stored = {};
+  }
   const combined = [];
 
-  if (stored.output_mcq) {
+  if (Array.isArray(stored.output_mcq?.questions)) {
     stored.output_mcq.questions.forEach((q) => {
       combined.push({
         question: q.question_statement,
         answer: q.answer,
         options: q.options,
         question_type: "MCQ",
       });
     });
   }
 
-  if (stored.output && questionType !== "get_mcq") {
+  if (Array.isArray(stored.output) && questionType !== "get_mcq") {
     stored.output.forEach((q) => {
       combined.push({
         question: q.question || q.question_statement,
         answer: q.answer,
         options: q.options,
         question_type: "Short",
       });
     });
   }
📝 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(() => {
const qaPairsFromStorage =
JSON.parse(localStorage.getItem("qaPairs")) || {};
if (qaPairsFromStorage) {
const combinedQaPairs = [];
if (qaPairsFromStorage["output_boolq"]) {
qaPairsFromStorage["output_boolq"]["Boolean_Questions"].forEach(
(question, index) => {
combinedQaPairs.push({
question,
question_type: "Boolean",
context: qaPairsFromStorage["output_boolq"]["Text"],
});
}
);
}
if (qaPairsFromStorage["output_mcq"]) {
qaPairsFromStorage["output_mcq"]["questions"].forEach((qaPair) => {
combinedQaPairs.push({
question: qaPair.question_statement,
question_type: "MCQ",
options: qaPair.options,
answer: qaPair.answer,
context: qaPair.context,
});
});
}
if (qaPairsFromStorage["output_mcq"] || questionType === "get_mcq") {
qaPairsFromStorage["output"].forEach((qaPair) => {
combinedQaPairs.push({
question: qaPair.question_statement,
question_type: "MCQ",
options: qaPair.options,
answer: qaPair.answer,
context: qaPair.context,
});
});
}
if (questionType == "get_boolq") {
qaPairsFromStorage["output"].forEach((qaPair) => {
combinedQaPairs.push({
question: qaPair,
question_type: "Boolean",
});
const stored = JSON.parse(localStorage.getItem("qaPairs")) || {};
const combined = [];
if (stored.output_mcq) {
stored.output_mcq.questions.forEach((q) => {
combined.push({
question: q.question_statement,
answer: q.answer,
options: q.options,
question_type: "MCQ",
});
} else if (qaPairsFromStorage["output"] && questionType !== "get_mcq") {
qaPairsFromStorage["output"].forEach((qaPair) => {
combinedQaPairs.push({
question:
qaPair.question || qaPair.question_statement || qaPair.Question,
options: qaPair.options,
answer: qaPair.answer || qaPair.Answer,
context: qaPair.context,
question_type: "Short",
});
});
}
setQaPairs(combinedQaPairs);
}
}, []);
const generateGoogleForm = async () => {
try {
const result = await apiClient.post("/generate_gform", {
qa_pairs: qaPairs,
question_type: questionType,
});
const formUrl = result.form_link;
window.open(formUrl, "_blank");
} catch (error) {
console.error("Failed to generate Google Form:", error);
}
};
const loadLogoAsBytes = async () => {
try {
const response = await fetch(logoPNG);
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} catch (error) {
console.error('Error loading logo:', error);
return null;
if (stored.output && questionType !== "get_mcq") {
stored.output.forEach((q) => {
combined.push({
question: q.question || q.question_statement,
answer: q.answer,
options: q.options,
question_type: "Short",
});
});
useEffect(() => {
let stored = {};
try {
stored = JSON.parse(localStorage.getItem("qaPairs") || "{}");
} catch {
stored = {};
}
const combined = [];
if (Array.isArray(stored.output_mcq?.questions)) {
stored.output_mcq.questions.forEach((q) => {
combined.push({
question: q.question_statement,
answer: q.answer,
options: q.options,
question_type: "MCQ",
});
});
}
if (Array.isArray(stored.output) && questionType !== "get_mcq") {
stored.output.forEach((q) => {
combined.push({
question: q.question || q.question_statement,
answer: q.answer,
options: q.options,
question_type: "Short",
});
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eduaid_web/src/pages/Output.jsx` around lines 95 - 118, The effect reading
localStorage in useEffect can throw if the stored value is missing or malformed
and will crash when accessing nested arrays like stored.output_mcq.questions or
stored.output.forEach; wrap the JSON.parse in a try/catch and guard downstream
accesses by verifying stored is an object and using Array.isArray before
iterating (e.g., check stored.output_mcq &&
Array.isArray(stored.output_mcq.questions) and stored.output &&
Array.isArray(stored.output)), and handle fallback empty arrays so combined
building always iterates safely without throwing (retain existing fields
question/question_statement, answer, options, question_type).

Comment on lines +145 to 147
<div className="text-white font-bold text-xl">
Question {currentIndex + 1} of {qaPairs.length}
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle zero-question state in progress/nav controls.

With an empty list, the UI shows Question 1 of 0 and Next can appear enabled. Also disable nav buttons while editing for consistent UX.

🔧 Proposed fix
 const qaPair = qaPairs[currentIndex];
 const shuffledOptions = shuffledOptionsMap[currentIndex];
 const isEditing = editingIndex === currentIndex;
+const totalQuestions = qaPairs.length;
+const hasQuestions = totalQuestions > 0;
 
 ...
-Question {currentIndex + 1} of {qaPairs.length}
+Question {hasQuestions ? currentIndex + 1 : 0} of {totalQuestions}
 
 ...
- disabled={currentIndex === 0}
+ disabled={isEditing || !hasQuestions || currentIndex === 0}
 
 ...
- disabled={currentIndex === qaPairs.length - 1}
+ disabled={
+   isEditing || !hasQuestions || currentIndex >= totalQuestions - 1
+ }

Also applies to: 209-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eduaid_web/src/pages/Output.jsx` around lines 145 - 147, The progress and nav
controls don't handle an empty qaPairs list or editing state: change the
progress header that uses currentIndex and qaPairs to conditionally render a
friendly empty state (e.g., "No questions" or nothing) when qaPairs.length ===
0, and only render "Question {currentIndex + 1} of {qaPairs.length}" when
qaPairs.length > 0; additionally, disable the navigation buttons (Prev/Next) and
any handlers like handlePrev/handleNext when qaPairs.length === 0 or when the
editing flag (e.g., isEditing) is true so navigation is unavailable while
editing or when there are no questions.

Comment on lines +157 to 246
<div className="flex-1 overflow-y-auto px-6 mt-4">
{qaPair && (
<div className="bg-[#ffffff0d] p-4 rounded-xl">
{!isEditing ? (
<>
<p className="text-white text-lg">{qaPair.question}</p>

{qaPair.options && (
<div className="mt-4">
{shuffledOptions.map((opt, i) => (
<div key={i} className="text-gray-200">
Option {i + 1}: {opt}
</div>
)}
))}
</div>
)}

{!isEditing ? (
<>
<div className="text-[#FFF4F4] text-sm sm:text-base my-1 sm:my-2 leading-relaxed">
{qaPair.question}
</div>
{qaPair.question_type !== "Boolean" && (
<>
<div className="text-[#E4E4E4] text-xs sm:text-sm mt-3 sm:mt-4">
Answer
</div>
<div className="text-[#FFF4F4] text-sm sm:text-base leading-relaxed">
{qaPair.answer}
</div>
{qaPair.options && qaPair.options.length > 0 && (
<div className="text-[#FFF4F4] text-sm sm:text-base mt-2 sm:mt-3">
{shuffledOptions.map((option, idx) => (
<div key={idx} className="mb-1 sm:mb-2">
<span className="text-[#E4E4E4] text-xs sm:text-sm">
Option {idx + 1}:
</span>{" "}
<span className="text-[#FFF4F4] text-sm sm:text-base">
{option}
</span>
</div>
))}
</div>
)}
</>
)}
</>
) : (
<>
<div className="text-[#E4E4E4] text-xs sm:text-sm mb-1">
Edit Question
</div>
<textarea
className="w-full bg-[#1a1a2e] text-[#FFF4F4] text-sm sm:text-base p-2 rounded border border-gray-600 focus:border-[#7600F2] focus:outline-none resize-none"
rows="3"
value={editedQuestion}
onChange={(e) => setEditedQuestion(e.target.value)}
/>

{qaPair.question_type !== "Boolean" && (
<>
<div className="text-[#E4E4E4] text-xs sm:text-sm mt-3 mb-1">
Edit Answer
</div>
<textarea
className="w-full bg-[#1a1a2e] text-[#FFF4F4] text-sm sm:text-base p-2 rounded border border-gray-600 focus:border-[#7600F2] focus:outline-none resize-none"
rows="2"
value={editedAnswer}
onChange={(e) => setEditedAnswer(e.target.value)}
/>

{editedOptions && editedOptions.length > 0 && (
<div className="mt-3">
<div className="text-[#E4E4E4] text-xs sm:text-sm mb-2">
Edit Options
</div>
{editedOptions.map((option, optIdx) => (
<div key={optIdx} className="mb-2">
<div className="text-[#E4E4E4] text-xs mb-1">
Option {optIdx + 1}
</div>
<input
type="text"
className="w-full bg-[#1a1a2e] text-[#FFF4F4] text-sm sm:text-base p-2 rounded border border-gray-600 focus:border-[#7600F2] focus:outline-none"
value={option}
onChange={(e) => handleOptionChange(optIdx, e.target.value)}
/>
</div>
))}
</div>
)}
</>
)}
</>
)}
</div>
);
})}
</div>
<button
className="mt-4 bg-teal-600 px-4 py-2 rounded text-white"
onClick={() => handleEditQuestion(currentIndex)}
>
<FiEdit2 /> Edit
</button>
</>
) : (
<>
<textarea
value={editedQuestion}
onChange={(e) => setEditedQuestion(e.target.value)}
className="w-full p-2 bg-black text-white rounded"
/>

<button
onClick={() => handleSaveQuestion(currentIndex)}
className="bg-green-600 px-4 py-2 mt-3 rounded text-white"
>
<FiCheck /> Save
</button>

{/* Action Buttons - Responsive layout */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 sm:gap-6 mx-4 sm:mx-auto pb-4 sm:pb-6">
<button
className="bg-[#518E8E] items-center flex gap-1 w-full sm:w-auto font-semibold text-white px-4 sm:px-6 py-3 sm:py-2 rounded-xl text-sm sm:text-base hover:bg-[#3a6b6b] transition-colors justify-center"
onClick={generateGoogleForm}
>
Generate Google form
</button>

<div className="relative w-full sm:w-auto">
<button
className="bg-[#518E8E] items-center flex gap-1 w-full sm:w-auto font-semibold text-white px-4 sm:px-6 py-3 sm:py-2 rounded-xl text-sm sm:text-base hover:bg-[#3a6b6b] transition-colors justify-center"
onClick={() => document.getElementById('pdfDropdown').classList.toggle('hidden')}
>
Generate PDF
</button>

<div
id="pdfDropdown"
className="hidden absolute bottom-full mb-1 left-0 sm:left-auto right-0 sm:right-auto bg-[#02000F] shadow-md text-white rounded-lg shadow-lg z-50 w-full sm:w-48"
>
<button
className="block w-full text-left px-4 py-2 hover:bg-gray-500 rounded-t-lg text-sm sm:text-base"
onClick={() => generatePDF('questions')}
>
Questions Only
</button>
<button
className="block w-full text-left px-4 py-2 hover:bg-gray-500 text-sm sm:text-base"
onClick={() => generatePDF('questions_answers')}
>
Questions with Answers
</button>
<button
className="block w-full text-left px-4 py-2 hover:bg-gray-500 rounded-b-lg text-sm sm:text-base"
onClick={() => generatePDF('answers')}
>
Answers Only
</button>
</div>
<button
onClick={handleCancelEdit}
className="ml-2 bg-gray-600 px-4 py-2 rounded text-white"
>
<FiX /> Cancel
</button>
</>
)}
</div>
</div>
)}
</div>

{/* Navigation Buttons */}
<div className="flex justify-between items-center px-6 pb-4">
<button
onClick={handlePrevious}
disabled={currentIndex === 0}
className={`px-4 py-2 rounded text-white ${
currentIndex === 0
? "bg-gray-500 cursor-not-allowed"
: "bg-teal-600 hover:bg-teal-700"
}`}
>
⬅ Previous
</button>

<span className="text-white text-sm">
Question {currentIndex + 1} of {qaPairs.length}
</span>

<button
onClick={handleNext}
disabled={currentIndex === qaPairs.length - 1}
className={`px-4 py-2 rounded text-white ${
currentIndex === qaPairs.length - 1
? "bg-gray-500 cursor-not-allowed"
: "bg-teal-600 hover:bg-teal-700"
}`}
>
Next ➡
</button>
</div>

<div className="flex justify-center gap-4 pb-6">
<button
onClick={generateGoogleForm}
className="bg-teal-600 px-6 py-2 rounded text-white"
>
Generate Google Form
</button>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Linked issue #474 acceptance flow is still incomplete in this screen.

The current UI provides navigation/editing, but there is no answer-attempt interaction, immediate correct/incorrect feedback, or final score/summary + retry-incorrect flow visible here. If this PR is intended to close #474, those behaviors are still missing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eduaid_web/src/pages/Output.jsx` around lines 157 - 246, The screen currently
only supports navigation and editing (qaPair, shuffledOptions, currentIndex,
qaPairs, isEditing, handleEditQuestion, handleSaveQuestion, handleCancelEdit,
handlePrevious, handleNext, generateGoogleForm) but lacks answer attempt flow,
immediate feedback, scoring, and retry-incorrect behavior; implement: add local
state for selectedAnswer and per-question result (correct/incorrect) and score
summary, render clickable answer buttons for shuffledOptions that call an
attempt handler (e.g., handleAttemptAnswer(questionIndex, option)), show
immediate feedback UI (correct/incorrect) and disable further attempts for that
question, track score and list of incorrect question indices, add end-of-quiz
summary view showing total correct, percentage, and a "Retry Incorrect" action
that resets only the incorrect questions (or navigates through them) while
preserving edits, and wire these flows into navigation
(handleNext/handlePrevious) so feedback and attempt state persist when paging
between questions.

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.

[GOOD FIRST ISSUE]: Improve Quiz-Taking UX with Progress, Feedback, and Summary Flow

1 participant