Add interactive quiz player feature in Output.jsx with answer selection and score calculation#553
Conversation
…on and score calculation
📝 WalkthroughWalkthroughAdds client-side quiz state and scoring UI to Output.jsx and adds a guard in Text_Input.jsx to require either text input or a Google Doc URL before saving. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
eduaid_web/src/pages/Output.jsx (1)
250-262:⚠️ Potential issue | 🔴 CriticalInvalid HTML: button nested inside another button.
The "Submit Quiz" button (lines 255-260) is nested inside the "Generate Google form" button (lines 250-262). This is invalid HTML—
<button>elements cannot contain interactive content. This causes:
- Both
onClickhandlers fire when clicking "Submit Quiz" (event bubbling)- Unpredictable browser behavior
- Accessibility violations
Move the "Submit Quiz" button outside and make it a sibling element.
🐛 Proposed fix
<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 - className="bg-green-600 text-white px-6 py-2 rounded-xl" - onClick={calculateScore} - > - Submit Quiz - </button> - </button> + + <button + className="bg-green-600 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-green-700 transition-colors justify-center" + onClick={calculateScore} + > + Submit Quiz + </button> <div className="relative w-full sm:w-auto">🤖 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 250 - 262, The "Submit Quiz" <button> is incorrectly nested inside the "Generate Google form" <button>, causing invalid HTML and mixed event handling; move the inner <button> that calls calculateScore so it becomes a sibling (not a child) of the outer button that calls generateGoogleForm, updating the JSX around the elements that use the generateGoogleForm and calculateScore onClick handlers and preserving their className/styling (e.g., the outer button with className "bg-[`#518E8E`] ...", and the inner with "bg-green-600 ...") so both buttons render as separate elements next to each other.
🧹 Nitpick comments (2)
eduaid_web/src/pages/Output.jsx (2)
35-40: Use functional state update to avoid stale closure issues.When updating state based on previous state, prefer the functional update pattern to ensure you're working with the latest state value, especially if multiple rapid clicks occur.
♻️ Proposed fix
const handleAnswerSelect = (questionIndex, option) => { - setUserAnswers({ - ...userAnswers, - [questionIndex]: option - }); + setUserAnswers((prev) => ({ + ...prev, + [questionIndex]: option, + })); };🤖 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 35 - 40, The state update in handleAnswerSelect uses the stale userAnswers object; change it to a functional state update by calling setUserAnswers with an updater function that receives the previous state and returns a new object merging prev and {[questionIndex]: option} so updates are based on the latest state (reference: handleAnswerSelect, setUserAnswers, userAnswers, questionIndex, option).
226-230: No visual feedback for selected answer.Users cannot see which option they've selected. Consider adding a visual indicator (e.g., background color or border) for the selected option.
♻️ Example implementation
<div key={idx} - className="mb-1 sm:mb-2 cursor-pointer" + className={`mb-1 sm:mb-2 cursor-pointer p-2 rounded ${ + userAnswers[index] === option + ? "bg-[`#518E8E`] border border-white" + : "hover:bg-[`#ffffff1a`]" + }`} onClick={() => handleAnswerSelect(index, option)} >🤖 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 226 - 230, The option div currently calls handleAnswerSelect(index, option) but provides no visual state; add a selected state (e.g., selectedAnswers or selectedAnswer per question) and update handleAnswerSelect to store the chosen option for that question, then apply a conditional className on the div (using idx/index/option) to add a visual indicator such as "bg-blue-100 border-blue-500" or a highlighted style when the stored selection equals option; ensure the click handler still calls handleAnswerSelect so UI and state stay in sync.
🤖 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 41-51: The options are being reshuffled on every render because
shuffleArray is called inline, so when setUserAnswers causes a re-render the
option order changes; fix this by computing shuffled options once when qaPairs
are loaded (inside the existing useEffect that builds combinedQaPairs) by
mapping each pair to include a shuffledOptions property using
shuffleArray(allOptions) and then call setQaPairs with those pairs, remove the
inline shuffleArray call from the render and use qaPair.shuffledOptions instead;
reference symbols: shuffleArray, qaPairs, setQaPairs, setUserAnswers, useEffect.
- Around line 44-48: The score loop treats all qaPairs as MCQs but Boolean
questions lack an answer property and are not rendered, causing them to always
be marked wrong; change the scoring logic in Output.jsx so you only score
rendered/answerable questions — e.g., build a filteredQuestions =
qaPairs.filter(q => q.question_type !== "Boolean" && q.answer !== undefined) (or
filter by presence of q.answer) and then iterate filteredQuestions to compare
against userAnswers, or if userAnswers is keyed by question id, match by q.id
instead of index; update the loop that references qaPairs, userAnswers and score
to use this filtered/mapped collection so Boolean questions are excluded from
scoring.
---
Outside diff comments:
In `@eduaid_web/src/pages/Output.jsx`:
- Around line 250-262: The "Submit Quiz" <button> is incorrectly nested inside
the "Generate Google form" <button>, causing invalid HTML and mixed event
handling; move the inner <button> that calls calculateScore so it becomes a
sibling (not a child) of the outer button that calls generateGoogleForm,
updating the JSX around the elements that use the generateGoogleForm and
calculateScore onClick handlers and preserving their className/styling (e.g.,
the outer button with className "bg-[`#518E8E`] ...", and the inner with
"bg-green-600 ...") so both buttons render as separate elements next to each
other.
---
Nitpick comments:
In `@eduaid_web/src/pages/Output.jsx`:
- Around line 35-40: The state update in handleAnswerSelect uses the stale
userAnswers object; change it to a functional state update by calling
setUserAnswers with an updater function that receives the previous state and
returns a new object merging prev and {[questionIndex]: option} so updates are
based on the latest state (reference: handleAnswerSelect, setUserAnswers,
userAnswers, questionIndex, option).
- Around line 226-230: The option div currently calls handleAnswerSelect(index,
option) but provides no visual state; add a selected state (e.g.,
selectedAnswers or selectedAnswer per question) and update handleAnswerSelect to
store the chosen option for that question, then apply a conditional className on
the div (using idx/index/option) to add a visual indicator such as "bg-blue-100
border-blue-500" or a highlighted style when the stored selection equals option;
ensure the click handler still calls handleAnswerSelect so UI and state stay in
sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8a727393-8067-400e-b509-594c32f1495c
⛔ Files ignored due to path filters (1)
eduaid_web/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (1)
eduaid_web/src/pages/Output.jsx
| const calculateScore = () => { | ||
| let score = 0; | ||
|
|
||
| qaPairs.forEach((q, i) => { | ||
| if (userAnswers[i] === q.answer) { | ||
| score++; | ||
| } | ||
| }); | ||
|
|
||
| alert(`Your Score: ${score} / ${qaPairs.length}`); | ||
| }; |
There was a problem hiding this comment.
Options reshuffle on every render, breaking selection UX.
The shuffleArray call at line 203 executes on every render. When a user selects an answer, setUserAnswers triggers a re-render, causing all options to reshuffle. This makes the quiz unusable—the selected option moves to a different position, and the visual feedback is lost.
Consider memoizing the shuffled options (e.g., compute once in useEffect and store in state alongside qaPairs).
🛠️ Suggested approach
Store shuffled options when qaPairs is loaded:
useEffect(() => {
// ... existing qaPairs loading logic ...
// After building combinedQaPairs, shuffle options once:
const pairsWithShuffledOptions = combinedQaPairs.map((pair) => {
if (pair.options && pair.options.length > 0) {
const allOptions = [...pair.options, pair.answer];
return { ...pair, shuffledOptions: shuffleArray(allOptions) };
}
return pair;
});
setQaPairs(pairsWithShuffledOptions);
}, []);Then in the render, use qaPair.shuffledOptions instead of computing shuffledOptions inline.
Also applies to: 200-203
🤖 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 41 - 51, The options are being
reshuffled on every render because shuffleArray is called inline, so when
setUserAnswers causes a re-render the option order changes; fix this by
computing shuffled options once when qaPairs are loaded (inside the existing
useEffect that builds combinedQaPairs) by mapping each pair to include a
shuffledOptions property using shuffleArray(allOptions) and then call setQaPairs
with those pairs, remove the inline shuffleArray call from the render and use
qaPair.shuffledOptions instead; reference symbols: shuffleArray, qaPairs,
setQaPairs, setUserAnswers, useEffect.
| qaPairs.forEach((q, i) => { | ||
| if (userAnswers[i] === q.answer) { | ||
| score++; | ||
| } | ||
| }); |
There was a problem hiding this comment.
Score calculation ignores Boolean questions.
Boolean questions (loaded at lines 95-101) don't have an answer property, so q.answer is undefined. Additionally, the UI (lines 215-242) doesn't render selectable options for Boolean questions—they're skipped with the question_type !== "Boolean" check. This means:
- Boolean questions cannot be answered
- They always count as incorrect in the score
If the quiz feature is MCQ-only for now, consider filtering Boolean questions from the score calculation or documenting this limitation.
💡 Option: Exclude Boolean questions from scoring
const calculateScore = () => {
let score = 0;
+ const scorableQuestions = qaPairs.filter((q) => q.question_type !== "Boolean");
- qaPairs.forEach((q, i) => {
+ scorableQuestions.forEach((q, i) => {
+ const originalIndex = qaPairs.indexOf(q);
- if (userAnswers[i] === q.answer) {
+ if (userAnswers[originalIndex] === q.answer) {
score++;
}
});
- alert(`Your Score: ${score} / ${qaPairs.length}`);
+ alert(`Your Score: ${score} / ${scorableQuestions.length}`);
};📝 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.
| qaPairs.forEach((q, i) => { | |
| if (userAnswers[i] === q.answer) { | |
| score++; | |
| } | |
| }); | |
| const calculateScore = () => { | |
| let score = 0; | |
| const scorableQuestions = qaPairs.filter((q) => q.question_type !== "Boolean"); | |
| scorableQuestions.forEach((q, i) => { | |
| const originalIndex = qaPairs.indexOf(q); | |
| if (userAnswers[originalIndex] === q.answer) { | |
| score++; | |
| } | |
| }); | |
| alert(`Your Score: ${score} / ${scorableQuestions.length}`); | |
| }; |
🤖 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 44 - 48, The score loop treats
all qaPairs as MCQs but Boolean questions lack an answer property and are not
rendered, causing them to always be marked wrong; change the scoring logic in
Output.jsx so you only score rendered/answerable questions — e.g., build a
filteredQuestions = qaPairs.filter(q => q.question_type !== "Boolean" &&
q.answer !== undefined) (or filter by presence of q.answer) and then iterate
filteredQuestions to compare against userAnswers, or if userAnswers is keyed by
question id, match by q.id instead of index; update the loop that references
qaPairs, userAnswers and score to use this filtered/mapped collection so Boolean
questions are excluded from scoring.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/Text_Input.jsx`:
- Around line 51-54: The emptiness guard uses raw text and docUrl so
whitespace-only input passes; trim both values first (e.g., create
normalizedText = text?.trim() and normalizedDocUrl = docUrl?.trim()) and replace
the check with if (!normalizedText && !normalizedDocUrl); then use
normalizedText / normalizedDocUrl in the subsequent branches and API payloads
instead of the original text and docUrl to ensure whitespace-only input is
rejected and consistent values are sent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ae1e6c6b-5ecd-4c92-bb64-91299324c98a
📒 Files selected for processing (1)
eduaid_web/src/pages/Text_Input.jsx
| if (!text && !docUrl) { | ||
| alert("Please upload a file or provide input before proceeding."); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Trim both fields before this emptiness check.
if (!text && !docUrl) still lets whitespace-only input through, so " " in either field bypasses the guard and reaches the backend as if content were present.
✂️ Proposed fix
- if (!text && !docUrl) {
+ const normalizedText = text.trim();
+ const normalizedDocUrl = docUrl.trim();
+
+ if (!normalizedText && !normalizedDocUrl) {
alert("Please upload a file or provide input before proceeding.");
return;
}Use normalizedText / normalizedDocUrl in the branches below as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@eduaid_web/src/pages/Text_Input.jsx` around lines 51 - 54, The emptiness
guard uses raw text and docUrl so whitespace-only input passes; trim both values
first (e.g., create normalizedText = text?.trim() and normalizedDocUrl =
docUrl?.trim()) and replace the check with if (!normalizedText &&
!normalizedDocUrl); then use normalizedText / normalizedDocUrl in the subsequent
branches and API payloads instead of the original text and docUrl to ensure
whitespace-only input is rejected and consistent values are sent.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
eduaid_web/src/pages/Output.jsx (1)
200-203:⚠️ Potential issue | 🟠 MajorDe-duplicate the option list before rendering it.
The backend payload already carries
answerseparately fromoptions, and for MCQs that answer text typically matches one of the provided options. AppendingqaPair.answeragain here can render the correct choice twice and bias the quiz.Suggested fix
- const combinedOptions = qaPair.options - ? [...qaPair.options, qaPair.answer] - : [qaPair.answer]; + const combinedOptions = qaPair.options + ? [...new Set([...qaPair.options, qaPair.answer])] + : qaPair.answer + ? [qaPair.answer] + : [];Also applies to: 225-230
🤖 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 200 - 203, The code appends qaPair.answer to qaPair.options which can duplicate the correct choice; update the logic that builds combinedOptions (used with shuffleArray) to first concatenate qaPair.options and qaPair.answer but then remove duplicates (e.g., by filtering or using a Set) before calling shuffleArray; apply the same de-duplication approach to the other occurrence that constructs options (the block around the second combinedOptions usage).
🤖 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 41-50: calculateScore currently uses qaPairs.length and counts
Short-type questions (which have no selectable options) as unanswered; modify
calculateScore to first derive the list of answerable items (e.g., filter
qaPairs by presence of selectable options or by type !== 'Short', using the same
property your rendering logic uses), then iterate that filtered array to compare
userAnswers (mapping indexes appropriately) against each item's answer and
increment score, and finally use filtered.length as the denominator in the alert
so only answerable questions affect the reported score.
- Around line 226-230: Replace the clickable <div> for each answer choice with a
real <button type="button"> so it is keyboard-focusable and accessible; keep the
existing onClick that calls handleAnswerSelect(index, option) but also add an
accessible selected state like aria-pressed={userAnswers[index] === option} (or
aria-selected if inside a role="listbox") and apply a conditional CSS class when
userAnswers[index] === option so the selection is visually indicated; ensure the
button uses the same key={idx} and preserves other props/styling.
---
Outside diff comments:
In `@eduaid_web/src/pages/Output.jsx`:
- Around line 200-203: The code appends qaPair.answer to qaPair.options which
can duplicate the correct choice; update the logic that builds combinedOptions
(used with shuffleArray) to first concatenate qaPair.options and qaPair.answer
but then remove duplicates (e.g., by filtering or using a Set) before calling
shuffleArray; apply the same de-duplication approach to the other occurrence
that constructs options (the block around the second combinedOptions usage).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3725a0cd-2805-40e6-ab6b-97df92126c61
📒 Files selected for processing (1)
eduaid_web/src/pages/Output.jsx
| const calculateScore = () => { | ||
| let score = 0; | ||
|
|
||
| qaPairs.forEach((q, i) => { | ||
| if (userAnswers[i] === q.answer) { | ||
| score++; | ||
| } | ||
| }); | ||
|
|
||
| alert(`Your Score: ${score} / ${qaPairs.length}`); |
There was a problem hiding this comment.
Only score questions that are actually answerable.
qaPairs in this component also includes Short items, but those never render selectable options. calculateScore() still counts them in qaPairs.length, so short-answer quizzes will always report unanswered items as wrong.
Suggested fix
const calculateScore = () => {
- let score = 0;
-
- qaPairs.forEach((q, i) => {
- if (userAnswers[i] === q.answer) {
- score++;
- }
- });
-
- alert(`Your Score: ${score} / ${qaPairs.length}`);
+ const scoreableCount = qaPairs.filter(
+ (q) => q.question_type === "MCQ" && Array.isArray(q.options) && q.answer != null
+ ).length;
+
+ const score = qaPairs.reduce((total, q, i) => {
+ const isScoreable =
+ q.question_type === "MCQ" && Array.isArray(q.options) && q.answer != null;
+
+ return total + (isScoreable && userAnswers[i] === q.answer ? 1 : 0);
+ }, 0);
+
+ alert(`Your Score: ${score} / ${scoreableCount}`);
};🤖 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 41 - 50, calculateScore
currently uses qaPairs.length and counts Short-type questions (which have no
selectable options) as unanswered; modify calculateScore to first derive the
list of answerable items (e.g., filter qaPairs by presence of selectable options
or by type !== 'Short', using the same property your rendering logic uses), then
iterate that filtered array to compare userAnswers (mapping indexes
appropriately) against each item's answer and increment score, and finally use
filtered.length as the denominator in the alert so only answerable questions
affect the reported score.
| <div | ||
| key={idx} | ||
| className="mb-1 sm:mb-2 cursor-pointer" | ||
| onClick={() => handleAnswerSelect(index, option)} | ||
| > |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check if the file exists and read the relevant section
if [ -f "eduaid_web/src/pages/Output.jsx" ]; then
echo "=== Lines 226-230 of Output.jsx ==="
sed -n '226,230p' eduaid_web/src/pages/Output.jsx
echo ""
echo "=== Context: Lines 215-240 ==="
sed -n '215,240p' eduaid_web/src/pages/Output.jsx
else
echo "File not found at eduaid_web/src/pages/Output.jsx"
find . -name "Output.jsx" -type f 2>/dev/null | head -5
fiRepository: AOSSIE-Org/EduAid
Length of output: 1736
🏁 Script executed:
# Let's find the handleAnswerSelect function and understand the full context
echo "=== Searching for handleAnswerSelect function ==="
rg -n "handleAnswerSelect" eduaid_web/src/pages/Output.jsx -A 5 -B 2
echo ""
echo "=== Searching for userAnswers state ==="
rg -n "userAnswers" eduaid_web/src/pages/Output.jsx -B 2 -A 2 | head -40
echo ""
echo "=== Checking for any keyboard event handlers in the file ==="
rg -n "onKeyDown|onKeyUp|onKeyPress" eduaid_web/src/pages/Output.jsxRepository: AOSSIE-Org/EduAid
Length of output: 1323
🏁 Script executed:
# Check if there's any conditional styling based on userAnswers[index] === option
echo "=== Checking for conditional styling or aria attributes ==="
sed -n '220,245p' eduaid_web/src/pages/Output.jsx
echo ""
echo "=== Full render section of answer options ==="
sed -n '210,250p' eduaid_web/src/pages/Output.jsx
echo ""
echo "=== Searching for any visual feedback patterns related to selected answers ==="
rg -n "className.*ring|className.*selected|className.*active|aria-" eduaid_web/src/pages/Output.jsx | head -20Repository: AOSSIE-Org/EduAid
Length of output: 3398
Use a real button for each answer choice.
A clickable div is not keyboard-focusable and exposes no semantic selected state, so keyboard and assistive-tech users cannot reliably complete the quiz. The quiz also provides no visual feedback that an answer choice was selected—the selected state is tracked in the userAnswers object (used only for scoring) but never displayed to users.
Suggested fix
- <div
+ <button
key={idx}
- className="mb-1 sm:mb-2 cursor-pointer"
+ type="button"
+ aria-pressed={userAnswers[index] === option}
+ className={`mb-1 sm:mb-2 block w-full text-left ${
+ userAnswers[index] === option
+ ? "ring-2 ring-[`#00CBE7`] rounded-md"
+ : ""
+ }`}
onClick={() => handleAnswerSelect(index, option)}
>
<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>
+ </button>🤖 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 226 - 230, Replace the
clickable <div> for each answer choice with a real <button type="button"> so it
is keyboard-focusable and accessible; keep the existing onClick that calls
handleAnswerSelect(index, option) but also add an accessible selected state like
aria-pressed={userAnswers[index] === option} (or aria-selected if inside a
role="listbox") and apply a conditional CSS class when userAnswers[index] ===
option so the selection is visually indicated; ensure the button uses the same
key={idx} and preserves other props/styling.
Description
This PR introduces a prototype interactive quiz player in Output.jsx.
Features Added
Users can select answers for each question
Selected answers are stored using React state
Score calculation logic implemented
Submit Quiz button displays the final score
Prototype UI for interactive quiz player implemented.
This feature improves the EduAid quiz experience by allowing users to interact with generated questions and immediately see their performance.
Files Modified
eduaid_web/src/pages/Output.jsx
Notes
Backend integration may require further refinement depending on API responses.
Summary by CodeRabbit
New Features
Bug Fixes / Validation
UI Enhancements