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
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<label>Title<input value={workspaceTitle} onChange={(e) => setWorkspaceTitle(e.target.value)} /></label>
|
||||
<button onClick={createWorkspace}>Create</button>
|
||||
</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">
|
||||
<h2>Recent</h2>
|
||||
<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>
|
||||
<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 className="card">
|
||||
<h2>Pending Actions</h2>
|
||||
<div className="big">{pendingActions.length}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{session && (
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue