Enhance Exercise Progression Path Builder with Planning Wizard Stepper
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
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
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 43s
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 a new Planning Wizard Stepper component to guide users through the exercise planning process in four steps. - Implemented logic to compute the maximum reachable step based on user input and current progress. - Updated state management to track the current wizard step and ensure it aligns with user interactions. - Enhanced the user interface to improve clarity and navigation through the planning stages. - Incremented application version to reflect these changes.
This commit is contained in:
parent
5692931d07
commit
ca3a9c6fa4
|
|
@ -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 (
|
||||
<nav
|
||||
aria-label="Planungsfortschritt"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
marginBottom: '16px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '10px',
|
||||
background: 'var(--surface2)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
{WIZARD_STEPS.map((step, idx) => {
|
||||
const reachable = step.id <= maxReachable
|
||||
const active = currentStep === step.id
|
||||
const done = step.id < currentStep && reachable
|
||||
const canClick = reachable && !disabled && step.id <= maxReachable
|
||||
return (
|
||||
<React.Fragment key={step.id}>
|
||||
{idx > 0 ? (
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
color: done || active ? 'var(--accent)' : 'var(--text3)',
|
||||
fontSize: '12px',
|
||||
padding: '0 2px',
|
||||
}}
|
||||
>
|
||||
→
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={active ? 'btn btn-primary' : 'btn btn-secondary'}
|
||||
disabled={!canClick}
|
||||
onClick={() => canClick && onStepChange(step.id)}
|
||||
style={{
|
||||
flex: '1 1 120px',
|
||||
minWidth: '100px',
|
||||
fontSize: '12px',
|
||||
padding: '8px 10px',
|
||||
opacity: reachable ? 1 : 0.45,
|
||||
borderColor: active
|
||||
? 'var(--accent)'
|
||||
: done
|
||||
? 'color-mix(in srgb, var(--accent) 40%, var(--border))'
|
||||
: undefined,
|
||||
}}
|
||||
title={step.label}
|
||||
aria-current={active ? 'step' : undefined}
|
||||
>
|
||||
<span style={{ marginRight: '6px', fontWeight: 700 }}>{step.id}</span>
|
||||
<span className="planning-step-label-full">{step.label}</span>
|
||||
<span className="planning-step-label-short" style={{ display: 'none' }}>
|
||||
{step.short}
|
||||
</span>
|
||||
</button>
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
<style>{`
|
||||
@media (max-width: 520px) {
|
||||
.planning-step-label-full { display: none !important; }
|
||||
.planning-step-label-short { display: inline !important; }
|
||||
}
|
||||
`}</style>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
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({
|
|||
>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>KI: Pfad zum Ziel</h3>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
||||
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.
|
||||
</p>
|
||||
{loadedPlanningHint && editableMajorSteps.length > 0 && pathSteps.length === 0 ? (
|
||||
<p
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--accent-dark)',
|
||||
margin: '0 0 10px',
|
||||
padding: '8px 10px',
|
||||
borderRadius: '8px',
|
||||
background: 'color-mix(in srgb, var(--accent) 8%, var(--surface))',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
Gespeicherte Planung für diesen Graph geladen — Roadmap anpassen und erneut matchen, oder neuen Vorschlag
|
||||
starten.
|
||||
|
||||
<PlanningWizardStepper
|
||||
currentStep={wizardStep}
|
||||
maxReachable={maxReachableStep}
|
||||
onStepChange={setWizardStep}
|
||||
disabled={disabled || loading || saving}
|
||||
/>
|
||||
|
||||
{error ? (
|
||||
<p className="form-error" style={{ marginTop: 0, marginBottom: '12px' }}>
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end' }}>
|
||||
|
||||
{wizardStep === 1 ? (
|
||||
<section aria-labelledby="planning-step-1-heading">
|
||||
<h4 id="planning-step-1-heading" style={{ margin: '0 0 10px', fontSize: '0.95rem' }}>
|
||||
Schritt 1 — Ziel & Start/Ziel
|
||||
</h4>
|
||||
{loadedPlanningHint && editableMajorSteps.length > 0 && pathSteps.length === 0 ? (
|
||||
<p
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--accent-dark)',
|
||||
margin: '0 0 10px',
|
||||
padding: '8px 10px',
|
||||
borderRadius: '8px',
|
||||
background: 'color-mix(in srgb, var(--accent) 8%, var(--surface))',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
Gespeicherte Planung geladen — Sie können bei Schritt 2 weitermachen oder hier neu starten.
|
||||
</p>
|
||||
) : null}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end' }}>
|
||||
<div className="form-row" style={{ flex: '2 1 240px', marginBottom: 0 }}>
|
||||
<label className="form-label">Ziel / Entwicklungsrichtung</label>
|
||||
<input
|
||||
|
|
@ -1142,78 +1263,56 @@ export default function ExerciseProgressionPathBuilder({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
||||
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.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-end', marginTop: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={analyzeStartTarget}
|
||||
title="Nur Ausgangslage, Zielzustand und Ergänzungen per KI — ohne Roadmap-Stufen"
|
||||
>
|
||||
{loadingStartTarget ? 'Analyse …' : 'Start/Ziel analysieren'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={suggestRoadmap}
|
||||
title={
|
||||
startSituation.trim() && targetState.trim()
|
||||
? 'Roadmap-Stufen aus den gesetzten Start/Ziel-Feldern'
|
||||
: 'Start/Ziel-Analyse und Roadmap-Stufen in einem Schritt'
|
||||
}
|
||||
>
|
||||
{loadingRoadmap ? 'Roadmap …' : 'Roadmap vorschlagen'}
|
||||
</button>
|
||||
{startTargetAnalyzed && !editableMajorSteps.length ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
Start/Ziel bereit — Roadmap als Nächstes
|
||||
</span>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={
|
||||
disabled ||
|
||||
loading ||
|
||||
saving ||
|
||||
!graphId ||
|
||||
editableMajorSteps.length < 2
|
||||
}
|
||||
onClick={matchExercisesFromRoadmap}
|
||||
title={
|
||||
editableMajorSteps.length < 2
|
||||
? 'Zuerst Roadmap vorschlagen'
|
||||
: roadmapDirty
|
||||
? 'Roadmap wurde bearbeitet — erneut matchen'
|
||||
: 'Bibliothek je Major Step durchsuchen'
|
||||
}
|
||||
>
|
||||
{loadingMatch ? 'Match …' : roadmapDirty ? 'Übungen neu matchen' : 'Übungen matchen'}
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.4 }}>
|
||||
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.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center', marginTop: '12px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={analyzeStartTarget}
|
||||
title="Nur Ausgangslage, Zielzustand und Ergänzungen per KI — ohne Roadmap-Stufen"
|
||||
>
|
||||
{loadingStartTarget ? 'Analyse …' : 'Start/Ziel analysieren'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={suggestRoadmap}
|
||||
title={
|
||||
startSituation.trim() && targetState.trim()
|
||||
? 'Roadmap-Stufen aus den gesetzten Start/Ziel-Feldern'
|
||||
: 'Start/Ziel-Analyse und Roadmap-Stufen in einem Schritt'
|
||||
}
|
||||
>
|
||||
{loadingRoadmap ? 'Roadmap …' : 'Roadmap vorschlagen →'}
|
||||
</button>
|
||||
{startTargetAnalyzed && !editableMajorSteps.length ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
Start/Ziel bereit
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<p className="form-error" style={{ marginTop: '10px' }}>
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{(progressionRoadmap?.goal_analysis ||
|
||||
progressionRoadmap?.pipeline_phase === 'start_target_only') ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
{(progressionRoadmap?.goal_analysis ||
|
||||
progressionRoadmap?.pipeline_phase === 'start_target_only') ? (
|
||||
<details
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
padding: '10px 12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||||
KI-Zielanalyse (Details)
|
||||
</summary>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<strong style={{ fontSize: '13px' }}>Zielanalyse</strong>
|
||||
{progressionRoadmap.llm_start_target_applied ? (
|
||||
|
|
@ -1261,45 +1360,32 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{(semanticBrief || targetSummary || pathSkillExpectations) && pathSteps.length > 0 ? (
|
||||
<div style={{ marginTop: '10px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{semanticBrief?.primary_topic ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
Thema: {semanticBrief.primary_topic}
|
||||
</span>
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
{Array.isArray(semanticBrief?.development_arc) &&
|
||||
semanticBrief.development_arc.slice(0, 3).map((phase) => (
|
||||
<span key={phase} className="exercise-tag">
|
||||
{phase}
|
||||
</span>
|
||||
))}
|
||||
{Array.isArray(targetSummary?.focus_areas) &&
|
||||
targetSummary.focus_areas.slice(0, 1).map((fa) => (
|
||||
<span key={fa} className="exercise-tag">
|
||||
Fokus: {fa}
|
||||
</span>
|
||||
))}
|
||||
{formatExpectedSkillNames(pathSkillExpectations, 5).map((name) => (
|
||||
<span
|
||||
key={`path-skill-${name}`}
|
||||
className="exercise-tag"
|
||||
style={{ borderColor: 'color-mix(in srgb, var(--accent) 55%, var(--border))' }}
|
||||
title="Pfadweite Fähigkeiten-Erwartung (Scoring)"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{editableMajorSteps.length > 0 ? (
|
||||
{wizardStep === 2 ? (
|
||||
<section aria-labelledby="planning-step-2-heading">
|
||||
<h4 id="planning-step-2-heading" style={{ margin: '0 0 10px', fontSize: '0.95rem' }}>
|
||||
Schritt 2 — Didaktische Roadmap
|
||||
</h4>
|
||||
{editableMajorSteps.length === 0 ? (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text2)', margin: 0 }}>
|
||||
Noch keine Roadmap — zuerst in Schritt 1 „Roadmap vorschlagen“.
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ marginLeft: '10px', fontSize: '12px' }}
|
||||
onClick={() => setWizardStep(1)}
|
||||
>
|
||||
Zu Schritt 1
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid color-mix(in srgb, var(--accent) 45%, var(--border))',
|
||||
|
|
@ -1307,7 +1393,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<strong style={{ fontSize: '13px' }}>Didaktische Roadmap — bearbeiten</strong>
|
||||
<strong style={{ fontSize: '13px' }}>Major Steps bearbeiten</strong>
|
||||
{roadmapDirty ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--danger)' }}>
|
||||
Geändert — bitte erneut matchen
|
||||
|
|
@ -1489,9 +1575,102 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{editableMajorSteps.length >= 2 ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
alignItems: 'center',
|
||||
marginTop: '14px',
|
||||
paddingTop: '14px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setWizardStep(1)}
|
||||
disabled={disabled || loading || saving}
|
||||
>
|
||||
← Zurück
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={disabled || loading || saving || !graphId}
|
||||
onClick={matchExercisesFromRoadmap}
|
||||
title={
|
||||
roadmapDirty
|
||||
? 'Roadmap wurde bearbeitet — erneut matchen'
|
||||
: 'Bibliothek je Major Step durchsuchen'
|
||||
}
|
||||
>
|
||||
{loadingMatch ? 'Match …' : roadmapDirty ? 'Übungen neu matchen →' : 'Übungen matchen →'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{pathQa && pathSteps.length > 0 ? (
|
||||
{wizardStep === 3 ? (
|
||||
<section aria-labelledby="planning-step-3-heading">
|
||||
<h4 id="planning-step-3-heading" style={{ margin: '0 0 10px', fontSize: '0.95rem' }}>
|
||||
Schritt 3 — Übungen & Qualität
|
||||
</h4>
|
||||
{pathSteps.length === 0 ? (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text2)', margin: 0 }}>
|
||||
Noch kein Match — zuerst in Schritt 2 „Übungen matchen“.
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ marginLeft: '10px', fontSize: '12px' }}
|
||||
onClick={() => setWizardStep(editableMajorSteps.length >= 2 ? 2 : 1)}
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{(semanticBrief || targetSummary || pathSkillExpectations) ? (
|
||||
<details style={{ marginBottom: '10px', fontSize: '12px' }}>
|
||||
<summary style={{ cursor: 'pointer', color: 'var(--accent-dark)', fontWeight: 600 }}>
|
||||
Pfad-Kontext (Thema, Fokus, Fähigkeiten)
|
||||
</summary>
|
||||
<div style={{ marginTop: '8px', display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
|
||||
{semanticBrief?.primary_topic ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--accent)' }}>
|
||||
Thema: {semanticBrief.primary_topic}
|
||||
</span>
|
||||
) : null}
|
||||
{Array.isArray(semanticBrief?.development_arc) &&
|
||||
semanticBrief.development_arc.slice(0, 3).map((phase) => (
|
||||
<span key={phase} className="exercise-tag">
|
||||
{phase}
|
||||
</span>
|
||||
))}
|
||||
{Array.isArray(targetSummary?.focus_areas) &&
|
||||
targetSummary.focus_areas.slice(0, 1).map((fa) => (
|
||||
<span key={fa} className="exercise-tag">
|
||||
Fokus: {fa}
|
||||
</span>
|
||||
))}
|
||||
{formatExpectedSkillNames(pathSkillExpectations, 5).map((name) => (
|
||||
<span
|
||||
key={`path-skill-${name}`}
|
||||
className="exercise-tag"
|
||||
style={{ borderColor: 'color-mix(in srgb, var(--accent) 55%, var(--border))' }}
|
||||
title="Pfadweite Fähigkeiten-Erwartung (Scoring)"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
{pathQa ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
|
|
@ -1528,7 +1707,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</p>
|
||||
) : Number(pathQa.off_topic_count) > 0 ? (
|
||||
<p style={{ margin: '6px 0 0', color: 'var(--danger)' }}>
|
||||
{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.
|
||||
</p>
|
||||
) : null}
|
||||
{pathQa.reorder_applied ? (
|
||||
|
|
@ -1547,88 +1726,7 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{gapFillOffers.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid color-mix(in srgb, var(--accent) 40%, var(--border))',
|
||||
background: 'color-mix(in srgb, var(--accent) 5%, var(--surface))',
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontSize: '13px' }}>Fehlende Schritte — mit KI anlegen</strong>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '6px 0 10px', lineHeight: 1.45 }}>
|
||||
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.'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{gapFillOffers.map((offer) => (
|
||||
<div
|
||||
key={offer.offer_id}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--surface2)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<span className="exercise-tag" style={{ marginBottom: '6px', display: 'inline-block' }}>
|
||||
{OFFER_SOURCE_LABELS[offer.source] || offer.source || 'Lücke'}
|
||||
{offer.phase ? ` · ${offer.phase}` : ''}
|
||||
</span>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600 }}>{offer.title_hint}</div>
|
||||
{offer.rationale ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)', margin: '4px 0 0' }}>{offer.rationale}</p>
|
||||
) : null}
|
||||
{offer.from_title && offer.to_title ? (
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '4px 0 0' }}>
|
||||
Zwischen „{offer.from_title}“ und „{offer.to_title}“
|
||||
{offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''}
|
||||
</p>
|
||||
) : null}
|
||||
<GapOfferContextPreview
|
||||
lines={gapOfferContextDisplayLines(offer, gapContextFallbackParams)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ fontSize: '12px', flexShrink: 0 }}
|
||||
disabled={
|
||||
quickSaving ||
|
||||
(isGapOfferBlockedByPathCapacity(offer, pathSteps.length, maxSteps) &&
|
||||
maxSteps >= PATH_STEPS_HARD_MAX)
|
||||
}
|
||||
onClick={() => handleGapFillClick(offer)}
|
||||
title={
|
||||
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'
|
||||
: 'Kontext prüfen, Ergänzungen mitgeben, dann KI-Entwurf'
|
||||
}
|
||||
>
|
||||
{generatingOfferId === offer.offer_id
|
||||
? 'KI erstellt Entwurf …'
|
||||
: 'Vorbereiten & KI anlegen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{pathSteps.length > 0 ? (
|
||||
<>
|
||||
<div style={{ marginTop: '14px' }}>
|
||||
{pathSteps.map((step, idx) => (
|
||||
<div
|
||||
key={`${step.exerciseId}-${step.proposalKey || ''}-${idx}`}
|
||||
|
|
@ -1637,14 +1735,12 @@ export default function ExerciseProgressionPathBuilder({
|
|||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '10px',
|
||||
alignItems: 'end',
|
||||
marginBottom: '12px',
|
||||
paddingBottom: '12px',
|
||||
borderBottom: idx < pathSteps.length - 1 ? '1px dashed var(--border)' : 'none',
|
||||
padding: step.isOffTopic ? '8px' : '10px 12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--border)',
|
||||
background: step.isOffTopic
|
||||
? 'color-mix(in srgb, var(--danger) 6%, transparent)'
|
||||
: undefined,
|
||||
borderRadius: step.isOffTopic ? '8px' : undefined,
|
||||
padding: step.isOffTopic ? '8px' : undefined,
|
||||
? 'color-mix(in srgb, var(--danger) 6%, var(--surface2))'
|
||||
: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||
|
|
@ -1749,6 +1845,170 @@ export default function ExerciseProgressionPathBuilder({
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '10px',
|
||||
alignItems: 'center',
|
||||
marginTop: '14px',
|
||||
paddingTop: '14px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setWizardStep(2)}
|
||||
disabled={disabled || loading || saving}
|
||||
>
|
||||
← Roadmap
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => setWizardStep(4)}
|
||||
disabled={disabled || loading || saving}
|
||||
>
|
||||
Weiter zu Lücken & Speichern →
|
||||
</button>
|
||||
{gapFillOffers.length > 0 ? (
|
||||
<span className="exercise-tag" style={{ borderColor: 'var(--danger)' }}>
|
||||
{gapFillOffers.length} Lücke(n) offen
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{wizardStep === 4 ? (
|
||||
<section aria-labelledby="planning-step-4-heading">
|
||||
<h4 id="planning-step-4-heading" style={{ margin: '0 0 10px', fontSize: '0.95rem' }}>
|
||||
Schritt 4 — Lücken schließen & Pfad speichern
|
||||
</h4>
|
||||
{pathSteps.length === 0 ? (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text2)', margin: 0 }}>
|
||||
Noch kein Pfad — zuerst Schritt 3 abschließen.
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ marginLeft: '10px', fontSize: '12px' }}
|
||||
onClick={() => setWizardStep(3)}
|
||||
>
|
||||
Zu Schritt 3
|
||||
</button>
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{gapFillOffers.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '14px',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid color-mix(in srgb, var(--accent) 40%, var(--border))',
|
||||
background: 'color-mix(in srgb, var(--accent) 5%, var(--surface))',
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontSize: '13px' }}>Fehlende Schritte — mit KI anlegen</strong>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '6px 0 10px', lineHeight: 1.45 }}>
|
||||
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.'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{gapFillOffers.map((offer) => (
|
||||
<div
|
||||
key={offer.offer_id}
|
||||
style={{
|
||||
padding: '10px 12px',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--surface2)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<span className="exercise-tag" style={{ marginBottom: '6px', display: 'inline-block' }}>
|
||||
{OFFER_SOURCE_LABELS[offer.source] || offer.source || 'Lücke'}
|
||||
{offer.phase ? ` · ${offer.phase}` : ''}
|
||||
</span>
|
||||
<div style={{ fontSize: '13px', fontWeight: 600 }}>{offer.title_hint}</div>
|
||||
{offer.rationale ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)', margin: '4px 0 0' }}>{offer.rationale}</p>
|
||||
) : null}
|
||||
{offer.from_title && offer.to_title ? (
|
||||
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '4px 0 0' }}>
|
||||
Zwischen „{offer.from_title}“ und „{offer.to_title}“
|
||||
{offer.replace_step_index != null ? ' (ersetzt themenfremden Schritt)' : ''}
|
||||
</p>
|
||||
) : null}
|
||||
<GapOfferContextPreview
|
||||
lines={gapOfferContextDisplayLines(offer, gapContextFallbackParams)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ fontSize: '12px', flexShrink: 0 }}
|
||||
disabled={
|
||||
quickSaving ||
|
||||
(isGapOfferBlockedByPathCapacity(offer, pathSteps.length, maxSteps) &&
|
||||
maxSteps >= PATH_STEPS_HARD_MAX)
|
||||
}
|
||||
onClick={() => handleGapFillClick(offer)}
|
||||
title={
|
||||
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'
|
||||
: 'Kontext prüfen, Ergänzungen mitgeben, dann KI-Entwurf'
|
||||
}
|
||||
>
|
||||
{generatingOfferId === offer.offer_id
|
||||
? 'KI erstellt Entwurf …'
|
||||
: 'Vorbereiten & KI anlegen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: 'var(--accent-dark)',
|
||||
margin: '0 0 14px',
|
||||
padding: '8px 10px',
|
||||
borderRadius: '8px',
|
||||
background: 'color-mix(in srgb, var(--accent) 8%, var(--surface))',
|
||||
}}
|
||||
>
|
||||
Keine offenen Lücken — Pfad kann direkt gespeichert werden.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<details style={{ marginBottom: '12px', fontSize: '12px' }}>
|
||||
<summary style={{ cursor: 'pointer', fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||||
Pfad-Übersicht ({pathSteps.length} Schritte)
|
||||
</summary>
|
||||
<ol style={{ margin: '10px 0 0', paddingLeft: '20px', lineHeight: 1.5 }}>
|
||||
{pathSteps.map((step, idx) => (
|
||||
<li key={`summary-${idx}`} style={{ marginBottom: '4px' }}>
|
||||
<strong>{step.exerciseTitle}</strong>
|
||||
{step.roadmapPhase ? ` (${step.roadmapPhase})` : ''}
|
||||
{step.isAiProposal ? ' — KI-Vorschlag, noch anlegen' : ''}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<div className="form-row">
|
||||
<label className="form-label">Notiz für Kanten (Fallback, optional)</label>
|
||||
<textarea
|
||||
|
|
@ -1759,7 +2019,15 @@ export default function ExerciseProgressionPathBuilder({
|
|||
placeholder="Wird pro Kante genutzt, wenn keine KI-Begründung vorliegt."
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '10px' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setWizardStep(3)}
|
||||
disabled={disabled || loading || saving}
|
||||
>
|
||||
← Match anpassen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
|
|
@ -1778,14 +2046,16 @@ export default function ExerciseProgressionPathBuilder({
|
|||
setSemanticBrief(null)
|
||||
setPathQa(null)
|
||||
setGapFillOffers([])
|
||||
setProgressionRoadmap(null)
|
||||
setPathSkillExpectations(null)
|
||||
setWizardStep(editableMajorSteps.length >= 2 ? 2 : 1)
|
||||
}}
|
||||
>
|
||||
Vorschlag verwerfen
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<ExerciseGapFillPrepModal
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user