Implement Roadmap Review Features and Enhance Progression Path Management
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added support for editable major steps in the roadmap, allowing users to modify phase, learning goals, and order before exercise matching. - Introduced a new `roadmap_override` feature to facilitate customized retrieval without re-invoking the roadmap AI. - Updated the `ExerciseProgressionPathBuilder` component to incorporate these new features, enhancing user interaction and flexibility. - Incremented application version to 0.8.207 to reflect these changes.
This commit is contained in:
parent
0677663268
commit
f074a8bef0
|
|
@ -188,7 +188,7 @@ Jede Phase: `(ctx) → ctx`, Zwischenergebnisse in API-Response für **Human-in-
|
||||||
| **F1** | `include_roadmap_preview` in API + deterministische A/B | 🔄 0.8.204 |
|
| **F1** | `include_roadmap_preview` in API + deterministische A/B | 🔄 0.8.204 |
|
||||||
| **F2** | LLM Phase A/B/C über `ai_prompts` (078/079), `include_llm_roadmap` | 🔄 0.8.205 |
|
| **F2** | LLM Phase A/B/C über `ai_prompts` (078/079), `include_llm_roadmap` | 🔄 0.8.205 |
|
||||||
| **F3** | Retrieval aus `stage_specs` (roadmap_first) | ✅ 0.8.206 |
|
| **F3** | Retrieval aus `stage_specs` (roadmap_first) | ✅ 0.8.206 |
|
||||||
| **F4** | UI Roadmap-Review | 🔲 |
|
| **F4** | UI Roadmap-Review | ✅ 0.8.207 |
|
||||||
| **F5** | Trainingsplanung: eigene Pipeline + ggf. Workflow-Engine | 🔲 |
|
| **F5** | Trainingsplanung: eigene Pipeline + ggf. Workflow-Engine | 🔲 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,12 @@ from planning_exercise_suggest import (
|
||||||
from planning_progression_roadmap import (
|
from planning_progression_roadmap import (
|
||||||
MajorStep,
|
MajorStep,
|
||||||
ProgressionRoadmapContext,
|
ProgressionRoadmapContext,
|
||||||
|
RoadmapOverridePayload,
|
||||||
StageSpecArtifact,
|
StageSpecArtifact,
|
||||||
build_roadmap_unfilled_gap_specs,
|
build_roadmap_unfilled_gap_specs,
|
||||||
progression_roadmap_to_api_dict,
|
progression_roadmap_to_api_dict,
|
||||||
resolve_step_exercise_kind_filter,
|
resolve_step_exercise_kind_filter,
|
||||||
|
roadmap_context_from_override,
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
stage_spec_retrieval_query,
|
stage_spec_retrieval_query,
|
||||||
)
|
)
|
||||||
|
|
@ -74,6 +76,8 @@ class ProgressionPathSuggestRequest(BaseModel):
|
||||||
include_roadmap_preview: bool = False
|
include_roadmap_preview: bool = False
|
||||||
include_llm_roadmap: bool = True
|
include_llm_roadmap: bool = True
|
||||||
roadmap_first: bool = False
|
roadmap_first: bool = False
|
||||||
|
roadmap_only: bool = False
|
||||||
|
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||||
progression_graph_id: Optional[int] = Field(default=None, ge=1)
|
progression_graph_id: Optional[int] = Field(default=None, ge=1)
|
||||||
exercise_kind_any: Optional[List[str]] = None
|
exercise_kind_any: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
@ -492,21 +496,29 @@ def suggest_progression_path(
|
||||||
cur, goal_query, semantic_brief
|
cur, goal_query, semantic_brief
|
||||||
)
|
)
|
||||||
|
|
||||||
path_target_profile, first_intent_summary, path_intent = _build_path_target_profile(
|
|
||||||
cur,
|
|
||||||
goal_query=goal_query,
|
|
||||||
semantic_brief=semantic_brief,
|
|
||||||
include_llm_intent=body.include_llm_intent,
|
|
||||||
)
|
|
||||||
|
|
||||||
roadmap_first = bool(body.roadmap_first)
|
roadmap_first = bool(body.roadmap_first)
|
||||||
include_roadmap = roadmap_first or body.include_roadmap_preview
|
roadmap_only = bool(body.roadmap_only)
|
||||||
|
include_roadmap = roadmap_first or body.include_roadmap_preview or roadmap_only
|
||||||
progression_roadmap: Optional[Dict[str, Any]] = None
|
progression_roadmap: Optional[Dict[str, Any]] = None
|
||||||
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
||||||
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
roadmap_edited = False
|
||||||
roadmap_gap_offers: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
if include_roadmap:
|
if body.roadmap_override is not None:
|
||||||
|
try:
|
||||||
|
roadmap_ctx = roadmap_context_from_override(
|
||||||
|
goal_query,
|
||||||
|
max_steps=max_steps,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
override=body.roadmap_override,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
||||||
|
progression_roadmap["roadmap_edited"] = True
|
||||||
|
roadmap_edited = True
|
||||||
|
max_steps = int(roadmap_ctx.max_steps)
|
||||||
|
roadmap_first = True
|
||||||
|
elif include_roadmap:
|
||||||
roadmap_ctx = run_progression_roadmap_pipeline(
|
roadmap_ctx = run_progression_roadmap_pipeline(
|
||||||
goal_query,
|
goal_query,
|
||||||
max_steps=max_steps,
|
max_steps=max_steps,
|
||||||
|
|
@ -516,6 +528,37 @@ def suggest_progression_path(
|
||||||
)
|
)
|
||||||
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
progression_roadmap = progression_roadmap_to_api_dict(roadmap_ctx)
|
||||||
|
|
||||||
|
if roadmap_only:
|
||||||
|
return {
|
||||||
|
"goal_query": goal_query,
|
||||||
|
"max_steps_requested": max_steps,
|
||||||
|
"steps": [],
|
||||||
|
"step_count": 0,
|
||||||
|
"target_profile_summary": None,
|
||||||
|
"semantic_brief_summary": brief_to_summary_dict(semantic_brief),
|
||||||
|
"semantic_llm_applied": semantic_llm_applied,
|
||||||
|
"query_intent_summary": {},
|
||||||
|
"progression_graph_id": body.progression_graph_id,
|
||||||
|
"path_qa": None,
|
||||||
|
"gap_fill_offers": [],
|
||||||
|
"progression_roadmap": progression_roadmap,
|
||||||
|
"roadmap_first": False,
|
||||||
|
"roadmap_only": True,
|
||||||
|
"roadmap_edited": roadmap_edited,
|
||||||
|
"roadmap_unfilled_count": 0,
|
||||||
|
"retrieval_phase": "roadmap_only",
|
||||||
|
}
|
||||||
|
|
||||||
|
path_target_profile, first_intent_summary, path_intent = _build_path_target_profile(
|
||||||
|
cur,
|
||||||
|
goal_query=goal_query,
|
||||||
|
semantic_brief=semantic_brief,
|
||||||
|
include_llm_intent=body.include_llm_intent,
|
||||||
|
)
|
||||||
|
|
||||||
|
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||||
|
roadmap_gap_offers: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
used: Set[int] = set()
|
used: Set[int] = set()
|
||||||
steps: List[Dict[str, Any]] = []
|
steps: List[Dict[str, Any]] = []
|
||||||
planned_ids: List[int] = []
|
planned_ids: List[int] = []
|
||||||
|
|
@ -721,6 +764,8 @@ def suggest_progression_path(
|
||||||
retrieval_parts.append("gap_fill_offers")
|
retrieval_parts.append("gap_fill_offers")
|
||||||
if include_roadmap:
|
if include_roadmap:
|
||||||
retrieval_parts.append("roadmap_preview")
|
retrieval_parts.append("roadmap_preview")
|
||||||
|
if roadmap_edited:
|
||||||
|
retrieval_parts.append("roadmap_edited")
|
||||||
if roadmap_unfilled:
|
if roadmap_unfilled:
|
||||||
retrieval_parts.append("roadmap_unfilled")
|
retrieval_parts.append("roadmap_unfilled")
|
||||||
|
|
||||||
|
|
@ -738,6 +783,8 @@ def suggest_progression_path(
|
||||||
"gap_fill_offers": gap_fill_offers,
|
"gap_fill_offers": gap_fill_offers,
|
||||||
"progression_roadmap": progression_roadmap,
|
"progression_roadmap": progression_roadmap,
|
||||||
"roadmap_first": roadmap_first,
|
"roadmap_first": roadmap_first,
|
||||||
|
"roadmap_only": False,
|
||||||
|
"roadmap_edited": roadmap_edited,
|
||||||
"roadmap_unfilled_count": len(roadmap_unfilled),
|
"roadmap_unfilled_count": len(roadmap_unfilled),
|
||||||
"retrieval_phase": "+".join(retrieval_parts),
|
"retrieval_phase": "+".join(retrieval_parts),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,13 @@ class StageSpecArtifact(BaseModel):
|
||||||
anti_patterns: List[str] = Field(default_factory=list)
|
anti_patterns: List[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class RoadmapOverridePayload(BaseModel):
|
||||||
|
"""Vom Trainer bearbeitete Major Steps — steuert Retrieval ohne erneute Roadmap-KI."""
|
||||||
|
|
||||||
|
major_steps: List[MajorStep] = Field(..., min_length=2, max_length=10)
|
||||||
|
stage_specs: Optional[List[StageSpecArtifact]] = None
|
||||||
|
|
||||||
|
|
||||||
class ProgressionRoadmapContext(BaseModel):
|
class ProgressionRoadmapContext(BaseModel):
|
||||||
goal_query: str
|
goal_query: str
|
||||||
max_steps: int = Field(ge=2, le=10, default=5)
|
max_steps: int = Field(ge=2, le=10, default=5)
|
||||||
|
|
@ -551,6 +558,80 @@ def build_stage_specs(
|
||||||
return specs
|
return specs
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_major_steps_for_override(
|
||||||
|
major_steps: Sequence[MajorStep],
|
||||||
|
*,
|
||||||
|
max_steps: int,
|
||||||
|
) -> List[MajorStep]:
|
||||||
|
"""Indizes 0…n-1, mindestens 2, höchstens max_steps Major Steps."""
|
||||||
|
cleaned: List[MajorStep] = []
|
||||||
|
for raw in list(major_steps)[:max_steps]:
|
||||||
|
goal = (raw.learning_goal or "").strip()
|
||||||
|
phase = (raw.phase or "vertiefung").strip().lower()
|
||||||
|
if not goal:
|
||||||
|
continue
|
||||||
|
cleaned.append(
|
||||||
|
MajorStep(
|
||||||
|
index=len(cleaned),
|
||||||
|
phase=phase,
|
||||||
|
learning_goal=goal,
|
||||||
|
consolidates=list(raw.consolidates or []),
|
||||||
|
rationale=(raw.rationale or "").strip(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if len(cleaned) < 2:
|
||||||
|
raise ValueError("Mindestens zwei Major Steps mit Lernziel nötig")
|
||||||
|
for i, step in enumerate(cleaned):
|
||||||
|
step.index = i
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
def roadmap_context_from_override(
|
||||||
|
goal_query: str,
|
||||||
|
*,
|
||||||
|
max_steps: int,
|
||||||
|
semantic_brief: PlanningSemanticBrief,
|
||||||
|
override: RoadmapOverridePayload,
|
||||||
|
) -> ProgressionRoadmapContext:
|
||||||
|
"""Phase F4: bearbeitete Roadmap → stage_specs → Retrieval (ohne LLM-Roadmap)."""
|
||||||
|
majors = normalize_major_steps_for_override(override.major_steps, max_steps=max_steps)
|
||||||
|
effective_max = len(majors)
|
||||||
|
goal_analysis = build_goal_analysis(goal_query, semantic_brief)
|
||||||
|
stage_specs: List[StageSpecArtifact]
|
||||||
|
if override.stage_specs and len(override.stage_specs) >= effective_max:
|
||||||
|
stage_specs = []
|
||||||
|
for i, spec in enumerate(override.stage_specs[:effective_max]):
|
||||||
|
stage_specs.append(
|
||||||
|
StageSpecArtifact(
|
||||||
|
major_step_index=i,
|
||||||
|
learning_goal=(spec.learning_goal or majors[i].learning_goal).strip(),
|
||||||
|
load_profile=list(spec.load_profile or []),
|
||||||
|
exercise_type=(spec.exercise_type or "").strip(),
|
||||||
|
success_criteria=list(spec.success_criteria or []),
|
||||||
|
anti_patterns=list(spec.anti_patterns or []),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not all(s.exercise_type for s in stage_specs):
|
||||||
|
rebuilt = build_stage_specs(majors, goal_analysis=goal_analysis)
|
||||||
|
for i, spec in enumerate(stage_specs):
|
||||||
|
if not spec.exercise_type:
|
||||||
|
spec.exercise_type = rebuilt[i].exercise_type
|
||||||
|
if not spec.load_profile:
|
||||||
|
spec.load_profile = list(rebuilt[i].load_profile)
|
||||||
|
else:
|
||||||
|
stage_specs = build_stage_specs(majors, goal_analysis=goal_analysis)
|
||||||
|
|
||||||
|
return ProgressionRoadmapContext(
|
||||||
|
goal_query=goal_query.strip(),
|
||||||
|
max_steps=effective_max,
|
||||||
|
semantic_brief=brief_to_summary_dict(semantic_brief),
|
||||||
|
goal_analysis=goal_analysis,
|
||||||
|
roadmap=RoadmapArtifact(major_steps=majors),
|
||||||
|
stage_specs=stage_specs,
|
||||||
|
pipeline_phase="roadmap_v1_edited",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_progression_roadmap_pipeline(
|
def run_progression_roadmap_pipeline(
|
||||||
goal_query: str,
|
goal_query: str,
|
||||||
*,
|
*,
|
||||||
|
|
@ -652,6 +733,9 @@ __all__ = [
|
||||||
"MicroObjective",
|
"MicroObjective",
|
||||||
"ProgressionRoadmapContext",
|
"ProgressionRoadmapContext",
|
||||||
"RoadmapArtifact",
|
"RoadmapArtifact",
|
||||||
|
"RoadmapOverridePayload",
|
||||||
|
"normalize_major_steps_for_override",
|
||||||
|
"roadmap_context_from_override",
|
||||||
"StageSpecArtifact",
|
"StageSpecArtifact",
|
||||||
"build_goal_analysis",
|
"build_goal_analysis",
|
||||||
"build_roadmap_unfilled_gap_specs",
|
"build_roadmap_unfilled_gap_specs",
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ def post_progression_path_suggest(
|
||||||
body.include_llm_intent
|
body.include_llm_intent
|
||||||
or body.include_llm_path_qa
|
or body.include_llm_path_qa
|
||||||
or body.include_ai_gap_fill
|
or body.include_ai_gap_fill
|
||||||
|
or body.include_llm_roadmap
|
||||||
)
|
)
|
||||||
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
||||||
if uses_ai:
|
if uses_ai:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ from planning_progression_roadmap import (
|
||||||
run_progression_roadmap_pipeline,
|
run_progression_roadmap_pipeline,
|
||||||
stage_spec_exercise_kind_filter,
|
stage_spec_exercise_kind_filter,
|
||||||
stage_spec_retrieval_query,
|
stage_spec_retrieval_query,
|
||||||
|
normalize_major_steps_for_override,
|
||||||
|
roadmap_context_from_override,
|
||||||
|
RoadmapOverridePayload,
|
||||||
)
|
)
|
||||||
from planning_exercise_semantics import build_semantic_brief
|
from planning_exercise_semantics import build_semantic_brief
|
||||||
|
|
||||||
|
|
@ -90,6 +93,40 @@ def test_build_roadmap_unfilled_gap_specs():
|
||||||
assert specs[0]["phase"] == "anwendung"
|
assert specs[0]["phase"] == "anwendung"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_major_steps_reindexes():
|
||||||
|
majors = normalize_major_steps_for_override(
|
||||||
|
[
|
||||||
|
MajorStep(index=9, phase="einstieg", learning_goal="Einstieg", consolidates=[]),
|
||||||
|
MajorStep(index=8, phase="perfektion", learning_goal="Ziel", consolidates=[]),
|
||||||
|
],
|
||||||
|
max_steps=5,
|
||||||
|
)
|
||||||
|
assert len(majors) == 2
|
||||||
|
assert majors[0].index == 0
|
||||||
|
assert majors[1].index == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_roadmap_context_from_override():
|
||||||
|
brief = build_semantic_brief("Mae Geri Perfektion")
|
||||||
|
override = RoadmapOverridePayload(
|
||||||
|
major_steps=[
|
||||||
|
MajorStep(index=0, phase="einstieg", learning_goal="Mae Geri Einstieg", consolidates=[]),
|
||||||
|
MajorStep(index=1, phase="grundlage", learning_goal="Stand und Hüfte", consolidates=[]),
|
||||||
|
MajorStep(index=2, phase="perfektion", learning_goal="Präzision unter Belastung", consolidates=[]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
ctx = roadmap_context_from_override(
|
||||||
|
"Mae Geri Perfektion",
|
||||||
|
max_steps=5,
|
||||||
|
semantic_brief=brief,
|
||||||
|
override=override,
|
||||||
|
)
|
||||||
|
assert ctx.pipeline_phase == "roadmap_v1_edited"
|
||||||
|
assert len(ctx.roadmap.major_steps) == 3
|
||||||
|
assert len(ctx.stage_specs) == 3
|
||||||
|
assert ctx.stage_specs[1].learning_goal == "Stand und Hüfte"
|
||||||
|
|
||||||
|
|
||||||
def test_api_dict_exposes_prompt_slug_catalog():
|
def test_api_dict_exposes_prompt_slug_catalog():
|
||||||
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
||||||
api = progression_roadmap_to_api_dict(ctx)
|
api = progression_roadmap_to_api_dict(ctx)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.206"
|
APP_VERSION = "0.8.207"
|
||||||
BUILD_DATE = "2026-06-07"
|
BUILD_DATE = "2026-06-07"
|
||||||
DB_SCHEMA_VERSION = "20260606086"
|
DB_SCHEMA_VERSION = "20260606086"
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
||||||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||||
"methods": "0.1.0",
|
"methods": "0.1.0",
|
||||||
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
"exercises": "2.37.1", # KI-Endpoints: feature_usage nach ai_calls consume
|
||||||
"planning_exercise_suggest": "0.18.0", # F3: roadmap_first Retrieval pro stage_spec
|
"planning_exercise_suggest": "0.19.0", # F4: roadmap_only + roadmap_override, editierbare Roadmap-UI
|
||||||
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
|
||||||
"training_programs": "0.1.0",
|
"training_programs": "0.1.0",
|
||||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||||
|
|
@ -53,6 +53,15 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.207",
|
||||||
|
"date": "2026-06-07",
|
||||||
|
"changes": [
|
||||||
|
"Phase F4: Roadmap-Review — roadmap_only, roadmap_override auf progression-path-suggest.",
|
||||||
|
"UI: Major Steps editierbar (Phase, Lernziel, Reihenfolge) vor Übungs-Match.",
|
||||||
|
"Zwei-Schritt-Flow: Roadmap vorschlagen → Übungen matchen.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.206",
|
"version": "0.8.206",
|
||||||
"date": "2026-06-07",
|
"date": "2026-06-07",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||||
|
|
||||||
**Stand:** 2026-06-07
|
**Stand:** 2026-06-07
|
||||||
**App-Version / DB-Schema:** App **`0.8.206`** (Planungs-KI Phase F3); DB **`20260606086`** — maßgeblich **`backend/version.py`**.
|
**App-Version / DB-Schema:** App **`0.8.207`** (Planungs-KI Phase F4); DB **`20260606086`** — maßgeblich **`backend/version.py`**.
|
||||||
|
|
||||||
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
||||||
|
|
||||||
|
|
@ -109,6 +109,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
||||||
| **E3** | `gap_fill_offers`, Off-Topic-Strip, voller KI-Call bei Lücken | ✅ **0.8.203** |
|
| **E3** | `gap_fill_offers`, Off-Topic-Strip, voller KI-Call bei Lücken | ✅ **0.8.203** |
|
||||||
| **F0–F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** |
|
| **F0–F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** |
|
||||||
| **F3** | `roadmap_first` — Retrieval pro `stage_spec` | ✅ **0.8.206** |
|
| **F3** | `roadmap_first` — Retrieval pro `stage_spec` | ✅ **0.8.206** |
|
||||||
|
| **F4** | Roadmap-Review UI + `roadmap_override` | ✅ **0.8.207** |
|
||||||
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | 🔲 |
|
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | 🔲 |
|
||||||
|
|
||||||
**Architektur-Entscheidung (2026-06-07):** Progressionsgraph = **Roadmap-first** (Ziel → Major Steps → Übungs-Match). **Keine Gruppenanalyse** im Graphen. Mitai Workflow-Engine **später** — jetzt `planning_progression_roadmap.py`. Spec: **`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md`** · Roadmap: **`docs/architecture/PLANNING_KI_ROADMAP.md`**
|
**Architektur-Entscheidung (2026-06-07):** Progressionsgraph = **Roadmap-first** (Ziel → Major Steps → Übungs-Match). **Keine Gruppenanalyse** im Graphen. Mitai Workflow-Engine **später** — jetzt `planning_progression_roadmap.py`. Spec: **`.claude/docs/working/PLANNING_PROGRESSION_ROADMAP_SPEC.md`** · Roadmap: **`docs/architecture/PLANNING_KI_ROADMAP.md`**
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,11 @@ Diese Roadmap ergänzt die **Architektur-Refaktor-Roadmap** (`UMSETZUNGSPLAN_ROA
|
||||||
- [x] Gap-Angebote für unbesetzte Roadmap-Stufen (`roadmap_unfilled`)
|
- [x] Gap-Angebote für unbesetzte Roadmap-Stufen (`roadmap_unfilled`)
|
||||||
- [ ] QA/Lücken vollständig an Roadmap koppeln (Brücken optional reduzieren)
|
- [ ] QA/Lücken vollständig an Roadmap koppeln (Brücken optional reduzieren)
|
||||||
|
|
||||||
### F4 — UI
|
### F4 — UI (0.8.207)
|
||||||
|
|
||||||
- [ ] Roadmap-Review im `ExerciseProgressionPathBuilder`
|
- [x] Roadmap-Review im `ExerciseProgressionPathBuilder`
|
||||||
- [ ] Major Steps editierbar vor Übungs-Match
|
- [x] Major Steps editierbar (Phase, Lernziel, Reihenfolge) vor Übungs-Match
|
||||||
|
- [x] API `roadmap_only` + `roadmap_override`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@ function mapApiStepToRow(step) {
|
||||||
aiSuggestion: step?.ai_suggestion || null,
|
aiSuggestion: step?.ai_suggestion || null,
|
||||||
semanticScore: step?.semantic_score,
|
semanticScore: step?.semantic_score,
|
||||||
isOffTopic: false,
|
isOffTopic: false,
|
||||||
|
roadmapMajorStepIndex:
|
||||||
|
step?.roadmap_major_step_index != null ? Number(step.roadmap_major_step_index) : null,
|
||||||
|
roadmapPhase: step?.roadmap_phase || null,
|
||||||
|
roadmapLearningGoal: step?.roadmap_learning_goal || null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +67,36 @@ const OFFER_SOURCE_LABELS = {
|
||||||
|
|
||||||
const PATH_STEPS_HARD_MAX = 10
|
const PATH_STEPS_HARD_MAX = 10
|
||||||
|
|
||||||
|
const ROADMAP_PHASES = ['einstieg', 'grundlage', 'vertiefung', 'anwendung', 'perfektion']
|
||||||
|
|
||||||
|
function mapMajorStepsFromApi(apiRoadmap) {
|
||||||
|
const raw = apiRoadmap?.roadmap?.major_steps
|
||||||
|
if (!Array.isArray(raw)) return []
|
||||||
|
return raw.map((s, i) => ({
|
||||||
|
index: i,
|
||||||
|
phase: s.phase || 'vertiefung',
|
||||||
|
learning_goal: (s.learning_goal || '').trim(),
|
||||||
|
consolidates: Array.isArray(s.consolidates) ? s.consolidates : [],
|
||||||
|
rationale: s.rationale || '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function reindexMajorSteps(rows) {
|
||||||
|
return rows.map((row, i) => ({ ...row, index: i }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function majorStepsToOverridePayload(rows) {
|
||||||
|
return {
|
||||||
|
major_steps: reindexMajorSteps(rows).map((row) => ({
|
||||||
|
index: row.index,
|
||||||
|
phase: row.phase || 'vertiefung',
|
||||||
|
learning_goal: row.learning_goal.trim(),
|
||||||
|
consolidates: row.consolidates || [],
|
||||||
|
rationale: row.rationale || '',
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Einfügen wächst den Pfad; Ersetzen (replace_step_index) nicht. */
|
/** Einfügen wächst den Pfad; Ersetzen (replace_step_index) nicht. */
|
||||||
function offerGrowsPath(offer) {
|
function offerGrowsPath(offer) {
|
||||||
const replaceIdx = offer?.replace_step_index
|
const replaceIdx = offer?.replace_step_index
|
||||||
|
|
@ -121,7 +155,6 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
const [goalQuery, setGoalQuery] = useState('')
|
const [goalQuery, setGoalQuery] = useState('')
|
||||||
const [maxSteps, setMaxSteps] = useState(5)
|
const [maxSteps, setMaxSteps] = useState(5)
|
||||||
const [segmentNotes, setSegmentNotes] = useState('')
|
const [segmentNotes, setSegmentNotes] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [targetSummary, setTargetSummary] = useState(null)
|
const [targetSummary, setTargetSummary] = useState(null)
|
||||||
|
|
@ -130,6 +163,11 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
const [pathSteps, setPathSteps] = useState([])
|
const [pathSteps, setPathSteps] = useState([])
|
||||||
const [gapFillOffers, setGapFillOffers] = useState([])
|
const [gapFillOffers, setGapFillOffers] = useState([])
|
||||||
const [progressionRoadmap, setProgressionRoadmap] = useState(null)
|
const [progressionRoadmap, setProgressionRoadmap] = useState(null)
|
||||||
|
const [editableMajorSteps, setEditableMajorSteps] = useState([])
|
||||||
|
const [roadmapDirty, setRoadmapDirty] = useState(false)
|
||||||
|
const [loadingRoadmap, setLoadingRoadmap] = useState(false)
|
||||||
|
const [loadingMatch, setLoadingMatch] = useState(false)
|
||||||
|
const loading = loadingRoadmap || loadingMatch
|
||||||
const [focusAreas, setFocusAreas] = useState([])
|
const [focusAreas, setFocusAreas] = useState([])
|
||||||
const [skillsCatalog, setSkillsCatalog] = useState([])
|
const [skillsCatalog, setSkillsCatalog] = useState([])
|
||||||
const [generatingOfferId, setGeneratingOfferId] = useState(null)
|
const [generatingOfferId, setGeneratingOfferId] = useState(null)
|
||||||
|
|
@ -173,6 +211,52 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
setPathSteps((prev) => (prev.length <= 2 ? prev : prev.filter((_, i) => i !== idx)))
|
setPathSteps((prev) => (prev.length <= 2 ? prev : prev.filter((_, i) => i !== idx)))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const patchMajorStep = useCallback((idx, patch) => {
|
||||||
|
setEditableMajorSteps((prev) =>
|
||||||
|
reindexMajorSteps(prev.map((row, i) => (i === idx ? { ...row, ...patch } : row))),
|
||||||
|
)
|
||||||
|
setRoadmapDirty(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const moveMajorStep = useCallback((idx, dir) => {
|
||||||
|
setEditableMajorSteps((prev) => {
|
||||||
|
const j = idx + dir
|
||||||
|
if (j < 0 || j >= prev.length) return prev
|
||||||
|
const next = [...prev]
|
||||||
|
const t = next[idx]
|
||||||
|
next[idx] = next[j]
|
||||||
|
next[j] = t
|
||||||
|
return reindexMajorSteps(next)
|
||||||
|
})
|
||||||
|
setRoadmapDirty(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeMajorStep = useCallback((idx) => {
|
||||||
|
setEditableMajorSteps((prev) => {
|
||||||
|
if (prev.length <= 2) return prev
|
||||||
|
return reindexMajorSteps(prev.filter((_, i) => i !== idx))
|
||||||
|
})
|
||||||
|
setRoadmapDirty(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const addMajorStep = useCallback(() => {
|
||||||
|
setEditableMajorSteps((prev) => {
|
||||||
|
if (prev.length >= PATH_STEPS_HARD_MAX) return prev
|
||||||
|
const phase = ROADMAP_PHASES[Math.min(prev.length, ROADMAP_PHASES.length - 1)]
|
||||||
|
return reindexMajorSteps([
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
index: prev.length,
|
||||||
|
phase,
|
||||||
|
learning_goal: '',
|
||||||
|
consolidates: [],
|
||||||
|
rationale: '',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
setRoadmapDirty(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const moveStep = useCallback((idx, dir) => {
|
const moveStep = useCallback((idx, dir) => {
|
||||||
setPathSteps((prev) => {
|
setPathSteps((prev) => {
|
||||||
const j = idx + dir
|
const j = idx + dir
|
||||||
|
|
@ -376,32 +460,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestPath = async () => {
|
const applyPathMatchResponse = (res, q) => {
|
||||||
const q = (goalQuery || '').trim()
|
|
||||||
if (q.length < 3) {
|
|
||||||
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!graphId) {
|
|
||||||
alert('Zuerst einen Graphen wählen.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setLoading(true)
|
|
||||||
setError('')
|
|
||||||
try {
|
|
||||||
const res = await api.suggestProgressionPath({
|
|
||||||
query: q,
|
|
||||||
max_steps: Number(maxSteps),
|
|
||||||
include_llm_intent: true,
|
|
||||||
include_path_qa: true,
|
|
||||||
include_llm_path_qa: true,
|
|
||||||
include_path_reorder: true,
|
|
||||||
include_ai_gap_fill: true,
|
|
||||||
include_roadmap_preview: true,
|
|
||||||
include_llm_roadmap: true,
|
|
||||||
roadmap_first: true,
|
|
||||||
progression_graph_id: Number(graphId),
|
|
||||||
})
|
|
||||||
const qa = res?.path_qa || null
|
const qa = res?.path_qa || null
|
||||||
const rawRows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow)
|
const rawRows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow)
|
||||||
const rows =
|
const rows =
|
||||||
|
|
@ -423,18 +482,99 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
: [],
|
: [],
|
||||||
)
|
)
|
||||||
setProgressionRoadmap(res?.progression_roadmap || null)
|
setProgressionRoadmap(res?.progression_roadmap || null)
|
||||||
|
setRoadmapDirty(false)
|
||||||
if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400))
|
if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400))
|
||||||
} catch (e) {
|
}
|
||||||
console.error(e)
|
|
||||||
setError(e.message || 'Pfad-Vorschlag fehlgeschlagen')
|
const suggestRoadmap = async () => {
|
||||||
|
const q = (goalQuery || '').trim()
|
||||||
|
if (q.length < 3) {
|
||||||
|
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!graphId) {
|
||||||
|
alert('Zuerst einen Graphen wählen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoadingRoadmap(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const res = await api.suggestProgressionPath({
|
||||||
|
query: q,
|
||||||
|
max_steps: Number(maxSteps),
|
||||||
|
include_llm_intent: false,
|
||||||
|
include_path_qa: false,
|
||||||
|
include_llm_path_qa: false,
|
||||||
|
include_path_reorder: false,
|
||||||
|
include_ai_gap_fill: false,
|
||||||
|
include_roadmap_preview: true,
|
||||||
|
include_llm_roadmap: true,
|
||||||
|
roadmap_only: true,
|
||||||
|
progression_graph_id: Number(graphId),
|
||||||
|
})
|
||||||
|
const majors = mapMajorStepsFromApi(res?.progression_roadmap)
|
||||||
|
if (majors.length < 2) {
|
||||||
|
throw new Error('Roadmap hat zu wenig Major Steps.')
|
||||||
|
}
|
||||||
|
setEditableMajorSteps(majors)
|
||||||
|
setMaxSteps(majors.length)
|
||||||
|
setProgressionRoadmap(res?.progression_roadmap || null)
|
||||||
|
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||||
setPathSteps([])
|
setPathSteps([])
|
||||||
setTargetSummary(null)
|
setTargetSummary(null)
|
||||||
setSemanticBrief(null)
|
|
||||||
setPathQa(null)
|
setPathQa(null)
|
||||||
setGapFillOffers([])
|
setGapFillOffers([])
|
||||||
|
setRoadmapDirty(false)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
setError(e.message || 'Roadmap-Vorschlag fehlgeschlagen')
|
||||||
|
setEditableMajorSteps([])
|
||||||
setProgressionRoadmap(null)
|
setProgressionRoadmap(null)
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoadingRoadmap(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchExercisesFromRoadmap = async () => {
|
||||||
|
const q = (goalQuery || '').trim()
|
||||||
|
if (q.length < 3) {
|
||||||
|
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!graphId) {
|
||||||
|
alert('Zuerst einen Graphen wählen.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const validSteps = editableMajorSteps.filter((s) => (s.learning_goal || '').trim().length >= 3)
|
||||||
|
if (validSteps.length < 2) {
|
||||||
|
alert('Mindestens zwei Major Steps mit Lernziel (je 3+ Zeichen) nötig.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoadingMatch(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const override = majorStepsToOverridePayload(validSteps)
|
||||||
|
const res = await api.suggestProgressionPath({
|
||||||
|
query: q,
|
||||||
|
max_steps: validSteps.length,
|
||||||
|
include_llm_intent: true,
|
||||||
|
include_path_qa: true,
|
||||||
|
include_llm_path_qa: true,
|
||||||
|
include_path_reorder: true,
|
||||||
|
include_ai_gap_fill: true,
|
||||||
|
include_roadmap_preview: true,
|
||||||
|
include_llm_roadmap: false,
|
||||||
|
roadmap_first: true,
|
||||||
|
roadmap_override: override,
|
||||||
|
progression_graph_id: Number(graphId),
|
||||||
|
})
|
||||||
|
applyPathMatchResponse(res, q)
|
||||||
|
setMaxSteps(validSteps.length)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
setError(e.message || 'Übungs-Match fehlgeschlagen')
|
||||||
|
} finally {
|
||||||
|
setLoadingMatch(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,6 +617,8 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
setPathQa(null)
|
setPathQa(null)
|
||||||
setGapFillOffers([])
|
setGapFillOffers([])
|
||||||
setProgressionRoadmap(null)
|
setProgressionRoadmap(null)
|
||||||
|
setEditableMajorSteps([])
|
||||||
|
setRoadmapDirty(false)
|
||||||
if (typeof onSaved === 'function') await onSaved()
|
if (typeof onSaved === 'function') await onSaved()
|
||||||
const msg =
|
const msg =
|
||||||
skippedAi > 0
|
skippedAi > 0
|
||||||
|
|
@ -501,8 +643,8 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
>
|
>
|
||||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>KI: Pfad zum Ziel</h3>
|
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>KI: Pfad zum Ziel</h3>
|
||||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
||||||
Ziel in Freitext formulieren — die Planungs-KI schlägt eine semantisch passende, aufbauende Reihenfolge vor,
|
Zuerst didaktische Roadmap vorschlagen und anpassen, dann Übungen je Major Step aus der Bibliothek matchen.
|
||||||
prüft Lücken (ggf. Brücken-Übungen) und optional per LLM-QS. Fehlende Schritte können mit KI als Übung angelegt werden.
|
Lücken können mit KI als Übung angelegt werden.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end' }}>
|
||||||
<div className="form-row" style={{ flex: '2 1 240px', marginBottom: 0 }}>
|
<div className="form-row" style={{ flex: '2 1 240px', marginBottom: 0 }}>
|
||||||
|
|
@ -531,9 +673,30 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
disabled={disabled || loading || saving || !graphId}
|
disabled={disabled || loading || saving || !graphId}
|
||||||
onClick={suggestPath}
|
onClick={suggestRoadmap}
|
||||||
>
|
>
|
||||||
{loading ? 'Vorschlag …' : 'Pfad vorschlagen'}
|
{loadingRoadmap ? 'Roadmap …' : 'Roadmap vorschlagen'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
loading ||
|
||||||
|
saving ||
|
||||||
|
!graphId ||
|
||||||
|
editableMajorSteps.length < 2
|
||||||
|
}
|
||||||
|
onClick={matchExercisesFromRoadmap}
|
||||||
|
title={
|
||||||
|
editableMajorSteps.length < 2
|
||||||
|
? 'Zuerst Roadmap vorschlagen'
|
||||||
|
: roadmapDirty
|
||||||
|
? 'Roadmap wurde bearbeitet — erneut matchen'
|
||||||
|
: 'Bibliothek je Major Step durchsuchen'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loadingMatch ? 'Match …' : roadmapDirty ? 'Übungen neu matchen' : 'Übungen matchen'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -565,7 +728,7 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{progressionRoadmap?.roadmap?.major_steps?.length > 0 ? (
|
{editableMajorSteps.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
|
|
@ -575,25 +738,113 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface))',
|
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong style={{ fontSize: '13px' }}>Didaktische Roadmap (Phase F)</strong>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '6px 0 10px', lineHeight: 1.45 }}>
|
<strong style={{ fontSize: '13px' }}>Didaktische Roadmap — bearbeiten</strong>
|
||||||
Ziel-zuerst-Planung: {progressionRoadmap.micro_objective_count ?? '?'} Zwischenziele →{' '}
|
{roadmapDirty ? (
|
||||||
{progressionRoadmap.major_step_count ?? progressionRoadmap.roadmap.major_steps.length} Major Steps.
|
<span className="exercise-tag" style={{ borderColor: 'var(--danger)' }}>
|
||||||
{progressionRoadmap.llm_roadmap_applied
|
Geändert — bitte erneut matchen
|
||||||
? ' (KI-Prompts aus Admin-Konfiguration)'
|
|
||||||
: ' (heuristischer Fallback — KI-Prompts in ai_prompts)'}
|
|
||||||
. Übungen unten: je Major Step aus der Bibliothek (roadmap-first).
|
|
||||||
</p>
|
|
||||||
<ol style={{ margin: 0, paddingLeft: '18px', fontSize: '13px', lineHeight: 1.5 }}>
|
|
||||||
{progressionRoadmap.roadmap.major_steps.map((step) => (
|
|
||||||
<li key={step.index} style={{ marginBottom: '6px' }}>
|
|
||||||
<span className="exercise-tag" style={{ marginRight: '6px' }}>
|
|
||||||
{step.phase}
|
|
||||||
</span>
|
</span>
|
||||||
{step.learning_goal}
|
) : pathSteps.length > 0 ? (
|
||||||
</li>
|
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||||
|
Gematcht
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 10px', lineHeight: 1.45 }}>
|
||||||
|
{progressionRoadmap?.micro_objective_count != null
|
||||||
|
? `${progressionRoadmap.micro_objective_count} Zwischenziele → `
|
||||||
|
: ''}
|
||||||
|
{editableMajorSteps.length} Major Steps
|
||||||
|
{progressionRoadmap?.llm_roadmap_applied
|
||||||
|
? ' (KI-Roadmap)'
|
||||||
|
: progressionRoadmap
|
||||||
|
? ' (heuristisch/KI)'
|
||||||
|
: ''}
|
||||||
|
. Phasen und Lernziele anpassen, dann „Übungen matchen“.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
{editableMajorSteps.map((step, idx) => (
|
||||||
|
<div
|
||||||
|
key={`major-${idx}-${step.index}`}
|
||||||
|
style={{
|
||||||
|
padding: '10px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
|
||||||
|
gap: '10px',
|
||||||
|
alignItems: 'end',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="form-row" style={{ marginBottom: 0, flex: '0 0 120px' }}>
|
||||||
|
<label className="form-label">Stufe {idx + 1} · Phase</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={step.phase}
|
||||||
|
onChange={(e) => patchMajorStep(idx, { phase: e.target.value })}
|
||||||
|
disabled={disabled || loading || saving}
|
||||||
|
>
|
||||||
|
{ROADMAP_PHASES.map((p) => (
|
||||||
|
<option key={p} value={p}>
|
||||||
|
{p}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-row" style={{ marginBottom: 0, flex: '2 1 240px' }}>
|
||||||
|
<label className="form-label">Lernziel</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={step.learning_goal}
|
||||||
|
onChange={(e) => patchMajorStep(idx, { learning_goal: e.target.value })}
|
||||||
|
placeholder="z. B. Grundstellung und Hüftmobilität für Mae Geri"
|
||||||
|
disabled={disabled || loading || saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||||
|
onClick={() => moveMajorStep(idx, -1)}
|
||||||
|
disabled={disabled || loading || saving || idx === 0}
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||||
|
onClick={() => moveMajorStep(idx, 1)}
|
||||||
|
disabled={disabled || loading || saving || idx >= editableMajorSteps.length - 1}
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn"
|
||||||
|
style={{ fontSize: '12px', padding: '4px 8px' }}
|
||||||
|
onClick={() => removeMajorStep(idx)}
|
||||||
|
disabled={disabled || loading || saving || editableMajorSteps.length <= 2}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{editableMajorSteps.length < PATH_STEPS_HARD_MAX ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ marginTop: '10px', fontSize: '12px' }}
|
||||||
|
onClick={addMajorStep}
|
||||||
|
disabled={disabled || loading || saving}
|
||||||
|
>
|
||||||
|
Major Step hinzufügen
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
@ -748,11 +999,18 @@ export default function ExerciseProgressionPathBuilder({
|
||||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
Schritt {idx + 1}
|
Schritt {idx + 1}
|
||||||
|
{step.roadmapMajorStepIndex != null
|
||||||
|
? ` · Roadmap ${step.roadmapMajorStepIndex + 1}`
|
||||||
|
: ''}
|
||||||
|
{step.roadmapPhase ? ` (${step.roadmapPhase})` : ''}
|
||||||
{step.isOffTopic ? ' (themenfremd)' : ''}
|
{step.isOffTopic ? ' (themenfremd)' : ''}
|
||||||
{step.isAiProposal ? ' (KI-Neu)' : step.isBridge ? ' (Brücke)' : ''}
|
{step.isAiProposal ? ' (KI-Neu)' : step.isBridge ? ' (Brücke)' : ''}
|
||||||
{!step.isAiProposal && !step.isOffTopic && idx === 0 ? ' (Einstieg)' : ''}
|
|
||||||
{!step.isAiProposal && idx === pathSteps.length - 1 ? ' (Zielnähe)' : ''}
|
|
||||||
</label>
|
</label>
|
||||||
|
{step.roadmapLearningGoal ? (
|
||||||
|
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '4px 0 0' }}>
|
||||||
|
Ziel: {step.roadmapLearningGoal}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
<div style={{ fontSize: '13px' }}>
|
<div style={{ fontSize: '13px' }}>
|
||||||
<strong>{step.exerciseTitle}</strong>
|
<strong>{step.exerciseTitle}</strong>
|
||||||
{step.exerciseId ? (
|
{step.exerciseId ? (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user