From 0adf20c9e10e3fadb2082289c0e7749f84b84996 Mon Sep 17 00:00:00 2001
From: Lars
Date: Wed, 10 Jun 2026 06:54:49 +0200
Subject: [PATCH] Enhance Gap Planning Context with Stage Overrides and Trainer
Supplements
- Added `stage_learning_goal_override` and `gap_trainer_supplements` parameters to `build_progression_path_gap_planning_context`, allowing for customized learning goals and additional trainer notes.
- Updated `gapOfferContextDisplayLines` to include trainer supplements in the context display.
- Enhanced `ExerciseProgressionPathBuilder` to utilize new parameters for improved gap fill offer handling.
- Incremented application version to 0.8.214 to reflect these changes.
---
backend/planning_exercise_form_context.py | 7 +
.../test_planning_exercise_form_context.py | 11 ++
backend/version.py | 2 +-
.../ExerciseProgressionPathBuilder.jsx | 134 ++++++++++---
.../exercises/ExerciseGapFillPrepModal.jsx | 186 ++++++++++++++++++
.../src/utils/planningContextForExerciseAi.js | 13 +-
6 files changed, 323 insertions(+), 30 deletions(-)
create mode 100644 frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
diff --git a/backend/planning_exercise_form_context.py b/backend/planning_exercise_form_context.py
index 3394261..6972c93 100644
--- a/backend/planning_exercise_form_context.py
+++ b/backend/planning_exercise_form_context.py
@@ -168,6 +168,8 @@ def build_progression_path_gap_planning_context(
resolved_structured: Optional[Mapping[str, Any]] = None,
stage_spec: Optional[Mapping[str, Any]] = None,
semantic_brief: Optional[Mapping[str, Any]] = None,
+ stage_learning_goal_override: Optional[str] = None,
+ gap_trainer_supplements: Optional[str] = None,
) -> Dict[str, Any]:
"""Kontext für KI-Neuanlage aus Progressionsgraph-Pfad-Lücke."""
offer = offer or {}
@@ -205,6 +207,11 @@ def build_progression_path_gap_planning_context(
semantic_brief=semantic_brief,
)
ctx.update(snap)
+ if stage_learning_goal_override and stage_learning_goal_override.strip():
+ ctx["stage_learning_goal"] = _trim_str(stage_learning_goal_override, limit=1200)
+ ctx["roadmap_learning_goal"] = ctx["stage_learning_goal"]
+ if gap_trainer_supplements and gap_trainer_supplements.strip():
+ ctx["gap_trainer_supplements"] = _trim_str(gap_trainer_supplements, limit=2000)
return sanitize_planning_context_for_ai(ctx)
diff --git a/backend/tests/test_planning_exercise_form_context.py b/backend/tests/test_planning_exercise_form_context.py
index 772e2ff..3079065 100644
--- a/backend/tests/test_planning_exercise_form_context.py
+++ b/backend/tests/test_planning_exercise_form_context.py
@@ -78,3 +78,14 @@ def test_gap_planning_context_carries_snapshot_fields():
)
assert ctx["start_situation"] == "Start"
assert ctx["stage_learning_goal"] == "Stufenziel"
+
+
+def test_gap_planning_context_trainer_supplements_and_stage_override():
+ ctx = build_progression_path_gap_planning_context(
+ goal_query="Kumite",
+ stage_spec={"learning_goal": "Original"},
+ stage_learning_goal_override="Angepasstes Stufenziel",
+ gap_trainer_supplements="Nur Partnerübung, Kindergruppe",
+ )
+ assert ctx["stage_learning_goal"] == "Angepasstes Stufenziel"
+ assert ctx["gap_trainer_supplements"] == "Nur Partnerübung, Kindergruppe"
diff --git a/backend/version.py b/backend/version.py
index f3eec53..e55663f 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.213"
+APP_VERSION = "0.8.214"
BUILD_DATE = "2026-06-07"
DB_SCHEMA_VERSION = "20260607087"
diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx
index 6b3f535..7288ec8 100644
--- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx
+++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx
@@ -4,6 +4,7 @@
import React, { useCallback, useEffect, useState } from 'react'
import api from '../utils/api'
import ExerciseAiQuickCreateModal from './exercises/ExerciseAiQuickCreateModal'
+import ExerciseGapFillPrepModal from './exercises/ExerciseGapFillPrepModal'
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
import {
aiPreviewToQuickCreateDraft,
@@ -13,6 +14,7 @@ import {
import {
buildPathGapPlanningContextForAi,
gapOfferContextDisplayLines,
+ initialStageLearningGoalFromOffer,
} from '../utils/planningContextForExerciseAi'
function applyResolvedStructuredFromRoadmap(progressionRoadmap, setters) {
@@ -252,6 +254,12 @@ export default function ExerciseProgressionPathBuilder({
const [quickSaving, setQuickSaving] = useState(false)
const [quickAiError, setQuickAiError] = useState('')
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
+ const [gapPrepOpen, setGapPrepOpen] = useState(false)
+ const [gapPrepTitle, setGapPrepTitle] = useState('')
+ const [gapPrepStageGoal, setGapPrepStageGoal] = useState('')
+ const [gapPrepSupplements, setGapPrepSupplements] = useState('')
+ const [gapPrepFocusAreaId, setGapPrepFocusAreaId] = useState('')
+ const [gapPrepError, setGapPrepError] = useState('')
useEffect(() => {
let cancelled = false
@@ -391,13 +399,6 @@ export default function ExerciseProgressionPathBuilder({
setQuickAiError('')
}
- const handleGapFillClick = async (offer) => {
- if (!confirmPathExpansionIfNeeded(offer, pathSteps.length, maxSteps, setMaxSteps)) {
- return
- }
- await runGapFillAiSuggest(offer)
- }
-
const gapContextFallbackParams = {
goalQuery,
semanticBrief,
@@ -410,21 +411,70 @@ export default function ExerciseProgressionPathBuilder({
roadmapNotes,
}
- const runGapFillAiSuggest = async (offer) => {
- const title = (offer?.title_hint || '').trim()
- if (title.length < 3) {
- alert('Titel-Hinweis fehlt — bitte Pfad erneut vorschlagen.')
+ const closeGapFillPrep = () => {
+ if (quickSaving) return
+ setGapPrepOpen(false)
+ setGapPrepError('')
+ }
+
+ const openGapFillPrep = (offer) => {
+ const defaultFocus = resolveDefaultFocusAreaId(targetSummary, focusAreas)
+ setActiveOffer(offer)
+ setGapPrepTitle((offer?.title_hint || '').trim())
+ setGapPrepStageGoal(initialStageLearningGoalFromOffer(offer, gapContextFallbackParams))
+ setGapPrepSupplements('')
+ setGapPrepFocusAreaId(defaultFocus ? String(defaultFocus) : '')
+ setActivePlanningContextLines(gapOfferContextDisplayLines(offer, gapContextFallbackParams))
+ setGapPrepError('')
+ setGapPrepOpen(true)
+ }
+
+ const handleGapFillClick = (offer) => {
+ if (!confirmPathExpansionIfNeeded(offer, pathSteps.length, maxSteps, setMaxSteps)) {
return
}
- const goalText = (offer?.goal_for_ai || offer?.sketch || '').trim()
- const focusId = resolveDefaultFocusAreaId(targetSummary, focusAreas)
+ openGapFillPrep(offer)
+ }
+
+ const submitGapFillPrep = async () => {
+ const title = (gapPrepTitle || '').trim()
+ if (title.length < 3) {
+ alert('Titel: mindestens 3 Zeichen.')
+ return
+ }
+ const focusId = parseInt(String(gapPrepFocusAreaId).trim(), 10)
+ if (!Number.isFinite(focusId) || focusId < 1) {
+ alert('Bitte einen Fokusbereich wählen.')
+ return
+ }
+ if (!activeOffer) return
+ setGapPrepError('')
+ await runGapFillAiSuggest(activeOffer, {
+ title,
+ stageLearningGoal: (gapPrepStageGoal || '').trim(),
+ supplements: (gapPrepSupplements || '').trim(),
+ focusAreaId: focusId,
+ })
+ }
+
+ const runGapFillAiSuggest = async (offer, prep = null) => {
+ const title = (prep?.title || offer?.title_hint || '').trim()
+ if (title.length < 3) {
+ alert('Titel: mindestens 3 Zeichen.')
+ return
+ }
+ const supplements = (prep?.supplements || '').trim()
+ const stageGoal = (prep?.stageLearningGoal || '').trim()
+ let goalText = (offer?.goal_for_ai || offer?.sketch || '').trim()
+ if (supplements) {
+ goalText = `${goalText}\n\nTrainer-Ergänzungen für diese Übung:\n${supplements}`.trim()
+ }
+ const focusId =
+ prep?.focusAreaId != null && Number.isFinite(Number(prep.focusAreaId))
+ ? Number(prep.focusAreaId)
+ : resolveDefaultFocusAreaId(targetSummary, focusAreas)
if (!focusId) {
- alert('Kein Fokusbereich verfügbar — bitte Kataloge laden oder manuell wählen.')
- setQuickTitle(title)
- setQuickSketch(goalText)
- setQuickFocusAreaId('')
- setActiveOffer(offer)
- setQuickCreateOpen(true)
+ alert('Kein Fokusbereich verfügbar — bitte im Vorbereitungsdialog wählen.')
return
}
const focusRow = (focusAreas || []).find((x) => Number(x.id) === focusId)
@@ -438,11 +488,16 @@ export default function ExerciseProgressionPathBuilder({
setQuickCreateDraft(null)
setQuickSaving(true)
setGeneratingOfferId(offer?.offer_id || null)
- const contextLines = gapOfferContextDisplayLines(offer, gapContextFallbackParams)
+ const contextParams = {
+ ...gapContextFallbackParams,
+ stageLearningGoalOverride: stageGoal,
+ gapTrainerSupplements: supplements,
+ }
+ const contextLines = gapOfferContextDisplayLines(offer, contextParams)
setActivePlanningContextLines(contextLines)
const planningContext = buildPathGapPlanningContextForAi({
offer,
- ...gapContextFallbackParams,
+ ...contextParams,
})
try {
const aiRes = await api.suggestExerciseAi({
@@ -450,7 +505,7 @@ export default function ExerciseProgressionPathBuilder({
goal: goalText || undefined,
execution: '',
preparation: '',
- trainer_notes: '',
+ trainer_notes: supplements || '',
focus_area_hint: focusHint || undefined,
focus_areas_context: [{ focus_area_id: focusId, is_primary: true }],
planning_context: planningContext || undefined,
@@ -469,12 +524,13 @@ export default function ExerciseProgressionPathBuilder({
sketchPlain: goalText,
}),
)
+ setGapPrepOpen(false)
setQuickCreateOpen(false)
} catch (e) {
console.error(e)
const msg = e?.message || String(e)
+ setGapPrepError(msg)
setQuickAiError(msg)
- setQuickCreateOpen(true)
} finally {
setQuickSaving(false)
setGeneratingOfferId(null)
@@ -1205,7 +1261,7 @@ export default function ExerciseProgressionPathBuilder({
Fehlende oder zu ersetzende Schritte ({pathSteps.length}/{maxSteps} im Pfad).
{pathSteps.length >= maxSteps
? ' Der Pfad ist voll — beim Einfügen können Sie die Pfadlänge dynamisch vergrößern (ohne neuen Vorschlag); Ersatz-Angebote ersetzen einen Schritt.'
- : ' „Mit KI anlegen“ erzeugt einen vollständigen Entwurf und fügt die Übung ein.'}
+ : ' Zuerst Kontext prüfen und ergänzen, dann KI-Entwurf erstellen und einfügen.'}
{gapFillOffers.map((offer) => (
@@ -1255,12 +1311,12 @@ export default function ExerciseProgressionPathBuilder({
: 'Pfad voll — Klick fragt, ob die Pfadlänge vergrößert werden soll'
: offer.replace_step_index != null
? 'Ersetzt den themenfremden Schritt im Pfad'
- : 'KI-Entwurf mit Pfad-Kontext generieren'
+ : 'Kontext prüfen, Ergänzungen mitgeben, dann KI-Entwurf'
}
>
{generatingOfferId === offer.offer_id
? 'KI erstellt Entwurf …'
- : 'Mit KI anlegen'}
+ : 'Vorbereiten & KI anlegen'}
@@ -1406,6 +1462,25 @@ export default function ExerciseProgressionPathBuilder({
>
) : null}
+
+
{
setQuickCreateDraft(null)
- setActivePlanningContextLines([])
- if (activeOffer) setQuickCreateOpen(true)
+ if (activeOffer) {
+ setGapPrepOpen(true)
+ } else {
+ setActivePlanningContextLines([])
+ }
}}
planningContextLines={activePlanningContextLines}
onApply={applyQuickCreateDraft}
diff --git a/frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx b/frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
new file mode 100644
index 0000000..196982a
--- /dev/null
+++ b/frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
@@ -0,0 +1,186 @@
+import React, { useEffect } from 'react'
+
+/**
+ * Vorbereitung vor KI-Übungsanlage aus Pfad-Lücke: Kontext prüfen, Ergänzungen mitgeben.
+ */
+export default function ExerciseGapFillPrepModal({
+ open,
+ onClose,
+ offer = null,
+ contextLines = [],
+ title = '',
+ onTitleChange,
+ stageLearningGoal = '',
+ onStageLearningGoalChange,
+ supplements = '',
+ onSupplementsChange,
+ focusAreaId = '',
+ onFocusAreaChange,
+ focusAreas = [],
+ busy = false,
+ error = '',
+ onSubmit,
+}) {
+ useEffect(() => {
+ if (!open) return undefined
+ const onKey = (e) => {
+ if (e.key === 'Escape' && !busy) {
+ e.preventDefault()
+ onClose()
+ }
+ }
+ window.addEventListener('keydown', onKey)
+ return () => window.removeEventListener('keydown', onKey)
+ }, [open, busy, onClose])
+
+ if (!open || !offer) return null
+
+ const readOnlyLines = (contextLines || []).filter(
+ (line) => line.label !== 'Stufen-Lernziel',
+ )
+
+ return (
+ {
+ if (e.target === e.currentTarget && !busy) onClose()
+ }}
+ >
+
e.stopPropagation()}
+ style={{ maxWidth: '680px' }}
+ >
+
+
+ Übung mit KI vorbereiten
+
+
+ Schließen
+
+
+
+
+ Prüfen und anpassen, was an die KI geht. Ergänzungen fließen in Ziel, Trainerhinweise und
+ Planungskontext ein — erst danach wird der Entwurf erzeugt.
+
+
+ {readOnlyLines.length > 0 ? (
+
+
Pfad-Kontext (aus Roadmap)
+
+ {readOnlyLines.map(({ label, value }) => (
+
+
{label}
+ {value}
+
+ ))}
+
+
+ ) : null}
+
+
+
+
+ Titel-Vorschlag *
+
+ onTitleChange(e.target.value)}
+ disabled={busy}
+ maxLength={280}
+ />
+
+
+
+ Stufen-Lernziel (anpassbar)
+
+
+
+
+ Ergänzungen / Anpassungen für die KI
+
+
+
+
+ Fokusbereich *
+
+ onFocusAreaChange(e.target.value)}
+ disabled={busy}
+ >
+ Bitte wählen …
+ {(focusAreas || []).map((fa) => (
+
+ {fa.name}
+
+ ))}
+
+
+
+
+ {offer.from_title && offer.to_title ? (
+
+ Einordnung: zwischen „{offer.from_title}“ und „{offer.to_title}“
+
+ ) : null}
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+
+
+ Abbrechen
+
+
+ {busy ? 'KI erstellt Entwurf …' : 'KI-Entwurf erstellen'}
+
+
+
+
+ )
+}
diff --git a/frontend/src/utils/planningContextForExerciseAi.js b/frontend/src/utils/planningContextForExerciseAi.js
index 3896470..345859b 100644
--- a/frontend/src/utils/planningContextForExerciseAi.js
+++ b/frontend/src/utils/planningContextForExerciseAi.js
@@ -53,6 +53,8 @@ export function buildPathGapPlanningContextForAi({
startSituation = '',
targetState = '',
roadmapNotes = '',
+ stageLearningGoalOverride = '',
+ gapTrainerSupplements = '',
} = {}) {
const afterIdx = Number(offer?.insert_after_index)
const stepA = Number.isFinite(afterIdx) && afterIdx >= 0 ? pathSteps[afterIdx] : null
@@ -105,7 +107,9 @@ export function buildPathGapPlanningContextForAi({
start_situation: start,
target_state: target,
roadmap_notes: notes,
- stage_learning_goal: stageSpec?.learning_goal || null,
+ stage_learning_goal:
+ (stageLearningGoalOverride || '').trim() || stageSpec?.learning_goal || null,
+ gap_trainer_supplements: (gapTrainerSupplements || '').trim() || null,
stage_phase: stageSpec?.phase || majorStep?.phase || null,
stage_exercise_type: stageSpec?.exercise_type || null,
stage_load_profile: Array.isArray(stageSpec?.load_profile)
@@ -158,5 +162,12 @@ export function gapOfferContextDisplayLines(offer, fallbackParams = null) {
if (Array.isArray(raw.skill_hints) && raw.skill_hints.length) {
push('Fähigkeiten-Fokus', raw.skill_hints.slice(0, 3).join(' · '))
}
+ push('Trainer-Ergänzungen', raw.gap_trainer_supplements)
return lines
}
+
+export function initialStageLearningGoalFromOffer(offer, fallbackParams = null) {
+ const lines = gapOfferContextDisplayLines(offer, fallbackParams)
+ const hit = lines.find((l) => l.label === 'Stufen-Lernziel')
+ return hit?.value || (offer?.title_hint || '').trim()
+}