Enhance Gap Planning Context with Stage Overrides and Trainer Supplements
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m25s
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 42s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m25s
- 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.
This commit is contained in:
parent
d4b1780193
commit
0adf20c9e1
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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.'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{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'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1406,6 +1462,25 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</>
|
||||
) : null}
|
||||
|
||||
<ExerciseGapFillPrepModal
|
||||
open={gapPrepOpen}
|
||||
onClose={closeGapFillPrep}
|
||||
offer={activeOffer}
|
||||
contextLines={activePlanningContextLines}
|
||||
title={gapPrepTitle}
|
||||
onTitleChange={setGapPrepTitle}
|
||||
stageLearningGoal={gapPrepStageGoal}
|
||||
onStageLearningGoalChange={setGapPrepStageGoal}
|
||||
supplements={gapPrepSupplements}
|
||||
onSupplementsChange={setGapPrepSupplements}
|
||||
focusAreaId={gapPrepFocusAreaId}
|
||||
onFocusAreaChange={setGapPrepFocusAreaId}
|
||||
focusAreas={focusAreas}
|
||||
busy={quickSaving}
|
||||
error={gapPrepError}
|
||||
onSubmit={submitGapFillPrep}
|
||||
/>
|
||||
|
||||
<ExerciseAiQuickCreateModal
|
||||
open={quickCreateOpen}
|
||||
onClose={closeQuickCreate}
|
||||
|
|
@ -1428,8 +1503,11 @@ export default function ExerciseProgressionPathBuilder({
|
|||
onDraftChange={setQuickCreateDraft}
|
||||
onDiscard={() => {
|
||||
setQuickCreateDraft(null)
|
||||
setActivePlanningContextLines([])
|
||||
if (activeOffer) setQuickCreateOpen(true)
|
||||
if (activeOffer) {
|
||||
setGapPrepOpen(true)
|
||||
} else {
|
||||
setActivePlanningContextLines([])
|
||||
}
|
||||
}}
|
||||
planningContextLines={activePlanningContextLines}
|
||||
onApply={applyQuickCreateDraft}
|
||||
|
|
|
|||
186
frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
Normal file
186
frontend/src/components/exercises/ExerciseGapFillPrepModal.jsx
Normal file
|
|
@ -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 (
|
||||
<div
|
||||
className="admin-modal-backdrop"
|
||||
role="presentation"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !busy) onClose()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="admin-modal-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="gap-fill-prep-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ maxWidth: '680px' }}
|
||||
>
|
||||
<div className="admin-modal-sheet__header">
|
||||
<h3 id="gap-fill-prep-title" className="admin-modal-sheet__title">
|
||||
Übung mit KI vorbereiten
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary admin-modal-sheet__close"
|
||||
disabled={busy}
|
||||
onClick={onClose}
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-modal-sheet__body">
|
||||
<p className="muted" style={{ marginTop: 0, marginBottom: '12px', lineHeight: 1.45 }}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
{readOnlyLines.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '14px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<strong style={{ display: 'block', marginBottom: '6px' }}>Pfad-Kontext (aus Roadmap)</strong>
|
||||
<dl style={{ margin: 0, display: 'grid', gap: '6px' }}>
|
||||
{readOnlyLines.map(({ label, value }) => (
|
||||
<div key={label}>
|
||||
<dt style={{ margin: 0, fontSize: '11px', color: 'var(--text3)' }}>{label}</dt>
|
||||
<dd style={{ margin: '2px 0 0', lineHeight: 1.45, color: 'var(--text2)' }}>{value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div style={{ display: 'grid', gap: '12px' }}>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="gap-prep-title">
|
||||
Titel-Vorschlag *
|
||||
</label>
|
||||
<input
|
||||
id="gap-prep-title"
|
||||
className="form-input"
|
||||
value={title}
|
||||
onChange={(e) => onTitleChange(e.target.value)}
|
||||
disabled={busy}
|
||||
maxLength={280}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="gap-prep-stage-goal">
|
||||
Stufen-Lernziel (anpassbar)
|
||||
</label>
|
||||
<textarea
|
||||
id="gap-prep-stage-goal"
|
||||
className="form-input"
|
||||
rows={2}
|
||||
value={stageLearningGoal}
|
||||
onChange={(e) => onStageLearningGoalChange(e.target.value)}
|
||||
disabled={busy}
|
||||
placeholder="Was soll diese Übung in der Roadmap-Stufe leisten?"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="gap-prep-supplements">
|
||||
Ergänzungen / Anpassungen für die KI
|
||||
</label>
|
||||
<textarea
|
||||
id="gap-prep-supplements"
|
||||
className="form-input"
|
||||
rows={3}
|
||||
value={supplements}
|
||||
onChange={(e) => onSupplementsChange(e.target.value)}
|
||||
disabled={busy}
|
||||
placeholder="z. B. nur Partnerübung, 10–12 Jahre, Fokus auf Reaktion unter Druck, keine Sprünge …"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="form-label" htmlFor="gap-prep-focus">
|
||||
Fokusbereich *
|
||||
</label>
|
||||
<select
|
||||
id="gap-prep-focus"
|
||||
className="form-input"
|
||||
value={focusAreaId}
|
||||
onChange={(e) => onFocusAreaChange(e.target.value)}
|
||||
disabled={busy}
|
||||
>
|
||||
<option value="">Bitte wählen …</option>
|
||||
{(focusAreas || []).map((fa) => (
|
||||
<option key={fa.id} value={fa.id}>
|
||||
{fa.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{offer.from_title && offer.to_title ? (
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '12px 0 0' }}>
|
||||
Einordnung: zwischen „{offer.from_title}“ und „{offer.to_title}“
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<p className="form-error" style={{ marginTop: '12px' }}>
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="admin-modal-sheet__footer" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={onClose}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" disabled={busy} onClick={onSubmit}>
|
||||
{busy ? 'KI erstellt Entwurf …' : 'KI-Entwurf erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user