From 98b279fa89f0781c246787b22d48bb258a131ba5 Mon Sep 17 00:00:00 2001
From: Lars
Date: Wed, 10 Jun 2026 07:14:18 +0200
Subject: [PATCH] Update version to 0.8.216 and enhance Exercise Progression
Path Builder
- Incremented application version to 0.8.216 to reflect recent changes.
- Added skill expectations handling in the Exercise Progression Path Builder, improving the integration of expected skills into the roadmap steps.
- Enhanced the mapping of major steps to include load profiles, success criteria, anti-patterns, and exercise types, enriching the user experience and functionality.
---
.../test_planning_exercise_path_builder.py | 18 +
backend/version.py | 2 +-
.../ExerciseProgressionPathBuilder.jsx | 314 ++++++++++++++----
3 files changed, 272 insertions(+), 62 deletions(-)
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 (
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+
))}
@@ -1361,6 +1528,30 @@ export default function ExerciseProgressionPathBuilder({
Ziel: {step.roadmapLearningGoal}
) : null}
+ {formatExpectedSkillNames(step.skillExpectations).length ? (
+
+ {formatExpectedSkillNames(step.skillExpectations).map((name) => (
+
+ {name}
+
+ ))}
+
+ ) : null}
{step.exerciseTitle}
{step.exerciseId ? (
@@ -1454,6 +1645,7 @@ export default function ExerciseProgressionPathBuilder({
setPathQa(null)
setGapFillOffers([])
setProgressionRoadmap(null)
+ setPathSkillExpectations(null)
}}
>
Vorschlag verwerfen