Didactopus/webui/src/App.jsx

606 lines
22 KiB
JavaScript

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 (
<div className="page">
<header className="hero">
<div>
<p className="eyebrow">Didactopus</p>
<h1>Choose a work mode.</h1>
<p>
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.
</p>
</div>
<div className="toolbar">
<button className="primary" onClick={() => onSelect("learner")}>Open learner pilot</button>
<button onClick={() => onSelect("review")}>Open review workbench</button>
</div>
</header>
<main className="grid">
<section className="card">
<h2>Learner workbench pilot</h2>
<p>
Use a guided-study surface built around question framing, source comparison, and
revision under uncertainty.
</p>
<ul className="plain-list">
<li>Loads the `Evidence Trail` pack.</li>
<li>Shows concept path and onboarding.</li>
<li>Separates observation from interpretation.</li>
<li>Treats revision as normal progress.</li>
</ul>
</section>
<section className="card">
<h2>Review workbench</h2>
<p>
Keep using the existing knowledge-candidate and synthesis workflow for pack-improvement
and review operations.
</p>
<ul className="plain-list">
<li>Review learner-derived candidates.</li>
<li>Promote pack improvements and skill bundles.</li>
<li>Inspect synthesis candidates across packs.</li>
</ul>
</section>
</main>
</div>
);
}
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 (
<div className="page narrow">
<section className="card">
<p className="eyebrow">Review Workbench</p>
<h1>Didactopus review workbench</h1>
<label>
Username
<input value={username} onChange={(e) => setUsername(e.target.value)} />
</label>
<label>
Password
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
</label>
<div className="toolbar">
<button className="primary" onClick={doLogin}>Login</button>
<button onClick={onBack}>Back</button>
</div>
{error ? <div className="error">{error}</div> : null}
</section>
</div>
);
}
function CandidateCard({ candidate, onReview, onPromote }) {
return (
<div className="card small">
<h3>{candidate.title}</h3>
<div className="muted">
{candidate.candidate_kind} · lane: {candidate.triage_lane} · status: {candidate.current_status}
</div>
<p>{candidate.summary}</p>
<div className="tiny">
confidence {candidate.confidence_hint.toFixed(2)} · novelty {candidate.novelty_score.toFixed(2)} · synthesis {candidate.synthesis_score.toFixed(2)}
</div>
<div className="actions">
<button onClick={() => onReview(candidate.candidate_id, "accept_pack_improvement")}>Accept as pack improvement</button>
<button onClick={() => onPromote(candidate.candidate_id, "curriculum_draft")}>Promote to curriculum draft</button>
<button onClick={() => onPromote(candidate.candidate_id, "reusable_skill_bundle")}>Promote to skill bundle</button>
<button onClick={() => onPromote(candidate.candidate_id, "archive")}>Archive</button>
</div>
</div>
);
}
function SynthesisCard({ item, onPromote }) {
return (
<div className="card small">
<h3>{item.source_concept_id} {item.target_concept_id}</h3>
<div className="muted">{item.source_pack_id} {item.target_pack_id}</div>
<p>{item.explanation}</p>
<div className="tiny">
total {item.score_total.toFixed(2)} · semantic {item.score_semantic.toFixed(2)} · structural {item.score_structural.toFixed(2)}
</div>
<button onClick={() => onPromote(item.synthesis_id)}>Promote into workflow</button>
</div>
);
}
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 (
<div className="page">
<header className="hero">
<div>
<p className="eyebrow">Review Workbench</p>
<h1>Review workbench + synthesis engine</h1>
<p>
Triages learner-derived knowledge into pack improvements, curriculum drafts, skill bundles,
or archive, while surfacing cross-pack synthesis proposals.
</p>
<div className="muted">{message}</div>
</div>
<div className="toolbar">
<button className="primary" onClick={seedCandidate}>Seed candidate</button>
<button onClick={handleRunSynthesis}>Run synthesis</button>
<button onClick={() => reload()}>Refresh</button>
<button onClick={onBack}>Back</button>
</div>
</header>
<main className="grid">
<section>
<h2>Knowledge candidates</h2>
<div className="stack">
{candidates.map((c) => (
<CandidateCard key={c.candidate_id} candidate={c} onReview={handleReview} onPromote={handlePromote} />
))}
</div>
</section>
<section>
<h2>Synthesis candidates</h2>
<div className="stack">
{synthesis.map((s) => (
<SynthesisCard key={s.synthesis_id} item={s} onPromote={handlePromoteSynthesis} />
))}
</div>
</section>
</main>
</div>
);
}
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 (
<div className="page narrow">
<section className="card">
<h1>Learner workbench pilot</h1>
<div className="error">{error}</div>
<button onClick={onBack}>Back</button>
</section>
</div>
);
}
if (!pack || !currentConcept) {
return (
<div className="page narrow">
<section className="card">
<h1>Learner workbench pilot</h1>
<p>Loading the `Evidence Trail` pack...</p>
</section>
</div>
);
}
return (
<div className="page learner-page">
<header className="hero learner-hero">
<div>
<p className="eyebrow">Learner Workbench Pilot</p>
<h1>{pack.title}</h1>
<p>{pack.subtitle}</p>
<div className="muted">
This pilot uses scientific virtues as operating rules: separate observation from interpretation,
preserve uncertainty, and treat revision as progress.
</div>
</div>
<div className="toolbar">
<button className="primary" onClick={advanceConcept} disabled={!nextConcept}>
{nextConcept ? `Next: ${nextConcept.title}` : "At final concept"}
</button>
<button onClick={onBack}>Back</button>
</div>
</header>
<main className="learner-grid">
<section className="card">
<p className="eyebrow">Onboarding</p>
<h2>{pack.onboarding.headline}</h2>
<p>{pack.onboarding.body}</p>
<div className="progress-strip">
<strong>Progress:</strong> {progress}% · {readiness?.mastered ?? 0}/{pack.concepts.length} concepts strongly supported
</div>
<ul className="plain-list">
{pack.onboarding.checklist.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section>
<section className="card">
<p className="eyebrow">Concept Path</p>
<div className="concept-path">
{pack.concepts.map((concept, index) => (
<button
key={concept.id}
className={`concept-chip${concept.id === currentConceptId ? " active" : ""}`}
onClick={() => setCurrentConceptId(concept.id)}
>
{concept.title}
<span className="chip-status">
{masteryMap[index]?.status || "locked"}
</span>
</button>
))}
</div>
<div className="concept-focus">
<h3>{currentConcept.title}</h3>
<div className="tiny">
Prerequisites: {currentConcept.prerequisites.length ? currentConcept.prerequisites.join(", ") : "none explicit"}
</div>
<p className="muted">{currentConcept.exerciseReward}</p>
</div>
</section>
<section className="card learner-form">
<p className="eyebrow">Current Study Record</p>
<h2>Keep observation and interpretation distinct.</h2>
<label>
Question
<textarea value={question} onChange={(e) => setQuestion(e.target.value)} placeholder="What are you trying to understand or test?" />
</label>
<label>
Observation
<textarea value={observation} onChange={(e) => setObservation(e.target.value)} placeholder="What did the model, species record, or source actually show?" />
</label>
<label>
Interpretation
<textarea value={interpretation} onChange={(e) => setInterpretation(e.target.value)} placeholder="What explanation currently fits best?" />
</label>
<label>
Uncertainty
<textarea value={uncertainty} onChange={(e) => setUncertainty(e.target.value)} placeholder="What remains unclear, weakly supported, or unresolved?" />
</label>
<label>
Revision trigger
<textarea value={revisionTrigger} onChange={(e) => setRevisionTrigger(e.target.value)} placeholder="What evidence or comparison would make you revise your current view?" />
</label>
<div className="toolbar">
<button className="primary" onClick={evaluateCurrentWork}>Evaluate this step</button>
</div>
</section>
<section className="card">
<p className="eyebrow">Virtues In Use</p>
<div className="virtue-grid">
<div className="virtue-card">
<strong>Curiosity</strong>
<p>Keep the question open long enough to learn from the evidence.</p>
</div>
<div className="virtue-card">
<strong>Honesty</strong>
<p>Write down what you actually observed before polishing an explanation.</p>
</div>
<div className="virtue-card">
<strong>Skepticism</strong>
<p>Ask whether the current claim is well supported or only convenient.</p>
</div>
<div className="virtue-card">
<strong>Revision</strong>
<p>Treat changed conclusions as progress, not as failure.</p>
</div>
</div>
</section>
<section className="card">
<p className="eyebrow">Evaluator Feedback</p>
<h2>Trust-preserving critique</h2>
{feedback ? (
<>
<div className="feedback-block">
<strong>Strengths</strong>
<ul className="plain-list">
{feedback.strengths.map((item) => <li key={item}>{item}</li>)}
</ul>
</div>
<div className="feedback-block">
<strong>Revision targets</strong>
<ul className="plain-list">
{feedback.gaps.length ? feedback.gaps.map((item) => <li key={item}>{item}</li>) : <li>No immediate gap detected; extend with another source comparison.</li>}
</ul>
</div>
<div className="compliance-note">
<strong>Next revision target:</strong> {feedback.nextRevision}
</div>
</>
) : (
<p className="muted">Submit a study record to get evaluator-style feedback grounded in the current concept and scientific-virtues framing.</p>
)}
</section>
<section className="card">
<p className="eyebrow">Backend Session Output</p>
<h2>Grounded mentor/practice/evaluator loop</h2>
{sessionOutput ? (
<div className="resource-list">
<div className="next-card">
<strong>Mentor</strong>
<p>{sessionOutput.mentor?.text}</p>
</div>
<div className="next-card">
<strong>Practice</strong>
<p>{sessionOutput.practice?.text}</p>
</div>
<div className="next-card">
<strong>Evaluator</strong>
<p>{sessionOutput.evaluator?.text}</p>
</div>
<div className="next-card">
<strong>Next step</strong>
<p>{sessionOutput.next_step?.text}</p>
</div>
</div>
) : (
<p className="muted">Run an evaluation to request grounded mentor, practice, evaluator, and next-step text from the backend session endpoint.</p>
)}
</section>
<section className="card">
<p className="eyebrow">Next Study Action</p>
<h2>{nextConcept ? nextConcept.title : "Complete the current concept carefully"}</h2>
<p>
{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."}
</p>
<div className="resource-list">
{nextCards.map((card) => (
<div className="next-card" key={card.id}>
<strong>{card.title}</strong>
<p>{card.reason}</p>
<div className="tiny">{card.why.join(" · ")}</div>
</div>
))}
</div>
<div className="feedback-block">
<strong>Milestones</strong>
<ul className="plain-list">
{milestones.map((item) => <li key={item}>{item}</li>)}
</ul>
</div>
<div className="compliance-note">
<strong>Source posture:</strong> {pack.compliance.sources} linked sources; attribution required: {pack.compliance.attributionRequired ? "yes" : "no"}.
</div>
</section>
</main>
</div>
);
}
export default function App() {
const [mode, setMode] = useState("launcher");
const [auth, setAuth] = useState(null);
if (mode === "learner") return <LearnerWorkbench onBack={() => setMode("launcher")} />;
if (mode === "review" && !auth) {
return <LoginView onAuth={setAuth} onBack={() => setMode("launcher")} />;
}
if (mode === "review" && auth) {
return <ReviewWorkbench auth={auth} onBack={() => { setAuth(null); setMode("launcher"); }} />;
}
return <LauncherView onSelect={setMode} />;
}