import { useState, useEffect, useCallback } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import styles from './ScenarioPanel.module.css' // Phase 3B progressive hints — used when the Gateway has stripped the full `hints` array from // the scenario payload (the multi-student, Platform-mediated deployment; see // platform/gateway/src/server.js's GET /lab/:token/api/scenarios/:scenarioId). Hints are // fetched one at a time from the Platform API, server-enforced sequential (can't skip to hint // 3), and every reveal is recorded against the student's attempt — none of that is possible // with the old fully-preloaded array this replaces for this deployment mode. // // In standalone/single-container dev mode there's no Gateway rewrite, so `scenario.hints` is // still the full array and ScenarioPanel never renders this component at all — see the // `hintCount != null` check at the call site. export default function ProgressiveHints({ scenarioId, totalHints }) { const [revealed, setRevealed] = useState([]) // [{level, title, body, command}] const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [copiedIdx, setCopiedIdx] = useState(null) const [assessmentHintPolicy, setAssessmentHintPolicy] = useState(null) // null = not in an assessment; 'free'|'disabled'|'flagged' // A scenario/environment change resets local reveal state — the server is still the source // of truth (GET /attempt would show the real highest_hint_level), this is just what's // rendered locally this session. Also checks whether this scenario is part of an active // Platform assessment, purely to show the policy indicator pre-emptively (the server enforces // the policy regardless, on every reveal call — this is UX, not the security boundary). useEffect(() => { setRevealed([]); setError(null); setAssessmentHintPolicy(null) fetch('/api/assessments/active').then((r) => r.json()).then((d) => { if (d.active && d.scenarios?.some((s) => s.scenario_id === scenarioId)) setAssessmentHintPolicy(d.hint_policy) }).catch(() => {}) }, [scenarioId]) const revealNext = useCallback(async () => { setLoading(true); setError(null) try { const r = await fetch(`api/scenarios/${scenarioId}/hint`, { method: 'POST' }) const d = await r.json() if (!r.ok) { setError(d.message || d.error || 'Could not reveal a hint.'); return } setRevealed((h) => [...h, { level: d.level, ...d.hint }]) } catch { setError('Could not reach the platform to reveal a hint.') } finally { setLoading(false) } }, [scenarioId]) function copyCmd(cmd, idx) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(cmd).then(() => { setCopiedIdx(idx); setTimeout(() => setCopiedIdx(null), 1800) }) } } if (totalHints === 0) return
{hint.command}