diff --git a/backend/tests/test_planning_exercise_path_builder.py b/backend/tests/test_planning_exercise_path_builder.py index dc9d598..84547f6 100644 --- a/backend/tests/test_planning_exercise_path_builder.py +++ b/backend/tests/test_planning_exercise_path_builder.py @@ -42,3 +42,21 @@ def test_annotate_roadmap_step_adds_metadata(): assert step["roadmap_phase"] == "grundlage" assert step["roadmap_match_source"] == "stage_spec" assert any("Roadmap:" in r for r in step["reasons"]) + + +def test_annotate_roadmap_step_adds_skill_expectations(): + spec = StageSpecArtifact(major_step_index=0, learning_goal="Timing und Distanz") + step = _annotate_roadmap_step( + {"exercise_id": 5, "title": "Test", "reasons": []}, + stage_spec=spec, + major_step=None, + skill_expectations={ + "scope": "progression_stage", + "expected_skills": [ + {"skill_id": 2, "skill_name": "Timing", "weight": 0.9}, + {"skill_id": 3, "skill_name": "Distanz", "weight": 0.8}, + ], + }, + ) + assert step["skill_expectations"]["expected_skills"][0]["skill_name"] == "Timing" + assert any("Fähigkeiten:" in r for r in step["reasons"]) diff --git a/backend/version.py b/backend/version.py index f266fd2..f33ed91 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.215" +APP_VERSION = "0.8.216" BUILD_DATE = "2026-06-07" DB_SCHEMA_VERSION = "20260607087" diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx index 7288ec8..52734f8 100644 --- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx +++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx @@ -107,6 +107,7 @@ function mapApiStepToRow(step) { step?.roadmap_major_step_index != null ? Number(step.roadmap_major_step_index) : null, roadmapPhase: step?.roadmap_phase || null, roadmapLearningGoal: step?.roadmap_learning_goal || null, + skillExpectations: step?.skill_expectations || null, } } @@ -137,16 +138,65 @@ const PATH_STEPS_HARD_MAX = 10 const ROADMAP_PHASES = ['einstieg', 'grundlage', 'vertiefung', 'anwendung', 'perfektion'] +const LOAD_PROFILE_OPTIONS = [ + 'koordination', + 'präzision', + 'kraft', + 'geschwindigkeit', + 'timing', + 'reaktion', + 'distanz', + 'gleichgewicht', + 'kime', + 'ausdauer', + 'beweglichkeit', +] + function mapMajorStepsFromApi(apiRoadmap) { const raw = apiRoadmap?.roadmap?.major_steps if (!Array.isArray(raw)) return [] - return raw.map((s, i) => ({ + const rows = 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 || '', + load_profile: [], + success_criteria: [], + anti_patterns: [], + exercise_type: '', })) + return mergeStageSpecsIntoMajorSteps(rows, apiRoadmap) +} + +function mergeStageSpecsIntoMajorSteps(rows, apiRoadmap) { + const specs = apiRoadmap?.stage_specs + if (!Array.isArray(specs) || !rows.length) return rows + return rows.map((row, i) => { + const spec = + specs.find((s) => Number(s.major_step_index) === i) || + specs.find((s) => Number(s.major_step_index) === row.index) || + specs[i] + if (!spec) return row + return { + ...row, + load_profile: Array.isArray(spec.load_profile) ? [...spec.load_profile] : [], + success_criteria: Array.isArray(spec.success_criteria) ? [...spec.success_criteria] : [], + anti_patterns: Array.isArray(spec.anti_patterns) ? [...spec.anti_patterns] : [], + exercise_type: (spec.exercise_type || '').trim(), + } + }) +} + +function linesToStringList(text) { + return String(text || '') + .split('\n') + .map((s) => s.trim()) + .filter(Boolean) +} + +function listToMultiline(arr) { + return Array.isArray(arr) ? arr.join('\n') : '' } function reindexMajorSteps(rows) { @@ -154,17 +204,35 @@ function reindexMajorSteps(rows) { } function majorStepsToOverridePayload(rows) { + const indexed = reindexMajorSteps(rows) return { - major_steps: reindexMajorSteps(rows).map((row) => ({ + major_steps: indexed.map((row) => ({ index: row.index, phase: row.phase || 'vertiefung', learning_goal: row.learning_goal.trim(), consolidates: row.consolidates || [], rationale: row.rationale || '', })), + stage_specs: indexed.map((row, i) => ({ + major_step_index: i, + learning_goal: row.learning_goal.trim(), + load_profile: Array.isArray(row.load_profile) ? row.load_profile : [], + exercise_type: (row.exercise_type || '').trim(), + success_criteria: Array.isArray(row.success_criteria) ? row.success_criteria : [], + anti_patterns: Array.isArray(row.anti_patterns) ? row.anti_patterns : [], + })), } } +function formatExpectedSkillNames(skillExpectations, limit = 4) { + const items = skillExpectations?.expected_skills + if (!Array.isArray(items)) return [] + return items + .map((s) => String(s?.skill_name || '').trim()) + .filter(Boolean) + .slice(0, limit) +} + /** Einfügen wächst den Pfad; Ersetzen (replace_step_index) nicht. */ function offerGrowsPath(offer) { const replaceIdx = offer?.replace_step_index @@ -234,6 +302,7 @@ export default function ExerciseProgressionPathBuilder({ const [pathSteps, setPathSteps] = useState([]) const [gapFillOffers, setGapFillOffers] = useState([]) const [progressionRoadmap, setProgressionRoadmap] = useState(null) + const [pathSkillExpectations, setPathSkillExpectations] = useState(null) const [editableMajorSteps, setEditableMajorSteps] = useState([]) const [roadmapDirty, setRoadmapDirty] = useState(false) const [loadingRoadmap, setLoadingRoadmap] = useState(false) @@ -331,6 +400,10 @@ export default function ExerciseProgressionPathBuilder({ learning_goal: '', consolidates: [], rationale: '', + load_profile: [], + success_criteria: [], + anti_patterns: [], + exercise_type: '', }, ]) }) @@ -629,8 +702,14 @@ export default function ExerciseProgressionPathBuilder({ : [], ) setProgressionRoadmap(res?.progression_roadmap || null) + setPathSkillExpectations(res?.path_skill_expectations || null) setRoadmapDirty(false) if (!segmentNotes.trim() && q) setSegmentNotes(q.slice(0, 400)) + if (res?.progression_roadmap?.stage_specs?.length) { + setEditableMajorSteps((prev) => + prev.length ? mergeStageSpecsIntoMajorSteps(prev, res.progression_roadmap) : prev, + ) + } } const applyStartTargetResponse = (res) => { @@ -737,12 +816,14 @@ export default function ExerciseProgressionPathBuilder({ setTargetSummary(null) setPathQa(null) setGapFillOffers([]) + setPathSkillExpectations(null) setRoadmapDirty(false) } catch (e) { console.error(e) setError(e.message || 'Roadmap-Vorschlag fehlgeschlagen') setEditableMajorSteps([]) setProgressionRoadmap(null) + setPathSkillExpectations(null) } finally { setLoadingRoadmap(false) } @@ -831,6 +912,7 @@ export default function ExerciseProgressionPathBuilder({ setPathQa(null) setGapFillOffers([]) setProgressionRoadmap(null) + setPathSkillExpectations(null) setEditableMajorSteps([]) setRoadmapDirty(false) if (typeof onSaved === 'function') await onSaved() @@ -1048,7 +1130,7 @@ export default function ExerciseProgressionPathBuilder({ ) : null} - {(semanticBrief || targetSummary) && pathSteps.length > 0 ? ( + {(semanticBrief || targetSummary || pathSkillExpectations) && pathSteps.length > 0 ? (
{semanticBrief?.primary_topic ? ( @@ -1067,6 +1149,16 @@ export default function ExerciseProgressionPathBuilder({ Fokus: {fa} ))} + {formatExpectedSkillNames(pathSkillExpectations, 5).map((name) => ( + + {name} + + ))}
) : null} @@ -1102,7 +1194,7 @@ export default function ExerciseProgressionPathBuilder({ : progressionRoadmap ? ' (heuristisch/KI)' : ''} - . Phasen und Lernziele anpassen, dann „Übungen matchen“. + . Phasen und Lernziele anpassen; optional Stufen-Details für Match und KI-Lücken, dann „Übungen matchen“.

{editableMajorSteps.map((step, idx) => ( @@ -1113,66 +1205,141 @@ export default function ExerciseProgressionPathBuilder({ borderRadius: '8px', background: 'var(--surface2)', border: '1px solid var(--border)', - display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', - gap: '10px', - alignItems: 'end', }} > -
- - -
-
- - patchMajorStep(idx, { learning_goal: e.target.value })} - placeholder="z. B. Grundstellung und Hüftmobilität für Mae Geri" - disabled={disabled || loading || saving} - /> -
-
- - - +
+
+ + +
+
+ + patchMajorStep(idx, { learning_goal: e.target.value })} + placeholder="z. B. Grundstellung und Hüftmobilität für Mae Geri" + disabled={disabled || loading || saving} + /> +
+
+ + + +
+
+ + Stufen-Details (Match & KI) + {step.load_profile?.length + ? ` · ${step.load_profile.slice(0, 2).join(', ')}` + : ''} + +
+
+ +
+ {LOAD_PROFILE_OPTIONS.map((opt) => { + const active = (step.load_profile || []).includes(opt) + return ( + + ) + })} +
+
+
+ +