import { useState, useEffect } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import styles from './ScenarioPanel.module.css'
// Inline markdown: renders without a wrapping
— safe for buttons/spans
const inlineComponents = {
p: ({ children }) => <>{children}>,
code: ({ children }) => {children},
}
function InlineMd({ children }) {
return (
{children}
)
}
async function resetProgress(scope, opts) {
await fetch('/api/progress/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scope, ...opts }),
})
}
export default function ScenarioPanel({ scenario, onProgressUpdate, onScenarioStart, isExamMode }) {
const [tab, setTab] = useState('problem')
const [setupState, setSetupState] = useState('idle') // idle | running | done | error
const [validating, setValidating] = useState(false)
const [validResult, setValidResult] = useState(null)
const [selectedOption, setSelectedOption] = useState(null)
const [mcqResult, setMcqResult] = useState(null)
const [submitting, setSubmitting] = useState(false)
const [hintsRevealed, setHintsRevealed] = useState([])
const [copiedCmd, setCopiedCmd] = useState(null)
// Reset state when scenario changes
useEffect(() => {
setTab('problem')
setSetupState('idle')
setValidResult(null)
setSelectedOption(null)
setMcqResult(null)
setHintsRevealed([])
if (scenario?.progress?.status === 'completed') {
setSetupState('done')
}
}, [scenario?.id])
async function runSetup() {
setSetupState('running')
try {
// Feature 2: teardown first to ensure clean cluster state
await fetch(`/api/scenarios/${scenario.id}/teardown`, { method: 'POST' }).catch(() => {})
await onScenarioStart?.(scenario.id)
await fetch(`/api/scenarios/${scenario.id}/setup`, { method: 'POST' })
setSetupState('done')
} catch {
setSetupState('error')
}
}
async function validate() {
setValidating(true)
setValidResult(null)
try {
const r = await fetch(`/api/scenarios/${scenario.id}/validate`, { method: 'POST' })
const d = await r.json()
setValidResult(d)
onProgressUpdate()
} catch {
setValidResult({ error: true })
}
setValidating(false)
}
async function submitMCQ() {
if (!selectedOption) return
setSubmitting(true)
try {
const r = await fetch(`/api/scenarios/${scenario.id}/answer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ selected: selectedOption })
})
const d = await r.json()
setMcqResult(d)
onProgressUpdate()
} catch {}
setSubmitting(false)
}
function copyCmd(cmd, idx) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(cmd).then(() => {
setCopiedCmd(idx)
setTimeout(() => setCopiedCmd(null), 1800)
})
} else {
const textArea = document.createElement("textarea")
textArea.value = cmd
textArea.style.position = "fixed"
textArea.style.left = "-999999px"
textArea.style.top = "-999999px"
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
document.execCommand('copy')
setCopiedCmd(idx)
setTimeout(() => setCopiedCmd(null), 1800)
} catch (err) {
console.error('Fallback copy failed', err)
}
textArea.remove()
}
}
if (!scenario) {
return (
⎈
Select a scenario
Choose from the left panel to start practising
)
}
const isCompleted = scenario.progress?.status === 'completed'
return (
{/* Scenario header */}
{scenario.category}
{scenario.difficulty}
{scenario.type === 'mcq' ? 'Multiple Choice' : 'Hands-on Task'}
{scenario.weight} pts
{scenario.title}
{scenario.progress?.status !== 'not_started' && scenario.progress?.attempts > 0 && (
{
if (!window.confirm(`Reset progress for "${scenario.title}"?`)) return
await resetProgress('scenario', { scenarioId: scenario.id })
setSelectedOption(null)
setMcqResult(null)
setValidResult(null)
setSetupState('idle')
setHintsRevealed([])
onProgressUpdate()
}}
>
↺ Reset
)}
{isCompleted && (
✓ Scenario completed
)}
{/* Tabs */}
{['problem', ...(isExamMode ? [] : ['hints']), ...(scenario.type === 'task' && !isExamMode ? ['validate'] : [])].map(t => (
setTab(t)}
>
{t === 'problem' ? '📄 Problem'
: t === 'hints' ? `💡 Hints (${scenario.hints?.length || 0})`
: '✓ Validate'}
))}
{/* Tab content */}
{/* PROBLEM TAB */}
{tab === 'problem' && (
{/* Setup section (if setup commands exist) */}
{scenario.setup_commands?.length > 0 && (
âš¡ Ready to start?
{setupState === 'idle' && (
â–¶ Start Scenario
)}
{setupState === 'running' && (
Setting up…
)}
{setupState === 'done' && (
✓ Environment ready
)}
{setupState === 'error' && (
⟳ Retry
)}
Click Start Scenario to provision the lab environment, then solve the challenge below.
)}
{/* Problem description */}
{scenario.description}
{/* MCQ options */}
{scenario.type === 'mcq' && (
Select your answer:
{scenario.options?.map(opt => {
const isSelected = selectedOption === opt.id
const showCorrect = mcqResult && opt.id === mcqResult.correct_option
const showWrong = mcqResult && isSelected && !mcqResult.correct
return (
!mcqResult && setSelectedOption(opt.id)}
disabled={!!mcqResult}
>
{opt.id.toUpperCase()}
{opt.text}
{showCorrect && ✓ }
{showWrong && ✗ }
)
})}
{!mcqResult ? (
{submitting ? 'Checking…' : 'Submit Answer'}
) : (
{mcqResult.correct ? '✓ Correct!' : '✗ Incorrect — see the highlighted answer above'}
{mcqResult.explanation && (
{mcqResult.explanation}
)}
)}
)}
)}
{/* HINTS TAB */}
{tab === 'hints' && (
{scenario.hints?.length === 0 && (
No hints available for this scenario.
)}
{scenario.hints?.map((hint, i) => {
const revealed = hintsRevealed.includes(i)
return (
setHintsRevealed(h => revealed ? h.filter(x => x !== i) : [...h, i])}>
Hint {i + 1}
{hint.title}
{revealed ? 'â–¾' : 'â–¸'}
{revealed && (
{hint.body}
{hint.command && (
{hint.command}
copyCmd(hint.command, i)}
>
{copiedCmd === i ? '✓ Copied' : 'Copy'}
)}
)}
)
})}
)}
{/* VALIDATE TAB */}
{tab === 'validate' && scenario.type === 'task' && (
{scenario.validation?.description}
{validating
? <> Running checks…>
: 'â–¶ Run Validation'}
{validResult && !validResult.error && (
{validResult.passed
? `✓ All ${validResult.checks.length} checks passed!`
: `${validResult.checks.filter(c => !c.passed).length} of ${validResult.checks.length} checks failed`}
Attempt #{validResult.attempts}
{validResult.checks.map((c, i) => (
{c.passed ? '✓' : '✗'}
{c.description}
{!c.passed && (
Expected: {c.expected}
Got: {c.actual || '(empty)'}
)}
))}
)}
{validResult?.error && (
âš Validation failed to run. Is the cluster reachable?
)}
)}
)
}