Enhance Exercise Progression Path Management with Dynamic Step Capacity
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 46s
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 1m13s

- Introduced logic to manage path capacity dynamically, allowing users to expand the maximum number of steps when inserting new offers.
- Implemented confirmation prompts for users when the path is full, enhancing user experience and decision-making.
- Updated the `ExerciseProgressionPathBuilder` component to reflect these changes, improving the handling of gap-fill offers and user interactions.
- Adjusted UI messages to clarify the implications of adding new steps and the conditions under which users can expand the path.
This commit is contained in:
Lars 2026-06-08 14:51:15 +02:00
parent d4e9bded23
commit 0677663268

View File

@ -61,6 +61,48 @@ const OFFER_SOURCE_LABELS = {
roadmap_unfilled: 'Roadmap-Stufe',
}
const PATH_STEPS_HARD_MAX = 10
/** Einfügen wächst den Pfad; Ersetzen (replace_step_index) nicht. */
function offerGrowsPath(offer) {
const replaceIdx = offer?.replace_step_index
return !(replaceIdx != null && Number.isFinite(Number(replaceIdx)))
}
function isGapOfferBlockedByPathCapacity(offer, pathLen, maxSteps) {
return offerGrowsPath(offer) && pathLen >= maxSteps
}
function neededMaxStepsAfterInsert(pathLen) {
return Math.min(PATH_STEPS_HARD_MAX, pathLen + 1)
}
/**
* Pfad voll, aber Einfügen gewünscht Nutzer fragen, ob maxSteps dynamisch wächst.
* @returns {boolean} true = fortfahren (ggf. maxSteps erhöht), false = abgebrochen
*/
function confirmPathExpansionIfNeeded(offer, pathLen, maxSteps, setMaxSteps) {
if (!isGapOfferBlockedByPathCapacity(offer, pathLen, maxSteps)) {
return true
}
if (maxSteps >= PATH_STEPS_HARD_MAX) {
alert(
`Maximale Pfadlänge (${PATH_STEPS_HARD_MAX} Schritte) erreicht. Bitte zuerst einen Schritt entfernen.`,
)
return false
}
const newMax = neededMaxStepsAfterInsert(pathLen)
const titleHint = (offer?.title_hint || 'diese Übung').trim()
const ok = window.confirm(
`Maximale Pfadlänge (${maxSteps}) ist erreicht.\n\n` +
`Soll die Pfadlänge auf ${newMax} Schritte vergrößert werden, um „${titleHint}“ einzufügen?\n\n` +
'Es wird kein neuer Pfad-Vorschlag generiert.',
)
if (!ok) return false
setMaxSteps(newMax)
return true
}
function resolveDefaultFocusAreaId(targetSummary, focusAreas) {
const targetName = targetSummary?.focus_areas?.[0]
if (targetName && Array.isArray(focusAreas) && focusAreas.length) {
@ -193,6 +235,13 @@ export default function ExerciseProgressionPathBuilder({
setQuickAiError('')
}
const handleGapFillClick = async (offer) => {
if (!confirmPathExpansionIfNeeded(offer, pathSteps.length, maxSteps, setMaxSteps)) {
return
}
await runGapFillAiSuggest(offer)
}
const runGapFillAiSuggest = async (offer) => {
const title = (offer?.title_hint || '').trim()
if (title.length < 3) {
@ -611,8 +660,10 @@ export default function ExerciseProgressionPathBuilder({
>
<strong style={{ fontSize: '13px' }}>Fehlende Schritte mit KI anlegen</strong>
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '6px 0 10px', lineHeight: 1.45 }}>
Die QS hat fehlende Zwischenschritte erkannt sie sind noch nicht im Pfad ({pathSteps.length}/{maxSteps} Schritte).
Mit KI anlegen startet einen vollständigen KI-Entwurf (Ziel, Anleitung, Fähigkeiten) und fügt die Übung ein.
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.'}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{gapFillOffers.map((offer) => (
@ -646,12 +697,20 @@ export default function ExerciseProgressionPathBuilder({
type="button"
className="btn btn-primary"
style={{ fontSize: '12px', flexShrink: 0 }}
disabled={quickSaving || pathSteps.length >= maxSteps}
onClick={() => runGapFillAiSuggest(offer)}
disabled={
quickSaving ||
(isGapOfferBlockedByPathCapacity(offer, pathSteps.length, maxSteps) &&
maxSteps >= PATH_STEPS_HARD_MAX)
}
onClick={() => handleGapFillClick(offer)}
title={
pathSteps.length >= maxSteps
? `Pfad hat bereits ${maxSteps} Schritte — zuerst einen Schritt entfernen.`
: 'KI-Entwurf mit Pfad-Kontext generieren'
isGapOfferBlockedByPathCapacity(offer, pathSteps.length, maxSteps)
? maxSteps >= PATH_STEPS_HARD_MAX
? `Maximal ${PATH_STEPS_HARD_MAX} Schritte — zuerst einen Schritt entfernen.`
: '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'
}
>
{generatingOfferId === offer.offer_id