Enhance ExercisePickerModal with Improved Planning Context Handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m39s

- Introduced `planningUnitId` and `expectPlanningSearch` props to better manage planning context for exercise suggestions.
- Refactored logic to resolve planning unit ID and construct active planning context, enhancing the accuracy of exercise suggestions.
- Implemented checks to block planning search when necessary, providing clearer user feedback in the UI.
- Updated `TrainingUnitEditPage` to pass the correct planning unit ID, ensuring seamless integration with the exercise picker.
This commit is contained in:
Lars 2026-05-22 22:30:29 +02:00
parent d019c20338
commit f5c886fc13
2 changed files with 88 additions and 15 deletions

View File

@ -38,7 +38,11 @@ export default function ExercisePickerModal({
multiSelect = false,
onSelectExercises = null,
enableQuickCreateDraft = false,
/** Planungs-Kontext für KI-Suche (TrainingUnitEditPage o. ä.) */
/** Gespeicherte training_units.id — aktiviert Planungs-KI (robuster als nur planningContext). */
planningUnitId = null,
/** true auf TrainingUnitEditPage: Hinweis wenn Einheit noch keine ID hat. */
expectPlanningSearch = false,
/** Planungs-Kontext für KI-Suche (Abschnitt, Anker, Plan …) */
planningContext = null,
/** Wenn gesetzt: z. B. ['simple'] oder ['combination'] — sonst alle Übungsarten */
exerciseKindAny = undefined,
@ -73,7 +77,30 @@ export default function ExercisePickerModal({
const [planningIntentResolved, setPlanningIntentResolved] = useState(null)
const pickerScrollRef = useRef(null)
const usePlanningSearch = Boolean(planningContext?.unitId && Number(planningContext.unitId) > 0)
const resolvedPlanningUnitId = useMemo(() => {
const raw = planningUnitId ?? planningContext?.unitId
const id = Number(raw)
return Number.isFinite(id) && id > 0 ? id : null
}, [planningUnitId, planningContext?.unitId])
const activePlanningContext = useMemo(() => {
if (!resolvedPlanningUnitId) return null
return {
unitId: resolvedPlanningUnitId,
sectionOrderIndex: planningContext?.sectionOrderIndex ?? 0,
phaseOrderIndex: planningContext?.phaseOrderIndex ?? null,
parallelStreamOrderIndex: planningContext?.parallelStreamOrderIndex ?? null,
anchorExerciseId: planningContext?.anchorExerciseId ?? null,
progressionGraphId: planningContext?.progressionGraphId ?? null,
plannedExerciseIds: Array.isArray(planningContext?.plannedExerciseIds)
? planningContext.plannedExerciseIds
: [],
intentHint: planningContext?.intentHint ?? null,
}
}, [resolvedPlanningUnitId, planningContext])
const usePlanningSearch = resolvedPlanningUnitId != null
const planningSearchBlocked = Boolean(expectPlanningSearch && !usePlanningSearch)
/** Gemeinsamer Suchtext — in Planung nur ein Feld; in Bibliothek beide Felder kombiniert. */
const effectivePickerQuery = useMemo(() => {
@ -264,32 +291,54 @@ export default function ExercisePickerModal({
const reload = useCallback(async () => {
if (!open || !catalogsReady) return
if (planningSearchBlocked) {
setList([])
setHasMore(false)
setPlanningContextSummary(null)
setPlanningTargetProfileSummary(null)
setPlanningLlmRankApplied(false)
setPlanningQueryIntentSummary(null)
setPlanningIntentResolved(null)
setLoading(false)
return
}
setLoading(true)
try {
if (usePlanningSearch) {
if (usePlanningSearch && activePlanningContext) {
const query = effectivePickerQuery
const res = await api.suggestPlanningExercises({
unit_id: Number(planningContext.unitId),
unit_id: Number(activePlanningContext.unitId),
section_order_index:
planningContext.sectionOrderIndex != null ? Number(planningContext.sectionOrderIndex) : null,
activePlanningContext.sectionOrderIndex != null
? Number(activePlanningContext.sectionOrderIndex)
: null,
phase_order_index:
planningContext.phaseOrderIndex != null ? Number(planningContext.phaseOrderIndex) : null,
activePlanningContext.phaseOrderIndex != null
? Number(activePlanningContext.phaseOrderIndex)
: null,
parallel_stream_order_index:
planningContext.parallelStreamOrderIndex != null
? Number(planningContext.parallelStreamOrderIndex)
activePlanningContext.parallelStreamOrderIndex != null
? Number(activePlanningContext.parallelStreamOrderIndex)
: null,
anchor_exercise_id:
planningContext.anchorExerciseId != null ? Number(planningContext.anchorExerciseId) : null,
activePlanningContext.anchorExerciseId != null
? Number(activePlanningContext.anchorExerciseId)
: null,
progression_graph_id:
planningContext.progressionGraphId != null ? Number(planningContext.progressionGraphId) : null,
activePlanningContext.progressionGraphId != null
? Number(activePlanningContext.progressionGraphId)
: null,
planned_exercise_ids:
Array.isArray(planningContext.plannedExerciseIds) && planningContext.plannedExerciseIds.length > 0
? planningContext.plannedExerciseIds.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0)
Array.isArray(activePlanningContext.plannedExerciseIds) &&
activePlanningContext.plannedExerciseIds.length > 0
? activePlanningContext.plannedExerciseIds
.map((x) => Number(x))
.filter((x) => Number.isFinite(x) && x > 0)
: undefined,
include_llm_intent: Boolean(query),
include_llm_rank: true,
query,
intent_hint: planningContext.intentHint || null,
intent_hint: activePlanningContext.intentHint || null,
limit: PAGE_SIZE,
exercise_kind_any:
Array.isArray(exerciseKindAny) && exerciseKindAny.length > 0 ? exerciseKindAny : undefined,
@ -344,7 +393,8 @@ export default function ExercisePickerModal({
catalogsReady,
queryBase,
usePlanningSearch,
planningContext,
planningSearchBlocked,
activePlanningContext,
effectivePickerQuery,
exerciseKindAny,
])
@ -580,7 +630,25 @@ export default function ExercisePickerModal({
) : null}
</div>
) : null}
{!usePlanningSearch ? (
{planningSearchBlocked ? (
<p
style={{
margin: '0 0 10px',
padding: '10px 12px',
borderRadius: '8px',
background: 'color-mix(in srgb, var(--danger) 12%, var(--surface2))',
border: '1px solid color-mix(in srgb, var(--danger) 35%, var(--border))',
fontSize: '13px',
color: 'var(--text1)',
lineHeight: 1.45,
}}
>
<strong>Planungs-KI noch nicht verfügbar.</strong> Die Einheit hat noch keine gespeicherte ID
bitte zuerst <strong>Speichern</strong>, dann den Übungspicker erneut öffnen. Bis dahin gilt nur die
Bibliothekssuche (Volltext).
</p>
) : null}
{!usePlanningSearch && !planningSearchBlocked ? (
<p
style={{
margin: '0 0 10px',

View File

@ -803,6 +803,11 @@ export default function TrainingUnitEditPage() {
open={exercisePickerOpen}
multiSelect
enableQuickCreateDraft
expectPlanningSearch
planningUnitId={
editingUnit?.id ??
(Number.isFinite(unitId) && unitId > 0 ? unitId : null)
}
planningContext={exercisePickerPlanningContext}
onClose={() => {
setExercisePickerOpen(false)