Didactopus/webui/src/App.jsx

171 lines
6.5 KiB
JavaScript

import React, { useEffect, useState } from "react";
import { login, refresh, fetchPacks, fetchLearnerState, putLearnerState, createRenderJob, listRenderJobs, listArtifacts, updateRetention, exportKnowledge } from "./api";
import { loadAuth, saveAuth, clearAuth } from "./authStore";
function LoginView({ onAuth }) {
const [username, setUsername] = useState("wesley");
const [password, setPassword] = useState("demo-pass");
const [error, setError] = useState("");
async function doLogin() {
try {
const result = await login(username, password);
saveAuth(result);
onAuth(result);
} catch { setError("Login failed"); }
}
return (
<div className="page narrow-page">
<section className="card narrow">
<h1>Didactopus login</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>
<button className="primary" onClick={doLogin}>Login</button>
{error ? <div className="error">{error}</div> : null}
</section>
</div>
);
}
export default function App() {
const [auth, setAuth] = useState(loadAuth());
const [packs, setPacks] = useState([]);
const [learnerId] = useState("wesley-learner");
const [packId, setPackId] = useState("");
const [jobs, setJobs] = useState([]);
const [artifacts, setArtifacts] = useState([]);
const [knowledge, setKnowledge] = useState(null);
const [format, setFormat] = useState("gif");
const [fps, setFps] = useState(2);
const [message, setMessage] = useState("");
async function refreshAuthToken() {
if (!auth?.refresh_token) return null;
try {
const result = await refresh(auth.refresh_token);
saveAuth(result);
setAuth(result);
return result;
} catch {
clearAuth();
setAuth(null);
return null;
}
}
async function guarded(fn) {
try { return await fn(auth.access_token); }
catch {
const next = await refreshAuthToken();
if (!next) throw new Error("auth failed");
return await fn(next.access_token);
}
}
async function reloadLists() {
setJobs(await guarded((token) => listRenderJobs(token, learnerId)));
setArtifacts(await guarded((token) => listArtifacts(token, learnerId)));
}
useEffect(() => {
if (!auth) return;
async function load() {
const p = await guarded((token) => fetchPacks(token));
setPacks(p);
setPackId(p[0]?.id || "");
await reloadLists();
}
load();
}, [auth]);
async function generateDemo() {
let state = await guarded((token) => fetchLearnerState(token, learnerId));
const base = Date.now();
const events = [
["intro", 0.30, "exercise", 0],
["intro", 0.78, "review", 1000],
["second", 0.42, "exercise", 2000],
["second", 0.72, "review", 3000],
["third", 0.25, "exercise", 4000],
["branch", 0.60, "exercise", 5000],
];
const latest = {};
for (const [cid, score, kind, offset] of events) {
const ts = new Date(base + offset).toISOString();
state.history.push({ concept_id: cid, dimension: "mastery", score, confidence_hint: 0.6, timestamp: ts, kind, source_id: `demo-${cid}-${offset}` });
latest[cid] = { concept_id: cid, dimension: "mastery", score, confidence: Math.min(0.9, score), evidence_count: (latest[cid]?.evidence_count || 0) + 1, last_updated: ts };
}
state.records = Object.values(latest);
await guarded((token) => putLearnerState(token, learnerId, state));
setMessage("Demo state generated.");
}
async function createJob() {
const result = await guarded((token) => createRenderJob(token, learnerId, packId, { learner_id: learnerId, pack_id: packId, format, fps, theme: "default", retention_class: "standard", retention_days: 30 }));
setMessage(`Render job ${result.job_id} queued.`);
setTimeout(() => reloadLists(), 500);
}
async function changeRetention(artifactId) {
await guarded((token) => updateRetention(token, artifactId, { retention_class: "archive", retention_days: 365 }));
await reloadLists();
setMessage(`Artifact ${artifactId} retention updated.`);
}
async function runKnowledgeExport() {
const result = await guarded((token) => exportKnowledge(token, learnerId, packId, { learner_id: learnerId, pack_id: packId, export_kind: "knowledge_snapshot" }));
setKnowledge(result);
setMessage("Knowledge export generated.");
}
if (!auth) return <LoginView onAuth={setAuth} />;
return (
<div className="page">
<header className="hero">
<div>
<h1>Didactopus artifact lifecycle + knowledge export</h1>
<p>Manage artifact retention and turn learner state into reusable knowledge outputs.</p>
<div className="muted">{message}</div>
</div>
<div className="controls">
<label>Pack
<select value={packId} onChange={(e) => setPackId(e.target.value)}>
{packs.map((p) => <option key={p.id} value={p.id}>{p.title}</option>)}
</select>
</label>
<label>Format
<select value={format} onChange={(e) => setFormat(e.target.value)}>
<option value="gif">GIF</option>
<option value="mp4">MP4</option>
</select>
</label>
<label>FPS
<input type="number" value={fps} onChange={(e) => setFps(Number(e.target.value || 2))} />
</label>
<button onClick={generateDemo}>Generate demo state</button>
<button onClick={createJob}>Create render job</button>
<button onClick={runKnowledgeExport}>Export knowledge</button>
<button onClick={reloadLists}>Refresh lists</button>
<button onClick={() => { clearAuth(); setAuth(null); }}>Logout</button>
</div>
</header>
<main className="layout threecol">
<section className="card">
<h2>Render jobs</h2>
<pre className="prebox">{JSON.stringify(jobs, null, 2)}</pre>
</section>
<section className="card">
<h2>Artifacts</h2>
<pre className="prebox">{JSON.stringify(artifacts, null, 2)}</pre>
{artifacts[0] ? <button onClick={() => changeRetention(artifacts[0].artifact_id)}>Archive newest artifact</button> : null}
</section>
<section className="card">
<h2>Knowledge export</h2>
<pre className="prebox">{JSON.stringify(knowledge, null, 2)}</pre>
</section>
</main>
</div>
);
}