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.'}
+ 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 ? ( ++ Einordnung: zwischen „{offer.from_title}“ und „{offer.to_title}“ +
+ ) : null} + + {error ? ( ++ {error} +
+ ) : null} +