feat(quiz): add hint and explain buttons with AI composer - #330
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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 |
Greptile SummaryThis PR adds on-demand Hint (pre-submission) and Ask AI to explain (post-submission) buttons to the quiz UI. Both handlers call Confidence Score: 4/5Safe to merge after adding error handling to the two new handlers; the silent failure + unintended card-selection side effect should be addressed first. One P1 finding: both handleAskHint and handleAskExplain silently fail with a card-selection side effect when the composer is unavailable, deviating from the established pattern in handleUpdateQuiz. The fix is small and well-defined. src/components/workspace-canvas/QuizContent.tsx — handleAskHint and handleAskExplain need else-branch toast errors and try/catch. Important Files Changed
Reviews (1): Last reviewed commit: "Add on-demand Hint and Explain buttons t..." | Re-trigger Greptile |
| const handleAskHint = () => { | ||
| if (!selectedCardIds.has(item.id)) { | ||
| toggleCardSelection(item.id); | ||
| } | ||
|
|
||
| // Stop propagation so card click doesn't open modal when interacting with quiz | ||
| const stopPropagation = (e: React.MouseEvent) => { | ||
| e.stopPropagation(); | ||
| }; | ||
| const composer = aui?.composer?.(); | ||
| if (composer) { | ||
| composer.setText( | ||
| `Give me a hint for this question in "${item.name}": ${currentQuestion.questionText}`, | ||
| ); | ||
| useUIStore.getState().setIsChatExpanded(true); | ||
| focusComposerInput(true); | ||
| } | ||
| }; | ||
|
|
||
| if (!currentQuestion && !showResults) { | ||
| // Template-created items have "Update me" name and should show generating skeleton | ||
| const isAwaitingGeneration = item.name === "Update me" && questions.length === 0; | ||
| const handleAskExplain = () => { | ||
| if (!selectedCardIds.has(item.id)) { | ||
| toggleCardSelection(item.id); | ||
| } | ||
|
|
||
| if (isAwaitingGeneration) { | ||
| return ( | ||
| <div className={cn("flex flex-col h-full", className)}> | ||
| {/* Question Area Skeleton */} | ||
| <div className={cn( | ||
| "flex-1 p-2", | ||
| "overflow-y-auto", | ||
| "workspace-card-readonly-editor", | ||
| "cursor-default" | ||
| )}> | ||
| <div className="mb-6"> | ||
| <div className="text-sm text-gray-500/60 prose prose-sm max-w-none dark:text-foreground/60 dark:prose-invert"> | ||
| <div className="flex items-center gap-2"> | ||
| <div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div> | ||
| Generating quiz questions... | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Options Skeleton */} | ||
| <div className="space-y-2"> | ||
| {[0, 1, 2, 3].map((index) => ( | ||
| <div | ||
| key={index} | ||
| className="w-full p-3 text-left rounded-lg border bg-white/5 border-white/10" | ||
| > | ||
| <div className="flex items-center gap-3"> | ||
| <span className="flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium border bg-white/10 border-white/20 text-foreground/60 dark:text-white/60"> | ||
| {String.fromCharCode(65 + index)} | ||
| </span> | ||
| <div className="text-sm text-foreground/40 flex-1 dark:text-white/40"> | ||
| <div className="w-3/4 h-3 bg-white/10 rounded animate-pulse"></div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
|
|
||
| {/* Progress Bar Skeleton */} | ||
| <div className="mt-4 flex items-center justify-between"> | ||
| <div className="flex-1 mx-4"> | ||
| <div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden"> | ||
| <div className="h-full bg-blue-400 rounded-full animate-pulse" style={{ width: '30%' }} /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| const composer = aui?.composer?.(); | ||
| if (composer) { | ||
| const userAnswer = | ||
| selectedAnswer !== null | ||
| ? currentQuestion.options[selectedAnswer] | ||
| : "N/A"; | ||
| const correctAnswer = | ||
| currentQuestion.options[currentQuestion.correctIndex]; | ||
| composer.setText( | ||
| `Explain this question in "${item.name}": ${currentQuestion.questionText}\n\nI answered: ${userAnswer}\nCorrect answer: ${correctAnswer}`, | ||
| ); | ||
| useUIStore.getState().setIsChatExpanded(true); | ||
| focusComposerInput(true); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Silent failure with unintended side-effect when composer is unavailable
Both handleAskHint and handleAskExplain lack the else branch that handleUpdateQuiz uses to surface a toast error. When aui?.composer?.() returns null, the button does nothing visible — but toggleCardSelection(item.id) has already fired, silently toggling the card's selection state. Users will see their card's selection change but receive no explanation. handleUpdateQuiz shows toast.error("Chat not available. Please try again.") exactly for this case.
Additionally, neither handler wraps composer.setText() in a try/catch, so a runtime throw would be unhandled — unlike handleUpdateQuiz which catches and toasts the error.
| const handleAskHint = () => { | |
| if (!selectedCardIds.has(item.id)) { | |
| toggleCardSelection(item.id); | |
| } | |
| // Stop propagation so card click doesn't open modal when interacting with quiz | |
| const stopPropagation = (e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| }; | |
| const composer = aui?.composer?.(); | |
| if (composer) { | |
| composer.setText( | |
| `Give me a hint for this question in "${item.name}": ${currentQuestion.questionText}`, | |
| ); | |
| useUIStore.getState().setIsChatExpanded(true); | |
| focusComposerInput(true); | |
| } | |
| }; | |
| if (!currentQuestion && !showResults) { | |
| // Template-created items have "Update me" name and should show generating skeleton | |
| const isAwaitingGeneration = item.name === "Update me" && questions.length === 0; | |
| const handleAskExplain = () => { | |
| if (!selectedCardIds.has(item.id)) { | |
| toggleCardSelection(item.id); | |
| } | |
| if (isAwaitingGeneration) { | |
| return ( | |
| <div className={cn("flex flex-col h-full", className)}> | |
| {/* Question Area Skeleton */} | |
| <div className={cn( | |
| "flex-1 p-2", | |
| "overflow-y-auto", | |
| "workspace-card-readonly-editor", | |
| "cursor-default" | |
| )}> | |
| <div className="mb-6"> | |
| <div className="text-sm text-gray-500/60 prose prose-sm max-w-none dark:text-foreground/60 dark:prose-invert"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 bg-blue-400 rounded-full animate-pulse"></div> | |
| Generating quiz questions... | |
| </div> | |
| </div> | |
| </div> | |
| {/* Options Skeleton */} | |
| <div className="space-y-2"> | |
| {[0, 1, 2, 3].map((index) => ( | |
| <div | |
| key={index} | |
| className="w-full p-3 text-left rounded-lg border bg-white/5 border-white/10" | |
| > | |
| <div className="flex items-center gap-3"> | |
| <span className="flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium border bg-white/10 border-white/20 text-foreground/60 dark:text-white/60"> | |
| {String.fromCharCode(65 + index)} | |
| </span> | |
| <div className="text-sm text-foreground/40 flex-1 dark:text-white/40"> | |
| <div className="w-3/4 h-3 bg-white/10 rounded animate-pulse"></div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Progress Bar Skeleton */} | |
| <div className="mt-4 flex items-center justify-between"> | |
| <div className="flex-1 mx-4"> | |
| <div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden"> | |
| <div className="h-full bg-blue-400 rounded-full animate-pulse" style={{ width: '30%' }} /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| const composer = aui?.composer?.(); | |
| if (composer) { | |
| const userAnswer = | |
| selectedAnswer !== null | |
| ? currentQuestion.options[selectedAnswer] | |
| : "N/A"; | |
| const correctAnswer = | |
| currentQuestion.options[currentQuestion.correctIndex]; | |
| composer.setText( | |
| `Explain this question in "${item.name}": ${currentQuestion.questionText}\n\nI answered: ${userAnswer}\nCorrect answer: ${correctAnswer}`, | |
| ); | |
| useUIStore.getState().setIsChatExpanded(true); | |
| focusComposerInput(true); | |
| } | |
| }; | |
| const handleAskHint = () => { | |
| if (!selectedCardIds.has(item.id)) { | |
| toggleCardSelection(item.id); | |
| } | |
| const composer = aui?.composer?.(); | |
| if (composer) { | |
| try { | |
| composer.setText( | |
| `Give me a hint for this question in "${item.name}": ${currentQuestion.questionText}`, | |
| ); | |
| useUIStore.getState().setIsChatExpanded(true); | |
| focusComposerInput(true); | |
| } catch (error) { | |
| toast.error("Failed to set hint. Please try again."); | |
| } | |
| } else { | |
| toast.error("Chat not available. Please try again."); | |
| } | |
| }; | |
| const handleAskExplain = () => { | |
| if (!selectedCardIds.has(item.id)) { | |
| toggleCardSelection(item.id); | |
| } | |
| const composer = aui?.composer?.(); | |
| if (composer) { | |
| try { | |
| const userAnswer = | |
| selectedAnswer !== null | |
| ? currentQuestion.options[selectedAnswer] | |
| : "N/A"; | |
| const correctAnswer = | |
| currentQuestion.options[currentQuestion.correctIndex]; | |
| composer.setText( | |
| `Explain this question in "${item.name}": ${currentQuestion.questionText}\n\nI answered: ${userAnswer}\nCorrect answer: ${correctAnswer}`, | |
| ); | |
| useUIStore.getState().setIsChatExpanded(true); | |
| focusComposerInput(true); | |
| } catch (error) { | |
| toast.error("Failed to set explanation. Please try again."); | |
| } | |
| } else { | |
| toast.error("Chat not available. Please try again."); | |
| } | |
| }; |
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* Create canonical item-data-schemas as single source of truth; remove quiz hint/explanation/source * Add .min(0) to correctIndex in canonical quizQuestionSchema Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Fix input schemas: require front/back/correctIndex, use permissive schema in autogen Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(quiz): add hint and explain buttons with AI composer (#330) * Add on-demand Hint and Explain buttons to quiz UI for AI interaction * fix(quiz): handle composer errors for hint actions Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Add on-demand Hint and Explain buttons to quiz UI that fill AI composer with contextual prompts without auto-sending, allowing users to request hints before answering or explanations after submitting.
Changes
Lightbulb,MessageCircleQuestion, andfocusComposerInputimports; implementhandleAskHintandhandleAskExplainhandlers that ensure card selection, expand chat panel, set composer text, and focus input; add pre-submission Hint button in left spacer area (shows when!isSubmitted); add post-submission "Ask AI to explain" button inside correct/incorrect feedback box (shows whenisSubmitted)