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 |
|
||||
| **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 |
|
||||
| **F4** | UI Roadmap-Review | 🔲 |
|
||||
| **F4** | UI Roadmap-Review | ✅ 0.8.207 |
|
||||
| **F5** | Trainingsplanung: eigene Pipeline + ggf. Workflow-Engine | 🔲 |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -53,10 +53,12 @@ from planning_exercise_suggest import (
|
|||
from planning_progression_roadmap import (
|
||||
MajorStep,
|
||||
ProgressionRoadmapContext,
|
||||
RoadmapOverridePayload,
|
||||
StageSpecArtifact,
|
||||
build_roadmap_unfilled_gap_specs,
|
||||
progression_roadmap_to_api_dict,
|
||||
resolve_step_exercise_kind_filter,
|
||||
roadmap_context_from_override,
|
||||
run_progression_roadmap_pipeline,
|
||||
stage_spec_retrieval_query,
|
||||
)
|
||||
|
|
@ -74,6 +76,8 @@ class ProgressionPathSuggestRequest(BaseModel):
|
|||
include_roadmap_preview: bool = False
|
||||
include_llm_roadmap: bool = True
|
||||
roadmap_first: bool = False
|
||||
roadmap_only: bool = False
|
||||
roadmap_override: Optional[RoadmapOverridePayload] = None
|
||||
progression_graph_id: Optional[int] = Field(default=None, ge=1)
|
||||
exercise_kind_any: Optional[List[str]] = None
|
||||
|
||||
|
|
@ -492,21 +496,29 @@ def suggest_progression_path(
|
|||
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)
|
||||
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
|
||||
roadmap_ctx: Optional[ProgressionRoadmapContext] = None
|
||||
roadmap_unfilled: List[Tuple[int, StageSpecArtifact]] = []
|
||||
roadmap_gap_offers: List[Dict[str, Any]] = []
|
||||
roadmap_edited = False
|
||||
|
||||
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(
|
||||
goal_query,
|
||||
max_steps=max_steps,
|
||||
|
|
@ -516,6 +528,37 @@ def suggest_progression_path(
|
|||
)
|
||||
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()
|
||||
steps: List[Dict[str, Any]] = []
|
||||
planned_ids: List[int] = []
|
||||
|
|
@ -721,6 +764,8 @@ def suggest_progression_path(
|
|||
retrieval_parts.append("gap_fill_offers")
|
||||
if include_roadmap:
|
||||
retrieval_parts.append("roadmap_preview")
|
||||
if roadmap_edited:
|
||||
retrieval_parts.append("roadmap_edited")
|
||||
if roadmap_unfilled:
|
||||
retrieval_parts.append("roadmap_unfilled")
|
||||
|
||||
|
|
@ -738,6 +783,8 @@ def suggest_progression_path(
|
|||
"gap_fill_offers": gap_fill_offers,
|
||||
"progression_roadmap": progression_roadmap,
|
||||
"roadmap_first": roadmap_first,
|
||||
"roadmap_only": False,
|
||||
"roadmap_edited": roadmap_edited,
|
||||
"roadmap_unfilled_count": len(roadmap_unfilled),
|
||||
"retrieval_phase": "+".join(retrieval_parts),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,13 @@ class StageSpecArtifact(BaseModel):
|
|||
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):
|
||||
goal_query: str
|
||||
max_steps: int = Field(ge=2, le=10, default=5)
|
||||
|
|
@ -551,6 +558,80 @@ def build_stage_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(
|
||||
goal_query: str,
|
||||
*,
|
||||
|
|
@ -652,6 +733,9 @@ __all__ = [
|
|||
"MicroObjective",
|
||||
"ProgressionRoadmapContext",
|
||||
"RoadmapArtifact",
|
||||
"RoadmapOverridePayload",
|
||||
"normalize_major_steps_for_override",
|
||||
"roadmap_context_from_override",
|
||||
"StageSpecArtifact",
|
||||
"build_goal_analysis",
|
||||
"build_roadmap_unfilled_gap_specs",
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ def post_progression_path_suggest(
|
|||
body.include_llm_intent
|
||||
or body.include_llm_path_qa
|
||||
or body.include_ai_gap_fill
|
||||
or body.include_llm_roadmap
|
||||
)
|
||||
club_id = resolve_club_id_for_probe(tenant) if uses_ai else None
|
||||
if uses_ai:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ from planning_progression_roadmap import (
|
|||
run_progression_roadmap_pipeline,
|
||||
stage_spec_exercise_kind_filter,
|
||||
stage_spec_retrieval_query,
|
||||
normalize_major_steps_for_override,
|
||||
roadmap_context_from_override,
|
||||
RoadmapOverridePayload,
|
||||
)
|
||||
from planning_exercise_semantics import build_semantic_brief
|
||||
|
||||
|
|
@ -90,6 +93,40 @@ def test_build_roadmap_unfilled_gap_specs():
|
|||
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():
|
||||
ctx = run_progression_roadmap_pipeline("Mae Geri", max_steps=3, include_llm_roadmap=False)
|
||||
api = progression_roadmap_to_api_dict(ctx)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.206"
|
||||
APP_VERSION = "0.8.207"
|
||||
BUILD_DATE = "2026-06-07"
|
||||
DB_SCHEMA_VERSION = "20260606086"
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ MODULE_VERSIONS = {
|
|||
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
|
||||
"methods": "0.1.0",
|
||||
"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_programs": "0.1.0",
|
||||
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
|
||||
|
|
@ -53,6 +53,15 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
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",
|
||||
"date": "2026-06-07",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||
|
||||
**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**.
|
||||
|
||||
|
|
@ -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** |
|
||||
| **F0–F2** | Roadmap-Pipeline + LLM-Prompts (078/079) | ✅ **0.8.205** |
|
||||
| **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) | 🔲 |
|
||||
|
||||
**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`)
|
||||
- [ ] QA/Lücken vollständig an Roadmap koppeln (Brücken optional reduzieren)
|
||||
|
||||
### F4 — UI
|
||||
### F4 — UI (0.8.207)
|
||||
|
||||
- [ ] Roadmap-Review im `ExerciseProgressionPathBuilder`
|
||||
- [ ] Major Steps editierbar vor Übungs-Match
|
||||
- [x] Roadmap-Review im `ExerciseProgressionPathBuilder`
|
||||
- [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,
|
||||
semanticScore: step?.semantic_score,
|
||||
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 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. */
|
||||
function offerGrowsPath(offer) {
|
||||
const replaceIdx = offer?.replace_step_index
|
||||
|
|
@ -121,7 +155,6 @@ export default function ExerciseProgressionPathBuilder({
|
|||
const [goalQuery, setGoalQuery] = useState('')
|
||||
const [maxSteps, setMaxSteps] = useState(5)
|
||||
const [segmentNotes, setSegmentNotes] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [targetSummary, setTargetSummary] = useState(null)
|
||||
|
|
@ -130,6 +163,11 @@ export default function ExerciseProgressionPathBuilder({
|
|||
const [pathSteps, setPathSteps] = useState([])
|
||||
const [gapFillOffers, setGapFillOffers] = useState([])
|
||||
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 [skillsCatalog, setSkillsCatalog] = useState([])
|
||||
const [generatingOfferId, setGeneratingOfferId] = useState(null)
|
||||
|
|
@ -173,6 +211,52 @@ export default function ExerciseProgressionPathBuilder({
|
|||
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) => {
|
||||
setPathSteps((prev) => {
|
||||
const j = idx + dir
|
||||
|
|
@ -376,7 +460,33 @@ export default function ExerciseProgressionPathBuilder({
|
|||
}
|
||||
}
|
||||
|
||||
const suggestPath = async () => {
|
||||
const applyPathMatchResponse = (res, q) => {
|
||||
const qa = res?.path_qa || null
|
||||
const rawRows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow)
|
||||
const rows =
|
||||
Array.isArray(qa?.stripped_off_topic_steps) && qa.stripped_off_topic_steps.length > 0
|
||||
? rawRows
|
||||
: applyOffTopicFlags(rawRows, qa)
|
||||
if (rows.length < 2) {
|
||||
throw new Error('Zu wenig Schritte im Vorschlag.')
|
||||
}
|
||||
setPathSteps(rows)
|
||||
setTargetSummary(res?.target_profile_summary || null)
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setPathQa(qa)
|
||||
setGapFillOffers(
|
||||
Array.isArray(res?.gap_fill_offers)
|
||||
? res.gap_fill_offers
|
||||
: Array.isArray(qa?.gap_fill_offers)
|
||||
? qa.gap_fill_offers
|
||||
: [],
|
||||
)
|
||||
setProgressionRoadmap(res?.progression_roadmap || null)
|
||||
setRoadmapDirty(false)
|
||||
if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400))
|
||||
}
|
||||
|
||||
const suggestRoadmap = async () => {
|
||||
const q = (goalQuery || '').trim()
|
||||
if (q.length < 3) {
|
||||
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
||||
|
|
@ -386,55 +496,85 @@ export default function ExerciseProgressionPathBuilder({
|
|||
alert('Zuerst einen Graphen wählen.')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
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([])
|
||||
setTargetSummary(null)
|
||||
setPathQa(null)
|
||||
setGapFillOffers([])
|
||||
setRoadmapDirty(false)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError(e.message || 'Roadmap-Vorschlag fehlgeschlagen')
|
||||
setEditableMajorSteps([])
|
||||
setProgressionRoadmap(null)
|
||||
} finally {
|
||||
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: true,
|
||||
include_llm_roadmap: false,
|
||||
roadmap_first: true,
|
||||
roadmap_override: override,
|
||||
progression_graph_id: Number(graphId),
|
||||
})
|
||||
const qa = res?.path_qa || null
|
||||
const rawRows = (Array.isArray(res?.steps) ? res.steps : []).map(mapApiStepToRow)
|
||||
const rows =
|
||||
Array.isArray(qa?.stripped_off_topic_steps) && qa.stripped_off_topic_steps.length > 0
|
||||
? rawRows
|
||||
: applyOffTopicFlags(rawRows, qa)
|
||||
if (rows.length < 2) {
|
||||
throw new Error('Zu wenig Schritte im Vorschlag.')
|
||||
}
|
||||
setPathSteps(rows)
|
||||
setTargetSummary(res?.target_profile_summary || null)
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setPathQa(qa)
|
||||
setGapFillOffers(
|
||||
Array.isArray(res?.gap_fill_offers)
|
||||
? res.gap_fill_offers
|
||||
: Array.isArray(qa?.gap_fill_offers)
|
||||
? qa.gap_fill_offers
|
||||
: [],
|
||||
)
|
||||
setProgressionRoadmap(res?.progression_roadmap || null)
|
||||
if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400))
|
||||
applyPathMatchResponse(res, q)
|
||||
setMaxSteps(validSteps.length)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError(e.message || 'Pfad-Vorschlag fehlgeschlagen')
|
||||
setPathSteps([])
|
||||
setTargetSummary(null)
|
||||
setSemanticBrief(null)
|
||||
setPathQa(null)
|
||||
setGapFillOffers([])
|
||||
setProgressionRoadmap(null)
|
||||
setError(e.message || 'Übungs-Match fehlgeschlagen')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoadingMatch(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -477,6 +617,8 @@ export default function ExerciseProgressionPathBuilder({
|
|||
setPathQa(null)
|
||||
setGapFillOffers([])
|
||||
setProgressionRoadmap(null)
|
||||
setEditableMajorSteps([])
|
||||
setRoadmapDirty(false)
|
||||
if (typeof onSaved === 'function') await onSaved()
|
||||
const msg =
|
||||
skippedAi > 0
|
||||
|
|
@ -501,8 +643,8 @@ export default function ExerciseProgressionPathBuilder({
|
|||
>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>KI: Pfad zum Ziel</h3>
|
||||
<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,
|
||||
prüft Lücken (ggf. Brücken-Übungen) und optional per LLM-QS. Fehlende Schritte können mit KI als Übung angelegt werden.
|
||||
Zuerst didaktische Roadmap vorschlagen und anpassen, dann Übungen je Major Step aus der Bibliothek matchen.
|
||||
Lücken können mit KI als Übung angelegt werden.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end' }}>
|
||||
<div className="form-row" style={{ flex: '2 1 240px', marginBottom: 0 }}>
|
||||
|
|
@ -531,9 +673,30 @@ export default function ExerciseProgressionPathBuilder({
|
|||
type="button"
|
||||
className="btn btn-primary"
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
|
@ -565,7 +728,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{progressionRoadmap?.roadmap?.major_steps?.length > 0 ? (
|
||||
{editableMajorSteps.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
|
|
@ -575,25 +738,113 @@ export default function ExerciseProgressionPathBuilder({
|
|||
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface))',
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontSize: '13px' }}>Didaktische Roadmap (Phase F)</strong>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '6px 0 10px', lineHeight: 1.45 }}>
|
||||
Ziel-zuerst-Planung: {progressionRoadmap.micro_objective_count ?? '?'} Zwischenziele →{' '}
|
||||
{progressionRoadmap.major_step_count ?? progressionRoadmap.roadmap.major_steps.length} Major Steps.
|
||||
{progressionRoadmap.llm_roadmap_applied
|
||||
? ' (KI-Prompts aus Admin-Konfiguration)'
|
||||
: ' (heuristischer Fallback — KI-Prompts in ai_prompts)'}
|
||||
. Übungen unten: je Major Step aus der Bibliothek (roadmap-first).
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<strong style={{ fontSize: '13px' }}>Didaktische Roadmap — bearbeiten</strong>
|
||||
{roadmapDirty ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--danger)' }}>
|
||||
Geändert — bitte erneut matchen
|
||||
</span>
|
||||
) : pathSteps.length > 0 ? (
|
||||
<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>
|
||||
<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>
|
||||
{step.learning_goal}
|
||||
</li>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
))}
|
||||
</ol>
|
||||
</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>
|
||||
) : null}
|
||||
|
||||
|
|
@ -748,11 +999,18 @@ export default function ExerciseProgressionPathBuilder({
|
|||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
<label className="form-label">
|
||||
Schritt {idx + 1}
|
||||
{step.roadmapMajorStepIndex != null
|
||||
? ` · Roadmap ${step.roadmapMajorStepIndex + 1}`
|
||||
: ''}
|
||||
{step.roadmapPhase ? ` (${step.roadmapPhase})` : ''}
|
||||
{step.isOffTopic ? ' (themenfremd)' : ''}
|
||||
{step.isAiProposal ? ' (KI-Neu)' : step.isBridge ? ' (Brücke)' : ''}
|
||||
{!step.isAiProposal && !step.isOffTopic && idx === 0 ? ' (Einstieg)' : ''}
|
||||
{!step.isAiProposal && idx === pathSteps.length - 1 ? ' (Zielnähe)' : ''}
|
||||
</label>
|
||||
{step.roadmapLearningGoal ? (
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '4px 0 0' }}>
|
||||
Ziel: {step.roadmapLearningGoal}
|
||||
</p>
|
||||
) : null}
|
||||
<div style={{ fontSize: '13px' }}>
|
||||
<strong>{step.exerciseTitle}</strong>
|
||||
{step.exerciseId ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user