Apply ZIP update: 090-didactopus-draft-pack-import-workflow-update.zip [2026-03-14T13:18:46]
This commit is contained in:
parent
55b170f918
commit
6a983676b4
29
docs/faq.md
29
docs/faq.md
|
|
@ -1,19 +1,24 @@
|
||||||
# FAQ
|
# 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
|
Because the transition from generated draft pack to curated workspace is one of the
|
||||||
staying organized once several candidate domains and draft packs exist.
|
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:
|
Even when online course contents can be ingested, people may still stall if the next
|
||||||
- tracking multiple projects
|
steps are awkward. Importing a draft pack into a workspace should feel like one
|
||||||
- reopening previous review sessions
|
smooth continuation, not a separate manual task.
|
||||||
- switching among draft packs
|
|
||||||
- treating Didactopus like an actual working environment
|
|
||||||
|
|
||||||
## 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
|
In this scaffold it:
|
||||||
quickly becomes messy. A workspace manager helps turn that mess into a manageable process.
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,14 @@
|
||||||
concepts:
|
concepts:
|
||||||
- id: bayes-prior
|
- id: bayes-prior
|
||||||
title: Bayes Prior
|
title: Bayes Prior
|
||||||
description: Prior beliefs before evidence in a probabilistic model.
|
description: Prior beliefs before evidence.
|
||||||
prerequisites: []
|
prerequisites: []
|
||||||
mastery_signals:
|
mastery_signals:
|
||||||
- Explain a prior distribution.
|
- Explain a prior distribution.
|
||||||
- id: bayes-posterior
|
- id: bayes-posterior
|
||||||
title: Bayes Posterior
|
title: Bayes Posterior
|
||||||
description: Updated beliefs after evidence in a probabilistic model.
|
description: Updated beliefs after evidence.
|
||||||
prerequisites:
|
prerequisites:
|
||||||
- bayes-prior
|
- bayes-prior
|
||||||
mastery_signals:
|
mastery_signals:
|
||||||
- Compare prior and posterior beliefs.
|
- 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.
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
||||||
[project]
|
[project]
|
||||||
name = "didactopus"
|
name = "didactopus"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Didactopus: workspace manager for local review UI"
|
description = "Didactopus: draft-pack import workflow for workspaces"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,21 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler):
|
||||||
json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id})
|
json_response(self, 200, {"ok": True, "workspace_id": self.active_workspace_id})
|
||||||
return
|
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:
|
if self.active_bridge is None:
|
||||||
json_response(self, 400, {"error": "no active workspace"})
|
json_response(self, 400, {"error": "no active workspace"})
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
import json
|
import json, shutil
|
||||||
from .review_schema import WorkspaceMeta, WorkspaceRegistry
|
from .review_schema import WorkspaceMeta, WorkspaceRegistry
|
||||||
|
|
||||||
def utc_now() -> str:
|
def utc_now() -> str:
|
||||||
|
|
@ -70,3 +70,34 @@ class WorkspaceManager:
|
||||||
if ws.workspace_id == workspace_id:
|
if ws.workspace_id == workspace_id:
|
||||||
return ws
|
return ws
|
||||||
return None
|
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
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,12 @@ def test_create_and_get_workspace(tmp_path: Path) -> None:
|
||||||
assert got is not None
|
assert got is not None
|
||||||
assert got.title == "Workspace One"
|
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 = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots")
|
||||||
mgr.create_workspace("ws1", "Workspace One")
|
meta = mgr.import_draft_pack(src, "ws2", title="Workspace Two")
|
||||||
mgr.touch_recent("ws1")
|
assert meta.workspace_id == "ws2"
|
||||||
reg = mgr.list_workspaces()
|
assert (tmp_path / "roots" / "ws2" / "draft_pack" / "pack.yaml").exists()
|
||||||
assert reg.recent_workspace_ids[0] == "ws1"
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export default function App() {
|
||||||
const [registry, setRegistry] = useState({ workspaces: [], recent_workspace_ids: [] });
|
const [registry, setRegistry] = useState({ workspaces: [], recent_workspace_ids: [] });
|
||||||
const [workspaceId, setWorkspaceId] = useState("");
|
const [workspaceId, setWorkspaceId] = useState("");
|
||||||
const [workspaceTitle, setWorkspaceTitle] = useState("");
|
const [workspaceTitle, setWorkspaceTitle] = useState("");
|
||||||
|
const [importSource, setImportSource] = useState("");
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const [selectedId, setSelectedId] = useState("");
|
const [selectedId, setSelectedId] = useState("");
|
||||||
const [pendingActions, setPendingActions] = useState([]);
|
const [pendingActions, setPendingActions] = useState([]);
|
||||||
|
|
@ -16,9 +17,7 @@ export default function App() {
|
||||||
const res = await fetch(`${API}/api/workspaces`);
|
const res = await fetch(`${API}/api/workspaces`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setRegistry(data);
|
setRegistry(data);
|
||||||
if (data.recent_workspace_ids?.length && !session) {
|
if (!session) setMessage("Choose, create, or import a workspace.");
|
||||||
setMessage("Choose or reopen a workspace.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -36,6 +35,27 @@ export default function App() {
|
||||||
await openWorkspace(workspaceId);
|
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) {
|
async function openWorkspace(id) {
|
||||||
const res = await fetch(`${API}/api/workspaces/open`, {
|
const res = await fetch(`${API}/api/workspaces/open`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
@ -138,6 +158,12 @@ export default function App() {
|
||||||
<label>Title<input value={workspaceTitle} onChange={(e) => setWorkspaceTitle(e.target.value)} /></label>
|
<label>Title<input value={workspaceTitle} onChange={(e) => setWorkspaceTitle(e.target.value)} /></label>
|
||||||
<button onClick={createWorkspace}>Create</button>
|
<button onClick={createWorkspace}>Create</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h2>Import Draft Pack</h2>
|
||||||
|
<label>Workspace ID<input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} /></label>
|
||||||
|
<label>Draft Pack Source Directory<input value={importSource} onChange={(e) => setImportSource(e.target.value)} placeholder="e.g. generated-pack" /></label>
|
||||||
|
<button onClick={importWorkspace}>Import into Workspace</button>
|
||||||
|
</div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2>Recent</h2>
|
<h2>Recent</h2>
|
||||||
<ul>{registry.recent_workspace_ids.map((id) => <li key={id}><button onClick={() => openWorkspace(id)}>{id}</button></li>)}</ul>
|
<ul>{registry.recent_workspace_ids.map((id) => <li key={id}><button onClick={() => openWorkspace(id)}>{id}</button></li>)}</ul>
|
||||||
|
|
@ -146,10 +172,6 @@ export default function App() {
|
||||||
<h2>All Workspaces</h2>
|
<h2>All Workspaces</h2>
|
||||||
<ul>{registry.workspaces.map((ws) => <li key={ws.workspace_id}><button onClick={() => openWorkspace(ws.workspace_id)}>{ws.title} ({ws.workspace_id})</button></li>)}</ul>
|
<ul>{registry.workspaces.map((ws) => <li key={ws.workspace_id}><button onClick={() => openWorkspace(ws.workspace_id)}>{ws.title} ({ws.workspace_id})</button></li>)}</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
|
||||||
<h2>Pending Actions</h2>
|
|
||||||
<div className="big">{pendingActions.length}</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{session && (
|
{session && (
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ button:hover { border-color: var(--accent); }
|
||||||
label { display: block; font-weight: 600; margin-bottom: 12px; }
|
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; }
|
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); }
|
.small { color: var(--muted); }
|
||||||
.big { font-size: 34px; font-weight: 700; }
|
|
||||||
.conflict { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; }
|
.conflict { border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px; }
|
||||||
ul { padding-left: 18px; }
|
ul { padding-left: 18px; }
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,9 @@
|
||||||
"created_at": "2026-03-13T12:00:00+00:00",
|
"created_at": "2026-03-13T12:00:00+00:00",
|
||||||
"last_opened_at": "2026-03-13T12:30:00+00:00",
|
"last_opened_at": "2026-03-13T12:30:00+00:00",
|
||||||
"notes": "Initial imported course pack"
|
"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": [
|
"recent_workspace_ids": [
|
||||||
"bayes-intro",
|
"bayes-intro"
|
||||||
"stats-foundations"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -5,10 +5,3 @@ concepts:
|
||||||
prerequisites: []
|
prerequisites: []
|
||||||
mastery_signals:
|
mastery_signals:
|
||||||
- Explain mean, median, and variance.
|
- 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.
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue