Apply ZIP update: 095-didactopus-import-validation-safety-update.zip [2026-03-14T13:18:49]
This commit is contained in:
parent
6a983676b4
commit
8c9776d6fe
|
|
@ -1,3 +1 @@
|
||||||
name: broken-pack
|
display_name Imported Pack Broken YAML
|
||||||
display_name: Broken Pack
|
|
||||||
version: 0.1.0-draft
|
|
||||||
|
|
|
||||||
34
docs/faq.md
34
docs/faq.md
|
|
@ -1,24 +1,28 @@
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
## Why add a draft-pack import feature?
|
## Why add import validation?
|
||||||
|
|
||||||
Because the transition from generated draft pack to curated workspace is one of the
|
Because reducing startup friction does not mean hiding risk. A user still needs
|
||||||
places where users can lose momentum.
|
a clear signal about whether a generated draft pack is structurally usable.
|
||||||
|
|
||||||
## How does this relate to the activation-energy goal?
|
## How does this support the activation-energy goal?
|
||||||
|
|
||||||
Even when online course contents can be ingested, people may still stall if the next
|
It removes uncertainty from the handoff step. Users can see whether a draft pack
|
||||||
steps are awkward. Importing a draft pack into a workspace should feel like one
|
looks valid before committing it into a workspace.
|
||||||
smooth continuation, not a separate manual task.
|
|
||||||
|
|
||||||
## What does import do?
|
## What does the preview check do?
|
||||||
|
|
||||||
In this scaffold it:
|
In this scaffold it checks:
|
||||||
- creates or updates a workspace
|
- required files
|
||||||
- copies a source draft-pack directory into that workspace
|
- basic YAML parsing
|
||||||
- makes it ready to open in the review UI
|
- key metadata presence
|
||||||
|
- concept count
|
||||||
|
- overwrite conditions
|
||||||
|
|
||||||
## Is the import workflow validated?
|
## Does preview guarantee correctness?
|
||||||
|
|
||||||
Only lightly in this scaffold. A future revision should add stronger schema checks,
|
No. It is a safety and structure check, not a guarantee of pedagogical quality.
|
||||||
collision handling, and overwrite safeguards.
|
|
||||||
|
## Can import still overwrite an existing workspace?
|
||||||
|
|
||||||
|
Yes, but only if overwrite is explicitly allowed.
|
||||||
|
|
|
||||||
|
|
@ -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: draft-pack import workflow for workspaces"
|
description = "Didactopus: import validation and safety layer"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,20 @@ from pathlib import Path
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
class ReviewConfig(BaseModel):
|
class ReviewConfig(BaseModel):
|
||||||
default_reviewer: str = "Unknown Reviewer"
|
default_reviewer: str = "Unknown Reviewer"
|
||||||
write_promoted_pack: bool = True
|
write_promoted_pack: bool = True
|
||||||
|
|
||||||
|
|
||||||
class BridgeConfig(BaseModel):
|
class BridgeConfig(BaseModel):
|
||||||
host: str = "127.0.0.1"
|
host: str = "127.0.0.1"
|
||||||
port: int = 8765
|
port: int = 8765
|
||||||
registry_path: str = "workspace_registry.json"
|
registry_path: str = "workspace_registry.json"
|
||||||
default_workspace_root: str = "workspaces"
|
default_workspace_root: str = "workspaces"
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(BaseModel):
|
class AppConfig(BaseModel):
|
||||||
review: ReviewConfig = Field(default_factory=ReviewConfig)
|
review: ReviewConfig = Field(default_factory=ReviewConfig)
|
||||||
bridge: BridgeConfig = Field(default_factory=BridgeConfig)
|
bridge: BridgeConfig = Field(default_factory=BridgeConfig)
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str | Path) -> AppConfig:
|
def load_config(path: str | Path) -> AppConfig:
|
||||||
with open(path, "r", encoding="utf-8") as handle:
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
data = yaml.safe_load(handle) or {}
|
data = yaml.safe_load(handle) or {}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,56 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
from .review_schema import ImportPreview
|
from .review_schema import ImportPreview
|
||||||
from .pack_validator import validate_pack_directory
|
|
||||||
from .semantic_qa import semantic_qa_for_pack
|
REQUIRED_FILES = ["pack.yaml", "concepts.yaml"]
|
||||||
|
|
||||||
def preview_draft_pack_import(source_dir: str | Path, workspace_id: str, overwrite_required: bool = False) -> ImportPreview:
|
def preview_draft_pack_import(source_dir: str | Path, workspace_id: str, overwrite_required: bool = False) -> ImportPreview:
|
||||||
result = validate_pack_directory(source_dir)
|
source = Path(source_dir)
|
||||||
semantic = semantic_qa_for_pack(source_dir) if result["ok"] else {"warnings": [], "summary": {}}
|
preview = ImportPreview(source_dir=str(source), workspace_id=workspace_id, overwrite_required=overwrite_required)
|
||||||
preview = ImportPreview(
|
|
||||||
source_dir=str(Path(source_dir)),
|
if not source.exists():
|
||||||
workspace_id=workspace_id,
|
preview.errors.append(f"Source directory does not exist: {source}")
|
||||||
overwrite_required=overwrite_required,
|
return preview
|
||||||
ok=result["ok"],
|
if not source.is_dir():
|
||||||
errors=list(result["errors"]),
|
preview.errors.append(f"Source path is not a directory: {source}")
|
||||||
warnings=list(result["warnings"]),
|
return preview
|
||||||
summary=dict(result["summary"]),
|
|
||||||
semantic_warnings=list(semantic["warnings"]),
|
for filename in REQUIRED_FILES:
|
||||||
)
|
if not (source / filename).exists():
|
||||||
|
preview.errors.append(f"Missing required file: {filename}")
|
||||||
|
|
||||||
|
pack_data = {}
|
||||||
|
concepts_data = {}
|
||||||
|
if not preview.errors:
|
||||||
|
try:
|
||||||
|
pack_data = yaml.safe_load((source / "pack.yaml").read_text(encoding="utf-8")) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
preview.errors.append(f"Could not parse pack.yaml: {exc}")
|
||||||
|
try:
|
||||||
|
concepts_data = yaml.safe_load((source / "concepts.yaml").read_text(encoding="utf-8")) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
preview.errors.append(f"Could not parse concepts.yaml: {exc}")
|
||||||
|
|
||||||
|
if not preview.errors:
|
||||||
|
if "name" not in pack_data:
|
||||||
|
preview.warnings.append("pack.yaml has no 'name' field.")
|
||||||
|
if "display_name" not in pack_data:
|
||||||
|
preview.warnings.append("pack.yaml has no 'display_name' field.")
|
||||||
|
concepts = concepts_data.get("concepts", [])
|
||||||
|
if not isinstance(concepts, list):
|
||||||
|
preview.errors.append("concepts.yaml top-level 'concepts' is not a list.")
|
||||||
|
else:
|
||||||
|
preview.summary = {
|
||||||
|
"pack_name": pack_data.get("name", ""),
|
||||||
|
"display_name": pack_data.get("display_name", ""),
|
||||||
|
"version": pack_data.get("version", ""),
|
||||||
|
"concept_count": len(concepts),
|
||||||
|
"has_conflict_report": (source / "conflict_report.md").exists(),
|
||||||
|
"has_review_report": (source / "review_report.md").exists(),
|
||||||
|
}
|
||||||
|
if len(concepts) == 0:
|
||||||
|
preview.warnings.append("concepts.yaml contains zero concepts.")
|
||||||
|
|
||||||
|
preview.ok = len(preview.errors) == 0
|
||||||
return preview
|
return preview
|
||||||
|
|
|
||||||
|
|
@ -73,16 +73,31 @@ 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-preview":
|
||||||
|
preview = self.workspace_manager.preview_import(
|
||||||
|
source_dir=payload["source_dir"],
|
||||||
|
workspace_id=payload["workspace_id"]
|
||||||
|
)
|
||||||
|
json_response(self, 200, preview.model_dump())
|
||||||
|
return
|
||||||
|
|
||||||
if self.path == "/api/workspaces/import":
|
if self.path == "/api/workspaces/import":
|
||||||
try:
|
try:
|
||||||
meta = self.workspace_manager.import_draft_pack(
|
meta = self.workspace_manager.import_draft_pack(
|
||||||
source_dir=payload["source_dir"],
|
source_dir=payload["source_dir"],
|
||||||
workspace_id=payload["workspace_id"],
|
workspace_id=payload["workspace_id"],
|
||||||
title=payload.get("title"),
|
title=payload.get("title"),
|
||||||
notes=payload.get("notes", "")
|
notes=payload.get("notes", ""),
|
||||||
|
allow_overwrite=bool(payload.get("allow_overwrite", False)),
|
||||||
)
|
)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
json_response(self, 404, {"error": str(exc)})
|
json_response(self, 404, {"ok": False, "error": str(exc)})
|
||||||
|
return
|
||||||
|
except FileExistsError as exc:
|
||||||
|
json_response(self, 409, {"ok": False, "error": str(exc)})
|
||||||
|
return
|
||||||
|
except ValueError as exc:
|
||||||
|
json_response(self, 400, {"ok": False, "error": str(exc)})
|
||||||
return
|
return
|
||||||
self.set_active_workspace(meta.workspace_id)
|
self.set_active_workspace(meta.workspace_id)
|
||||||
json_response(self, 200, {"ok": True, "workspace": meta.model_dump()})
|
json_response(self, 200, {"ok": True, "workspace": meta.model_dump()})
|
||||||
|
|
@ -105,7 +120,7 @@ class ReviewBridgeHandler(BaseHTTPRequestHandler):
|
||||||
json_response(self, 404, {"error": "not found"})
|
json_response(self, 404, {"error": "not found"})
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="Didactopus local review bridge server with workspace manager")
|
parser = argparse.ArgumentParser(description="Didactopus local review bridge server with import validation")
|
||||||
parser.add_argument("--config", default="configs/config.example.yaml")
|
parser.add_argument("--config", default="configs/config.example.yaml")
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,12 @@ class WorkspaceMeta(BaseModel):
|
||||||
class WorkspaceRegistry(BaseModel):
|
class WorkspaceRegistry(BaseModel):
|
||||||
workspaces: list[WorkspaceMeta] = Field(default_factory=list)
|
workspaces: list[WorkspaceMeta] = Field(default_factory=list)
|
||||||
recent_workspace_ids: list[str] = Field(default_factory=list)
|
recent_workspace_ids: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
class ImportPreview(BaseModel):
|
||||||
|
ok: bool = False
|
||||||
|
source_dir: str
|
||||||
|
workspace_id: str
|
||||||
|
overwrite_required: bool = False
|
||||||
|
errors: list[str] = Field(default_factory=list)
|
||||||
|
warnings: list[str] = Field(default_factory=list)
|
||||||
|
summary: dict = Field(default_factory=dict)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
||||||
from datetime import datetime, UTC
|
from datetime import datetime, UTC
|
||||||
import json, shutil
|
import json, shutil
|
||||||
from .review_schema import WorkspaceMeta, WorkspaceRegistry
|
from .review_schema import WorkspaceMeta, WorkspaceRegistry
|
||||||
|
from .import_validator import preview_draft_pack_import
|
||||||
|
|
||||||
def utc_now() -> str:
|
def utc_now() -> str:
|
||||||
return datetime.now(UTC).isoformat()
|
return datetime.now(UTC).isoformat()
|
||||||
|
|
@ -71,12 +72,23 @@ class WorkspaceManager:
|
||||||
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:
|
def preview_import(self, source_dir: str | Path, workspace_id: str):
|
||||||
source_dir = Path(source_dir)
|
preview = preview_draft_pack_import(source_dir, workspace_id)
|
||||||
if not source_dir.exists():
|
existing = self.get_workspace(workspace_id)
|
||||||
raise FileNotFoundError(f"Draft pack source does not exist: {source_dir}")
|
if existing is not None:
|
||||||
|
preview.overwrite_required = True
|
||||||
|
preview.warnings.append(f"Workspace '{workspace_id}' already exists and import will overwrite draft_pack.")
|
||||||
|
return preview
|
||||||
|
|
||||||
meta = self.get_workspace(workspace_id)
|
def import_draft_pack(self, source_dir: str | Path, workspace_id: str, title: str | None = None, notes: str = "", allow_overwrite: bool = False) -> WorkspaceMeta:
|
||||||
|
preview = self.preview_import(source_dir, workspace_id)
|
||||||
|
if not preview.ok:
|
||||||
|
raise ValueError("Draft pack preview failed: " + "; ".join(preview.errors))
|
||||||
|
existing = self.get_workspace(workspace_id)
|
||||||
|
if existing is not None and not allow_overwrite:
|
||||||
|
raise FileExistsError(f"Workspace '{workspace_id}' already exists; set allow_overwrite to replace its draft pack.")
|
||||||
|
|
||||||
|
meta = existing
|
||||||
if meta is None:
|
if meta is None:
|
||||||
meta = self.create_workspace(workspace_id, title or workspace_id, notes=notes)
|
meta = self.create_workspace(workspace_id, title or workspace_id, notes=notes)
|
||||||
else:
|
else:
|
||||||
|
|
@ -86,7 +98,7 @@ class WorkspaceManager:
|
||||||
target_draft = workspace_dir / "draft_pack"
|
target_draft = workspace_dir / "draft_pack"
|
||||||
if target_draft.exists():
|
if target_draft.exists():
|
||||||
shutil.rmtree(target_draft)
|
shutil.rmtree(target_draft)
|
||||||
shutil.copytree(source_dir, target_draft)
|
shutil.copytree(Path(source_dir), target_draft)
|
||||||
|
|
||||||
registry = self.load_registry()
|
registry = self.load_registry()
|
||||||
for ws in registry.workspaces:
|
for ws in registry.workspaces:
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from didactopus.import_validator import preview_draft_pack_import
|
from didactopus.import_validator import preview_draft_pack_import
|
||||||
|
|
||||||
def test_preview_includes_semantic_warnings(tmp_path: Path) -> None:
|
def test_valid_preview(tmp_path: Path) -> None:
|
||||||
(tmp_path / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8")
|
src = tmp_path / "src"
|
||||||
(tmp_path / "concepts.yaml").write_text(
|
src.mkdir()
|
||||||
"concepts:\n - id: c1\n title: Prior and Posterior\n description: Beliefs before and after evidence.\n - id: c2\n title: Posterior Analysis\n description: Beliefs before and after evidence.\n",
|
(src / "pack.yaml").write_text("name: p\ndisplay_name: P\nversion: 0.1.0\n", encoding="utf-8")
|
||||||
encoding="utf-8"
|
(src / "concepts.yaml").write_text("concepts: []\n", encoding="utf-8")
|
||||||
)
|
preview = preview_draft_pack_import(src, "ws1")
|
||||||
(tmp_path / "roadmap.yaml").write_text("stages:\n - id: s1\n title: Foundations\n concepts: [c1]\n - id: s2\n title: Advanced Inference\n concepts: [c2]\n", encoding="utf-8")
|
assert preview.ok is True
|
||||||
(tmp_path / "projects.yaml").write_text("projects: []\n", encoding="utf-8")
|
assert preview.summary["concept_count"] == 0
|
||||||
(tmp_path / "rubrics.yaml").write_text("rubrics: []\n", encoding="utf-8")
|
|
||||||
preview = preview_draft_pack_import(tmp_path, "ws1")
|
def test_missing_required_file(tmp_path: Path) -> None:
|
||||||
assert isinstance(preview.semantic_warnings, list)
|
src = tmp_path / "src"
|
||||||
|
src.mkdir()
|
||||||
|
(src / "pack.yaml").write_text("name: p\n", encoding="utf-8")
|
||||||
|
preview = preview_draft_pack_import(src, "ws1")
|
||||||
|
assert preview.ok is False
|
||||||
|
assert any("Missing required file" in e for e in preview.errors)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,25 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from didactopus.workspace_manager import WorkspaceManager
|
from didactopus.workspace_manager import WorkspaceManager
|
||||||
|
|
||||||
def test_create_and_get_workspace(tmp_path: Path) -> None:
|
def test_import_preview_and_overwrite_warning(tmp_path: Path) -> None:
|
||||||
mgr = WorkspaceManager(tmp_path / "registry.json", tmp_path / "roots")
|
|
||||||
meta = mgr.create_workspace("ws1", "Workspace One")
|
|
||||||
assert meta.workspace_id == "ws1"
|
|
||||||
got = mgr.get_workspace("ws1")
|
|
||||||
assert got is not None
|
|
||||||
assert got.title == "Workspace One"
|
|
||||||
|
|
||||||
def test_import_draft_pack(tmp_path: Path) -> None:
|
|
||||||
src = tmp_path / "srcpack"
|
src = tmp_path / "srcpack"
|
||||||
src.mkdir()
|
src.mkdir()
|
||||||
(src / "pack.yaml").write_text("name: x\n", encoding="utf-8")
|
(src / "pack.yaml").write_text("name: x\ndisplay_name: X\nversion: 0.1.0\n", encoding="utf-8")
|
||||||
(src / "concepts.yaml").write_text("concepts: []\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")
|
||||||
meta = mgr.import_draft_pack(src, "ws2", title="Workspace Two")
|
mgr.create_workspace("ws2", "Workspace Two")
|
||||||
assert meta.workspace_id == "ws2"
|
preview = mgr.preview_import(src, "ws2")
|
||||||
assert (tmp_path / "roots" / "ws2" / "draft_pack" / "pack.yaml").exists()
|
assert preview.overwrite_required is True
|
||||||
|
|
||||||
|
def test_import_draft_pack_requires_overwrite_flag(tmp_path: Path) -> None:
|
||||||
|
src = tmp_path / "srcpack"
|
||||||
|
src.mkdir()
|
||||||
|
(src / "pack.yaml").write_text("name: x\ndisplay_name: X\nversion: 0.1.0\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("ws2", "Workspace Two")
|
||||||
|
try:
|
||||||
|
mgr.import_draft_pack(src, "ws2", title="Workspace Two", allow_overwrite=False)
|
||||||
|
assert False, "Expected FileExistsError"
|
||||||
|
except FileExistsError:
|
||||||
|
assert True
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ export default function App() {
|
||||||
const [workspaceId, setWorkspaceId] = useState("");
|
const [workspaceId, setWorkspaceId] = useState("");
|
||||||
const [workspaceTitle, setWorkspaceTitle] = useState("");
|
const [workspaceTitle, setWorkspaceTitle] = useState("");
|
||||||
const [importSource, setImportSource] = useState("");
|
const [importSource, setImportSource] = useState("");
|
||||||
|
const [importPreview, setImportPreview] = useState(null);
|
||||||
|
const [allowOverwrite, setAllowOverwrite] = useState(false);
|
||||||
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([]);
|
||||||
|
|
@ -17,7 +19,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 (!session) setMessage("Choose, create, or import a workspace.");
|
if (!session) setMessage("Choose, create, preview, or import a workspace.");
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -35,6 +37,22 @@ export default function App() {
|
||||||
await openWorkspace(workspaceId);
|
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);
|
||||||
|
if (data.ok) {
|
||||||
|
setMessage("Import preview ready.");
|
||||||
|
} else {
|
||||||
|
setMessage("Import preview found blocking errors.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function importWorkspace() {
|
async function importWorkspace() {
|
||||||
if (!workspaceId || !importSource) return;
|
if (!workspaceId || !importSource) return;
|
||||||
const res = await fetch(`${API}/api/workspaces/import`, {
|
const res = await fetch(`${API}/api/workspaces/import`, {
|
||||||
|
|
@ -43,7 +61,8 @@ export default function App() {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
workspace_id: workspaceId,
|
workspace_id: workspaceId,
|
||||||
title: workspaceTitle || workspaceId,
|
title: workspaceTitle || workspaceId,
|
||||||
source_dir: importSource
|
source_dir: importSource,
|
||||||
|
allow_overwrite: allowOverwrite
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
@ -138,10 +157,10 @@ export default function App() {
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<header className="hero">
|
<header className="hero">
|
||||||
<div>
|
<div>
|
||||||
<h1>Didactopus Workspace Manager</h1>
|
<h1>Didactopus Import Validation</h1>
|
||||||
<p>
|
<p>
|
||||||
Reduce the activation-energy hump from raw course material to reviewed domain pack
|
Reduce the activation-energy hump from generated draft packs to curated review workspaces
|
||||||
by organizing draft-pack curation as a manageable local workflow.
|
by previewing structure, warnings, and overwrite risk before import.
|
||||||
</p>
|
</p>
|
||||||
<div className="small">{message}</div>
|
<div className="small">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -159,10 +178,14 @@ export default function App() {
|
||||||
<button onClick={createWorkspace}>Create</button>
|
<button onClick={createWorkspace}>Create</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2>Import Draft Pack</h2>
|
<h2>Preview / Import Draft Pack</h2>
|
||||||
<label>Workspace ID<input value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} /></label>
|
<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>
|
<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>
|
<label className="checkline"><input type="checkbox" checked={allowOverwrite} onChange={(e) => setAllowOverwrite(e.target.checked)} /> Allow overwrite of existing workspace draft_pack</label>
|
||||||
|
<div className="button-row">
|
||||||
|
<button onClick={previewImport}>Preview</button>
|
||||||
|
<button onClick={importWorkspace}>Import</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2>Recent</h2>
|
<h2>Recent</h2>
|
||||||
|
|
@ -174,6 +197,27 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{importPreview && (
|
||||||
|
<section className="preview-grid">
|
||||||
|
<div className="card">
|
||||||
|
<h2>Import Preview</h2>
|
||||||
|
<div><strong>OK:</strong> {String(importPreview.ok)}</div>
|
||||||
|
<div><strong>Overwrite Required:</strong> {String(importPreview.overwrite_required)}</div>
|
||||||
|
<div><strong>Pack:</strong> {importPreview.summary?.display_name || importPreview.summary?.pack_name || "-"}</div>
|
||||||
|
<div><strong>Version:</strong> {importPreview.summary?.version || "-"}</div>
|
||||||
|
<div><strong>Concept Count:</strong> {importPreview.summary?.concept_count ?? "-"}</div>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h2>Errors</h2>
|
||||||
|
<ul>{(importPreview.errors || []).length ? importPreview.errors.map((x, i) => <li key={i}>{x}</li>) : <li>none</li>}</ul>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<h2>Warnings</h2>
|
||||||
|
<ul>{(importPreview.warnings || []).length ? importPreview.warnings.map((x, i) => <li key={i}>{x}</li>) : <li>none</li>}</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{session && (
|
{session && (
|
||||||
<main className="layout">
|
<main className="layout">
|
||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: var(--b
|
||||||
button { border: 1px solid var(--border); background: white; border-radius: 12px; padding: 10px 14px; cursor: pointer; }
|
button { border: 1px solid var(--border); background: white; border-radius: 12px; padding: 10px 14px; cursor: pointer; }
|
||||||
button:hover { border-color: var(--accent); }
|
button:hover { border-color: var(--accent); }
|
||||||
.summary-grid { margin-top: 16px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
.summary-grid { margin-top: 16px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
||||||
|
.preview-grid { margin-top: 16px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||||||
.layout { margin-top: 16px; display: grid; grid-template-columns: 290px 1fr 360px; gap: 16px; }
|
.layout { margin-top: 16px; display: grid; grid-template-columns: 290px 1fr 360px; gap: 16px; }
|
||||||
.card { background: var(--card); border: 1px solid var(--border); border-radius: 18px; padding: 16px; }
|
.card { background: var(--card); border: 1px solid var(--border); border-radius: 18px; padding: 16px; }
|
||||||
.sidebar, .content, .rightbar { display: flex; flex-direction: column; gap: 16px; }
|
.sidebar, .content, .rightbar { display: flex; flex-direction: column; gap: 16px; }
|
||||||
|
|
@ -30,7 +31,10 @@ input, textarea, select { width: 100%; margin-top: 6px; border: 1px solid var(--
|
||||||
.small { color: var(--muted); }
|
.small { color: var(--muted); }
|
||||||
.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; }
|
||||||
|
.button-row { display: flex; gap: 10px; }
|
||||||
|
.checkline { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.checkline input { width: auto; margin-top: 0; }
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.summary-grid { grid-template-columns: repeat(2, 1fr); }
|
.summary-grid, .preview-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
.layout { grid-template-columns: 1fr; }
|
.layout { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue