171 lines
6.5 KiB
JavaScript
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>
|
|
);
|
|
}
|