Compare commits
No commits in common. "59d45c2942c7247a0235f436e086cfdff3955104" and "3837bd23169e3f1b016d216992d1189a2c0d8352" have entirely different histories.
59d45c2942
...
3837bd2316
25
README.md
25
README.md
|
|
@ -140,31 +140,6 @@ For the fastest included example, use the MIT OCW Information and Entropy demo.
|
|||
- progress visualization
|
||||
- skill export
|
||||
|
||||
## Learner Workbench Pilot
|
||||
|
||||
Didactopus now also includes a learner-workbench pilot in the web UI.
|
||||
|
||||
The current split is:
|
||||
|
||||
- review workbench for candidate triage, synthesis, and promotion
|
||||
- learner workbench pilot for guided study and reflective revision
|
||||
|
||||
The learner-workbench pilot currently uses the `Evidence Trail` sample pack and
|
||||
focuses on:
|
||||
|
||||
- question framing
|
||||
- observation versus interpretation
|
||||
- source comparison
|
||||
- bibliography growth
|
||||
- revision under uncertainty
|
||||
|
||||
The backend entrypoint for that pilot is `POST /api/learner-workbench/session`.
|
||||
The frontend pilot pack payload is [evidence-trail-pack.json](/home/netuser/bin/Didactopus/webui/public/packs/evidence-trail-pack.json), and the underlying pack lives in [domain-packs/evidence-trail](/home/netuser/bin/Didactopus/domain-packs/evidence-trail).
|
||||
|
||||
This is still a pilot rather than the final learner UX. It is best understood as
|
||||
the first integrated learner-workbench path inside the main repository, not as a
|
||||
finished replacement for the existing learner-session demos.
|
||||
|
||||
## `doclift` Bundle Ingestion
|
||||
|
||||
When your source material starts as legacy office documents, the intended
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
# evo-edu Evidence Trail Pilot
|
||||
|
||||
This note captures the first concrete integration path between `evo-edu.org`
|
||||
and Didactopus.
|
||||
|
||||
## Why this pack first
|
||||
|
||||
`Evidence Trail` is the best first pilot because it already matches the
|
||||
strongest current Didactopus behaviors:
|
||||
|
||||
- question framing
|
||||
- source-grounded study
|
||||
- concept sequencing
|
||||
- reflective revision
|
||||
- bibliography growth
|
||||
|
||||
It is a better fit for the first learner-workbench pilot than a more
|
||||
model-heavy pack such as `Population Change`.
|
||||
|
||||
## Current repo artifacts
|
||||
|
||||
This pilot now has a first pack skeleton in:
|
||||
|
||||
- `domain-packs/evidence-trail/pack.yaml`
|
||||
- `domain-packs/evidence-trail/concepts.yaml`
|
||||
- `domain-packs/evidence-trail/pack_compliance_manifest.json`
|
||||
- `domain-packs/evidence-trail/pack.frontend.json`
|
||||
|
||||
and a learner-facing frontend payload copy in:
|
||||
|
||||
- `webui/public/packs/evidence-trail-pack.json`
|
||||
|
||||
## Immediate next UI step
|
||||
|
||||
The current `webui/` is still review-workbench-first. The next useful step is
|
||||
to add a learner-workbench path that can load `evidence-trail-pack.json` and
|
||||
show:
|
||||
|
||||
- onboarding headline/body/checklist
|
||||
- current concept
|
||||
- prerequisite path
|
||||
- one grounded prompt
|
||||
- learner response
|
||||
- evaluator feedback
|
||||
- next study action
|
||||
|
||||
## Integration back to evo-edu.org
|
||||
|
||||
Once a learner-workbench route exists, `evo-edu.org` should link to it from:
|
||||
|
||||
- `/evo/packs/evidence-trail/`
|
||||
- `/evo/packs/evidence-trail/study-log.html`
|
||||
- `/evo/pathways/self-learners/`
|
||||
|
||||
The framing should remain:
|
||||
|
||||
- guided-study workbench
|
||||
- source-grounded reflection companion
|
||||
|
||||
not generic AI tutor.
|
||||
|
|
@ -87,7 +87,7 @@ Target features:
|
|||
|
||||
### 5. Learner workbench UI
|
||||
|
||||
Status: pilot in progress
|
||||
Status: planned
|
||||
|
||||
Why important:
|
||||
|
||||
|
|
@ -103,23 +103,6 @@ Target features:
|
|||
- evaluator feedback
|
||||
- recommended next step
|
||||
|
||||
Current pilot state:
|
||||
|
||||
- a backend learner-workbench path exists in `didactopus.learner_workbench`
|
||||
- the API exposes `POST /api/learner-workbench/session`
|
||||
- the web UI now has a launcher that separates review workbench from learner workbench
|
||||
- the first pilot pack exists at `domain-packs/evidence-trail/`
|
||||
- the frontend can load a static learner-pack payload from `webui/public/packs/evidence-trail-pack.json`
|
||||
- the current pilot explicitly emphasizes question framing, observation versus interpretation, uncertainty, and revision
|
||||
|
||||
Next steps:
|
||||
|
||||
- connect the learner-workbench pilot more directly to the standard learner-session backend
|
||||
- persist learner-workbench state instead of treating each step as a stateless interaction
|
||||
- ground the pilot more deeply in source fragments instead of mostly pack-level structure
|
||||
- decide which scientific-virtues framing belongs in the stable learner path versus remaining pilot-specific
|
||||
- document a simple local run path for using the learner workbench outside ad hoc development
|
||||
|
||||
### 6. Adaptive diagnostics and practice refinement
|
||||
|
||||
Status: planned
|
||||
|
|
@ -212,9 +195,8 @@ Examples:
|
|||
## Suggested Implementation Sequence
|
||||
|
||||
1. Strengthen `didactopus.learner_session` into the standard session backend.
|
||||
2. Fold the learner-workbench pilot into that backend without losing its stronger study-state framing.
|
||||
3. Build a small model-benchmark harness around the unified learner backend.
|
||||
4. Add accessible learner HTML and text-first outputs.
|
||||
5. Add local TTS and STT support to the same session flow.
|
||||
6. Expand adaptive practice and diagnostics.
|
||||
7. Improve review, impact analysis, and incremental update support.
|
||||
2. Build a small model-benchmark harness around that backend.
|
||||
3. Add accessible learner HTML and text-first outputs.
|
||||
4. Add local TTS and STT support to the same session flow.
|
||||
5. Expand adaptive practice and diagnostics.
|
||||
6. Improve review, impact analysis, and incremental update support.
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
concepts:
|
||||
- id: question-framing
|
||||
title: Question Framing
|
||||
description: >-
|
||||
Start from a question narrow enough to investigate and revise. A good
|
||||
study question gives the learner a way to compare evidence rather than
|
||||
only gather disconnected facts.
|
||||
prerequisites: []
|
||||
mastery_signals:
|
||||
- State a focused question that can be investigated with tools and sources.
|
||||
- Distinguish the main question from broader background curiosity.
|
||||
mastery_profile: {}
|
||||
- id: observation-vs-interpretation
|
||||
title: Observation Versus Interpretation
|
||||
description: >-
|
||||
Separate what the model, species account, or source actually shows from
|
||||
the explanation you currently prefer. This distinction is necessary for
|
||||
trustworthy revision.
|
||||
prerequisites:
|
||||
- question-framing
|
||||
mastery_signals:
|
||||
- Describe a model or source observation before drawing a conclusion.
|
||||
- Mark which parts of an explanation are inference rather than direct observation.
|
||||
mastery_profile: {}
|
||||
- id: source-comparison
|
||||
title: Source Comparison
|
||||
description: >-
|
||||
Compare sources by relevance, grounding, and limits rather than treating
|
||||
all sources as equally strong. The learner should note where a source
|
||||
supports, complicates, or fails to resolve a claim.
|
||||
prerequisites:
|
||||
- observation-vs-interpretation
|
||||
mastery_signals:
|
||||
- Explain why one source is stronger or weaker for the current question.
|
||||
- Identify at least one unresolved conflict or limitation across sources.
|
||||
mastery_profile: {}
|
||||
- id: bibliography-growth
|
||||
title: Bibliography Growth
|
||||
description: >-
|
||||
Extend a question through linked citations, related species records, and
|
||||
literature trails. Bibliography growth should deepen the inquiry rather
|
||||
than inflate the reading list without purpose.
|
||||
prerequisites:
|
||||
- source-comparison
|
||||
mastery_signals:
|
||||
- Add a useful new source that sharpens or tests the current explanation.
|
||||
- Explain how a new citation changes the study path.
|
||||
mastery_profile: {}
|
||||
- id: revision-under-uncertainty
|
||||
title: Revision Under Uncertainty
|
||||
description: >-
|
||||
Treat revision as a normal part of inquiry. The learner should be able to
|
||||
state what changed, why it changed, and what uncertainty still remains.
|
||||
prerequisites:
|
||||
- bibliography-growth
|
||||
mastery_signals:
|
||||
- Write a revised explanation after comparing evidence.
|
||||
- State one uncertainty or next evidence move instead of pretending closure.
|
||||
mastery_profile: {}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
{
|
||||
"id": "evidence-trail",
|
||||
"title": "Evidence Trail",
|
||||
"subtitle": "Draft evo-edu-aligned pack for guided independent study through question framing, source comparison, bibliography growth, and reflective revision.",
|
||||
"level": "self-learner",
|
||||
"concepts": [
|
||||
{
|
||||
"id": "question-framing",
|
||||
"title": "Question Framing",
|
||||
"prerequisites": [],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Question Framing progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "observation-vs-interpretation",
|
||||
"title": "Observation Versus Interpretation",
|
||||
"prerequisites": [
|
||||
"question-framing"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Observation Versus Interpretation progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "source-comparison",
|
||||
"title": "Source Comparison",
|
||||
"prerequisites": [
|
||||
"observation-vs-interpretation"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Source Comparison progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "bibliography-growth",
|
||||
"title": "Bibliography Growth",
|
||||
"prerequisites": [
|
||||
"source-comparison"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Bibliography Growth progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "revision-under-uncertainty",
|
||||
"title": "Revision Under Uncertainty",
|
||||
"prerequisites": [
|
||||
"bibliography-growth"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Revision Under Uncertainty progress recorded"
|
||||
}
|
||||
],
|
||||
"onboarding": {
|
||||
"headline": "Start with one question, one source trail, and one explicit revision.",
|
||||
"body": "Begin with a focused question, compare model output and source material carefully, then record what changed in your understanding and why.",
|
||||
"checklist": [
|
||||
"State the question you are trying to answer.",
|
||||
"Compare one model observation with one cited source or species account.",
|
||||
"Write one interpretation and one uncertainty.",
|
||||
"Record what evidence would make you revise your current view."
|
||||
]
|
||||
},
|
||||
"compliance": {
|
||||
"sources": 6,
|
||||
"attributionRequired": true,
|
||||
"shareAlikeRequired": true,
|
||||
"noncommercialOnly": true,
|
||||
"flags": [
|
||||
"derived-from-evo-edu-materials"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
name: evidence-trail
|
||||
display_name: Evidence Trail
|
||||
version: 0.1.0-draft
|
||||
schema_version: '1'
|
||||
didactopus_min_version: 0.1.0
|
||||
didactopus_max_version: 0.9.99
|
||||
description: Draft evo-edu-aligned pack for guided independent study through question framing, source comparison, bibliography growth, and reflective revision.
|
||||
author: Wesley R. Elsberry
|
||||
license: CC BY-NC-SA 4.0
|
||||
audience_level: self-learner
|
||||
dependencies: []
|
||||
overrides: []
|
||||
profile_templates: {}
|
||||
cross_pack_links: []
|
||||
supporting_artifacts:
|
||||
- pack_compliance_manifest.json
|
||||
first_session_headline: Start with one question, one source trail, and one explicit revision.
|
||||
first_session_body: Begin with a focused question, compare model output and source material carefully, then record what changed in your understanding and why.
|
||||
first_session_checklist:
|
||||
- State the question you are trying to answer.
|
||||
- Compare one model observation with one cited source or species account.
|
||||
- Write one interpretation and one uncertainty.
|
||||
- Record what evidence would make you revise your current view.
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"pack_id": "evidence-trail",
|
||||
"display_name": "Evidence Trail",
|
||||
"attribution_required": true,
|
||||
"share_alike_required": true,
|
||||
"noncommercial_only": true,
|
||||
"restrictive_flags": [
|
||||
"derived-from-evo-edu-materials"
|
||||
],
|
||||
"derived_from_sources": [
|
||||
{
|
||||
"title": "evo-edu.org Evidence Trail pack",
|
||||
"kind": "site_pack",
|
||||
"locator": "https://evo-edu.org/evo/packs/evidence-trail/"
|
||||
},
|
||||
{
|
||||
"title": "evo-edu.org Evidence Trail study log",
|
||||
"kind": "site_support_material",
|
||||
"locator": "https://evo-edu.org/evo/packs/evidence-trail/study-log.html"
|
||||
},
|
||||
{
|
||||
"title": "evo-edu.org Scientific Virtues",
|
||||
"kind": "site_background",
|
||||
"locator": "https://evo-edu.org/evo/scientific-virtues/"
|
||||
},
|
||||
{
|
||||
"title": "evo-edu.org Notebook",
|
||||
"kind": "site_background",
|
||||
"locator": "https://evo-edu.org/evo/notebook/"
|
||||
},
|
||||
{
|
||||
"title": "EcoSpecies",
|
||||
"kind": "supporting_resource",
|
||||
"locator": "https://evo-edu.org/apps/ecospecies/"
|
||||
},
|
||||
{
|
||||
"title": "Literature Explorer",
|
||||
"kind": "supporting_resource",
|
||||
"locator": "https://evo-edu.org/apps/literature-explorer/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ from .db import Base, engine
|
|||
from .models import (
|
||||
LoginRequest, TokenPair, KnowledgeCandidateCreate, KnowledgeCandidateUpdate,
|
||||
ReviewCreate, PromoteRequest, SynthesisRunRequest, SynthesisPromoteRequest,
|
||||
CreateLearnerRequest, LearnerWorkbenchSessionRequest
|
||||
CreateLearnerRequest
|
||||
)
|
||||
from .repository import (
|
||||
authenticate_user, get_user_by_id, create_learner, learner_owned_by_user,
|
||||
|
|
@ -16,7 +16,6 @@ from .repository import (
|
|||
)
|
||||
from .auth import issue_access_token, issue_refresh_token, decode_token, new_token_id
|
||||
from .synthesis import generate_synthesis_candidates
|
||||
from .learner_workbench import build_pack_workbench_session
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
|
@ -59,25 +58,6 @@ def api_create_learner(payload: CreateLearnerRequest, user = Depends(current_use
|
|||
create_learner(user.id, payload.learner_id, payload.display_name)
|
||||
return {"ok": True, "learner_id": payload.learner_id}
|
||||
|
||||
|
||||
@app.post("/api/learner-workbench/session")
|
||||
def api_learner_workbench_session(payload: LearnerWorkbenchSessionRequest):
|
||||
try:
|
||||
return build_pack_workbench_session(
|
||||
pack_id=payload.pack_id,
|
||||
concept_id=payload.concept_id,
|
||||
learner_goal=payload.learner_goal,
|
||||
question=payload.question,
|
||||
observation=payload.observation,
|
||||
interpretation=payload.interpretation,
|
||||
uncertainty=payload.uncertainty,
|
||||
revision_trigger=payload.revision_trigger,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc))
|
||||
except KeyError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@app.post("/api/knowledge-candidates")
|
||||
def api_create_candidate(payload: KnowledgeCandidateCreate, reviewer = Depends(require_reviewer)):
|
||||
candidate_id = create_candidate(payload)
|
||||
|
|
|
|||
|
|
@ -27,16 +27,6 @@ def _grounding_block(step: dict) -> str:
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _scientific_virtues_block() -> str:
|
||||
return (
|
||||
"Scientific virtues operating guidance:\n"
|
||||
"- Distinguish observation from interpretation.\n"
|
||||
"- Preserve uncertainty where the evidence does not settle the issue.\n"
|
||||
"- Treat revision as progress when better evidence changes the conclusion.\n"
|
||||
"- Prefer source-grounded comparison over unsupported confidence."
|
||||
)
|
||||
|
||||
|
||||
def _generate_role_text(
|
||||
provider: ModelProvider,
|
||||
*,
|
||||
|
|
@ -81,10 +71,9 @@ def build_graph_grounded_session(
|
|||
mentor_prompt = (
|
||||
f"{_grounding_block(primary)}\n\n"
|
||||
f"{_grounding_block(secondary)}\n\n"
|
||||
f"{_scientific_virtues_block()}\n\n"
|
||||
f"Learner goal: {learner_goal}\n"
|
||||
"Respond as Didactopus mentor. Give a short grounded orientation, explain why these concepts come first, "
|
||||
"ask one focused question that keeps the learner doing the reasoning, and explicitly separate what should be observed from what should be interpreted."
|
||||
"and ask one focused question that keeps the learner doing the reasoning."
|
||||
)
|
||||
mentor_text = _generate_role_text(
|
||||
provider,
|
||||
|
|
@ -98,10 +87,8 @@ def build_graph_grounded_session(
|
|||
|
||||
practice_prompt = (
|
||||
f"{_grounding_block(primary)}\n\n"
|
||||
f"{_scientific_virtues_block()}\n\n"
|
||||
f"Learner goal: {learner_goal}\n"
|
||||
"Create one reasoning-heavy practice task for the learner. Keep it grounded in the supporting lessons, do not provide the full solution, "
|
||||
"and require the learner to identify observation, interpretation, and one condition that would justify revision."
|
||||
"Create one reasoning-heavy practice task for the learner. Keep it grounded in the supporting lessons and do not provide the full solution."
|
||||
)
|
||||
practice_text = _generate_role_text(
|
||||
provider,
|
||||
|
|
@ -116,12 +103,10 @@ def build_graph_grounded_session(
|
|||
evaluation = evaluate_submission_with_skill(context, primary["concept_key"].split("::", 1)[-1], learner_submission)
|
||||
evaluator_prompt = (
|
||||
f"{_grounding_block(primary)}\n\n"
|
||||
f"{_scientific_virtues_block()}\n\n"
|
||||
f"Practice task: {practice_text}\n"
|
||||
f"Learner submission: {learner_submission}\n"
|
||||
f"Deterministic evaluator result: verdict={evaluation['verdict']}, aggregated={evaluation['aggregated']}\n"
|
||||
"Respond as Didactopus evaluator. Summarize strengths, real gaps, and one next revision target without pretending supported caveats are missing. "
|
||||
"Reward honest uncertainty, careful source-grounded distinction between observation and interpretation, and justified revision."
|
||||
"Respond as Didactopus evaluator. Summarize strengths, real gaps, and one next revision target without pretending supported caveats are missing."
|
||||
)
|
||||
evaluator_text = _generate_role_text(
|
||||
provider,
|
||||
|
|
@ -136,10 +121,8 @@ def build_graph_grounded_session(
|
|||
next_step_prompt = (
|
||||
f"{_grounding_block(primary)}\n\n"
|
||||
f"{_grounding_block(secondary)}\n\n"
|
||||
f"{_scientific_virtues_block()}\n\n"
|
||||
f"Evaluator feedback: {evaluator_text}\n"
|
||||
"Respond as Didactopus mentor. Give the next study action, explain why it follows from the grounded concept path, "
|
||||
"and state what new evidence or comparison would most help the learner revise or strengthen the current view."
|
||||
"Respond as Didactopus mentor. Give the next study action and explain why it follows from the grounded concept path."
|
||||
)
|
||||
next_step_text = _generate_role_text(
|
||||
provider,
|
||||
|
|
|
|||
|
|
@ -1,196 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from .config import AppConfig
|
||||
from .model_provider import ModelProvider
|
||||
from .role_prompts import system_prompt_for_role
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DOMAIN_PACKS = ROOT / "domain-packs"
|
||||
|
||||
|
||||
def _load_yaml(path: Path) -> dict:
|
||||
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
def load_pack_context(pack_id: str) -> dict:
|
||||
pack_dir = DOMAIN_PACKS / pack_id
|
||||
if not pack_dir.exists():
|
||||
raise FileNotFoundError(f"Unknown pack: {pack_id}")
|
||||
pack = _load_yaml(pack_dir / "pack.yaml")
|
||||
concepts = (_load_yaml(pack_dir / "concepts.yaml")).get("concepts", []) or []
|
||||
by_id = {concept.get("id"): concept for concept in concepts}
|
||||
return {
|
||||
"pack_dir": pack_dir,
|
||||
"pack": pack,
|
||||
"concepts": concepts,
|
||||
"by_id": by_id,
|
||||
}
|
||||
|
||||
|
||||
def _scientific_virtues_block() -> str:
|
||||
return (
|
||||
"Scientific virtues operating guidance:\n"
|
||||
"- Distinguish observation from interpretation.\n"
|
||||
"- Preserve uncertainty where the evidence does not settle the issue.\n"
|
||||
"- Treat revision as progress when better evidence changes the conclusion.\n"
|
||||
"- Prefer source-grounded comparison over unsupported confidence."
|
||||
)
|
||||
|
||||
|
||||
def _concept_block(concept: dict, by_id: dict[str, dict]) -> str:
|
||||
prerequisite_titles = [by_id.get(pid, {}).get("title", pid) for pid in concept.get("prerequisites", []) or []]
|
||||
mastery_signals = concept.get("mastery_signals", []) or []
|
||||
lines = [
|
||||
f"Concept: {concept.get('title', '')}",
|
||||
f"Prerequisites: {', '.join(prerequisite_titles or ['none explicit'])}",
|
||||
f"Description: {concept.get('description', '')}",
|
||||
]
|
||||
if mastery_signals:
|
||||
lines.append("Mastery signals:")
|
||||
lines.extend(f"- {item}" for item in mastery_signals)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _generate(provider: ModelProvider, *, role: str, prompt: str, temperature: float, max_tokens: int) -> dict:
|
||||
response = provider.generate(
|
||||
prompt,
|
||||
role=role,
|
||||
system_prompt=system_prompt_for_role(role),
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
return {
|
||||
"text": response.text.strip(),
|
||||
"provider": response.provider,
|
||||
"model_name": response.model_name,
|
||||
}
|
||||
|
||||
|
||||
def _feedback(question: str, observation: str, interpretation: str, uncertainty: str, revision_trigger: str) -> dict:
|
||||
strengths: list[str] = []
|
||||
gaps: list[str] = []
|
||||
if question.strip():
|
||||
strengths.append("You stated a concrete study question.")
|
||||
else:
|
||||
gaps.append("State a clear question before treating the inquiry as settled.")
|
||||
if observation.strip():
|
||||
strengths.append("You recorded an explicit observation.")
|
||||
else:
|
||||
gaps.append("Record what was observed before moving to interpretation.")
|
||||
if interpretation.strip():
|
||||
strengths.append("You proposed an interpretation.")
|
||||
else:
|
||||
gaps.append("State the interpretation you currently think fits best.")
|
||||
if uncertainty.strip():
|
||||
strengths.append("You preserved uncertainty instead of smoothing it away.")
|
||||
else:
|
||||
gaps.append("Name one uncertainty or limitation in the current explanation.")
|
||||
if revision_trigger.strip():
|
||||
strengths.append("You identified what evidence could justify revision.")
|
||||
else:
|
||||
gaps.append("Say what result or source would make you revise your view.")
|
||||
return {
|
||||
"strengths": strengths,
|
||||
"gaps": gaps,
|
||||
"next_revision_target": gaps[0] if gaps else "Compare one more source or example before treating the interpretation as stable.",
|
||||
}
|
||||
|
||||
|
||||
def build_pack_workbench_session(
|
||||
*,
|
||||
pack_id: str,
|
||||
concept_id: str,
|
||||
learner_goal: str,
|
||||
question: str,
|
||||
observation: str,
|
||||
interpretation: str,
|
||||
uncertainty: str,
|
||||
revision_trigger: str,
|
||||
) -> dict:
|
||||
context = load_pack_context(pack_id)
|
||||
pack = context["pack"]
|
||||
concepts = context["concepts"]
|
||||
by_id = context["by_id"]
|
||||
concept = by_id.get(concept_id) or (concepts[0] if concepts else None)
|
||||
if concept is None:
|
||||
raise KeyError(f"No concepts found for pack: {pack_id}")
|
||||
index = concepts.index(concept)
|
||||
next_concept = concepts[index + 1] if index + 1 < len(concepts) else concept
|
||||
|
||||
provider = ModelProvider(AppConfig().model_provider)
|
||||
pack_block = (
|
||||
f"Pack: {pack.get('display_name', pack_id)}\n"
|
||||
f"Description: {pack.get('description', '')}\n"
|
||||
f"Audience level: {pack.get('audience_level', 'novice-friendly')}"
|
||||
)
|
||||
learner_state_block = (
|
||||
f"Learner goal: {learner_goal}\n"
|
||||
f"Question: {question}\n"
|
||||
f"Observation: {observation}\n"
|
||||
f"Interpretation: {interpretation}\n"
|
||||
f"Uncertainty: {uncertainty}\n"
|
||||
f"Revision trigger: {revision_trigger}"
|
||||
)
|
||||
|
||||
mentor = _generate(
|
||||
provider,
|
||||
role="mentor",
|
||||
prompt=(
|
||||
f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_concept_block(next_concept, by_id)}\n\n"
|
||||
f"{_scientific_virtues_block()}\n\n{learner_state_block}\n"
|
||||
"Respond as Didactopus mentor. Give a short grounded orientation, explain why this concept matters now, "
|
||||
"and ask one focused question that helps the learner distinguish observation from interpretation."
|
||||
),
|
||||
temperature=0.2,
|
||||
max_tokens=260,
|
||||
)
|
||||
practice = _generate(
|
||||
provider,
|
||||
role="practice",
|
||||
prompt=(
|
||||
f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_scientific_virtues_block()}\n\n{learner_state_block}\n"
|
||||
"Respond as Didactopus practice designer. Create one compact task that asks for evidence comparison, honest uncertainty, and a revision condition."
|
||||
),
|
||||
temperature=0.25,
|
||||
max_tokens=220,
|
||||
)
|
||||
evaluator = _generate(
|
||||
provider,
|
||||
role="evaluator",
|
||||
prompt=(
|
||||
f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_scientific_virtues_block()}\n\n{learner_state_block}\n"
|
||||
"Respond as Didactopus evaluator. Name strengths first, then real gaps, and give one revision target without pretending stated caveats are absent."
|
||||
),
|
||||
temperature=0.2,
|
||||
max_tokens=240,
|
||||
)
|
||||
next_step = _generate(
|
||||
provider,
|
||||
role="mentor",
|
||||
prompt=(
|
||||
f"{pack_block}\n\n{_concept_block(concept, by_id)}\n\n{_concept_block(next_concept, by_id)}\n\n"
|
||||
f"{_scientific_virtues_block()}\n\nEvaluator feedback: {evaluator['text']}\n"
|
||||
"Respond as Didactopus mentor. Give the next study action and say what new evidence or comparison would most help the learner revise or strengthen the current view."
|
||||
),
|
||||
temperature=0.2,
|
||||
max_tokens=220,
|
||||
)
|
||||
|
||||
feedback = _feedback(question, observation, interpretation, uncertainty, revision_trigger)
|
||||
return {
|
||||
"pack_id": pack_id,
|
||||
"concept_id": concept.get("id"),
|
||||
"concept_title": concept.get("title"),
|
||||
"next_concept_id": next_concept.get("id"),
|
||||
"next_concept_title": next_concept.get("title"),
|
||||
"mentor": mentor,
|
||||
"practice": practice,
|
||||
"evaluator": evaluator,
|
||||
"next_step": next_step,
|
||||
"feedback": feedback,
|
||||
}
|
||||
|
|
@ -65,14 +65,3 @@ class MasteryRecord(BaseModel):
|
|||
class LearnerState(BaseModel):
|
||||
learner_id: str
|
||||
records: list[MasteryRecord] = Field(default_factory=list)
|
||||
|
||||
|
||||
class LearnerWorkbenchSessionRequest(BaseModel):
|
||||
pack_id: str
|
||||
concept_id: str
|
||||
learner_goal: str = ""
|
||||
question: str = ""
|
||||
observation: str = ""
|
||||
interpretation: str = ""
|
||||
uncertainty: str = ""
|
||||
revision_trigger: str = ""
|
||||
|
|
|
|||
|
|
@ -18,17 +18,10 @@ def _variant_suffix(role: str, variant: str) -> str:
|
|||
"project_advisor": " Emphasize realistic next steps and avoid grandiose scope.",
|
||||
"evaluator": " Preserve learner trust by naming strengths first, avoiding invented omissions, and framing revisions as specific improvements rather than blanket criticism.",
|
||||
},
|
||||
"scientific_virtues": {
|
||||
"mentor": " Reinforce curiosity, careful observation, and honest separation of observation from interpretation. Preserve uncertainty where the evidence is incomplete and treat revision as progress.",
|
||||
"practice": " Design tasks that ask the learner to compare evidence, separate observation from interpretation, and state what result would change their current view.",
|
||||
"learner": " Keep an earnest learner voice that distinguishes observation from inference, notes uncertainty honestly, and stays willing to revise.",
|
||||
"project_advisor": " Encourage projects that foreground source quality, evidence comparison, and explicit revision rather than polished unsupported synthesis.",
|
||||
"evaluator": " Evaluate scientific habits as well as correctness: distinguish observation from interpretation, reward justified revision, preserve caveats, and challenge weakly supported claims without overstating certainty.",
|
||||
},
|
||||
"concise": {
|
||||
"mentor": " Keep the response compact: no more than four short paragraphs or bullets worth of content.",
|
||||
"practice": " Keep the task compact and direct.",
|
||||
"learner": " Keep reflections concise and concrete.",
|
||||
"learner": " Keep the reflection short and direct.",
|
||||
"project_advisor": " Keep the advice short and concrete.",
|
||||
"evaluator": " Keep the evaluation compact and specific.",
|
||||
},
|
||||
|
|
@ -44,16 +37,14 @@ def mentor_system_prompt() -> str:
|
|||
"You are Didactopus in mentor mode. Help the learner think through the topic without doing the work for them. "
|
||||
"Prefer Socratic questions, prerequisite reminders, and hints over finished solutions. "
|
||||
"When responding to a learner attempt or evaluator note, acknowledge what the learner already did correctly before naming gaps. "
|
||||
"Do not claim a caveat, limitation, or nuance is missing if the learner already stated one; instead say how to sharpen or extend it. "
|
||||
"Separate observation from interpretation, name uncertainty when the evidence is incomplete, and frame revision as a normal part of inquiry."
|
||||
"Do not claim a caveat, limitation, or nuance is missing if the learner already stated one; instead say how to sharpen or extend it."
|
||||
)
|
||||
|
||||
|
||||
def practice_system_prompt() -> str:
|
||||
return (
|
||||
"You are Didactopus in practice-design mode. Generate short, reasoning-heavy tasks that force the learner "
|
||||
"to explain, compare, or derive ideas rather than copy answers. Prefer tasks that ask what was observed, what was inferred, "
|
||||
"what evidence supports the claim, and what result would justify revision."
|
||||
"to explain, compare, or derive ideas rather than copy answers."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -77,8 +68,7 @@ def evaluator_system_prompt() -> str:
|
|||
"Point out weak assumptions and missing justification instead of giving the polished final answer. "
|
||||
"Before saying something is missing, first verify whether the learner already included it. "
|
||||
"If the learner stated a caveat, limitation, or nuance, quote or paraphrase that part and evaluate its quality rather than pretending it is absent. "
|
||||
"Do not invent omissions that are contradicted by the learner's actual text. "
|
||||
"Treat honest revision, explicit uncertainty, and careful separation of observation from interpretation as strengths in scientific reasoning."
|
||||
"Do not invent omissions that are contradicted by the learner's actual text."
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_api_files_exist():
|
||||
assert (ROOT / "src/didactopus/api.py").exists()
|
||||
assert (ROOT / "src/didactopus/storage.py").exists()
|
||||
assert (ROOT / "src/didactopus/learner_workbench.py").exists()
|
||||
assert (ROOT / "data/packs/bayes-pack.json").exists()
|
||||
assert Path("src/didactopus/api.py").exists()
|
||||
assert Path("src/didactopus/storage.py").exists()
|
||||
assert Path("data/packs/bayes-pack.json").exists()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_frontend_scaffold_exists():
|
||||
assert (ROOT / "webui/src/App.jsx").exists()
|
||||
assert (ROOT / "webui/src/api.js").exists()
|
||||
assert Path("webui/src/App.jsx").exists()
|
||||
assert Path("webui/src/api.js").exists()
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
from didactopus import learner_workbench as learner_workbench_module
|
||||
|
||||
|
||||
class _FakeProvider:
|
||||
def __init__(self, _config) -> None:
|
||||
pass
|
||||
|
||||
def generate(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
role: str,
|
||||
system_prompt: str | None = None,
|
||||
temperature: float | None = None,
|
||||
max_tokens: int | None = None,
|
||||
):
|
||||
return type(
|
||||
"Response",
|
||||
(),
|
||||
{
|
||||
"text": f"{role}: {prompt.splitlines()[0]}",
|
||||
"provider": "fake",
|
||||
"model_name": "fake-model",
|
||||
},
|
||||
)()
|
||||
|
||||
|
||||
def test_build_pack_workbench_session_uses_evidence_trail_pack(monkeypatch) -> None:
|
||||
monkeypatch.setattr(learner_workbench_module, "ModelProvider", _FakeProvider)
|
||||
|
||||
payload = learner_workbench_module.build_pack_workbench_session(
|
||||
pack_id="evidence-trail",
|
||||
concept_id="question-framing",
|
||||
learner_goal="Understand how to compare evidence.",
|
||||
question="Which question should guide the study step?",
|
||||
observation="The pack starts with question framing.",
|
||||
interpretation="Question framing organizes the next evidence move.",
|
||||
uncertainty="I do not yet know which source will best test the interpretation.",
|
||||
revision_trigger="A stronger source trail could change the current framing.",
|
||||
)
|
||||
|
||||
assert payload["pack_id"] == "evidence-trail"
|
||||
assert payload["concept_id"] == "question-framing"
|
||||
assert payload["next_concept_id"] == "observation-vs-interpretation"
|
||||
assert payload["mentor"]["provider"] == "fake"
|
||||
assert payload["feedback"]["strengths"]
|
||||
assert payload["feedback"]["gaps"] == []
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def test_ui_files_exist():
|
||||
assert (ROOT / "webui/src/App.jsx").exists()
|
||||
assert (ROOT / "webui/src/storage.js").exists()
|
||||
assert (ROOT / "webui/public/packs/bayes-pack.json").exists()
|
||||
assert (ROOT / "webui/public/packs/evidence-trail-pack.json").exists()
|
||||
assert Path("webui/src/App.jsx").exists()
|
||||
assert Path("webui/src/storage.js").exists()
|
||||
assert Path("webui/public/packs/bayes-pack.json").exists()
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
{
|
||||
"id": "evidence-trail",
|
||||
"title": "Evidence Trail",
|
||||
"subtitle": "Draft evo-edu-aligned pack for guided independent study through question framing, source comparison, bibliography growth, and reflective revision.",
|
||||
"level": "self-learner",
|
||||
"concepts": [
|
||||
{
|
||||
"id": "question-framing",
|
||||
"title": "Question Framing",
|
||||
"prerequisites": [],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Question Framing progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "observation-vs-interpretation",
|
||||
"title": "Observation Versus Interpretation",
|
||||
"prerequisites": [
|
||||
"question-framing"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Observation Versus Interpretation progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "source-comparison",
|
||||
"title": "Source Comparison",
|
||||
"prerequisites": [
|
||||
"observation-vs-interpretation"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Source Comparison progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "bibliography-growth",
|
||||
"title": "Bibliography Growth",
|
||||
"prerequisites": [
|
||||
"source-comparison"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Bibliography Growth progress recorded"
|
||||
},
|
||||
{
|
||||
"id": "revision-under-uncertainty",
|
||||
"title": "Revision Under Uncertainty",
|
||||
"prerequisites": [
|
||||
"bibliography-growth"
|
||||
],
|
||||
"masteryDimension": "mastery",
|
||||
"exerciseReward": "Revision Under Uncertainty progress recorded"
|
||||
}
|
||||
],
|
||||
"onboarding": {
|
||||
"headline": "Start with one question, one source trail, and one explicit revision.",
|
||||
"body": "Begin with a focused question, compare model output and source material carefully, then record what changed in your understanding and why.",
|
||||
"checklist": [
|
||||
"State the question you are trying to answer.",
|
||||
"Compare one model observation with one cited source or species account.",
|
||||
"Write one interpretation and one uncertainty.",
|
||||
"Record what evidence would make you revise your current view."
|
||||
]
|
||||
},
|
||||
"compliance": {
|
||||
"sources": 6,
|
||||
"attributionRequired": true,
|
||||
"shareAlikeRequired": true,
|
||||
"noncommercialOnly": true,
|
||||
"flags": [
|
||||
"derived-from-evo-edu-materials"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +1,10 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
login,
|
||||
listCandidates,
|
||||
createCandidate,
|
||||
createReview,
|
||||
promoteCandidate,
|
||||
runSynthesis,
|
||||
listSynthesisCandidates,
|
||||
promoteSynthesis,
|
||||
createLearnerWorkbenchSession,
|
||||
} from "./api";
|
||||
import {
|
||||
applyEvidence,
|
||||
buildMasteryMap,
|
||||
progressPercent,
|
||||
recommendNext,
|
||||
milestoneMessages,
|
||||
claimReadiness,
|
||||
} from "./engine";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { login, listCandidates, createCandidate, createReview, promoteCandidate, runSynthesis, listSynthesisCandidates, promoteSynthesis } from "./api";
|
||||
|
||||
function LauncherView({ onSelect }) {
|
||||
return (
|
||||
<div className="page">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Didactopus</p>
|
||||
<h1>Choose a work mode.</h1>
|
||||
<p>
|
||||
The current prototype now has two distinct entry points: the existing review workbench
|
||||
and a learner-workbench pilot using the evo-edu `Evidence Trail` pack.
|
||||
</p>
|
||||
</div>
|
||||
<div className="toolbar">
|
||||
<button className="primary" onClick={() => onSelect("learner")}>Open learner pilot</button>
|
||||
<button onClick={() => onSelect("review")}>Open review workbench</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="grid">
|
||||
<section className="card">
|
||||
<h2>Learner workbench pilot</h2>
|
||||
<p>
|
||||
Use a guided-study surface built around question framing, source comparison, and
|
||||
revision under uncertainty.
|
||||
</p>
|
||||
<ul className="plain-list">
|
||||
<li>Loads the `Evidence Trail` pack.</li>
|
||||
<li>Shows concept path and onboarding.</li>
|
||||
<li>Separates observation from interpretation.</li>
|
||||
<li>Treats revision as normal progress.</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="card">
|
||||
<h2>Review workbench</h2>
|
||||
<p>
|
||||
Keep using the existing knowledge-candidate and synthesis workflow for pack-improvement
|
||||
and review operations.
|
||||
</p>
|
||||
<ul className="plain-list">
|
||||
<li>Review learner-derived candidates.</li>
|
||||
<li>Promote pack improvements and skill bundles.</li>
|
||||
<li>Inspect synthesis candidates across packs.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginView({ onAuth, onBack }) {
|
||||
function LoginView({ onAuth }) {
|
||||
const [username, setUsername] = useState("reviewer");
|
||||
const [password, setPassword] = useState("demo-pass");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
async function doLogin() {
|
||||
try {
|
||||
const result = await login(username, password);
|
||||
|
|
@ -81,24 +13,13 @@ function LoginView({ onAuth, onBack }) {
|
|||
setError("Login failed");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page narrow">
|
||||
<section className="card">
|
||||
<p className="eyebrow">Review Workbench</p>
|
||||
<h1>Didactopus review workbench</h1>
|
||||
<label>
|
||||
Username
|
||||
<input value={username} onChange={(e) => setUsername(e.target.value)} />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||
</label>
|
||||
<div className="toolbar">
|
||||
<button className="primary" onClick={doLogin}>Login</button>
|
||||
<button onClick={onBack}>Back</button>
|
||||
</div>
|
||||
<label>Username<input value={username} onChange={(e) => setUsername(e.target.value)} /></label>
|
||||
<label>Password<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /></label>
|
||||
<button className="primary" onClick={doLogin}>Login</button>
|
||||
{error ? <div className="error">{error}</div> : null}
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -109,13 +30,9 @@ function CandidateCard({ candidate, onReview, onPromote }) {
|
|||
return (
|
||||
<div className="card small">
|
||||
<h3>{candidate.title}</h3>
|
||||
<div className="muted">
|
||||
{candidate.candidate_kind} · lane: {candidate.triage_lane} · status: {candidate.current_status}
|
||||
</div>
|
||||
<div className="muted">{candidate.candidate_kind} · lane: {candidate.triage_lane} · status: {candidate.current_status}</div>
|
||||
<p>{candidate.summary}</p>
|
||||
<div className="tiny">
|
||||
confidence {candidate.confidence_hint.toFixed(2)} · novelty {candidate.novelty_score.toFixed(2)} · synthesis {candidate.synthesis_score.toFixed(2)}
|
||||
</div>
|
||||
<div className="tiny">confidence {candidate.confidence_hint.toFixed(2)} · novelty {candidate.novelty_score.toFixed(2)} · synthesis {candidate.synthesis_score.toFixed(2)}</div>
|
||||
<div className="actions">
|
||||
<button onClick={() => onReview(candidate.candidate_id, "accept_pack_improvement")}>Accept as pack improvement</button>
|
||||
<button onClick={() => onPromote(candidate.candidate_id, "curriculum_draft")}>Promote to curriculum draft</button>
|
||||
|
|
@ -132,15 +49,14 @@ function SynthesisCard({ item, onPromote }) {
|
|||
<h3>{item.source_concept_id} ↔ {item.target_concept_id}</h3>
|
||||
<div className="muted">{item.source_pack_id} → {item.target_pack_id}</div>
|
||||
<p>{item.explanation}</p>
|
||||
<div className="tiny">
|
||||
total {item.score_total.toFixed(2)} · semantic {item.score_semantic.toFixed(2)} · structural {item.score_structural.toFixed(2)}
|
||||
</div>
|
||||
<div className="tiny">total {item.score_total.toFixed(2)} · semantic {item.score_semantic.toFixed(2)} · structural {item.score_structural.toFixed(2)}</div>
|
||||
<button onClick={() => onPromote(item.synthesis_id)}>Promote into workflow</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ReviewWorkbench({ auth, onBack }) {
|
||||
export default function App() {
|
||||
const [auth, setAuth] = useState(null);
|
||||
const [candidates, setCandidates] = useState([]);
|
||||
const [synthesis, setSynthesis] = useState([]);
|
||||
const [message, setMessage] = useState("");
|
||||
|
|
@ -152,7 +68,9 @@ function ReviewWorkbench({ auth, onBack }) {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (auth?.access_token) reload(auth.access_token);
|
||||
if (auth?.access_token) {
|
||||
reload(auth.access_token);
|
||||
}
|
||||
}, [auth]);
|
||||
|
||||
async function seedCandidate() {
|
||||
|
|
@ -169,7 +87,7 @@ function ReviewWorkbench({ auth, onBack }) {
|
|||
confidence_hint: 0.73,
|
||||
novelty_score: 0.66,
|
||||
synthesis_score: 0.42,
|
||||
triage_lane: "pack_improvement",
|
||||
triage_lane: "pack_improvement"
|
||||
};
|
||||
await createCandidate(auth.access_token, payload);
|
||||
await reload();
|
||||
|
|
@ -181,7 +99,7 @@ function ReviewWorkbench({ auth, onBack }) {
|
|||
review_kind: "human_review",
|
||||
verdict,
|
||||
rationale: "Accepted in reviewer workbench demo.",
|
||||
requested_changes: "",
|
||||
requested_changes: ""
|
||||
});
|
||||
await reload();
|
||||
setMessage(`Review added to candidate ${candidateId}.`);
|
||||
|
|
@ -191,7 +109,7 @@ function ReviewWorkbench({ auth, onBack }) {
|
|||
await promoteCandidate(auth.access_token, candidateId, {
|
||||
promotion_target: target,
|
||||
target_object_id: "",
|
||||
promotion_status: "approved",
|
||||
promotion_status: "approved"
|
||||
});
|
||||
await reload();
|
||||
setMessage(`Candidate ${candidateId} promoted to ${target}.`);
|
||||
|
|
@ -209,23 +127,20 @@ function ReviewWorkbench({ auth, onBack }) {
|
|||
setMessage(`Synthesis candidate ${synthesisId} promoted into workflow.`);
|
||||
}
|
||||
|
||||
if (!auth) return <LoginView onAuth={setAuth} />;
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Review Workbench</p>
|
||||
<h1>Review workbench + synthesis engine</h1>
|
||||
<p>
|
||||
Triages learner-derived knowledge into pack improvements, curriculum drafts, skill bundles,
|
||||
or archive, while surfacing cross-pack synthesis proposals.
|
||||
</p>
|
||||
<p>Triages learner-derived knowledge into pack improvements, curriculum drafts, skill bundles, or archive, while surfacing cross-pack synthesis proposals.</p>
|
||||
<div className="muted">{message}</div>
|
||||
</div>
|
||||
<div className="toolbar">
|
||||
<button className="primary" onClick={seedCandidate}>Seed candidate</button>
|
||||
<button onClick={seedCandidate}>Seed candidate</button>
|
||||
<button onClick={handleRunSynthesis}>Run synthesis</button>
|
||||
<button onClick={() => reload()}>Refresh</button>
|
||||
<button onClick={onBack}>Back</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -250,356 +165,3 @@ function ReviewWorkbench({ auth, onBack }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LearnerWorkbench({ onBack }) {
|
||||
const [pack, setPack] = useState(null);
|
||||
const [error, setError] = useState("");
|
||||
const [currentConceptId, setCurrentConceptId] = useState("");
|
||||
const [learnerState, setLearnerState] = useState({ records: [], history: [] });
|
||||
const [question, setQuestion] = useState("");
|
||||
const [observation, setObservation] = useState("");
|
||||
const [interpretation, setInterpretation] = useState("");
|
||||
const [uncertainty, setUncertainty] = useState("");
|
||||
const [revisionTrigger, setRevisionTrigger] = useState("");
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
const [sessionOutput, setSessionOutput] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch("/packs/evidence-trail-pack.json")
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Failed to load evidence-trail pack");
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (!active) return;
|
||||
setPack(data);
|
||||
setCurrentConceptId(data.concepts?.[0]?.id || "");
|
||||
})
|
||||
.catch(() => {
|
||||
if (!active) return;
|
||||
setError("Could not load the learner-workbench pack.");
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const currentConcept = useMemo(
|
||||
() => pack?.concepts?.find((concept) => concept.id === currentConceptId) || null,
|
||||
[pack, currentConceptId]
|
||||
);
|
||||
|
||||
const nextConcept = useMemo(() => {
|
||||
if (!pack?.concepts || !currentConcept) return null;
|
||||
const index = pack.concepts.findIndex((concept) => concept.id === currentConcept.id);
|
||||
return index >= 0 ? pack.concepts[index + 1] || null : null;
|
||||
}, [pack, currentConcept]);
|
||||
|
||||
const masteryMap = useMemo(
|
||||
() => (pack ? buildMasteryMap(learnerState, pack) : []),
|
||||
[learnerState, pack]
|
||||
);
|
||||
const progress = useMemo(
|
||||
() => (pack ? progressPercent(learnerState, pack) : 0),
|
||||
[learnerState, pack]
|
||||
);
|
||||
const nextCards = useMemo(
|
||||
() => (pack ? recommendNext(learnerState, pack) : []),
|
||||
[learnerState, pack]
|
||||
);
|
||||
const readiness = useMemo(
|
||||
() => (pack ? claimReadiness(learnerState, pack) : null),
|
||||
[learnerState, pack]
|
||||
);
|
||||
const milestones = useMemo(
|
||||
() => (pack ? milestoneMessages(learnerState, pack) : []),
|
||||
[learnerState, pack]
|
||||
);
|
||||
|
||||
function advanceConcept() {
|
||||
if (nextConcept) setCurrentConceptId(nextConcept.id);
|
||||
}
|
||||
|
||||
async function evaluateCurrentWork() {
|
||||
const score =
|
||||
(question.trim() ? 0.18 : 0) +
|
||||
(observation.trim() ? 0.24 : 0) +
|
||||
(interpretation.trim() ? 0.20 : 0) +
|
||||
(uncertainty.trim() ? 0.18 : 0) +
|
||||
(revisionTrigger.trim() ? 0.20 : 0);
|
||||
const confidenceHint =
|
||||
0.45 +
|
||||
(observation.trim() ? 0.10 : 0) +
|
||||
(interpretation.trim() ? 0.08 : 0) +
|
||||
(uncertainty.trim() ? 0.08 : 0) +
|
||||
(revisionTrigger.trim() ? 0.09 : 0);
|
||||
|
||||
const nextState = applyEvidence(learnerState, {
|
||||
concept_id: currentConcept.id,
|
||||
dimension: currentConcept.masteryDimension || "mastery",
|
||||
score: Math.min(1, score),
|
||||
confidence_hint: Math.min(1, confidenceHint),
|
||||
timestamp: new Date().toISOString(),
|
||||
note: "Evidence Trail learner-workbench submission",
|
||||
});
|
||||
setLearnerState(nextState);
|
||||
|
||||
try {
|
||||
const session = await createLearnerWorkbenchSession({
|
||||
pack_id: pack.id,
|
||||
concept_id: currentConcept.id,
|
||||
learner_goal: question || `Work through ${currentConcept.title} in the Evidence Trail pack.`,
|
||||
question,
|
||||
observation,
|
||||
interpretation,
|
||||
uncertainty,
|
||||
revision_trigger: revisionTrigger,
|
||||
});
|
||||
setFeedback({
|
||||
strengths: session.feedback?.strengths || [],
|
||||
gaps: session.feedback?.gaps || [],
|
||||
nextRevision: session.feedback?.next_revision_target || "Compare one more source or example before moving on.",
|
||||
});
|
||||
setSessionOutput(session);
|
||||
} catch {
|
||||
setFeedback({
|
||||
strengths: [],
|
||||
gaps: ["Backend session generation failed; local progress was still recorded."],
|
||||
nextRevision: "Check the learner-workbench API path and retry.",
|
||||
});
|
||||
setSessionOutput(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="page narrow">
|
||||
<section className="card">
|
||||
<h1>Learner workbench pilot</h1>
|
||||
<div className="error">{error}</div>
|
||||
<button onClick={onBack}>Back</button>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!pack || !currentConcept) {
|
||||
return (
|
||||
<div className="page narrow">
|
||||
<section className="card">
|
||||
<h1>Learner workbench pilot</h1>
|
||||
<p>Loading the `Evidence Trail` pack...</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page learner-page">
|
||||
<header className="hero learner-hero">
|
||||
<div>
|
||||
<p className="eyebrow">Learner Workbench Pilot</p>
|
||||
<h1>{pack.title}</h1>
|
||||
<p>{pack.subtitle}</p>
|
||||
<div className="muted">
|
||||
This pilot uses scientific virtues as operating rules: separate observation from interpretation,
|
||||
preserve uncertainty, and treat revision as progress.
|
||||
</div>
|
||||
</div>
|
||||
<div className="toolbar">
|
||||
<button className="primary" onClick={advanceConcept} disabled={!nextConcept}>
|
||||
{nextConcept ? `Next: ${nextConcept.title}` : "At final concept"}
|
||||
</button>
|
||||
<button onClick={onBack}>Back</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="learner-grid">
|
||||
<section className="card">
|
||||
<p className="eyebrow">Onboarding</p>
|
||||
<h2>{pack.onboarding.headline}</h2>
|
||||
<p>{pack.onboarding.body}</p>
|
||||
<div className="progress-strip">
|
||||
<strong>Progress:</strong> {progress}% · {readiness?.mastered ?? 0}/{pack.concepts.length} concepts strongly supported
|
||||
</div>
|
||||
<ul className="plain-list">
|
||||
{pack.onboarding.checklist.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<p className="eyebrow">Concept Path</p>
|
||||
<div className="concept-path">
|
||||
{pack.concepts.map((concept, index) => (
|
||||
<button
|
||||
key={concept.id}
|
||||
className={`concept-chip${concept.id === currentConceptId ? " active" : ""}`}
|
||||
onClick={() => setCurrentConceptId(concept.id)}
|
||||
>
|
||||
{concept.title}
|
||||
<span className="chip-status">
|
||||
{masteryMap[index]?.status || "locked"}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="concept-focus">
|
||||
<h3>{currentConcept.title}</h3>
|
||||
<div className="tiny">
|
||||
Prerequisites: {currentConcept.prerequisites.length ? currentConcept.prerequisites.join(", ") : "none explicit"}
|
||||
</div>
|
||||
<p className="muted">{currentConcept.exerciseReward}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card learner-form">
|
||||
<p className="eyebrow">Current Study Record</p>
|
||||
<h2>Keep observation and interpretation distinct.</h2>
|
||||
<label>
|
||||
Question
|
||||
<textarea value={question} onChange={(e) => setQuestion(e.target.value)} placeholder="What are you trying to understand or test?" />
|
||||
</label>
|
||||
<label>
|
||||
Observation
|
||||
<textarea value={observation} onChange={(e) => setObservation(e.target.value)} placeholder="What did the model, species record, or source actually show?" />
|
||||
</label>
|
||||
<label>
|
||||
Interpretation
|
||||
<textarea value={interpretation} onChange={(e) => setInterpretation(e.target.value)} placeholder="What explanation currently fits best?" />
|
||||
</label>
|
||||
<label>
|
||||
Uncertainty
|
||||
<textarea value={uncertainty} onChange={(e) => setUncertainty(e.target.value)} placeholder="What remains unclear, weakly supported, or unresolved?" />
|
||||
</label>
|
||||
<label>
|
||||
Revision trigger
|
||||
<textarea value={revisionTrigger} onChange={(e) => setRevisionTrigger(e.target.value)} placeholder="What evidence or comparison would make you revise your current view?" />
|
||||
</label>
|
||||
<div className="toolbar">
|
||||
<button className="primary" onClick={evaluateCurrentWork}>Evaluate this step</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<p className="eyebrow">Virtues In Use</p>
|
||||
<div className="virtue-grid">
|
||||
<div className="virtue-card">
|
||||
<strong>Curiosity</strong>
|
||||
<p>Keep the question open long enough to learn from the evidence.</p>
|
||||
</div>
|
||||
<div className="virtue-card">
|
||||
<strong>Honesty</strong>
|
||||
<p>Write down what you actually observed before polishing an explanation.</p>
|
||||
</div>
|
||||
<div className="virtue-card">
|
||||
<strong>Skepticism</strong>
|
||||
<p>Ask whether the current claim is well supported or only convenient.</p>
|
||||
</div>
|
||||
<div className="virtue-card">
|
||||
<strong>Revision</strong>
|
||||
<p>Treat changed conclusions as progress, not as failure.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<p className="eyebrow">Evaluator Feedback</p>
|
||||
<h2>Trust-preserving critique</h2>
|
||||
{feedback ? (
|
||||
<>
|
||||
<div className="feedback-block">
|
||||
<strong>Strengths</strong>
|
||||
<ul className="plain-list">
|
||||
{feedback.strengths.map((item) => <li key={item}>{item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="feedback-block">
|
||||
<strong>Revision targets</strong>
|
||||
<ul className="plain-list">
|
||||
{feedback.gaps.length ? feedback.gaps.map((item) => <li key={item}>{item}</li>) : <li>No immediate gap detected; extend with another source comparison.</li>}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="compliance-note">
|
||||
<strong>Next revision target:</strong> {feedback.nextRevision}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="muted">Submit a study record to get evaluator-style feedback grounded in the current concept and scientific-virtues framing.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<p className="eyebrow">Backend Session Output</p>
|
||||
<h2>Grounded mentor/practice/evaluator loop</h2>
|
||||
{sessionOutput ? (
|
||||
<div className="resource-list">
|
||||
<div className="next-card">
|
||||
<strong>Mentor</strong>
|
||||
<p>{sessionOutput.mentor?.text}</p>
|
||||
</div>
|
||||
<div className="next-card">
|
||||
<strong>Practice</strong>
|
||||
<p>{sessionOutput.practice?.text}</p>
|
||||
</div>
|
||||
<div className="next-card">
|
||||
<strong>Evaluator</strong>
|
||||
<p>{sessionOutput.evaluator?.text}</p>
|
||||
</div>
|
||||
<div className="next-card">
|
||||
<strong>Next step</strong>
|
||||
<p>{sessionOutput.next_step?.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="muted">Run an evaluation to request grounded mentor, practice, evaluator, and next-step text from the backend session endpoint.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="card">
|
||||
<p className="eyebrow">Next Study Action</p>
|
||||
<h2>{nextConcept ? nextConcept.title : "Complete the current concept carefully"}</h2>
|
||||
<p>
|
||||
{nextConcept
|
||||
? `Move forward when you can justify your interpretation and name what evidence would change it. The next concept extends this work into ${nextConcept.title.toLowerCase()}.`
|
||||
: "You are at the end of the current concept path. Review your uncertainty and revision trigger before treating the topic as settled."}
|
||||
</p>
|
||||
<div className="resource-list">
|
||||
{nextCards.map((card) => (
|
||||
<div className="next-card" key={card.id}>
|
||||
<strong>{card.title}</strong>
|
||||
<p>{card.reason}</p>
|
||||
<div className="tiny">{card.why.join(" · ")}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="feedback-block">
|
||||
<strong>Milestones</strong>
|
||||
<ul className="plain-list">
|
||||
{milestones.map((item) => <li key={item}>{item}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="compliance-note">
|
||||
<strong>Source posture:</strong> {pack.compliance.sources} linked sources; attribution required: {pack.compliance.attributionRequired ? "yes" : "no"}.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [mode, setMode] = useState("launcher");
|
||||
const [auth, setAuth] = useState(null);
|
||||
|
||||
if (mode === "learner") return <LearnerWorkbench onBack={() => setMode("launcher")} />;
|
||||
if (mode === "review" && !auth) {
|
||||
return <LoginView onAuth={setAuth} onBack={() => setMode("launcher")} />;
|
||||
}
|
||||
if (mode === "review" && auth) {
|
||||
return <ReviewWorkbench auth={auth} onBack={() => { setAuth(null); setMode("launcher"); }} />;
|
||||
}
|
||||
return <LauncherView onSelect={setMode} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,13 +53,3 @@ export async function promoteSynthesis(token, synthesisId, payload) {
|
|||
if (!res.ok) throw new Error("promoteSynthesis failed");
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function createLearnerWorkbenchSession(payload) {
|
||||
const res = await fetch(`${API}/learner-workbench/session`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error("createLearnerWorkbenchSession failed");
|
||||
return await res.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,8 @@ body { margin:0; background:var(--bg); color:var(--text); font-family:Arial, Hel
|
|||
.grid { display:grid; grid-template-columns:1fr 1fr; gap:18px; }
|
||||
.stack { display:grid; gap:14px; }
|
||||
.card.small h3 { margin-top:0; }
|
||||
.eyebrow { margin:0 0 8px; font-size:12px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--accent); }
|
||||
label { display:block; font-weight:600; margin-bottom:10px; }
|
||||
input, textarea { width:100%; margin-top:6px; border:1px solid var(--border); border-radius:10px; padding:10px; font:inherit; background:white; }
|
||||
textarea { min-height:108px; resize:vertical; }
|
||||
input { width:100%; margin-top:6px; border:1px solid var(--border); border-radius:10px; padding:10px; font:inherit; background:white; }
|
||||
button { border:1px solid var(--border); background:white; border-radius:12px; padding:10px 12px; cursor:pointer; margin-right:8px; margin-top:8px; }
|
||||
button.primary { background:var(--accent); color:white; border-color:var(--accent); }
|
||||
.actions { display:flex; flex-wrap:wrap; gap:8px; }
|
||||
|
|
@ -21,26 +19,7 @@ button.primary { background:var(--accent); color:white; border-color:var(--accen
|
|||
.muted { color:var(--muted); }
|
||||
.tiny { font-size:12px; color:var(--muted); }
|
||||
.error { color:#b42318; margin-top:10px; }
|
||||
.plain-list { margin:0; padding-left:18px; }
|
||||
.learner-page { max-width:1600px; }
|
||||
.learner-grid { display:grid; grid-template-columns:1.1fr .9fr; gap:18px; }
|
||||
.learner-hero { background:linear-gradient(135deg, #eef4ff, #ffffff); }
|
||||
.concept-path { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:14px; }
|
||||
.concept-chip { margin:0; border-radius:16px; display:flex; flex-direction:column; gap:4px; align-items:flex-start; }
|
||||
.concept-chip.active { background:var(--accent); color:white; border-color:var(--accent); }
|
||||
.chip-status { font-size:11px; text-transform:uppercase; letter-spacing:.05em; opacity:.8; }
|
||||
.concept-focus { border-top:1px solid var(--border); padding-top:12px; }
|
||||
.learner-form { grid-row:span 2; }
|
||||
.virtue-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||||
.virtue-card { border:1px solid var(--border); border-radius:14px; padding:14px; background:#fbfcfe; }
|
||||
.compliance-note { margin-top:10px; padding:10px 12px; border-radius:12px; background:#f3f6fb; color:var(--muted); }
|
||||
.progress-strip { margin:10px 0 14px; padding:10px 12px; border-radius:12px; background:#eef4ff; color:#214d9b; }
|
||||
.feedback-block { margin-top:12px; padding:12px 14px; border:1px solid var(--border); border-radius:14px; background:#fbfcfe; }
|
||||
.resource-list { display:grid; gap:10px; margin-top:12px; }
|
||||
.next-card { border:1px solid var(--border); border-radius:14px; padding:12px 14px; background:#fbfcfe; }
|
||||
@media (max-width: 1100px) {
|
||||
.grid { grid-template-columns:1fr; }
|
||||
.hero { flex-direction:column; }
|
||||
.learner-grid { grid-template-columns:1fr; }
|
||||
.virtue-grid { grid-template-columns:1fr; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue