From 614c2dcfaa01c9345cbb862f7db5391a66d611fb Mon Sep 17 00:00:00 2001
From: Lars
Date: Fri, 22 May 2026 22:38:21 +0200
Subject: [PATCH] Enhance Planning Exercise Suggestion with Client Context and
Group ID Support
- Made `unit_id` and `group_id` optional in `PlanningExerciseSuggestRequest` to support client context without a saved unit.
- Refactored `_load_group_recent_exercise_ids` to handle cases where `exclude_unit_id` is optional.
- Introduced `build_client_planning_context_pack` for improved context handling in client-free searches.
- Updated `suggest_planning_exercises` to utilize the new client context pack when `unit_id` is not provided.
- Incremented version to 0.8.172 and updated changelog to reflect these enhancements in the planning AI capabilities.
---
backend/planning_exercise_suggest.py | 123 +++++++++++++++---
backend/version.py | 12 +-
.../src/components/ExercisePickerModal.jsx | 96 +++++++++++---
.../components/TrainingUnitSectionsEditor.jsx | 34 ++++-
.../planning/TrainingUnitFormShell.jsx | 2 +
.../TrainingFrameworkProgramEditPage.jsx | 86 ++++++++++--
frontend/src/pages/TrainingUnitEditPage.jsx | 44 +++++--
7 files changed, 330 insertions(+), 67 deletions(-)
diff --git a/backend/planning_exercise_suggest.py b/backend/planning_exercise_suggest.py
index 47aef4a..8f5617b 100644
--- a/backend/planning_exercise_suggest.py
+++ b/backend/planning_exercise_suggest.py
@@ -48,7 +48,8 @@ _LLM_RERANK_PRE_LIMIT = 32
class PlanningExerciseSuggestRequest(BaseModel):
- unit_id: int = Field(..., ge=1)
+ unit_id: Optional[int] = Field(default=None, ge=1)
+ group_id: Optional[int] = Field(default=None, ge=1)
section_order_index: Optional[int] = Field(default=None, ge=0)
phase_order_index: Optional[int] = Field(default=None, ge=0)
parallel_stream_order_index: Optional[int] = Field(default=None, ge=0)
@@ -196,26 +197,42 @@ def _load_progression_successors(
def _load_group_recent_exercise_ids(
cur,
group_id: Optional[int],
- exclude_unit_id: int,
+ exclude_unit_id: Optional[int] = None,
limit: int = 40,
) -> Set[int]:
if not group_id:
return set()
- cur.execute(
- """
- SELECT tusi.exercise_id AS eid
- FROM training_units tu
- INNER JOIN training_unit_sections tus ON tus.training_unit_id = tu.id
- INNER JOIN training_unit_section_items tusi ON tusi.section_id = tus.id
- WHERE tu.group_id = %s
- AND tu.id <> %s
- AND tusi.exercise_id IS NOT NULL
- AND COALESCE(tu.status, '') <> 'cancelled'
- ORDER BY tu.planned_date DESC NULLS LAST, tu.id DESC, tusi.order_index DESC
- LIMIT 200
- """,
- (int(group_id), int(exclude_unit_id)),
- )
+ if exclude_unit_id is not None:
+ cur.execute(
+ """
+ SELECT tusi.exercise_id AS eid
+ FROM training_units tu
+ INNER JOIN training_unit_sections tus ON tus.training_unit_id = tu.id
+ INNER JOIN training_unit_section_items tusi ON tusi.section_id = tus.id
+ WHERE tu.group_id = %s
+ AND tu.id <> %s
+ AND tusi.exercise_id IS NOT NULL
+ AND COALESCE(tu.status, '') <> 'cancelled'
+ ORDER BY tu.planned_date DESC NULLS LAST, tu.id DESC, tusi.order_index DESC
+ LIMIT 200
+ """,
+ (int(group_id), int(exclude_unit_id)),
+ )
+ else:
+ cur.execute(
+ """
+ SELECT tusi.exercise_id AS eid
+ FROM training_units tu
+ INNER JOIN training_unit_sections tus ON tus.training_unit_id = tu.id
+ INNER JOIN training_unit_section_items tusi ON tusi.section_id = tus.id
+ WHERE tu.group_id = %s
+ AND tusi.exercise_id IS NOT NULL
+ AND COALESCE(tu.status, '') <> 'cancelled'
+ ORDER BY tu.planned_date DESC NULLS LAST, tu.id DESC, tusi.order_index DESC
+ LIMIT 200
+ """,
+ (int(group_id),),
+ )
out: Set[int] = set()
for r in cur.fetchall():
if r.get("eid") is None:
@@ -364,13 +381,82 @@ def build_planning_exercise_context_pack(
}
+def build_client_planning_context_pack(
+ cur,
+ *,
+ tenant: TenantContext,
+ body: PlanningExerciseSuggestRequest,
+) -> Dict[str, Any]:
+ """Freie / Client-Kontext-Suche ohne persistierte training_units.id (Formular, Rahmen-Slot)."""
+ role = tenant.global_role
+ if not _has_planning_role(role):
+ raise HTTPException(status_code=403, detail="Nur Trainer dürfen Planungs-Vorschläge abrufen")
+
+ planned_ids: List[int] = []
+ if body.planned_exercise_ids:
+ seen: Set[int] = set()
+ for raw in body.planned_exercise_ids:
+ try:
+ eid = int(raw)
+ except (TypeError, ValueError):
+ continue
+ if eid < 1 or eid in seen:
+ continue
+ seen.add(eid)
+ planned_ids.append(eid)
+
+ anchor_id = _resolve_anchor_from_plan(planned_ids, body.anchor_exercise_id)
+ anchor_skills = _load_skill_ids_for_exercise(cur, anchor_id)
+ progression_ids, progression_notes = _load_progression_successors(
+ cur, body.progression_graph_id, anchor_id
+ )
+
+ group_id = body.group_id
+ group_name = None
+ if group_id:
+ cur.execute("SELECT name FROM training_groups WHERE id = %s", (int(group_id),))
+ gr = cur.fetchone()
+ if gr:
+ group_name = (gr.get("name") or "").strip() or None
+
+ group_recent = _load_group_recent_exercise_ids(cur, group_id, exclude_unit_id=None)
+ titles = _load_exercise_titles(cur, [x for x in [anchor_id] if x])
+ anchor_title = titles.get(anchor_id) if anchor_id else None
+
+ return {
+ "unit_id": None,
+ "unit": {
+ "id": None,
+ "framework_slot_id": None,
+ "origin_framework_slot_id": None,
+ },
+ "unit_title": None,
+ "group_id": group_id,
+ "group_name": group_name,
+ "section_order_index": body.section_order_index,
+ "section_title": None,
+ "planned_exercise_ids": planned_ids,
+ "anchor_exercise_id": anchor_id,
+ "anchor_title": anchor_title,
+ "anchor_skill_ids": sorted(anchor_skills),
+ "progression_graph_id": body.progression_graph_id,
+ "progression_successor_ids": sorted(progression_ids),
+ "progression_edge_notes": progression_notes,
+ "group_recent_exercise_ids": sorted(group_recent),
+ "context_mode": "client_free",
+ }
+
+
def suggest_planning_exercises(
cur,
*,
tenant: TenantContext,
body: PlanningExerciseSuggestRequest,
) -> Dict[str, Any]:
- pack = build_planning_exercise_context_pack(cur, tenant=tenant, body=body)
+ if body.unit_id:
+ pack = build_planning_exercise_context_pack(cur, tenant=tenant, body=body)
+ else:
+ pack = build_client_planning_context_pack(cur, tenant=tenant, body=body)
pack = _apply_client_planned_override(cur, pack, body)
query = _normalize_query(body.query)
heuristic_intent = resolve_planning_exercise_intent(query, body.intent_hint)
@@ -601,6 +687,7 @@ def suggest_planning_exercises(
"anchor_title": pack.get("anchor_title"),
"anchor_exercise_id": pack.get("anchor_exercise_id"),
"progression_graph_id": pack.get("progression_graph_id"),
+ "context_mode": pack.get("context_mode") or ("unit" if pack.get("unit_id") else "client_free"),
}
return {
diff --git a/backend/version.py b/backend/version.py
index c0cb927..4808aa8 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
-APP_VERSION = "0.8.171"
+APP_VERSION = "0.8.172"
BUILD_DATE = "2026-05-22"
DB_SCHEMA_VERSION = "20260531073"
@@ -28,7 +28,7 @@ MODULE_VERSIONS = {
"skill_profiles": "1.0.0", # Phase 3: gewichtetes Fähigkeiten-Profil + skill-discovery/suggestions
"methods": "0.1.0",
"exercises": "2.37.0", # Planungs-KI P1: Szenario-Pipeline + Query-Intent-Overlay
- "planning_exercise_suggest": "0.4.0", # include_llm_intent, scenario_kind, query_intent_summary
+ "planning_exercise_suggest": "0.4.1", # unit_id optional; client_free Kontext; group_id
"training_units": "0.4.0", # POST .../publish-to-framework: Ablauf aus geplanter Einheit → Rahmen-Slot-Blueprint
"training_programs": "0.1.0",
"planning": "0.15.0", # Vorlagen: Strukturvorschau, Bearbeiten inkl. Split-Sessions + Beschreibung
@@ -43,6 +43,14 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
+ {
+ "version": "0.8.172",
+ "date": "2026-05-22",
+ "changes": [
+ "Planungs-KI UI: Menüpunkt „Planungs-KI: Übung vorschlagen“ im +-Dialog; Freitext-Suche ohne gespeicherte unit_id (client_free).",
+ "API exercise-suggest: unit_id optional, group_id für Client-Kontext.",
+ ],
+ },
{
"version": "0.8.171",
"date": "2026-05-22",
diff --git a/frontend/src/components/ExercisePickerModal.jsx b/frontend/src/components/ExercisePickerModal.jsx
index 7a28d26..e5a7518 100644
--- a/frontend/src/components/ExercisePickerModal.jsx
+++ b/frontend/src/components/ExercisePickerModal.jsx
@@ -40,7 +40,11 @@ export default function ExercisePickerModal({
enableQuickCreateDraft = false,
/** Gespeicherte training_units.id — aktiviert Planungs-KI (robuster als nur planningContext). */
planningUnitId = null,
- /** true auf TrainingUnitEditPage: Hinweis wenn Einheit noch keine ID hat. */
+ /** 'planning' = Planungs-KI-API; 'library' = Volltext-Bibliothek */
+ pickerMode = 'library',
+ /** Planungs-KI auch ohne gespeicherte unit_id (Client-Kontext / Freitext). */
+ enableFreePlanningSearch = false,
+ /** true auf TrainingUnitEditPage: Hinweis wenn Planungs-KI ohne Einheit und ohne Freitext. */
expectPlanningSearch = false,
/** Planungs-Kontext für KI-Suche (Abschnitt, Anker, Plan …) */
planningContext = null,
@@ -84,9 +88,11 @@ export default function ExercisePickerModal({
}, [planningUnitId, planningContext?.unitId])
const activePlanningContext = useMemo(() => {
- if (!resolvedPlanningUnitId) return null
- return {
- unitId: resolvedPlanningUnitId,
+ if (pickerMode !== 'planning') return null
+ const groupIdRaw = planningContext?.groupId
+ const groupId = Number(groupIdRaw)
+ const base = {
+ groupId: Number.isFinite(groupId) && groupId > 0 ? groupId : null,
sectionOrderIndex: planningContext?.sectionOrderIndex ?? 0,
phaseOrderIndex: planningContext?.phaseOrderIndex ?? null,
parallelStreamOrderIndex: planningContext?.parallelStreamOrderIndex ?? null,
@@ -97,10 +103,24 @@ export default function ExercisePickerModal({
: [],
intentHint: planningContext?.intentHint ?? null,
}
- }, [resolvedPlanningUnitId, planningContext])
+ if (!resolvedPlanningUnitId) {
+ if (!enableFreePlanningSearch && !planningContext) return null
+ return { unitId: null, ...base }
+ }
+ return {
+ unitId: resolvedPlanningUnitId,
+ ...base,
+ }
+ }, [pickerMode, resolvedPlanningUnitId, enableFreePlanningSearch, planningContext])
- const usePlanningSearch = resolvedPlanningUnitId != null
- const planningSearchBlocked = Boolean(expectPlanningSearch && !usePlanningSearch)
+ const usePlanningSearch = pickerMode === 'planning' && activePlanningContext != null
+ const useFreePlanningSearch = usePlanningSearch && !resolvedPlanningUnitId
+ const planningSearchBlocked = Boolean(
+ pickerMode === 'planning' &&
+ expectPlanningSearch &&
+ !resolvedPlanningUnitId &&
+ !enableFreePlanningSearch
+ )
/** Gemeinsamer Suchtext — in Planung nur ein Feld; in Bibliothek beide Felder kombiniert. */
const effectivePickerQuery = useMemo(() => {
@@ -306,8 +326,7 @@ export default function ExercisePickerModal({
try {
if (usePlanningSearch && activePlanningContext) {
const query = effectivePickerQuery
- const res = await api.suggestPlanningExercises({
- unit_id: Number(activePlanningContext.unitId),
+ const requestBody = {
section_order_index:
activePlanningContext.sectionOrderIndex != null
? Number(activePlanningContext.sectionOrderIndex)
@@ -336,13 +355,20 @@ export default function ExercisePickerModal({
.filter((x) => Number.isFinite(x) && x > 0)
: undefined,
include_llm_intent: Boolean(query),
- include_llm_rank: true,
+ include_llm_rank: Boolean(query),
query,
- intent_hint: activePlanningContext.intentHint || null,
+ intent_hint: activePlanningContext.intentHint || (useFreePlanningSearch && query ? 'free_search' : null),
limit: PAGE_SIZE,
exercise_kind_any:
Array.isArray(exerciseKindAny) && exerciseKindAny.length > 0 ? exerciseKindAny : undefined,
- })
+ }
+ if (resolvedPlanningUnitId) {
+ requestBody.unit_id = Number(resolvedPlanningUnitId)
+ }
+ if (activePlanningContext.groupId) {
+ requestBody.group_id = Number(activePlanningContext.groupId)
+ }
+ const res = await api.suggestPlanningExercises(requestBody)
setPlanningContextSummary(res?.context_summary || null)
setPlanningTargetProfileSummary(res?.target_profile_summary || null)
setPlanningLlmRankApplied(Boolean(res?.llm_rank_applied))
@@ -397,6 +423,8 @@ export default function ExercisePickerModal({
activePlanningContext,
effectivePickerQuery,
exerciseKindAny,
+ resolvedPlanningUnitId,
+ useFreePlanningSearch,
])
useEffect(() => {
@@ -643,12 +671,11 @@ export default function ExercisePickerModal({
lineHeight: 1.45,
}}
>
- Planungs-KI noch nicht verfügbar. Die Einheit hat noch keine gespeicherte ID —
- bitte zuerst Speichern , dann den Übungspicker erneut öffnen. Bis dahin gilt nur die
- Bibliothekssuche (Volltext).
+ Planungs-KI noch nicht verfügbar. Bitte zuerst Speichern oder den
+ Menüpunkt Planungs-KI: Übung vorschlagen nutzen (Freitext ohne gespeicherte Einheit).
) : null}
- {!usePlanningSearch && !planningSearchBlocked ? (
+ {pickerMode === 'library' && expectPlanningSearch ? (
- Bibliothekssuche (Volltext) — Planungs-KI mit
- Kontext (Einheit, Plan, Anker) gibt es in der{' '}
- Trainingseinheit bearbeiten , nach dem Speichern der Einheit.
+ Bibliothekssuche (Volltext) — für Planungs-KI mit
+ Kontext oder Freitext-Anfrage den Menüpunkt{' '}
+ Planungs-KI: Übung vorschlagen … unter dem + wählen.
+
+ ) : null}
+ {useFreePlanningSearch ? (
+
+ Freie Planungs-KI — Anker und bisherige Übungen aus
+ dem Formular; nach Speichern kommen Gruppe, Historie und Rahmen dazu.
+
+ ) : null}
+ {!usePlanningSearch && !planningSearchBlocked && !expectPlanningSearch ? (
+
+ Bibliothekssuche (Volltext)
) : null}
diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx
index 8c65d3c..1955d1e 100644
--- a/frontend/src/components/TrainingUnitSectionsEditor.jsx
+++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx
@@ -253,6 +253,7 @@ export default function TrainingUnitSectionsEditor({
sections,
onSectionsChange,
onRequestExercisePick,
+ onRequestPlanningExercisePick,
onRequestTrainingModulePick,
onPeekExercise,
showExecutionExtras = false,
@@ -2591,12 +2592,23 @@ export default function TrainingUnitSectionsEditor({
) : (
+ {onRequestPlanningExercisePick ? (
+
+ onRequestPlanningExercisePick?.({ sectionIndex: sIdx })
+ }
+ >
+ + Planungs-KI
+
+ ) : null}
onRequestExercisePick?.({ sectionIndex: sIdx })}
>
- + Übung
+ + Übung (Bibliothek)
+ {onRequestPlanningExercisePick ? (
+ {
+ const { sIdx, beforeIx } = insertChooser
+ closeInsertChooser()
+ onRequestPlanningExercisePick?.({
+ sectionIndex: sIdx,
+ insertBeforeIndex: beforeIx,
+ })
+ }}
+ >
+ Planungs-KI: Übung vorschlagen …
+
+ ) : null}
{
const { sIdx, beforeIx } = insertChooser
closeInsertChooser()
@@ -2819,7 +2847,7 @@ export default function TrainingUnitSectionsEditor({
})
}}
>
- Übung auswählen …
+ {onRequestPlanningExercisePick ? 'Übung aus Bibliothek …' : 'Übung auswählen …'}
{onRequestTrainingModulePick ? (
{
+ if (!sectionPickerCtx) return null
+ const { slotIdx, sectionIndex: sIdx, itemIndex: iIdx, insertBeforeIndex: beforeIx } = sectionPickerCtx
+ const slot = form.slots?.[slotIdx]
+ const secs = slot?.sections?.length ? slot.sections : [defaultSection('Ablauf')]
+ const sec = secs[sIdx]
+ let anchorExerciseId = null
+ if (sec?.items?.length) {
+ if (typeof iIdx === 'number') {
+ const item = sec.items[iIdx]
+ if (item?.exercise_id) {
+ anchorExerciseId = Number(item.exercise_id)
+ } else {
+ for (let i = iIdx - 1; i >= 0; i -= 1) {
+ if (sec.items[i]?.exercise_id) {
+ anchorExerciseId = Number(sec.items[i].exercise_id)
+ break
+ }
+ }
+ }
+ } else if (typeof beforeIx === 'number') {
+ for (let i = beforeIx - 1; i >= 0; i -= 1) {
+ if (sec.items[i]?.exercise_id) {
+ anchorExerciseId = Number(sec.items[i].exercise_id)
+ break
+ }
+ }
+ } else {
+ for (let i = sec.items.length - 1; i >= 0; i -= 1) {
+ if (sec.items[i]?.exercise_id) {
+ anchorExerciseId = Number(sec.items[i].exercise_id)
+ break
+ }
+ }
+ }
+ }
+ const plannedExerciseIds = []
+ const seenPlan = new Set()
+ for (const s of secs) {
+ for (const it of s?.items || []) {
+ if (String(it?.item_type || '').toLowerCase() === 'note') continue
+ const eid = Number(it?.exercise_id)
+ if (!Number.isFinite(eid) || eid < 1 || seenPlan.has(eid)) continue
+ seenPlan.add(eid)
+ plannedExerciseIds.push(eid)
+ }
+ }
+ return {
+ unitId: null,
+ groupId: null,
+ sectionOrderIndex: sIdx,
+ anchorExerciseId: Number.isFinite(anchorExerciseId) && anchorExerciseId > 0 ? anchorExerciseId : null,
+ progressionGraphId: null,
+ plannedExerciseIds,
+ }
+ }, [sectionPickerCtx, form.slots])
+
+ const openFrameworkExercisePicker = useCallback((slotIdx, ctx, pickerMode = 'library') => {
+ setSectionPickerCtx({
+ slotIdx,
+ sectionIndex: ctx.sectionIndex,
+ itemIndex: typeof ctx.itemIndex === 'number' ? ctx.itemIndex : undefined,
+ insertBeforeIndex:
+ typeof ctx.insertBeforeIndex === 'number' && Number.isFinite(ctx.insertBeforeIndex)
+ ? ctx.insertBeforeIndex
+ : undefined,
+ pickerMode,
+ })
+ }, [])
const [editingGoalIdx, setEditingGoalIdx] = useState(null)
const [goalMenuGi, setGoalMenuGi] = useState(null)
/** Nur schmal: Stammdaten | Plan (Ziele übereinander, darunter Slots) — Desktop: alles sichtbar */
@@ -911,17 +981,8 @@ export default function TrainingFrameworkProgramEditPage() {
),
}))
}}
- onRequestExercisePick={({ sectionIndex, itemIndex, insertBeforeIndex }) =>
- setSectionPickerCtx({
- slotIdx: si,
- sectionIndex,
- itemIndex: typeof itemIndex === 'number' ? itemIndex : undefined,
- insertBeforeIndex:
- typeof insertBeforeIndex === 'number' && Number.isFinite(insertBeforeIndex)
- ? insertBeforeIndex
- : undefined,
- })
- }
+ onRequestExercisePick={(ctx) => openFrameworkExercisePicker(si, ctx, 'library')}
+ onRequestPlanningExercisePick={(ctx) => openFrameworkExercisePicker(si, ctx, 'planning')}
onPeekExercise={(id, variantId) =>
setPeekCtx({ exerciseId: id, variantId: variantId ?? null })
}
@@ -1366,6 +1427,9 @@ export default function TrainingFrameworkProgramEditPage() {
open={sectionPickerCtx != null}
multiSelect
enableQuickCreateDraft
+ pickerMode={sectionPickerCtx?.pickerMode === 'planning' ? 'planning' : 'library'}
+ enableFreePlanningSearch
+ planningContext={frameworkPlanningContext}
onClose={() => setSectionPickerCtx(null)}
onSelectExercises={async (picked) => {
if (!sectionPickerCtx || !picked?.length) return
diff --git a/frontend/src/pages/TrainingUnitEditPage.jsx b/frontend/src/pages/TrainingUnitEditPage.jsx
index be15947..84ce9a7 100644
--- a/frontend/src/pages/TrainingUnitEditPage.jsx
+++ b/frontend/src/pages/TrainingUnitEditPage.jsx
@@ -124,9 +124,9 @@ export default function TrainingUnitEditPage() {
const [saveModuleOpen, setSaveModuleOpen] = useState(false)
const exercisePickerPlanningContext = useMemo(() => {
+ if (!exercisePickerTarget) return null
const resolvedUnitId =
editingUnit?.id ?? (Number.isFinite(unitId) && unitId > 0 ? unitId : null)
- if (!resolvedUnitId) return null
const target = exercisePickerTarget
const secs = formData.sections || []
const sIdx = target?.sIdx ?? 0
@@ -145,6 +145,14 @@ export default function TrainingUnitEditPage() {
}
}
}
+ } else if (typeof target?.insertBeforeIndex === 'number') {
+ const beforeIx = target.insertBeforeIndex
+ for (let i = beforeIx - 1; i >= 0; i -= 1) {
+ if (sec.items[i]?.exercise_id) {
+ anchorExerciseId = Number(sec.items[i].exercise_id)
+ break
+ }
+ }
} else {
for (let i = sec.items.length - 1; i >= 0; i -= 1) {
if (sec.items[i]?.exercise_id) {
@@ -165,14 +173,29 @@ export default function TrainingUnitEditPage() {
plannedExerciseIds.push(eid)
}
}
+ const groupIdRaw = Number(formData.group_id)
return {
- unitId: Number(resolvedUnitId),
+ unitId: resolvedUnitId ? Number(resolvedUnitId) : null,
+ groupId: Number.isFinite(groupIdRaw) && groupIdRaw > 0 ? groupIdRaw : null,
sectionOrderIndex: sIdx,
anchorExerciseId: Number.isFinite(anchorExerciseId) && anchorExerciseId > 0 ? anchorExerciseId : null,
progressionGraphId: null,
plannedExerciseIds,
}
- }, [editingUnit?.id, unitId, exercisePickerTarget, formData.sections])
+ }, [editingUnit?.id, unitId, exercisePickerTarget, formData.sections, formData.group_id])
+
+ const openExercisePicker = useCallback(({ sectionIndex, itemIndex, insertBeforeIndex, pickerMode = 'library' }) => {
+ setExercisePickerTarget({
+ sIdx: sectionIndex,
+ iIdx: typeof itemIndex === 'number' ? itemIndex : undefined,
+ insertBeforeIndex:
+ typeof insertBeforeIndex === 'number' && Number.isFinite(insertBeforeIndex)
+ ? insertBeforeIndex
+ : undefined,
+ pickerMode,
+ })
+ setExercisePickerOpen(true)
+ }, [])
const goBack = useCallback(() => {
goNavReturn(navigate, location, {
@@ -739,17 +762,8 @@ export default function TrainingUnitEditPage() {
onRequestPublishToFramework={() => editingUnit?.id && setPublishFrameworkOpen(true)}
onRequestSaveAsModule={() => editingUnit?.id && setSaveModuleOpen(true)}
onRequestTrainingModulePick={(ctx) => void openModuleApplyModal(ctx)}
- onRequestExercisePick={({ sectionIndex, itemIndex, insertBeforeIndex }) => {
- setExercisePickerTarget({
- sIdx: sectionIndex,
- iIdx: typeof itemIndex === 'number' ? itemIndex : undefined,
- insertBeforeIndex:
- typeof insertBeforeIndex === 'number' && Number.isFinite(insertBeforeIndex)
- ? insertBeforeIndex
- : undefined,
- })
- setExercisePickerOpen(true)
- }}
+ onRequestExercisePick={(ctx) => openExercisePicker({ ...ctx, pickerMode: 'library' })}
+ onRequestPlanningExercisePick={(ctx) => openExercisePicker({ ...ctx, pickerMode: 'planning' })}
onPeekExercise={(id, variantId, peekExtras) =>
setPlanningPeekCtx({ exerciseId: id, variantId: variantId ?? null, peekExtras: peekExtras ?? null })
}
@@ -804,6 +818,8 @@ export default function TrainingUnitEditPage() {
multiSelect
enableQuickCreateDraft
expectPlanningSearch
+ pickerMode={exercisePickerTarget?.pickerMode === 'planning' ? 'planning' : 'library'}
+ enableFreePlanningSearch
planningUnitId={
editingUnit?.id ??
(Number.isFinite(unitId) && unitId > 0 ? unitId : null)