Compare commits
2 Commits
3837bd2316
...
59d45c2942
| Author | SHA1 | Date |
|---|---|---|
|
|
59d45c2942 | |
|
|
25aae8ef54 |
25
README.md
25
README.md
|
|
@ -140,6 +140,31 @@ For the fastest included example, use the MIT OCW Information and Entropy demo.
|
||||||
- progress visualization
|
- progress visualization
|
||||||
- skill export
|
- 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
|
## `doclift` Bundle Ingestion
|
||||||
|
|
||||||
When your source material starts as legacy office documents, the intended
|
When your source material starts as legacy office documents, the intended
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# 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
|
### 5. Learner workbench UI
|
||||||
|
|
||||||
Status: planned
|
Status: pilot in progress
|
||||||
|
|
||||||
Why important:
|
Why important:
|
||||||
|
|
||||||
|
|
@ -103,6 +103,23 @@ Target features:
|
||||||
- evaluator feedback
|
- evaluator feedback
|
||||||
- recommended next step
|
- 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
|
### 6. Adaptive diagnostics and practice refinement
|
||||||
|
|
||||||
Status: planned
|
Status: planned
|
||||||
|
|
@ -195,8 +212,9 @@ Examples:
|
||||||
## Suggested Implementation Sequence
|
## Suggested Implementation Sequence
|
||||||
|
|
||||||
1. Strengthen `didactopus.learner_session` into the standard session backend.
|
1. Strengthen `didactopus.learner_session` into the standard session backend.
|
||||||
2. Build a small model-benchmark harness around that backend.
|
2. Fold the learner-workbench pilot into that backend without losing its stronger study-state framing.
|
||||||
3. Add accessible learner HTML and text-first outputs.
|
3. Build a small model-benchmark harness around the unified learner backend.
|
||||||
4. Add local TTS and STT support to the same session flow.
|
4. Add accessible learner HTML and text-first outputs.
|
||||||
5. Expand adaptive practice and diagnostics.
|
5. Add local TTS and STT support to the same session flow.
|
||||||
6. Improve review, impact analysis, and incremental update support.
|
6. Expand adaptive practice and diagnostics.
|
||||||
|
7. Improve review, impact analysis, and incremental update support.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
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: {}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"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 (
|
from .models import (
|
||||||
LoginRequest, TokenPair, KnowledgeCandidateCreate, KnowledgeCandidateUpdate,
|
LoginRequest, TokenPair, KnowledgeCandidateCreate, KnowledgeCandidateUpdate,
|
||||||
ReviewCreate, PromoteRequest, SynthesisRunRequest, SynthesisPromoteRequest,
|
ReviewCreate, PromoteRequest, SynthesisRunRequest, SynthesisPromoteRequest,
|
||||||
CreateLearnerRequest
|
CreateLearnerRequest, LearnerWorkbenchSessionRequest
|
||||||
)
|
)
|
||||||
from .repository import (
|
from .repository import (
|
||||||
authenticate_user, get_user_by_id, create_learner, learner_owned_by_user,
|
authenticate_user, get_user_by_id, create_learner, learner_owned_by_user,
|
||||||
|
|
@ -16,6 +16,7 @@ from .repository import (
|
||||||
)
|
)
|
||||||
from .auth import issue_access_token, issue_refresh_token, decode_token, new_token_id
|
from .auth import issue_access_token, issue_refresh_token, decode_token, new_token_id
|
||||||
from .synthesis import generate_synthesis_candidates
|
from .synthesis import generate_synthesis_candidates
|
||||||
|
from .learner_workbench import build_pack_workbench_session
|
||||||
|
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
@ -58,6 +59,25 @@ def api_create_learner(payload: CreateLearnerRequest, user = Depends(current_use
|
||||||
create_learner(user.id, payload.learner_id, payload.display_name)
|
create_learner(user.id, payload.learner_id, payload.display_name)
|
||||||
return {"ok": True, "learner_id": payload.learner_id}
|
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")
|
@app.post("/api/knowledge-candidates")
|
||||||
def api_create_candidate(payload: KnowledgeCandidateCreate, reviewer = Depends(require_reviewer)):
|
def api_create_candidate(payload: KnowledgeCandidateCreate, reviewer = Depends(require_reviewer)):
|
||||||
candidate_id = create_candidate(payload)
|
candidate_id = create_candidate(payload)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,16 @@ def _grounding_block(step: dict) -> str:
|
||||||
return "\n".join(lines)
|
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(
|
def _generate_role_text(
|
||||||
provider: ModelProvider,
|
provider: ModelProvider,
|
||||||
*,
|
*,
|
||||||
|
|
@ -71,9 +81,10 @@ def build_graph_grounded_session(
|
||||||
mentor_prompt = (
|
mentor_prompt = (
|
||||||
f"{_grounding_block(primary)}\n\n"
|
f"{_grounding_block(primary)}\n\n"
|
||||||
f"{_grounding_block(secondary)}\n\n"
|
f"{_grounding_block(secondary)}\n\n"
|
||||||
|
f"{_scientific_virtues_block()}\n\n"
|
||||||
f"Learner goal: {learner_goal}\n"
|
f"Learner goal: {learner_goal}\n"
|
||||||
"Respond as Didactopus mentor. Give a short grounded orientation, explain why these concepts come first, "
|
"Respond as Didactopus mentor. Give a short grounded orientation, explain why these concepts come first, "
|
||||||
"and ask one focused question that keeps the learner doing the reasoning."
|
"ask one focused question that keeps the learner doing the reasoning, and explicitly separate what should be observed from what should be interpreted."
|
||||||
)
|
)
|
||||||
mentor_text = _generate_role_text(
|
mentor_text = _generate_role_text(
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -87,8 +98,10 @@ def build_graph_grounded_session(
|
||||||
|
|
||||||
practice_prompt = (
|
practice_prompt = (
|
||||||
f"{_grounding_block(primary)}\n\n"
|
f"{_grounding_block(primary)}\n\n"
|
||||||
|
f"{_scientific_virtues_block()}\n\n"
|
||||||
f"Learner goal: {learner_goal}\n"
|
f"Learner goal: {learner_goal}\n"
|
||||||
"Create one reasoning-heavy practice task for the learner. Keep it grounded in the supporting lessons and do not provide the full solution."
|
"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."
|
||||||
)
|
)
|
||||||
practice_text = _generate_role_text(
|
practice_text = _generate_role_text(
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -103,10 +116,12 @@ def build_graph_grounded_session(
|
||||||
evaluation = evaluate_submission_with_skill(context, primary["concept_key"].split("::", 1)[-1], learner_submission)
|
evaluation = evaluate_submission_with_skill(context, primary["concept_key"].split("::", 1)[-1], learner_submission)
|
||||||
evaluator_prompt = (
|
evaluator_prompt = (
|
||||||
f"{_grounding_block(primary)}\n\n"
|
f"{_grounding_block(primary)}\n\n"
|
||||||
|
f"{_scientific_virtues_block()}\n\n"
|
||||||
f"Practice task: {practice_text}\n"
|
f"Practice task: {practice_text}\n"
|
||||||
f"Learner submission: {learner_submission}\n"
|
f"Learner submission: {learner_submission}\n"
|
||||||
f"Deterministic evaluator result: verdict={evaluation['verdict']}, aggregated={evaluation['aggregated']}\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."
|
"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."
|
||||||
)
|
)
|
||||||
evaluator_text = _generate_role_text(
|
evaluator_text = _generate_role_text(
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -121,8 +136,10 @@ def build_graph_grounded_session(
|
||||||
next_step_prompt = (
|
next_step_prompt = (
|
||||||
f"{_grounding_block(primary)}\n\n"
|
f"{_grounding_block(primary)}\n\n"
|
||||||
f"{_grounding_block(secondary)}\n\n"
|
f"{_grounding_block(secondary)}\n\n"
|
||||||
|
f"{_scientific_virtues_block()}\n\n"
|
||||||
f"Evaluator feedback: {evaluator_text}\n"
|
f"Evaluator feedback: {evaluator_text}\n"
|
||||||
"Respond as Didactopus mentor. Give the next study action and explain why it follows from the grounded concept path."
|
"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."
|
||||||
)
|
)
|
||||||
next_step_text = _generate_role_text(
|
next_step_text = _generate_role_text(
|
||||||
provider,
|
provider,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
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,3 +65,14 @@ class MasteryRecord(BaseModel):
|
||||||
class LearnerState(BaseModel):
|
class LearnerState(BaseModel):
|
||||||
learner_id: str
|
learner_id: str
|
||||||
records: list[MasteryRecord] = Field(default_factory=list)
|
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,10 +18,17 @@ def _variant_suffix(role: str, variant: str) -> str:
|
||||||
"project_advisor": " Emphasize realistic next steps and avoid grandiose scope.",
|
"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.",
|
"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": {
|
"concise": {
|
||||||
"mentor": " Keep the response compact: no more than four short paragraphs or bullets worth of content.",
|
"mentor": " Keep the response compact: no more than four short paragraphs or bullets worth of content.",
|
||||||
"practice": " Keep the task compact and direct.",
|
"practice": " Keep the task compact and direct.",
|
||||||
"learner": " Keep the reflection short and direct.",
|
"learner": " Keep reflections concise and concrete.",
|
||||||
"project_advisor": " Keep the advice short and concrete.",
|
"project_advisor": " Keep the advice short and concrete.",
|
||||||
"evaluator": " Keep the evaluation compact and specific.",
|
"evaluator": " Keep the evaluation compact and specific.",
|
||||||
},
|
},
|
||||||
|
|
@ -37,14 +44,16 @@ def mentor_system_prompt() -> str:
|
||||||
"You are Didactopus in mentor mode. Help the learner think through the topic without doing the work for them. "
|
"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. "
|
"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. "
|
"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."
|
"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."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def practice_system_prompt() -> str:
|
def practice_system_prompt() -> str:
|
||||||
return (
|
return (
|
||||||
"You are Didactopus in practice-design mode. Generate short, reasoning-heavy tasks that force the learner "
|
"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."
|
"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."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,7 +77,8 @@ def evaluator_system_prompt() -> str:
|
||||||
"Point out weak assumptions and missing justification instead of giving the polished final answer. "
|
"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. "
|
"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. "
|
"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."
|
"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."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def test_api_files_exist():
|
def test_api_files_exist():
|
||||||
assert Path("src/didactopus/api.py").exists()
|
assert (ROOT / "src/didactopus/api.py").exists()
|
||||||
assert Path("src/didactopus/storage.py").exists()
|
assert (ROOT / "src/didactopus/storage.py").exists()
|
||||||
assert Path("data/packs/bayes-pack.json").exists()
|
assert (ROOT / "src/didactopus/learner_workbench.py").exists()
|
||||||
|
assert (ROOT / "data/packs/bayes-pack.json").exists()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def test_frontend_scaffold_exists():
|
def test_frontend_scaffold_exists():
|
||||||
assert Path("webui/src/App.jsx").exists()
|
assert (ROOT / "webui/src/App.jsx").exists()
|
||||||
assert Path("webui/src/api.js").exists()
|
assert (ROOT / "webui/src/api.js").exists()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
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,6 +1,11 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def test_ui_files_exist():
|
def test_ui_files_exist():
|
||||||
assert Path("webui/src/App.jsx").exists()
|
assert (ROOT / "webui/src/App.jsx").exists()
|
||||||
assert Path("webui/src/storage.js").exists()
|
assert (ROOT / "webui/src/storage.js").exists()
|
||||||
assert Path("webui/public/packs/bayes-pack.json").exists()
|
assert (ROOT / "webui/public/packs/bayes-pack.json").exists()
|
||||||
|
assert (ROOT / "webui/public/packs/evidence-trail-pack.json").exists()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"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,10 +1,78 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { login, listCandidates, createCandidate, createReview, promoteCandidate, runSynthesis, listSynthesisCandidates, promoteSynthesis } from "./api";
|
import {
|
||||||
|
login,
|
||||||
|
listCandidates,
|
||||||
|
createCandidate,
|
||||||
|
createReview,
|
||||||
|
promoteCandidate,
|
||||||
|
runSynthesis,
|
||||||
|
listSynthesisCandidates,
|
||||||
|
promoteSynthesis,
|
||||||
|
createLearnerWorkbenchSession,
|
||||||
|
} from "./api";
|
||||||
|
import {
|
||||||
|
applyEvidence,
|
||||||
|
buildMasteryMap,
|
||||||
|
progressPercent,
|
||||||
|
recommendNext,
|
||||||
|
milestoneMessages,
|
||||||
|
claimReadiness,
|
||||||
|
} from "./engine";
|
||||||
|
|
||||||
function LoginView({ onAuth }) {
|
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 }) {
|
||||||
const [username, setUsername] = useState("reviewer");
|
const [username, setUsername] = useState("reviewer");
|
||||||
const [password, setPassword] = useState("demo-pass");
|
const [password, setPassword] = useState("demo-pass");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
async function doLogin() {
|
async function doLogin() {
|
||||||
try {
|
try {
|
||||||
const result = await login(username, password);
|
const result = await login(username, password);
|
||||||
|
|
@ -13,13 +81,24 @@ function LoginView({ onAuth }) {
|
||||||
setError("Login failed");
|
setError("Login failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page narrow">
|
<div className="page narrow">
|
||||||
<section className="card">
|
<section className="card">
|
||||||
|
<p className="eyebrow">Review Workbench</p>
|
||||||
<h1>Didactopus review workbench</h1>
|
<h1>Didactopus review workbench</h1>
|
||||||
<label>Username<input value={username} onChange={(e) => setUsername(e.target.value)} /></label>
|
<label>
|
||||||
<label>Password<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} /></label>
|
Username
|
||||||
<button className="primary" onClick={doLogin}>Login</button>
|
<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>
|
||||||
{error ? <div className="error">{error}</div> : null}
|
{error ? <div className="error">{error}</div> : null}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,9 +109,13 @@ function CandidateCard({ candidate, onReview, onPromote }) {
|
||||||
return (
|
return (
|
||||||
<div className="card small">
|
<div className="card small">
|
||||||
<h3>{candidate.title}</h3>
|
<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>
|
<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">
|
<div className="actions">
|
||||||
<button onClick={() => onReview(candidate.candidate_id, "accept_pack_improvement")}>Accept as pack improvement</button>
|
<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>
|
<button onClick={() => onPromote(candidate.candidate_id, "curriculum_draft")}>Promote to curriculum draft</button>
|
||||||
|
|
@ -49,14 +132,15 @@ function SynthesisCard({ item, onPromote }) {
|
||||||
<h3>{item.source_concept_id} ↔ {item.target_concept_id}</h3>
|
<h3>{item.source_concept_id} ↔ {item.target_concept_id}</h3>
|
||||||
<div className="muted">{item.source_pack_id} → {item.target_pack_id}</div>
|
<div className="muted">{item.source_pack_id} → {item.target_pack_id}</div>
|
||||||
<p>{item.explanation}</p>
|
<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>
|
<button onClick={() => onPromote(item.synthesis_id)}>Promote into workflow</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
function ReviewWorkbench({ auth, onBack }) {
|
||||||
const [auth, setAuth] = useState(null);
|
|
||||||
const [candidates, setCandidates] = useState([]);
|
const [candidates, setCandidates] = useState([]);
|
||||||
const [synthesis, setSynthesis] = useState([]);
|
const [synthesis, setSynthesis] = useState([]);
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
|
|
@ -68,9 +152,7 @@ export default function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (auth?.access_token) {
|
if (auth?.access_token) reload(auth.access_token);
|
||||||
reload(auth.access_token);
|
|
||||||
}
|
|
||||||
}, [auth]);
|
}, [auth]);
|
||||||
|
|
||||||
async function seedCandidate() {
|
async function seedCandidate() {
|
||||||
|
|
@ -87,7 +169,7 @@ export default function App() {
|
||||||
confidence_hint: 0.73,
|
confidence_hint: 0.73,
|
||||||
novelty_score: 0.66,
|
novelty_score: 0.66,
|
||||||
synthesis_score: 0.42,
|
synthesis_score: 0.42,
|
||||||
triage_lane: "pack_improvement"
|
triage_lane: "pack_improvement",
|
||||||
};
|
};
|
||||||
await createCandidate(auth.access_token, payload);
|
await createCandidate(auth.access_token, payload);
|
||||||
await reload();
|
await reload();
|
||||||
|
|
@ -99,7 +181,7 @@ export default function App() {
|
||||||
review_kind: "human_review",
|
review_kind: "human_review",
|
||||||
verdict,
|
verdict,
|
||||||
rationale: "Accepted in reviewer workbench demo.",
|
rationale: "Accepted in reviewer workbench demo.",
|
||||||
requested_changes: ""
|
requested_changes: "",
|
||||||
});
|
});
|
||||||
await reload();
|
await reload();
|
||||||
setMessage(`Review added to candidate ${candidateId}.`);
|
setMessage(`Review added to candidate ${candidateId}.`);
|
||||||
|
|
@ -109,7 +191,7 @@ export default function App() {
|
||||||
await promoteCandidate(auth.access_token, candidateId, {
|
await promoteCandidate(auth.access_token, candidateId, {
|
||||||
promotion_target: target,
|
promotion_target: target,
|
||||||
target_object_id: "",
|
target_object_id: "",
|
||||||
promotion_status: "approved"
|
promotion_status: "approved",
|
||||||
});
|
});
|
||||||
await reload();
|
await reload();
|
||||||
setMessage(`Candidate ${candidateId} promoted to ${target}.`);
|
setMessage(`Candidate ${candidateId} promoted to ${target}.`);
|
||||||
|
|
@ -127,20 +209,23 @@ export default function App() {
|
||||||
setMessage(`Synthesis candidate ${synthesisId} promoted into workflow.`);
|
setMessage(`Synthesis candidate ${synthesisId} promoted into workflow.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!auth) return <LoginView onAuth={setAuth} />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<header className="hero">
|
<header className="hero">
|
||||||
<div>
|
<div>
|
||||||
|
<p className="eyebrow">Review Workbench</p>
|
||||||
<h1>Review workbench + synthesis engine</h1>
|
<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 className="muted">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<button onClick={seedCandidate}>Seed candidate</button>
|
<button className="primary" onClick={seedCandidate}>Seed candidate</button>
|
||||||
<button onClick={handleRunSynthesis}>Run synthesis</button>
|
<button onClick={handleRunSynthesis}>Run synthesis</button>
|
||||||
<button onClick={() => reload()}>Refresh</button>
|
<button onClick={() => reload()}>Refresh</button>
|
||||||
|
<button onClick={onBack}>Back</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -165,3 +250,356 @@ export default function App() {
|
||||||
</div>
|
</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,3 +53,13 @@ export async function promoteSynthesis(token, synthesisId, payload) {
|
||||||
if (!res.ok) throw new Error("promoteSynthesis failed");
|
if (!res.ok) throw new Error("promoteSynthesis failed");
|
||||||
return await res.json();
|
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,8 +10,10 @@ body { margin:0; background:var(--bg); color:var(--text); font-family:Arial, Hel
|
||||||
.grid { display:grid; grid-template-columns:1fr 1fr; gap:18px; }
|
.grid { display:grid; grid-template-columns:1fr 1fr; gap:18px; }
|
||||||
.stack { display:grid; gap:14px; }
|
.stack { display:grid; gap:14px; }
|
||||||
.card.small h3 { margin-top:0; }
|
.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; }
|
label { display:block; font-weight:600; margin-bottom:10px; }
|
||||||
input { width:100%; margin-top:6px; border:1px solid var(--border); border-radius:10px; padding:10px; font:inherit; background:white; }
|
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; }
|
||||||
button { border:1px solid var(--border); background:white; border-radius:12px; padding:10px 12px; cursor:pointer; margin-right:8px; margin-top:8px; }
|
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); }
|
button.primary { background:var(--accent); color:white; border-color:var(--accent); }
|
||||||
.actions { display:flex; flex-wrap:wrap; gap:8px; }
|
.actions { display:flex; flex-wrap:wrap; gap:8px; }
|
||||||
|
|
@ -19,7 +21,26 @@ button.primary { background:var(--accent); color:white; border-color:var(--accen
|
||||||
.muted { color:var(--muted); }
|
.muted { color:var(--muted); }
|
||||||
.tiny { font-size:12px; color:var(--muted); }
|
.tiny { font-size:12px; color:var(--muted); }
|
||||||
.error { color:#b42318; margin-top:10px; }
|
.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) {
|
@media (max-width: 1100px) {
|
||||||
.grid { grid-template-columns:1fr; }
|
.grid { grid-template-columns:1fr; }
|
||||||
.hero { flex-direction:column; }
|
.hero { flex-direction:column; }
|
||||||
|
.learner-grid { grid-template-columns:1fr; }
|
||||||
|
.virtue-grid { grid-template-columns:1fr; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue