From 3b483346defe0eb1af2f3f9665a9e1d2ea038df2 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 10 Jun 2026 16:04:15 +0200 Subject: [PATCH] Enhance Progression Graph Editor with Skills Catalog and AI Draft Handling - Introduced skills catalog management in the `ProgressionGraphEditor`, allowing for improved context in AI suggestions. - Updated the loading mechanism to fetch both focus areas and skills catalog concurrently, enhancing performance. - Implemented `ensureQuickCreateDraftFromAiSuggestion` utility to streamline the creation of drafts from AI suggestions. - Enhanced slot management by integrating AI context into the gap fill preparation process, improving user experience. - Incremented application version to reflect these updates. --- .../src/components/ProgressionGraphEditor.jsx | 139 ++++++++++++------ frontend/src/utils/exerciseAiQuickCreate.js | 18 +++ 2 files changed, 111 insertions(+), 46 deletions(-) diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index 0bfbc57..04c0ae4 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -12,13 +12,14 @@ import { aiPreviewToQuickCreateDraft, buildQuickCreateAiPreview, buildQuickCreateExercisePayloadFromDraft, + ensureQuickCreateDraftFromAiSuggestion, } from '../utils/exerciseAiQuickCreate' import { buildPathGapPlanningContextForAi, gapOfferContextDisplayLines, initialStageLearningGoalFromOffer, } from '../utils/planningContextForExerciseAi' -import ExerciseAiQuickCreateModal from './exercises/ExerciseAiQuickCreateModal' +import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal' import { addSlotToDraft, applyEvaluateResponseToDraft, @@ -77,6 +78,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const [semanticBrief, setSemanticBrief] = useState(null) const [targetSummary, setTargetSummary] = useState(null) const [focusAreas, setFocusAreas] = useState([]) + const [skillsCatalog, setSkillsCatalog] = useState([]) const [activeOffer, setActiveOffer] = useState(null) const [activeOfferSlotIndex, setActiveOfferSlotIndex] = useState(null) @@ -89,11 +91,11 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const [generatingOfferId, setGeneratingOfferId] = useState(null) const [gapAiBusy, setGapAiBusy] = useState(false) const [currentEdges, setCurrentEdges] = useState([]) - const [slotQuickCreateOpen, setSlotQuickCreateOpen] = useState(false) const [slotQuickCreateIndex, setSlotQuickCreateIndex] = useState(null) const [slotQuickCreateDraft, setSlotQuickCreateDraft] = useState(null) const [slotQuickSaving, setSlotQuickSaving] = useState(false) const [slotQuickError, setSlotQuickError] = useState('') + const [activePlanningContextLines, setActivePlanningContextLines] = useState([]) const loadGraph = useCallback(async () => { if (!graphId) return @@ -130,13 +132,20 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa useEffect(() => { let cancelled = false - api - .listFocusAreas({ status: 'active' }) - .then((fa) => { - if (!cancelled) setFocusAreas(Array.isArray(fa) ? fa : []) + Promise.all([ + api.listFocusAreas({ status: 'active' }), + api.listSkillsCatalog({ status: 'active' }), + ]) + .then(([fa, sk]) => { + if (cancelled) return + setFocusAreas(Array.isArray(fa) ? fa : []) + setSkillsCatalog(Array.isArray(sk) ? sk : []) }) .catch(() => { - if (!cancelled) setFocusAreas([]) + if (!cancelled) { + setFocusAreas([]) + setSkillsCatalog([]) + } }) return () => { cancelled = true @@ -434,6 +443,20 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) } + const slotOfferContext = (slotIndex) => { + const slot = draft?.slots?.[slotIndex] + if (!slot) return null + return { + offer_id: `slot-${slotIndex}`, + title_hint: slot.primary?.exerciseTitle || slot.learning_goal, + roadmap_major_step_index: slot.majorStepIndex, + phase: slot.phase, + source: 'roadmap_unfilled', + goal_for_ai: slot.learning_goal, + sketch: slot.learning_goal, + } + } + const openGapFillPrep = (offer, slotIndex = null) => { const defaultFocus = resolveDefaultFocusAreaId(targetSummary, focusAreas) setActiveOffer(offer) @@ -442,6 +465,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa setGapPrepStageGoal(initialStageLearningGoalFromOffer(offer, gapContextParams)) setGapPrepSupplements('') setGapPrepFocusAreaId(defaultFocus ? String(defaultFocus) : '') + setActivePlanningContextLines(gapOfferContextDisplayLines(offer, gapContextParams)) setGapPrepError('') setGapPrepOpen(true) } @@ -472,12 +496,17 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa setGapAiBusy(true) setGeneratingOfferId(offer?.offer_id || null) setGapPrepError('') + setSlotQuickError('') + const contextParams = { + ...gapContextParams, + stageLearningGoalOverride: stageGoal, + gapTrainerSupplements: supplements, + } + setActivePlanningContextLines(gapOfferContextDisplayLines(offer, contextParams)) try { const planningContext = buildPathGapPlanningContextForAi({ offer, - ...gapContextParams, - stageLearningGoalOverride: stageGoal, - gapTrainerSupplements: supplements, + ...contextParams, }) const aiRes = await api.suggestExerciseAi({ title, @@ -507,15 +536,19 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa ai_suggestion: aiDraft, has_ai_payload: true, } - setDraft((prev) => applyGapOfferToDraft(prev, enrichedOffer, { slotIndex })) - if (slotIndex != null && Number.isFinite(slotIndex)) { - setSlotQuickCreateIndex(slotIndex) - setSlotQuickCreateDraft(aiDraft) - setSlotQuickCreateOpen(true) + const resolvedSlot = + slotIndex != null && Number.isFinite(slotIndex) + ? slotIndex + : activeOfferSlotIndex != null && Number.isFinite(activeOfferSlotIndex) + ? activeOfferSlotIndex + : null + if (resolvedSlot != null) { + setSlotQuickCreateIndex(resolvedSlot) + setDraft((prev) => applyGapOfferToDraft(prev, enrichedOffer, { slotIndex: resolvedSlot })) } + setSlotQuickCreateDraft(aiDraft) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) setGapPrepOpen(false) - setActiveOffer(null) } catch (e) { setGapPrepError(e.message || 'KI-Anlage fehlgeschlagen') } finally { @@ -528,25 +561,27 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const slot = draft?.slots?.[slotIndex] if (!slot) return const primary = slot.primary + const offer = slotOfferContext(slotIndex) setSlotQuickCreateIndex(slotIndex) setSlotQuickError('') - if (primary?.kind === 'proposal' && primary.aiSuggestion) { - setSlotQuickCreateDraft(primary.aiSuggestion) - setSlotQuickCreateOpen(true) - return - } + setActiveOffer(offer) setActiveOfferSlotIndex(slotIndex) - openGapFillPrep( - { - offer_id: `slot-${slotIndex}`, - title_hint: primary?.exerciseTitle || slot.learning_goal, - roadmap_major_step_index: slot.majorStepIndex, - phase: slot.phase, - source: 'roadmap_unfilled', - goal_for_ai: slot.learning_goal, - }, - slotIndex, - ) + setActivePlanningContextLines(gapOfferContextDisplayLines(offer, gapContextParams)) + + if (primary?.kind === 'proposal' && primary.aiSuggestion) { + const focusId = resolveDefaultFocusAreaId(targetSummary, focusAreas) + const draftReady = ensureQuickCreateDraftFromAiSuggestion(primary.aiSuggestion, { + title: primary.exerciseTitle || slot.learning_goal, + focusAreaId: focusId, + sketchPlain: (offer?.goal_for_ai || slot.learning_goal || '').trim(), + }) + if (draftReady) { + setSlotQuickCreateDraft(draftReady) + return + } + } + + openGapFillPrep(offer, slotIndex) } const applySlotQuickCreate = async () => { @@ -558,11 +593,15 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const created = await api.createExercise(payload) if (!created?.id) throw new Error('Anlegen fehlgeschlagen') setDraft((prev) => setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created)) - setSlotQuickCreateOpen(false) setSlotQuickCreateDraft(null) setSlotQuickCreateIndex(null) + setActiveOffer(null) + setActiveOfferSlotIndex(null) + setActivePlanningContextLines([]) } catch (e) { - setSlotQuickError(e.message || 'Übung konnte nicht angelegt werden') + const msg = e.message || 'Übung konnte nicht angelegt werden' + setSlotQuickError(msg) + alert(msg) } finally { setSlotQuickSaving(false) } @@ -766,20 +805,28 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa /> ) : null} - { - if (slotQuickSaving) return - setSlotQuickCreateOpen(false) - setSlotQuickCreateDraft(null) - setSlotQuickError('') - }} - title={draft?.slots?.[slotQuickCreateIndex]?.primary?.exerciseTitle || ''} + { + if (slotQuickSaving) return + setSlotQuickCreateDraft(null) + setSlotQuickError('') + if (activeOffer) { + setGapPrepOpen(true) + } else { + setActivePlanningContextLines([]) + } + }} + planningContextLines={activePlanningContextLines} + onApply={applySlotQuickCreate} + focusAreas={focusAreas} + skillsCatalog={skillsCatalog} + dialogTitle="Progressions-Slot — KI-Entwurf bearbeiten" + hint="Texte prüfen und anpassen, dann als Übung speichern — sie wird dem Slot zugeordnet." + applyLabel={slotQuickSaving ? 'Wird angelegt …' : 'Anlegen und Slot zuweisen'} + applyDisabled={slotQuickSaving} + zIndex={2100} />