feat: interactive quiz UX - single question + navigation + progress (…#482
Conversation
📝 WalkthroughWalkthroughRefactored 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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: 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).
| 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]) : [] | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| const qaPair = qaPairs[currentIndex]; | ||
| const shuffledOptions = shuffledOptionsMap[currentIndex]; |
There was a problem hiding this comment.
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.
| 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", | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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.
| 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).
| <div className="text-white font-bold text-xl"> | ||
| Question {currentIndex + 1} of {qaPairs.length} | ||
| </div> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
…fixes #474
Addressed Issues:
Fixes #474
Demo Flow:
Additional Notes:
Checklist
AI Usage Disclosure
Summary by CodeRabbit
New Features
UI/UX Changes