Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions frontend/src/components/content/InteractivePracticeBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef, useState } from 'react'
import { CheckCircle2, Circle, ArrowRight } from 'lucide-react'
import { CheckCircle2, Circle, XCircle, ArrowRight } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
Expand All @@ -26,6 +26,7 @@ export function InteractivePracticeBlock({
const interaction = block.interaction
const startedAt = useRef<number | null>(null)
const [selectedOptionId, setSelectedOptionId] = useState<string | null>(null)
const [submitted, setSubmitted] = useState(false)
const [responseText, setResponseText] = useState('')

if (interaction?.type !== 'multiple_choice') {
Expand All @@ -47,11 +48,14 @@ export function InteractivePracticeBlock({
<p className="text-base font-medium leading-7 text-slate-900">{interaction.prompt}</p>
{interaction.options.map((option) => {
const selected = option.option_id === selectedOptionId
const isCorrectOption = option.option_id === interaction.correct_option_id
const showCorrect = submitted && isCorrectOption
const showIncorrect = submitted && selected && !isCorrectOption
return (
<button
key={option.option_id}
type="button"
disabled={disabled}
disabled={disabled || submitted}
onClick={() => {
if (startedAt.current === null) {
startedAt.current = Date.now()
Expand All @@ -60,23 +64,33 @@ export function InteractivePracticeBlock({
}}
className={[
'w-full rounded-xl border px-4 py-4 text-left transition-colors',
selected
showCorrect
? 'border-emerald-500 bg-emerald-50 shadow-sm'
: 'border-slate-200 bg-slate-50 hover:border-slate-300 hover:bg-white',
disabled ? 'cursor-not-allowed opacity-70' : '',
: showIncorrect
? 'border-red-400 bg-red-50 shadow-sm'
: selected
? 'border-emerald-500 bg-emerald-50 shadow-sm'
: 'border-slate-200 bg-slate-50 hover:border-slate-300 hover:bg-white',
disabled || submitted ? 'cursor-not-allowed opacity-70' : '',
].join(' ')}
>
<div className="flex items-start gap-3">
{selected ? (
{showCorrect ? (
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-emerald-600" />
) : showIncorrect ? (
<XCircle className="mt-0.5 h-5 w-5 shrink-0 text-red-500" />
) : selected ? (
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-emerald-600" />
) : (
<Circle className="mt-0.5 h-5 w-5 shrink-0 text-slate-400" />
)}
<div className="space-y-1">
<p className="text-sm font-semibold text-slate-900">{option.label}</p>
<p className="whitespace-pre-line text-sm leading-6 text-slate-700">
{option.body}
</p>
{submitted && option.body && (
<p className="whitespace-pre-line text-sm leading-6 text-slate-700">
{option.body}
</p>
)}
</div>
</div>
</button>
Expand Down Expand Up @@ -107,6 +121,7 @@ export function InteractivePracticeBlock({
if (!selectedOptionId) {
return
}
setSubmitted(true)
onSubmit({
blockId: block.block_id ?? block.title,
selectedOptionId,
Expand Down
17 changes: 11 additions & 6 deletions src/dibble/services/llm_prompting.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,17 @@ def _block_schema_contract(content_type: RequestedContentType) -> str:
'{"kind":"summary","title":"...","body":"..."},'
'{"kind":"practice_problem","title":"...","body":"...",'
'"interaction":{"type":"multiple_choice","prompt":"...","options":['
'{"option_id":"A","label":"Option A","body":"..."},'
'{"option_id":"B","label":"Option B","body":"..."}],'
'{"option_id":"A","label":"Option A","body":"why a student might pick this"},'
'{"option_id":"B","label":"Option B","body":"why a student might pick this"}],'
'"correct_option_id":"B",'
'"reveal":{"trigger":"after_selection","prompt":"...","support":"...","placeholder":"..."}}}'
']}. Always include at least one summary block and one practice_problem block. '
"For the practice_problem block, put the learner's actual choice set in interaction.options, "
"keep body to a short cue, and use plain text only."
"keep body to a short cue, and use plain text only. "
"CRITICAL: option body must NEVER reveal whether the option is correct or incorrect. "
"Do not use words like 'Correct', 'Right', 'Wrong', or 'Incorrect' in option body. "
"Option body should describe the reasoning or common misconception behind the choice, "
"not whether it is the right answer. Correctness feedback is shown separately after submission."
)
return (
'{"blocks":[{"kind":"summary","title":"...","body":"..."},{"kind":"instruction","title":"...","body":"..."}]}. '
Expand All @@ -296,11 +300,12 @@ def _stream_schema_contract(content_type: RequestedContentType) -> str:
return (
'{"block_index":0,"block":{"kind":"practice_problem","title":"...","body":"...",'
'"interaction":{"type":"multiple_choice","prompt":"...","options":['
'{"option_id":"A","label":"Option A","body":"..."},'
'{"option_id":"B","label":"Option B","body":"..."}],'
'{"option_id":"A","label":"Option A","body":"why a student might pick this"},'
'{"option_id":"B","label":"Option B","body":"why a student might pick this"}],'
'"correct_option_id":"B",'
'"reveal":{"trigger":"after_selection","prompt":"...","support":"...","placeholder":"..."}}},"done":true}. '
"Emit one complete block object per line for interactive practice blocks."
"Emit one complete block object per line for interactive practice blocks. "
"Option body must NEVER reveal whether the option is correct or incorrect."
)
return (
'{"block_index":0,"kind":"summary","title":"...","body_delta":"...","done":true}. '
Expand Down
Loading