diff --git a/docs/faq.md b/docs/faq.md index 89e63ae..d7c10bd 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,19 +1,24 @@ # FAQ -## Why add a workspace manager? +## Why add a draft-pack import feature? -Because the activation-energy problem is not just parsing content. It is also -staying organized once several candidate domains and draft packs exist. +Because the transition from generated draft pack to curated workspace is one of the +places where users can lose momentum. -## What problem does this solve? +## How does this relate to the activation-energy goal? -It reduces the friction of: -- tracking multiple projects -- reopening previous review sessions -- switching among draft packs -- treating Didactopus like an actual working environment +Even when online course contents can be ingested, people may still stall if the next +steps are awkward. Importing a draft pack into a workspace should feel like one +smooth continuation, not a separate manual task. -## Does this help with online course ingestion? +## What does import do? -Yes. One of the barriers to using online course contents is that the setup work -quickly becomes messy. A workspace manager helps turn that mess into a manageable process. +In this scaffold it: +- creates or updates a workspace +- copies a source draft-pack directory into that workspace +- makes it ready to open in the review UI + +## Is the import workflow validated? + +Only lightly in this scaffold. A future revision should add stronger schema checks, +collision handling, and overwrite safeguards. diff --git a/generated-pack/concepts.yaml b/generated-pack/concepts.yaml index 3a65b82..02ad6d9 100644 --- a/generated-pack/concepts.yaml +++ b/generated-pack/concepts.yaml @@ -1,21 +1,14 @@ concepts: - id: bayes-prior title: Bayes Prior - description: Prior beliefs before evidence in a probabilistic model. + description: Prior beliefs before evidence. prerequisites: [] mastery_signals: - Explain a prior distribution. - id: bayes-posterior title: Bayes Posterior - description: Updated beliefs after evidence in a probabilistic model. + description: Updated beliefs after evidence. prerequisites: - bayes-prior mastery_signals: - Compare prior and posterior beliefs. - - id: model-checking - title: Model Checking - description: Evaluate whether model assumptions and fit remain plausible. - prerequisites: - - bayes-posterior - mastery_signals: - - Critique a model fit. diff --git a/pyproject.toml b/pyproject.toml index d8d63dd..38fb750 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "didactopus" version = "0.1.0" -description = "Didactopus: workspace manager for local review UI" +description = "Didactopus: draft-pack import workflow for workspaces" readme = "README.md" requires-python = ">=3.10" license = {text = "MIT"} diff --git a/src/didactopus/review_bridge_server.py b/src/didactopus/review_bridge_server.py index e206d64..c136cac 100644 --- a/src/didactopus/review_bridge_server.py +++ b/src/didactopus/review_bridge_server.py @@ -73,6 +73,21 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler): json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id}) return + if self.path == "/api/workspaces/import": + try: + meta = self.workspace_manager.import_draft_pack( + source_dir=payload["source_dir"], + workspace_id=payload["workspace_id"], + title=payload.get("title"), + notes=payload.get("notes", "") + ) + except FileNotFoundError as exc: + json_response(self, 404, {"error": str(exc)}) + return + self.set_active_workspace(meta.workspace_id) + json_response(self, 200, {"ok": True, "workspace": meta.model_dump()}) + return + if self.active_bridge is None: json_response(self, 400, {"error": "no active workspace"}) return diff --git a/src/didactopus/workspace_manager.py b/src/didactopus/workspace_manager.py index 36769f8..c32c72e 100644 --- a/src/didactopus/workspace_manager.py +++ b/src/didactopus/workspace_manager.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path from datetime import datetime, UTC -import json +import json, shutil from .review_schema import WorkspaceMeta, WorkspaceRegistry def utc_now() -> str: @@ -70,3 +70,34 @@ class WorkspaceManager: if ws.workspace_id == workspace_id: return ws return None + + def import_draft_pack(self, source_dir: str | Path, workspace_id: str, title: str | None = None, notes: str = "") -> WorkspaceMeta: + source_dir = Path(source_dir) + if not source_dir.exists(): + raise FileNotFoundError(f"Draft pack source does not exist: {source_dir}") + + meta = self.get_workspace(workspace_id) + if meta is None: + meta = self.create_workspace(workspace_id, title or workspace_id, notes=notes) + else: + self.touch_recent(workspace_id) + + workspace_dir = Path(meta.path) + target_draft = workspace_dir / "draft_pack" + if target_draft.exists(): + shutil.rmtree(target_draft) + shutil.copytree(source_dir, target_draft) + + registry = self.load_registry() + for ws in registry.workspaces: + if ws.workspace_id == workspace_id: + ws.last_opened_at = utc_now() + if title: + ws.title = title + if notes: + ws.notes = notes + meta = ws + break + registry.recent_workspace_ids = [workspace_id] + [w for w in registry.recent_workspace_ids if w != workspace_id] + self.save_registry(registry) + return meta diff --git a/tests/test_workspace_manager.py b/tests/test_workspace_manager.py index 05091e0..91f2ca4 100644 --- a/tests/test_workspace_manager.py +++ b/tests/test_workspace_manager.py @@ -9,9 +9,12 @@ def test_create_and_get_workspace(tmp_path: Path) -> None: assert got is not None assert got.title == "Workspace One" -def test_recent_tracking(tmp_path: Path) -> None: +def test_import_draft_pack(tmp_path: Path) -> None: + src = tmp_path / "srcpack" + src.mkdir() + (src / "pack.yaml").write_text("name: x\n", encoding="utf-8") + (src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8") mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots") - mgr.create_workspace("ws1", "Workspace One") - mgr.touch_recent("ws1") - reg = mgr.list_workspaces() - assert reg.recent_workspace_ids[0] == "ws1" + meta = mgr.import_draft_pack(src, "ws2", title="Workspace Two") + assert meta.workspace_id == "ws2" + assert (tmp_path / "roots" / "ws2" / "draft_pack" / "pack.yaml").exists() diff --git a/webui/src/App.jsx b/webui/src/App.jsx index 07569b6..87c3027 100644 --- a/webui/src/App.jsx +++ b/webui/src/App.jsx @@ -7,6 +7,7 @@ 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 [session, setSession] = useState(null); const [selectedId, setSelectedId] = useState(""); const [pendingActions, setPendingActions] = useState([]); @@ -16,9 +17,7 @@ export default function App() { const res = await fetch(`${API}/api/workspaces`); const data = await res.json(); setRegistry(data); - if (data.recent_workspace_ids?.length && !session) { - setMessage("Choose or reopen a workspace."); - } + if (!session) setMessage("Choose, create, or import a workspace."); } useEffect(() => { @@ -36,6 +35,27 @@ export default function App() { await openWorkspace(workspaceId); } + 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 + }) + }); + 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", @@ -138,6 +158,12 @@ export default function App() { +
+

