diff --git a/frontend/src/components/ExerciseProgressionPathBuilder.jsx b/frontend/src/components/ExerciseProgressionPathBuilder.jsx index 5784f75..b6a2deb 100644 --- a/frontend/src/components/ExerciseProgressionPathBuilder.jsx +++ b/frontend/src/components/ExerciseProgressionPathBuilder.jsx @@ -1,7 +1,7 @@ /** * Planungs-KI Phase C3/E3: Ziel → Übungspfad vorschlagen → Lücken mit KI anlegen → in Graph speichern. */ -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import api from '../utils/api' import ExerciseAiQuickCreateModal from './exercises/ExerciseAiQuickCreateModal' import ExerciseGapFillPrepModal from './exercises/ExerciseGapFillPrepModal' @@ -136,6 +136,93 @@ const OFFER_SOURCE_LABELS = { const PATH_STEPS_HARD_MAX = 10 +const WIZARD_STEPS = [ + { id: 1, label: 'Ziel & Start/Ziel', short: 'Ziel' }, + { id: 2, label: 'Roadmap', short: 'Roadmap' }, + { id: 3, label: 'Match', short: 'Match' }, + { id: 4, label: 'Lücken & Speichern', short: 'Speichern' }, +] + +function computeMaxReachableStep(editableMajorSteps, pathSteps) { + if (pathSteps.length > 0) return 4 + if (editableMajorSteps.length >= 2) return 2 + return 1 +} + +function PlanningWizardStepper({ currentStep, maxReachable, onStepChange, disabled }) { + return ( + + ) +} + const ROADMAP_PHASES = ['einstieg', 'grundlage', 'vertiefung', 'anwendung', 'perfektion'] const LOAD_PROFILE_OPTIONS = [ @@ -355,6 +442,12 @@ export default function ExerciseProgressionPathBuilder({ const [gapPrepFocusAreaId, setGapPrepFocusAreaId] = useState('') const [gapPrepError, setGapPrepError] = useState('') const [loadedPlanningHint, setLoadedPlanningHint] = useState(false) + const [wizardStep, setWizardStep] = useState(1) + + const maxReachableStep = useMemo( + () => computeMaxReachableStep(editableMajorSteps, pathSteps), + [editableMajorSteps, pathSteps], + ) const buildPlanningArtifact = useCallback( () => @@ -404,6 +497,7 @@ export default function ExerciseProgressionPathBuilder({ setRoadmapDirty(false) setStartTargetAnalyzed(false) setError('') + setWizardStep(1) api .getExerciseProgressionGraph(Number(graphId)) @@ -422,6 +516,7 @@ export default function ExerciseProgressionPathBuilder({ const majors = mapMajorStepsFromApi(art.progression_roadmap) if (majors.length >= 2) { setEditableMajorSteps(majors) + setWizardStep(2) } } if ( @@ -442,6 +537,12 @@ export default function ExerciseProgressionPathBuilder({ } }, [graphId]) + useEffect(() => { + if (wizardStep > maxReachableStep) { + setWizardStep(maxReachableStep) + } + }, [wizardStep, maxReachableStep]) + useEffect(() => { let cancelled = false Promise.all([ @@ -931,6 +1032,7 @@ export default function ExerciseProgressionPathBuilder({ setPathSkillExpectations(null) setRoadmapDirty(false) setLoadedPlanningHint(false) + setWizardStep(2) await persistPlanningRoadmapToGraph() } catch (e) { console.error(e) @@ -980,6 +1082,7 @@ export default function ExerciseProgressionPathBuilder({ applyPathMatchResponse(res, q) setMaxSteps(validSteps.length) setLoadedPlanningHint(false) + setWizardStep(3) await persistPlanningRoadmapToGraph() } catch (e) { console.error(e) @@ -1033,6 +1136,7 @@ export default function ExerciseProgressionPathBuilder({ setPathSkillExpectations(null) setEditableMajorSteps([]) setRoadmapDirty(false) + setWizardStep(1) if (typeof onSaved === 'function') await onSaved() const msg = skippedAi > 0 @@ -1057,26 +1161,43 @@ export default function ExerciseProgressionPathBuilder({ >

KI: Pfad zum Ziel

- Zuerst didaktische Roadmap vorschlagen und anpassen, dann Übungen je Major Step aus der Bibliothek matchen. - Lücken können mit KI als Übung angelegt werden. + In vier Schritten: Ziel festlegen → Roadmap bearbeiten → Übungen matchen → Lücken schließen und speichern.

- {loadedPlanningHint && editableMajorSteps.length > 0 && pathSteps.length === 0 ? ( -

- Gespeicherte Planung für diesen Graph geladen — Roadmap anpassen und erneut matchen, oder neuen Vorschlag - starten. + + + + {error ? ( +

+ {error}

) : null} -
+ + {wizardStep === 1 ? ( +
+

+ Schritt 1 — Ziel & Start/Ziel +

+ {loadedPlanningHint && editableMajorSteps.length > 0 && pathSteps.length === 0 ? ( +

+ Gespeicherte Planung geladen — Sie können bei Schritt 2 weitermachen oder hier neu starten. +

+ ) : null} +
-

- Optional zuerst „Start/Ziel analysieren“, anpassen, dann Roadmap-Stufen. Sind Start und Ziel leer, - geschieht die Analyse beim Roadmap-Vorschlag automatisch mit. Manuelle Eingaben haben immer Vorrang. -

-
- - - {startTargetAnalyzed && !editableMajorSteps.length ? ( - - Start/Ziel bereit — Roadmap als Nächstes - - ) : null} - -
+

+ Optional zuerst „Start/Ziel analysieren“, anpassen, dann Roadmap-Stufen. Sind Start und Ziel leer, + geschieht die Analyse beim Roadmap-Vorschlag automatisch mit. Manuelle Eingaben haben immer Vorrang. +

+
+ + + {startTargetAnalyzed && !editableMajorSteps.length ? ( + + Start/Ziel bereit + + ) : null} +
- {error ? ( -

- {error} -

- ) : null} - - {(progressionRoadmap?.goal_analysis || - progressionRoadmap?.pipeline_phase === 'start_target_only') ? ( -
+ {(progressionRoadmap?.goal_analysis || + progressionRoadmap?.pipeline_phase === 'start_target_only') ? ( +
+ + KI-Zielanalyse (Details) + +
Zielanalyse {progressionRoadmap.llm_start_target_applied ? ( @@ -1261,45 +1360,32 @@ export default function ExerciseProgressionPathBuilder({

) : null}
-
- ) : null} - - {(semanticBrief || targetSummary || pathSkillExpectations) && pathSteps.length > 0 ? ( -
- {semanticBrief?.primary_topic ? ( - - Thema: {semanticBrief.primary_topic} - +
+
) : null} - {Array.isArray(semanticBrief?.development_arc) && - semanticBrief.development_arc.slice(0, 3).map((phase) => ( - - {phase} - - ))} - {Array.isArray(targetSummary?.focus_areas) && - targetSummary.focus_areas.slice(0, 1).map((fa) => ( - - Fokus: {fa} - - ))} - {formatExpectedSkillNames(pathSkillExpectations, 5).map((name) => ( - - {name} - - ))} -
+
) : null} - {editableMajorSteps.length > 0 ? ( + {wizardStep === 2 ? ( +
+

+ Schritt 2 — Didaktische Roadmap +

+ {editableMajorSteps.length === 0 ? ( +

+ Noch keine Roadmap — zuerst in Schritt 1 „Roadmap vorschlagen“. + +

+ ) : (
- Didaktische Roadmap — bearbeiten + Major Steps bearbeiten {roadmapDirty ? ( Geändert — bitte erneut matchen @@ -1489,9 +1575,102 @@ export default function ExerciseProgressionPathBuilder({ ) : null}
+ )} + {editableMajorSteps.length >= 2 ? ( +
+ + +
+ ) : null} +
) : null} - {pathQa && pathSteps.length > 0 ? ( + {wizardStep === 3 ? ( +
+

+ Schritt 3 — Übungen & Qualität +

+ {pathSteps.length === 0 ? ( +

+ Noch kein Match — zuerst in Schritt 2 „Übungen matchen“. + +

+ ) : ( + <> + {(semanticBrief || targetSummary || pathSkillExpectations) ? ( +
+ + Pfad-Kontext (Thema, Fokus, Fähigkeiten) + +
+ {semanticBrief?.primary_topic ? ( + + Thema: {semanticBrief.primary_topic} + + ) : null} + {Array.isArray(semanticBrief?.development_arc) && + semanticBrief.development_arc.slice(0, 3).map((phase) => ( + + {phase} + + ))} + {Array.isArray(targetSummary?.focus_areas) && + targetSummary.focus_areas.slice(0, 1).map((fa) => ( + + Fokus: {fa} + + ))} + {formatExpectedSkillNames(pathSkillExpectations, 5).map((name) => ( + + {name} + + ))} +
+
+ ) : null} + + {pathQa ? (
) : Number(pathQa.off_topic_count) > 0 ? (

- {pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema — siehe Lücken-Angebote unten. + {pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema — Lücken in Schritt 4 schließen.

) : null} {pathQa.reorder_applied ? ( @@ -1547,88 +1726,7 @@ export default function ExerciseProgressionPathBuilder({
) : null} - {gapFillOffers.length > 0 ? ( -
- Fehlende Schritte — mit KI anlegen -

- 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.' - : ' Zuerst Kontext prüfen und ergänzen, dann KI-Entwurf erstellen und einfügen.'} -

- {gapFillOffers.map((offer) => ( -
-
-
- - {OFFER_SOURCE_LABELS[offer.source] || offer.source || 'Lücke'} - {offer.phase ? ` · ${offer.phase}` : ''} - -
{offer.title_hint}
- {offer.rationale ? ( -

{offer.rationale}

- ) : null} - {offer.from_title && offer.to_title ? ( -

- Zwischen „{offer.from_title}“ und „{offer.to_title}“ - {offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''} -

- ) : null} - -
- -
-
- ))} -
-
- ) : null} - - {pathSteps.length > 0 ? ( - <> -
{pathSteps.map((step, idx) => (
@@ -1749,6 +1845,170 @@ export default function ExerciseProgressionPathBuilder({
))}
+
+ + + {gapFillOffers.length > 0 ? ( + + {gapFillOffers.length} Lücke(n) offen + + ) : null} +
+ + )} +
+ ) : null} + + {wizardStep === 4 ? ( +
+

+ Schritt 4 — Lücken schließen & Pfad speichern +

+ {pathSteps.length === 0 ? ( +

+ Noch kein Pfad — zuerst Schritt 3 abschließen. + +

+ ) : ( + <> + {gapFillOffers.length > 0 ? ( +
+ Fehlende Schritte — mit KI anlegen +

+ 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.' + : ' Zuerst Kontext prüfen und ergänzen, dann KI-Entwurf erstellen und einfügen.'} +

+
+ {gapFillOffers.map((offer) => ( +
+
+
+ + {OFFER_SOURCE_LABELS[offer.source] || offer.source || 'Lücke'} + {offer.phase ? ` · ${offer.phase}` : ''} + +
{offer.title_hint}
+ {offer.rationale ? ( +

{offer.rationale}

+ ) : null} + {offer.from_title && offer.to_title ? ( +

+ Zwischen „{offer.from_title}“ und „{offer.to_title}“ + {offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''} +

+ ) : null} + +
+ +
+
+ ))} +
+
+ ) : ( +

+ Keine offenen Lücken — Pfad kann direkt gespeichert werden. +

+ )} + +
+ + Pfad-Übersicht ({pathSteps.length} Schritte) + +
    + {pathSteps.map((step, idx) => ( +
  1. + {step.exerciseTitle} + {step.roadmapPhase ? ` (${step.roadmapPhase})` : ''} + {step.isAiProposal ? ' — KI-Vorschlag, noch anlegen' : ''} +
  2. + ))} +
+
+