import React, { useEffect, useMemo, useState } from "react"; const API = "http://127.0.0.1:8765"; const statuses = ["needs_review", "trusted", "provisional", "rejected"]; export default function App() { const [registry, setRegistry] = useState({ workspaces: [], recent_workspace_ids: [] }); const [workspaceId, setWorkspaceId] = useState(""); const [workspaceTitle, setWorkspaceTitle] = useState(""); const [importSource, setImportSource] = useState(""); const [importPreview, setImportPreview] = useState(null); const [allowOverwrite, setAllowOverwrite] = useState(false); const [session, setSession] = useState(null); const [selectedId, setSelectedId] = useState(""); const [pendingActions, setPendingActions] = useState([]); const [message, setMessage] = useState("Connecting to local Didactopus bridge..."); async function loadRegistry() { const res = await fetch(`${API}/api/workspaces`); const data = await res.json(); setRegistry(data); if (!session) setMessage("Choose, create, preview, or import a workspace."); } useEffect(() => { loadRegistry().catch(() => setMessage("Could not connect to local review bridge. Start the Python bridge service first.")); }, []); async function createWorkspace() { if (!workspaceId || !workspaceTitle) return; await fetch(`${API}/api/workspaces/create`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: workspaceId, title: workspaceTitle }) }); await loadRegistry(); await openWorkspace(workspaceId); } async function previewImport() { if (!workspaceId || !importSource) return; const res = await fetch(`${API}/api/workspaces/import-preview`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: workspaceId, source_dir: importSource }) }); const data = await res.json(); setImportPreview(data); setMessage(data.ok ? "Import preview ready." : "Import preview found blocking errors."); } async function importWorkspace() { if (!workspaceId || !importSource) return; const res = await fetch(`${API}/api/workspaces/import`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: workspaceId, title: workspaceTitle || workspaceId, source_dir: importSource, allow_overwrite: allowOverwrite }) }); const data = await res.json(); if (!data.ok) { setMessage(data.error || "Import failed."); return; } await loadRegistry(); await openWorkspace(workspaceId); setMessage(`Imported draft pack from ${importSource} into workspace ${workspaceId}.`); } async function openWorkspace(id) { const res = await fetch(`${API}/api/workspaces/open`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ workspace_id: id }) }); const opened = await res.json(); if (!opened.ok) { setMessage("Could not open workspace."); return; } const sessionRes = await fetch(`${API}/api/load`); const sessionData = await sessionRes.json(); setSession(sessionData.session); setSelectedId(sessionData.session?.draft_pack?.concepts?.[0]?.concept_id || ""); setPendingActions([]); setMessage(`Opened workspace ${id}.`); await loadRegistry(); } const selected = useMemo(() => { if (!session) return null; return session.draft_pack.concepts.find((c) => c.concept_id === selectedId) || null; }, [session, selectedId]); function queueAction(action) { setPendingActions((prev) => [...prev, action]); } function patchConcept(conceptId, patch, rationale) { if (!session) return; const concepts = session.draft_pack.concepts.map((c) => c.concept_id === conceptId ? { ...c, ...patch } : c ); setSession({ ...session, draft_pack: { ...session.draft_pack, concepts } }); if (patch.status !== undefined) queueAction({ action_type: "set_status", target: conceptId, payload: { status: patch.status }, rationale }); if (patch.title !== undefined) queueAction({ action_type: "edit_title", target: conceptId, payload: { title: patch.title }, rationale }); if (patch.description !== undefined) queueAction({ action_type: "edit_description", target: conceptId, payload: { description: patch.description }, rationale }); if (patch.prerequisites !== undefined) queueAction({ action_type: "edit_prerequisites", target: conceptId, payload: { prerequisites: patch.prerequisites }, rationale }); if (patch.notes !== undefined) queueAction({ action_type: "edit_notes", target: conceptId, payload: { notes: patch.notes }, rationale }); } function resolveConflict(conflict) { if (!session) return; setSession({ ...session, draft_pack: { ...session.draft_pack, conflicts: session.draft_pack.conflicts.filter((c) => c !== conflict) } }); queueAction({ action_type: "resolve_conflict", target: "", payload: { conflict }, rationale: "Resolved in UI" }); } async function saveChanges() { if (!pendingActions.length) { setMessage("No pending changes to save."); return; } const res = await fetch(`${API}/api/save`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({ actions: pendingActions }) }); const data = await res.json(); setSession(data.session); setPendingActions([]); setMessage("Saved review state."); } async function exportPromoted() { const res = await fetch(`${API}/api/export`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({}) }); const data = await res.json(); setMessage(`Exported promoted pack to ${data.promoted_pack_dir}`); } return (

Didactopus Semantic QA

Reduce the activation-energy hump from generated draft packs to curated review workspaces by surfacing semantic curation issues before import.

{message}

Create Workspace

Preview / Import Draft Pack

Recent

    {registry.recent_workspace_ids.map((id) =>
  • )}

All Workspaces

    {registry.workspaces.map((ws) =>
  • )}
{importPreview && (

Import Preview

OK: {String(importPreview.ok)}
Overwrite Required: {String(importPreview.overwrite_required)}
Pack: {importPreview.summary?.display_name || importPreview.summary?.pack_name || "-"}
Version: {importPreview.summary?.version || "-"}
Concepts: {importPreview.summary?.concept_count ?? "-"}
Roadmap Stages: {importPreview.summary?.roadmap_stage_count ?? "-"}
Projects: {importPreview.summary?.project_count ?? "-"}
Rubrics: {importPreview.summary?.rubric_count ?? "-"}

Validation Errors

    {(importPreview.errors || []).length ? importPreview.errors.map((x, i) =>
  • {x}
  • ) :
  • none
  • }

Validation Warnings

    {(importPreview.warnings || []).length ? importPreview.warnings.map((x, i) =>
  • {x}
  • ) :
  • none
  • }

Semantic QA Warnings

    {(importPreview.semantic_warnings || []).length ? importPreview.semantic_warnings.map((x, i) =>
  • {x}
  • ) :
  • none
  • }
)} {session && (
{selected && (

Concept Editor