Import Draft Pack

+ + + +

Recent

@@ -146,10 +172,6 @@ export default function App() {

All Workspaces

-
-

Pending Actions

-
{pendingActions.length}
-
{session && ( diff --git a/webui/src/styles.css b/webui/src/styles.css index 893f322..fea0c8c 100644 --- a/webui/src/styles.css +++ b/webui/src/styles.css @@ -28,7 +28,6 @@ button:hover { border-color: var(--accent); } label { display: block; font-weight: 600; margin-bottom: 12px; } input, textarea, select { width: 100%; margin-top: 6px; border: 1px solid var(--border); border-radius: 10px; padding: 10px; font: inherit; background: white; } .small { color: var(--muted); } -.big { font-size: 34px; font-weight: 700; } .conflict { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; } ul { padding-left: 18px; } @media (max-width: 1100px) { diff --git a/workspace_registry.json b/workspace_registry.json index 0529980..0adae9b 100644 --- a/workspace_registry.json +++ b/workspace_registry.json @@ -7,18 +7,9 @@ "created_at": "2026-03-13T12:00:00+00:00", "last_opened_at": "2026-03-13T12:30:00+00:00", "notes": "Initial imported course pack" - }, - { - "workspace_id": "stats-foundations", - "title": "Statistics Foundations", - "path": "workspaces/stats-foundations", - "created_at": "2026-03-13T12:05:00+00:00", - "last_opened_at": "2026-03-13T12:20:00+00:00", - "notes": "Secondary draft pack" } ], "recent_workspace_ids": [ - "bayes-intro", - "stats-foundations" + "bayes-intro" ] } \ No newline at end of file diff --git a/workspaces/bayes-intro/draft_pack/concepts.yaml b/workspaces/bayes-intro/draft_pack/concepts.yaml index 8eedf05..cbe82cc 100644 --- a/workspaces/bayes-intro/draft_pack/concepts.yaml +++ b/workspaces/bayes-intro/draft_pack/concepts.yaml @@ -5,10 +5,3 @@ concepts: prerequisites: [] mastery_signals: - Explain mean, median, and variance. - - id: probability-basics - title: Probability Basics - description: Basic event probability and conditional probability. - prerequisites: - - descriptive-statistics - mastery_signals: - - Compute a simple conditional probability.