Implement Phase C2 Enhancements for Exercise Suggestions
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 18s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 40s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 18s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
Test Suite / pytest-backend (pull_request) Successful in 38s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 13s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m13s
- Incremented version to 0.8.184, reflecting the implementation of Phase C2 features. - Added support for displaying variant lists and suggested variant names in exercise suggestions. - Enhanced the ExercisePickerModal to allow selection of exercise variants and improved handling of variant IDs. - Updated backend logic to enrich planning hits with variant metadata, ensuring accurate exercise variant selection. - Documented changes in the changelog to highlight the new capabilities in planning AI functionality.
This commit is contained in:
parent
b2157d8a40
commit
a34e748be5
|
|
@ -189,7 +189,7 @@ Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“:
|
|||
| **A** | Voll-Library Hybrid-Ranking | ✅ **0.8.177** |
|
||||
| **B** | Text-Signale guidance/Rahmen-Ziele | ✅ **0.8.181** |
|
||||
| **C1** | Graph auto-match + variantenbewusste Nachfolger | ✅ **0.8.183** |
|
||||
| **C2** | Varianten in Trefferliste / Picker | 🔲 |
|
||||
| **C2** | Varianten in Trefferliste / Picker | ✅ **0.8.184** |
|
||||
| **C3** | Graph-Builder (Ziel → Pfad → speichern) | 🔲 |
|
||||
| **D** | Neu-Anlage: Pack an `suggestExerciseAi` | 🔲 |
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ Wenn `hits` leer oder Trainer wählt „Mit KI anlegen“:
|
|||
|
||||
- **Ungespeicherte Plan-Änderungen:** ✅ Client übergibt `planned_exercise_ids[]` aus Formular (TrainingUnitEditPage).
|
||||
- **Progressionsgraph-ID:** ✅ Auto-Match vom Anker (**C1**); manuelle Auswahl in UI noch offen.
|
||||
- **Anker-Variante:** ✅ Client + DB (**C1**); Picker wählt Variante bei Treffer noch nicht (**C2**).
|
||||
- **Anker-Variante:** ✅ Client + DB (**C1**); Picker wählt Variante bei Treffer (**C2** — Dropdown + Graph-Vorschlag).
|
||||
- **Graph-Builder:** Ziel eingeben → aufbauende Übungen → in Graph speichern (**C3**) — Compound-Nutzen über viele Pläne.
|
||||
- **Varianten-Suche:** Library-Picker nutzt `include_variants`; Planungs-KI rankt primär **Übungsebene** — Varianten-Expansion nur gezielt (**C2**).
|
||||
- **Enrichment:** Superadmin-Tool für Skills; Datenqualität der Bibliothek entscheidend für Profil-Score.
|
||||
|
|
@ -419,8 +419,14 @@ Treffer: optional `hits[].suggested_variant_id`.
|
|||
|
||||
---
|
||||
|
||||
## 20. Phase C2 / C3 — Roadmap (offen)
|
||||
## 20. Phase C2 — Varianten in Treffern (0.8.184) ✅
|
||||
|
||||
**C2:** Varianten in Trefferliste / Picker-Auswahl bei Graph-Treffern.
|
||||
- API: `variants[]`, `suggested_variant_name` pro Treffer (Batch aus `exercise_variants`).
|
||||
- **`ExercisePickerModal`:** Dropdown pro Treffer; Graph-`suggested_variant_id` vorausgewählt; Übernahme setzt `exercise_variant_id`.
|
||||
- **`hydrateExercisePlanningRow`:** übernimmt `exercise_variant_id` / `suggested_variant_id` in die Planungszeile.
|
||||
|
||||
**C3:** Graph-Builder — Ziel eingeben, aufbauende Übungen vorschlagen, nach Review in Graph speichern (`POST …/edges/sequence`). Nutzen über viele Pläne hinweg.
|
||||
---
|
||||
|
||||
## 21. Phase C3 — Graph-Builder (Roadmap, offen)
|
||||
|
||||
Ziel eingeben → aufbauende Übungen vorschlagen → nach Review in Graph speichern (`POST …/edges/sequence`). Nutzen über viele Pläne hinweg.
|
||||
|
|
|
|||
|
|
@ -211,6 +211,79 @@ def _resolve_anchor_variant_id(
|
|||
return None
|
||||
|
||||
|
||||
def _enrich_planning_hits_with_variant_meta(
|
||||
cur,
|
||||
hits: Sequence[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Variantennamen und -listen für Treffer mit suggested_variant_id (Phase C2)."""
|
||||
if not hits:
|
||||
return []
|
||||
variant_ids: Set[int] = set()
|
||||
exercise_ids: Set[int] = set()
|
||||
for h in hits:
|
||||
exercise_ids.add(int(h["id"]))
|
||||
raw = h.get("suggested_variant_id")
|
||||
if raw is not None:
|
||||
try:
|
||||
vid = int(raw)
|
||||
except (TypeError, ValueError):
|
||||
vid = 0
|
||||
if vid > 0:
|
||||
variant_ids.add(vid)
|
||||
|
||||
names_by_variant: Dict[int, str] = {}
|
||||
if variant_ids:
|
||||
ph = ",".join(["%s"] * len(variant_ids))
|
||||
cur.execute(
|
||||
f"SELECT id, variant_name FROM exercise_variants WHERE id IN ({ph})",
|
||||
list(variant_ids),
|
||||
)
|
||||
for row in cur.fetchall():
|
||||
n = (row.get("variant_name") or "").strip()
|
||||
if n:
|
||||
names_by_variant[int(row["id"])] = n
|
||||
|
||||
variants_by_exercise: Dict[int, List[Dict[str, Any]]] = {}
|
||||
if exercise_ids:
|
||||
ph = ",".join(["%s"] * len(exercise_ids))
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT exercise_id, id, variant_name, sequence_order
|
||||
FROM exercise_variants
|
||||
WHERE exercise_id IN ({ph})
|
||||
ORDER BY exercise_id, sequence_order NULLS LAST, id
|
||||
""",
|
||||
list(exercise_ids),
|
||||
)
|
||||
for row in cur.fetchall():
|
||||
eid = int(row["exercise_id"])
|
||||
variants_by_exercise.setdefault(eid, []).append(
|
||||
{
|
||||
"id": int(row["id"]),
|
||||
"variant_name": (row.get("variant_name") or "").strip() or None,
|
||||
"sequence_order": row.get("sequence_order"),
|
||||
}
|
||||
)
|
||||
|
||||
out: List[Dict[str, Any]] = []
|
||||
for h in hits:
|
||||
item = dict(h)
|
||||
eid = int(item["id"])
|
||||
vars_for_ex = variants_by_exercise.get(eid) or []
|
||||
if vars_for_ex:
|
||||
item["variants"] = vars_for_ex
|
||||
raw_vid = item.get("suggested_variant_id")
|
||||
if raw_vid is not None:
|
||||
try:
|
||||
vid = int(raw_vid)
|
||||
except (TypeError, ValueError):
|
||||
vid = 0
|
||||
if vid > 0:
|
||||
item["suggested_variant_name"] = names_by_variant.get(vid)
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def _finalize_progression_context(
|
||||
cur,
|
||||
tenant: TenantContext,
|
||||
|
|
@ -728,6 +801,7 @@ def suggest_planning_exercises(
|
|||
hits = hits[: int(body.limit)]
|
||||
|
||||
hits = hits[: int(body.limit)]
|
||||
hits = _enrich_planning_hits_with_variant_meta(cur, hits)
|
||||
|
||||
context_summary = {
|
||||
"unit_title": pack.get("unit_title"),
|
||||
|
|
|
|||
43
backend/tests/test_planning_exercise_suggest_variants.py
Normal file
43
backend/tests/test_planning_exercise_suggest_variants.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""Tests Phase C2: Varianten-Metadaten in Planungs-Treffern."""
|
||||
from planning_exercise_suggest import _enrich_planning_hits_with_variant_meta
|
||||
|
||||
|
||||
class _Cur:
|
||||
def __init__(self, rows):
|
||||
self._rows = rows
|
||||
self.last_query = ''
|
||||
|
||||
def execute(self, query, params=None):
|
||||
self.last_query = query
|
||||
self._params = params
|
||||
|
||||
def fetchall(self):
|
||||
q = self.last_query
|
||||
if 'FROM exercise_variants WHERE id IN' in q:
|
||||
return [
|
||||
{'id': 7, 'variant_name': 'Leicht'},
|
||||
]
|
||||
if 'FROM exercise_variants' in q and 'exercise_id IN' in q:
|
||||
return [
|
||||
{
|
||||
'exercise_id': 10,
|
||||
'id': 7,
|
||||
'variant_name': 'Leicht',
|
||||
'sequence_order': 1,
|
||||
},
|
||||
{
|
||||
'exercise_id': 10,
|
||||
'id': 8,
|
||||
'variant_name': 'Schwer',
|
||||
'sequence_order': 2,
|
||||
},
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def test_enrich_planning_hits_with_variant_meta():
|
||||
hits = [{'id': 10, 'title': 'Test', 'suggested_variant_id': 7}]
|
||||
out = _enrich_planning_hits_with_variant_meta(_Cur([]), hits)
|
||||
assert out[0]['suggested_variant_name'] == 'Leicht'
|
||||
assert len(out[0]['variants']) == 2
|
||||
assert out[0]['variants'][0]['id'] == 7
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.183"
|
||||
APP_VERSION = "0.8.184"
|
||||
BUILD_DATE = "2026-05-23"
|
||||
DB_SCHEMA_VERSION = "20260531074"
|
||||
|
||||
|
|
@ -29,7 +29,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.11.0", # Phase C1: Graph auto-match + variantenbewusste Nachfolger
|
||||
"planning_exercise_suggest": "0.12.0", # Phase C2: Varianten in Treffern + Übernahme
|
||||
"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
|
||||
|
|
@ -44,6 +44,14 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.8.184",
|
||||
"date": "2026-05-23",
|
||||
"changes": [
|
||||
"Planungs-KI Phase C2: Treffer mit Variantenliste + suggested_variant_name; Picker wählt Graph-Variante vor Übernahme.",
|
||||
"hydrateExercisePlanningRow übernimmt exercise_variant_id / suggested_variant_id in die Planungszeile.",
|
||||
],
|
||||
},
|
||||
{
|
||||
"version": "0.8.183",
|
||||
"date": "2026-05-23",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||
|
||||
**Stand:** 2026-05-23
|
||||
**App-Version / DB-Schema:** App **`0.8.183`** (Planungs-KI Phase C1); DB **`20260531074`** — maßgeblich **`backend/version.py`**.
|
||||
**App-Version / DB-Schema:** App **`0.8.184`** (Planungs-KI Phase C2); DB **`20260531074`** — maßgeblich **`backend/version.py`**.
|
||||
|
||||
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
|||
| **A** | Voll-Library deterministisch ranken (kein OR-Profil-Pool) | ✅ **0.8.177** |
|
||||
| **B** | Text-Signale aus guidance/Rahmen-Zielen (`planning_text_signals`) | ✅ **0.8.181** |
|
||||
| **C1** | Progressionsgraph auto-match + variantenbewusste Nachfolger | ✅ **0.8.183** |
|
||||
| **C2** | Varianten in Trefferliste / Picker-Auswahl | 🔲 |
|
||||
| **C2** | Varianten in Trefferliste / Picker-Auswahl | ✅ **0.8.184** |
|
||||
| **C3** | Graph-Builder: Ziel → Pfad vorschlagen → in Graph speichern | 🔲 |
|
||||
| **D** | `planning_context` an `suggestExerciseAi` (Neu-Anlage) | 🔲 |
|
||||
|
||||
|
|
@ -247,11 +247,10 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
|||
|
||||
### Planungs-KI (priorisiert)
|
||||
|
||||
1. **C2 — Varianten in Treffern:** Planungs-Picker: bei `suggested_variant_id` Variante vorauswählen; optional Varianten-Ranking bei `deepen`/`progression`.
|
||||
2. **C3 — Graph-Builder:** Modus „Pfad zum Ziel“ → sequenzielle Vorschläge → `POST …/edges/sequence` nach Review.
|
||||
3. **Graph-Auswahl UI:** Dropdown neben Auto-Match; Rahmen-Slot / Framework mit Default-Graph verknüpfen.
|
||||
4. **Enrichment:** Skills für Kern-Übungen nachziehen (sonst schwaches Profil-Ranking).
|
||||
5. **D — Neu-Anlage:** `planning_context_json` an `POST /api/exercises/ai/suggest`.
|
||||
1. **C3 — Graph-Builder:** Modus „Pfad zum Ziel“ → sequenzielle Vorschläge → `POST …/edges/sequence` nach Review.
|
||||
2. **Graph-Auswahl UI:** Dropdown neben Auto-Match; Rahmen-Slot mit Default-Graph verknüpfen.
|
||||
3. **Enrichment:** Skills für Kern-Übungen nachziehen (sonst schwaches Profil-Ranking).
|
||||
4. **D — Neu-Anlage:** `planning_context_json` an `POST /api/exercises/ai/suggest`.
|
||||
|
||||
### Allgemein
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
buildQuickCreateExercisePayloadFromDraft,
|
||||
aiPreviewToQuickCreateDraft,
|
||||
} from '../utils/exerciseAiQuickCreate'
|
||||
import { resolveExercisePickVariantId } from '../utils/exercisePlanningPick'
|
||||
|
||||
const PAGE_SIZE = 100
|
||||
/** Backend POST /api/planning/exercise-suggest erlaubt max. 50 */
|
||||
|
|
@ -89,6 +90,7 @@ export default function ExercisePickerModal({
|
|||
const [planningHasSearched, setPlanningHasSearched] = useState(false)
|
||||
const [planningSubmittedQuery, setPlanningSubmittedQuery] = useState('')
|
||||
const [planningSearchTick, setPlanningSearchTick] = useState(0)
|
||||
const [variantPickByExerciseId, setVariantPickByExerciseId] = useState({})
|
||||
const pickerScrollRef = useRef(null)
|
||||
|
||||
const resolvedPlanningUnitId = useMemo(() => {
|
||||
|
|
@ -171,8 +173,38 @@ export default function ExercisePickerModal({
|
|||
} = useExerciseAiQuickCreateFields(effectivePickerQuery, { enabled: open && enableQuickCreateDraft })
|
||||
|
||||
const toggleMultiPick = (ex) => {
|
||||
setMultiPicked((prev) => {
|
||||
if (prev.some((p) => p.id === ex.id)) return prev.filter((p) => p.id !== ex.id)
|
||||
const vid = resolveExercisePickVariantId(ex, variantPickByExerciseId[ex.id])
|
||||
return [...prev, { ...ex, exercise_variant_id: vid }]
|
||||
})
|
||||
}
|
||||
|
||||
const buildExercisePickPayload = (ex) => {
|
||||
const vid = resolveExercisePickVariantId(ex, variantPickByExerciseId[ex.id])
|
||||
return {
|
||||
...ex,
|
||||
exercise_variant_id: vid,
|
||||
suggested_variant_id: vid ?? ex.suggested_variant_id ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
const setVariantPickForExercise = (exerciseId, variantId) => {
|
||||
const eid = Number(exerciseId)
|
||||
if (!Number.isFinite(eid) || eid < 1) return
|
||||
setVariantPickByExerciseId((prev) => ({
|
||||
...prev,
|
||||
[eid]: variantId === '' || variantId == null ? null : Number(variantId),
|
||||
}))
|
||||
setMultiPicked((prev) =>
|
||||
prev.some((p) => p.id === ex.id) ? prev.filter((p) => p.id !== ex.id) : [...prev, ex]
|
||||
prev.map((p) =>
|
||||
Number(p.id) === eid
|
||||
? {
|
||||
...p,
|
||||
exercise_variant_id: resolveExercisePickVariantId(p, variantId === '' ? null : Number(variantId)),
|
||||
}
|
||||
: p,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +262,54 @@ export default function ExercisePickerModal({
|
|||
/>
|
||||
)
|
||||
|
||||
const renderPlanningVariantPick = (ex) => {
|
||||
if (!usePlanningSearch || !ex?.id) return null
|
||||
const variants = Array.isArray(ex.variants) ? ex.variants : []
|
||||
const resolved = resolveExercisePickVariantId(ex, variantPickByExerciseId[ex.id])
|
||||
if (ex.suggested_variant_name && !variants.length) {
|
||||
return (
|
||||
<span className="exercise-tag" style={{ marginTop: 6, display: 'inline-block' }}>
|
||||
Variante: {ex.suggested_variant_name}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (variants.length === 0) return null
|
||||
return (
|
||||
<div
|
||||
style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<label className="form-label" style={{ margin: 0, fontSize: '11px' }}>
|
||||
Variante
|
||||
</label>
|
||||
<select
|
||||
className="form-input"
|
||||
style={{ fontSize: '12px', padding: '4px 8px', maxWidth: '100%', flex: '1 1 160px' }}
|
||||
value={resolved ?? ''}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
setVariantPickForExercise(ex.id, v === '' ? null : Number(v))
|
||||
}}
|
||||
>
|
||||
<option value="">— Standard —</option>
|
||||
{variants.map((v) => (
|
||||
<option key={v.id} value={v.id}>
|
||||
{v.variant_name || `Variante #${v.id}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{ex.suggested_variant_id && Number(ex.suggested_variant_id) === Number(resolved) ? (
|
||||
<span style={{ fontSize: '11px', color: 'var(--accent-dark)' }}>Progressionsgraph</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setVariantPickByExerciseId({})
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
let cancelled = false
|
||||
|
|
@ -505,11 +585,18 @@ export default function ExercisePickerModal({
|
|||
title: h.title,
|
||||
summary: h.summary,
|
||||
focus_area: h.focus_area,
|
||||
variants: Array.isArray(h.variants) ? h.variants : [],
|
||||
suggested_variant_id: h.suggested_variant_id ?? null,
|
||||
suggested_variant_name: h.suggested_variant_name ?? null,
|
||||
_planningScore: h.score,
|
||||
_planningReasons: Array.isArray(h.reasons) ? h.reasons : [],
|
||||
updated_at: new Date().toISOString(),
|
||||
}))
|
||||
const initialVariants = {}
|
||||
for (const h of hits) {
|
||||
if (h.suggested_variant_id) initialVariants[h.id] = Number(h.suggested_variant_id)
|
||||
}
|
||||
setVariantPickByExerciseId(initialVariants)
|
||||
setList(hits)
|
||||
setHasMore(false)
|
||||
} catch (e) {
|
||||
|
|
@ -581,7 +668,8 @@ export default function ExercisePickerModal({
|
|||
estimateSize: (index) => {
|
||||
const ex = list[index]
|
||||
const rc = ex?._planningReasons?.length || 0
|
||||
return rc > 0 ? 96 + Math.min(rc, 3) * 14 : 88
|
||||
const hasVariant = usePlanningSearch && Array.isArray(ex?.variants) && ex.variants.length > 0
|
||||
return rc > 0 || hasVariant ? 96 + Math.min(rc, 3) * 14 + (hasVariant ? 36 : 0) : 88
|
||||
},
|
||||
overscan: 8,
|
||||
getItemKey: (index) => String(list[index]?.id ?? index),
|
||||
|
|
@ -591,10 +679,11 @@ export default function ExercisePickerModal({
|
|||
|
||||
const adoptExistingExercise = async (ex) => {
|
||||
if (!ex?.id) return
|
||||
const payload = buildExercisePickPayload(ex)
|
||||
if (multiSelect && typeof onSelectExercises === 'function') {
|
||||
await Promise.resolve(onSelectExercises([ex]))
|
||||
await Promise.resolve(onSelectExercises([payload]))
|
||||
} else if (typeof onSelectExercise === 'function') {
|
||||
await Promise.resolve(onSelectExercise(ex))
|
||||
await Promise.resolve(onSelectExercise(payload))
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
|
@ -1186,6 +1275,7 @@ export default function ExercisePickerModal({
|
|||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{renderPlanningVariantPick(ex)}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
|
|
@ -1232,10 +1322,7 @@ export default function ExercisePickerModal({
|
|||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectExercise(ex)
|
||||
onClose()
|
||||
}}
|
||||
onClick={() => adoptExistingExercise(ex)}
|
||||
style={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
|
|
@ -1303,7 +1390,7 @@ export default function ExercisePickerModal({
|
|||
className="btn btn-primary"
|
||||
disabled={!multiPicked.length}
|
||||
onClick={() => {
|
||||
onSelectExercises([...multiPicked])
|
||||
onSelectExercises(multiPicked.map((p) => buildExercisePickPayload(p)))
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -500,13 +500,17 @@ export default function ExerciseProgressionGraphPanel({
|
|||
|
||||
const applyPickedExercise = async (ex) => {
|
||||
const title = ex.title || `Übung #${ex.id}`
|
||||
const variants = await loadVariantsForExercise(ex.id)
|
||||
const variants = Array.isArray(ex.variants) && ex.variants.length
|
||||
? ex.variants
|
||||
: await loadVariantsForExercise(ex.id)
|
||||
const variantId =
|
||||
ex.exercise_variant_id ?? ex.suggested_variant_id ?? null
|
||||
|
||||
if (pickContext?.kind === 'sequence') {
|
||||
patchSeqStep(pickContext.index, {
|
||||
exerciseId: ex.id,
|
||||
exerciseTitle: title,
|
||||
variantId: null,
|
||||
variantId: variantId != null ? Number(variantId) : null,
|
||||
variants,
|
||||
})
|
||||
setPickContext(null)
|
||||
|
|
@ -516,7 +520,7 @@ export default function ExerciseProgressionGraphPanel({
|
|||
const patch = {
|
||||
exerciseId: ex.id,
|
||||
exerciseTitle: title,
|
||||
variantId: null,
|
||||
variantId: variantId != null ? Number(variantId) : null,
|
||||
variants,
|
||||
}
|
||||
if (pickContext.slot === 'first') setFirstEp(patch)
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ export function mapPlanningHitsToListRows(hits) {
|
|||
summary: h.summary,
|
||||
focus_area: h.focus_area,
|
||||
focus_area_names: h.focus_area ? [h.focus_area] : [],
|
||||
variants: Array.isArray(h.variants) ? h.variants : [],
|
||||
suggested_variant_id: h.suggested_variant_id ?? null,
|
||||
suggested_variant_name: h.suggested_variant_name ?? null,
|
||||
updated_at: new Date().toISOString(),
|
||||
_planningScore: h.score,
|
||||
_planningReasons: Array.isArray(h.reasons) ? h.reasons : [],
|
||||
suggested_variant_id: h.suggested_variant_id ?? null,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
18
frontend/src/utils/exercisePlanningPick.js
Normal file
18
frontend/src/utils/exercisePlanningPick.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/** Varianten-Auswahl beim Übernehmen aus Planungs-KI-Treffern. */
|
||||
export function resolveExercisePickVariantId(exercise, overrideVariantId) {
|
||||
if (exercise == null) return null
|
||||
const raw =
|
||||
overrideVariantId !== undefined && overrideVariantId !== null && overrideVariantId !== ''
|
||||
? overrideVariantId
|
||||
: exercise.exercise_variant_id ?? exercise.suggested_variant_id
|
||||
if (raw == null || raw === '') return null
|
||||
const vid = Number(raw)
|
||||
return Number.isFinite(vid) && vid > 0 ? vid : null
|
||||
}
|
||||
|
||||
export function variantLabelForPick(variants, variantId) {
|
||||
if (variantId == null) return null
|
||||
const list = Array.isArray(variants) ? variants : []
|
||||
const row = list.find((v) => Number(v.id) === Number(variantId))
|
||||
return (row?.variant_name || '').trim() || null
|
||||
}
|
||||
|
|
@ -325,6 +325,18 @@ export async function hydrateExercisePlanningRow(exercise) {
|
|||
row.exercise_variant_id = ''
|
||||
} else {
|
||||
row.variants = variants
|
||||
const pickVidRaw =
|
||||
exercise?.exercise_variant_id ?? exercise?.suggested_variant_id ?? null
|
||||
const pickVid =
|
||||
pickVidRaw != null && pickVidRaw !== '' ? Number(pickVidRaw) : NaN
|
||||
if (Number.isFinite(pickVid) && pickVid > 0) {
|
||||
const known = variants.some((v) => Number(v.id) === pickVid)
|
||||
if (known || !variants.length) row.exercise_variant_id = pickVid
|
||||
}
|
||||
if (row.exercise_variant_id !== '' && variants.length > 0) {
|
||||
const ok = variants.some((v) => Number(v.id) === Number(row.exercise_variant_id))
|
||||
if (!ok) row.exercise_variant_id = ''
|
||||
}
|
||||
}
|
||||
Object.assign(row, meta)
|
||||
if (row.exercise_kind === 'combination') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user