import type { ProfileView, CourseView } from '../types'; import React, { useState } from 'react'; import { Lock, Video, Camera, MessageSquare, ScanLine, XCircle, CheckCircle2, ShieldCheck, Award, Eye, ArrowRight } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Button, Spinner, Surface } from '@/components/ds'; import { isUnlocked } from '@/lib/provingGround'; import { useSubmitChallenge, useVerifyEvidence } from '@/lib/krowHooks'; import RoleplayChallenge from './RoleplayChallenge'; import MediaChallenge from './MediaChallenge'; import VideoRecorder from './VideoRecorder'; import HazardIdentifier from './HazardIdentifier'; import OwliverAvatar from '@/components/krow/OwliverAvatar'; import { useTranslation } from 'react-i18next'; /** * The challenge itself: learn, demonstrate, verify, proof. * * Presentation only — every step, transition and mutation below is the original * flow. Styling comes from the design system so the runner reads as part of the * Forge workspace it is embedded in rather than a widget dropped into it. */ const STEPS = ['Learn', 'Demonstrate', 'Verify', 'Proof']; const TYPE_ICON = { roleplay: MessageSquare, photo: Camera, video: Video, photo_identify: ScanLine }; const TYPE_LABEL = { roleplay: 'Chat with Owliver', photo: 'Photo Proof', video: 'Record Yourself', photo_identify: 'Identify Hazards' }; /** Props for the local `Stepper` helper in this file. Optional throughout: the JavaScript validated none of them and callers omit freely. */ interface StepperProps { active?: any; } function Stepper({ active }: StepperProps) { return (
{STEPS.map((s, i) => { const done = i < active; const cur = i === active; return (
{done ? '✓' : i + 1} {s}
{i < STEPS.length - 1 && (
); })}
); } /** The runner's shell — one surface, one padding, every step. */ /** Props for the local `Frame` helper in this file. Optional throughout: the JavaScript validated none of them and callers omit freely. */ interface FrameProps { children?: any; className?: any; } function Frame({ children, className }: FrameProps) { return ( {children} ); } interface ChallengeRunnerProps { course: CourseView; profile: ProfileView; onPassed?: (...args: any[]) => void; } export default function ChallengeRunner({ course, profile, onPassed }: ChallengeRunnerProps) { const { t } = useTranslation(); const [step, setStep] = useState(0); const [evaluating, setEvaluating] = useState(false); const [result, setResult] = useState(null); const [evidence, setEvidence] = useState(null); const [supervisorVerified, setSupervisorVerified] = useState(false); const submit = useSubmitChallenge(); const verify = useVerifyEvidence(); const ch = course?.challenge || {}; const type = ch.type || 'roleplay'; const Icon = TYPE_ICON[type] || MessageSquare; const lock = isUnlocked(course, profile); const handleSubmit = async (payload) => { setEvaluating(true); let mediaUrl = null, transcript = '', identified = null; if (type === 'roleplay') transcript = payload; else if (type === 'photo_identify') { mediaUrl = ch.image_url; identified = payload; } else mediaUrl = payload; try { const res = await submit.mutateAsync({ profile, course, type, mediaUrl, transcript, identified }); setResult(res.result); setEvidence(res.evidence); setSupervisorVerified(false); setStep(2); if (res.passed && onPassed) onPassed(); } catch { setStep(1); } finally { setEvaluating(false); } }; const doVerify = async () => { if (!evidence) return; try { await verify.mutateAsync({ evidenceId: evidence.id }); setSupervisorVerified(true); } catch {} }; const passed = evidence?.ai_verdict === 'verified'; // 0 — Learn if (step === 0) { return ( {!lock.unlocked ? ( <>

{t('challengeRunner.realWorkUnlocksLearningCompleteThese')}

) : ( <>

{t('challengeRunner.demonstration')}

{TYPE_LABEL[type]}

{ch.prompt || course?.description}

)} ); } // Evaluating if (evaluating) { return (

{t('challengeRunner.owliverIsEvaluatingYourProof')}

{t('challengeRunner.measuringWhetherYouCanActuallyPerform')}

); } // 1 — Demonstrate if (step === 1) { return ( {/* The roleplay widget opens with this same prompt as its first message, so repeating it above the transcript would say it twice. */} {type !== 'roleplay' && (

{t('challengeRunner.owliver')}

{ch.prompt || course?.description}

)} {type === 'roleplay' && } {type === 'photo' && } {type === 'video' && } {type === 'photo_identify' && } ); } // 2 — Verify if (step === 2) { const rubric = result?.rubric || {}; return (
{passed ?
{result?.feedback && (

{result.feedback}

)} {Object.keys(rubric).length > 0 && (
{Object.entries(rubric).map(([k, v]) => (
{k.replace(/_/g, ' ')} {Math.round(v as number)}/100
))}
)}
{passed ? ( ) : ( )} ); } // 3 — Proof return (

{t('challengeRunner.proofStored')}

{t('challengeRunner.capability')} {course.proof_skill || course.title}

{[ { label: 'AI verdict', value: evidence?.ai_verdict, tone: 'text-success', capitalize: true }, { label: 'Score', value: `${Math.round(evidence?.ai_score || 0)}/100` }, { label: 'Supervisor', value: supervisorVerified ? 'Verified' : 'Pending' }, { label: 'Proof type', value: TYPE_LABEL[type] }, ].map((row) => (
{row.label}
{row.value}
))}

); }