import React, { useEffect, useMemo, useState } from "react"; import { login, listCandidates, createCandidate, createReview, promoteCandidate, runSynthesis, listSynthesisCandidates, promoteSynthesis, createLearnerWorkbenchSession, } from "./api"; import { applyEvidence, buildMasteryMap, progressPercent, recommendNext, milestoneMessages, claimReadiness, } from "./engine"; function LauncherView({ onSelect }) { return (

Didactopus

Choose a work mode.

The current prototype now has two distinct entry points: the existing review workbench and a learner-workbench pilot using the evo-edu `Evidence Trail` pack.

Learner workbench pilot

Use a guided-study surface built around question framing, source comparison, and revision under uncertainty.

  • Loads the `Evidence Trail` pack.
  • Shows concept path and onboarding.
  • Separates observation from interpretation.
  • Treats revision as normal progress.

Review workbench

Keep using the existing knowledge-candidate and synthesis workflow for pack-improvement and review operations.

  • Review learner-derived candidates.
  • Promote pack improvements and skill bundles.
  • Inspect synthesis candidates across packs.
); } function LoginView({ onAuth, onBack }) { const [username, setUsername] = useState("reviewer"); const [password, setPassword] = useState("demo-pass"); const [error, setError] = useState(""); async function doLogin() { try { const result = await login(username, password); onAuth(result); } catch { setError("Login failed"); } } return (

Review Workbench

Didactopus review workbench

{error ?
{error}
: null}
); } function CandidateCard({ candidate, onReview, onPromote }) { return (

{candidate.title}

{candidate.candidate_kind} · lane: {candidate.triage_lane} · status: {candidate.current_status}

{candidate.summary}

confidence {candidate.confidence_hint.toFixed(2)} · novelty {candidate.novelty_score.toFixed(2)} · synthesis {candidate.synthesis_score.toFixed(2)}
); } function SynthesisCard({ item, onPromote }) { return (

{item.source_concept_id} ↔ {item.target_concept_id}

{item.source_pack_id} → {item.target_pack_id}

{item.explanation}

total {item.score_total.toFixed(2)} · semantic {item.score_semantic.toFixed(2)} · structural {item.score_structural.toFixed(2)}
); } function ReviewWorkbench({ auth, onBack }) { const [candidates, setCandidates] = useState([]); const [synthesis, setSynthesis] = useState([]); const [message, setMessage] = useState(""); async function reload(token = auth?.access_token) { if (!token) return; setCandidates(await listCandidates(token)); setSynthesis(await listSynthesisCandidates(token)); } useEffect(() => { if (auth?.access_token) reload(auth.access_token); }, [auth]); async function seedCandidate() { const payload = { source_type: "learner_export", source_artifact_id: null, learner_id: "wesley-learner", pack_id: "biology-pack", candidate_kind: "hidden_prerequisite", title: "Possible hidden prerequisite for drift", summary: "Learner evidence suggests probability intuition should be explicit before drift.", structured_payload: { affected_concept: "drift", suggested_prereq: "variation" }, evidence_summary: "Repeated confusion on stochastic interpretation.", confidence_hint: 0.73, novelty_score: 0.66, synthesis_score: 0.42, triage_lane: "pack_improvement", }; await createCandidate(auth.access_token, payload); await reload(); setMessage("Seed candidate created."); } async function handleReview(candidateId, verdict) { await createReview(auth.access_token, candidateId, { review_kind: "human_review", verdict, rationale: "Accepted in reviewer workbench demo.", requested_changes: "", }); await reload(); setMessage(`Review added to candidate ${candidateId}.`); } async function handlePromote(candidateId, target) { await promoteCandidate(auth.access_token, candidateId, { promotion_target: target, target_object_id: "", promotion_status: "approved", }); await reload(); setMessage(`Candidate ${candidateId} promoted to ${target}.`); } async function handleRunSynthesis() { await runSynthesis(auth.access_token, { source_pack_id: "biology-pack", target_pack_id: "math-pack", limit: 12 }); await reload(); setMessage("Synthesis run completed."); } async function handlePromoteSynthesis(synthesisId) { await promoteSynthesis(auth.access_token, synthesisId, { promotion_target: "pack_improvement" }); await reload(); setMessage(`Synthesis candidate ${synthesisId} promoted into workflow.`); } return (

Review Workbench

Review workbench + synthesis engine

Triages learner-derived knowledge into pack improvements, curriculum drafts, skill bundles, or archive, while surfacing cross-pack synthesis proposals.

{message}

Knowledge candidates

{candidates.map((c) => ( ))}

Synthesis candidates

{synthesis.map((s) => ( ))}
); } function LearnerWorkbench({ onBack }) { const [pack, setPack] = useState(null); const [error, setError] = useState(""); const [currentConceptId, setCurrentConceptId] = useState(""); const [learnerState, setLearnerState] = useState({ records: [], history: [] }); const [question, setQuestion] = useState(""); const [observation, setObservation] = useState(""); const [interpretation, setInterpretation] = useState(""); const [uncertainty, setUncertainty] = useState(""); const [revisionTrigger, setRevisionTrigger] = useState(""); const [feedback, setFeedback] = useState(null); const [sessionOutput, setSessionOutput] = useState(null); useEffect(() => { let active = true; fetch("/packs/evidence-trail-pack.json") .then((res) => { if (!res.ok) throw new Error("Failed to load evidence-trail pack"); return res.json(); }) .then((data) => { if (!active) return; setPack(data); setCurrentConceptId(data.concepts?.[0]?.id || ""); }) .catch(() => { if (!active) return; setError("Could not load the learner-workbench pack."); }); return () => { active = false; }; }, []); const currentConcept = useMemo( () => pack?.concepts?.find((concept) => concept.id === currentConceptId) || null, [pack, currentConceptId] ); const nextConcept = useMemo(() => { if (!pack?.concepts || !currentConcept) return null; const index = pack.concepts.findIndex((concept) => concept.id === currentConcept.id); return index >= 0 ? pack.concepts[index + 1] || null : null; }, [pack, currentConcept]); const masteryMap = useMemo( () => (pack ? buildMasteryMap(learnerState, pack) : []), [learnerState, pack] ); const progress = useMemo( () => (pack ? progressPercent(learnerState, pack) : 0), [learnerState, pack] ); const nextCards = useMemo( () => (pack ? recommendNext(learnerState, pack) : []), [learnerState, pack] ); const readiness = useMemo( () => (pack ? claimReadiness(learnerState, pack) : null), [learnerState, pack] ); const milestones = useMemo( () => (pack ? milestoneMessages(learnerState, pack) : []), [learnerState, pack] ); function advanceConcept() { if (nextConcept) setCurrentConceptId(nextConcept.id); } async function evaluateCurrentWork() { const score = (question.trim() ? 0.18 : 0) + (observation.trim() ? 0.24 : 0) + (interpretation.trim() ? 0.20 : 0) + (uncertainty.trim() ? 0.18 : 0) + (revisionTrigger.trim() ? 0.20 : 0); const confidenceHint = 0.45 + (observation.trim() ? 0.10 : 0) + (interpretation.trim() ? 0.08 : 0) + (uncertainty.trim() ? 0.08 : 0) + (revisionTrigger.trim() ? 0.09 : 0); const nextState = applyEvidence(learnerState, { concept_id: currentConcept.id, dimension: currentConcept.masteryDimension || "mastery", score: Math.min(1, score), confidence_hint: Math.min(1, confidenceHint), timestamp: new Date().toISOString(), note: "Evidence Trail learner-workbench submission", }); setLearnerState(nextState); try { const session = await createLearnerWorkbenchSession({ pack_id: pack.id, concept_id: currentConcept.id, learner_goal: question || `Work through ${currentConcept.title} in the Evidence Trail pack.`, question, observation, interpretation, uncertainty, revision_trigger: revisionTrigger, }); setFeedback({ strengths: session.feedback?.strengths || [], gaps: session.feedback?.gaps || [], nextRevision: session.feedback?.next_revision_target || "Compare one more source or example before moving on.", }); setSessionOutput(session); } catch { setFeedback({ strengths: [], gaps: ["Backend session generation failed; local progress was still recorded."], nextRevision: "Check the learner-workbench API path and retry.", }); setSessionOutput(null); } } if (error) { return (

Learner workbench pilot

{error}
); } if (!pack || !currentConcept) { return (

Learner workbench pilot

Loading the `Evidence Trail` pack...

); } return (

Learner Workbench Pilot

{pack.title}

{pack.subtitle}

This pilot uses scientific virtues as operating rules: separate observation from interpretation, preserve uncertainty, and treat revision as progress.

Onboarding

{pack.onboarding.headline}

{pack.onboarding.body}

Progress: {progress}% · {readiness?.mastered ?? 0}/{pack.concepts.length} concepts strongly supported
    {pack.onboarding.checklist.map((item) => (
  • {item}
  • ))}

Concept Path

{pack.concepts.map((concept, index) => ( ))}

{currentConcept.title}

Prerequisites: {currentConcept.prerequisites.length ? currentConcept.prerequisites.join(", ") : "none explicit"}

{currentConcept.exerciseReward}

Current Study Record

Keep observation and interpretation distinct.