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 (

Didactopus login

{error ?
{error}
: null}
); } 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 ; return (

Didactopus artifact lifecycle + knowledge export

Manage artifact retention and turn learner state into reusable knowledge outputs.

{message}

Render jobs

{JSON.stringify(jobs, null, 2)}

Artifacts

{JSON.stringify(artifacts, null, 2)}
{artifacts[0] ? : null}

Knowledge export

{JSON.stringify(knowledge, null, 2)}
); }