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 (
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 (
);
}
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)}
onReview(candidate.candidate_id, "accept_pack_improvement")}>Accept as pack improvement
onPromote(candidate.candidate_id, "curriculum_draft")}>Promote to curriculum draft
onPromote(candidate.candidate_id, "reusable_skill_bundle")}>Promote to skill bundle
onPromote(candidate.candidate_id, "archive")}>Archive
);
}
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)}
onPromote(item.synthesis_id)}>Promote into workflow
);
}
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}
Seed candidate
Run synthesis
reload()}>Refresh
Back
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}
Back
);
}
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.
{nextConcept ? `Next: ${nextConcept.title}` : "At final concept"}
Back
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) => (
setCurrentConceptId(concept.id)}
>
{concept.title}
{masteryMap[index]?.status || "locked"}
))}
{currentConcept.title}
Prerequisites: {currentConcept.prerequisites.length ? currentConcept.prerequisites.join(", ") : "none explicit"}
{currentConcept.exerciseReward}
Current Study Record
Keep observation and interpretation distinct.
Question
Observation
Interpretation
Uncertainty
Revision trigger
Evaluate this step
Virtues In Use
Curiosity
Keep the question open long enough to learn from the evidence.
Honesty
Write down what you actually observed before polishing an explanation.
Skepticism
Ask whether the current claim is well supported or only convenient.
Revision
Treat changed conclusions as progress, not as failure.
Evaluator Feedback
Trust-preserving critique
{feedback ? (
<>
Strengths
{feedback.strengths.map((item) => {item} )}
Revision targets
{feedback.gaps.length ? feedback.gaps.map((item) => {item} ) : No immediate gap detected; extend with another source comparison. }
Next revision target: {feedback.nextRevision}
>
) : (
Submit a study record to get evaluator-style feedback grounded in the current concept and scientific-virtues framing.
)}
Backend Session Output
Grounded mentor/practice/evaluator loop
{sessionOutput ? (
Mentor
{sessionOutput.mentor?.text}
Practice
{sessionOutput.practice?.text}
Evaluator
{sessionOutput.evaluator?.text}
Next step
{sessionOutput.next_step?.text}
) : (
Run an evaluation to request grounded mentor, practice, evaluator, and next-step text from the backend session endpoint.
)}
Next Study Action
{nextConcept ? nextConcept.title : "Complete the current concept carefully"}
{nextConcept
? `Move forward when you can justify your interpretation and name what evidence would change it. The next concept extends this work into ${nextConcept.title.toLowerCase()}.`
: "You are at the end of the current concept path. Review your uncertainty and revision trigger before treating the topic as settled."}
{nextCards.map((card) => (
{card.title}
{card.reason}
{card.why.join(" · ")}
))}
Milestones
{milestones.map((item) => {item} )}
Source posture: {pack.compliance.sources} linked sources; attribution required: {pack.compliance.attributionRequired ? "yes" : "no"}.
);
}
export default function App() {
const [mode, setMode] = useState("launcher");
const [auth, setAuth] = useState(null);
if (mode === "learner") return setMode("launcher")} />;
if (mode === "review" && !auth) {
return setMode("launcher")} />;
}
if (mode === "review" && auth) {
return { setAuth(null); setMode("launcher"); }} />;
}
return ;
}