Enhance Progression Graph Editor with Skills Catalog and AI Draft Handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m21s
- 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.
This commit is contained in:
parent
e0ddfa6ce5
commit
3b483346de
|
|
@ -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}
|
||||
|
||||
<ExerciseAiQuickCreateModal
|
||||
open={slotQuickCreateOpen}
|
||||
onClose={() => {
|
||||
if (slotQuickSaving) return
|
||||
setSlotQuickCreateOpen(false)
|
||||
setSlotQuickCreateDraft(null)
|
||||
setSlotQuickError('')
|
||||
}}
|
||||
title={draft?.slots?.[slotQuickCreateIndex]?.primary?.exerciseTitle || ''}
|
||||
<ExerciseAiSuggestPreviewModal
|
||||
draft={slotQuickCreateDraft}
|
||||
onDraftChange={setSlotQuickCreateDraft}
|
||||
busy={slotQuickSaving}
|
||||
error={slotQuickError}
|
||||
onSubmit={applySlotQuickCreate}
|
||||
onDiscard={() => {
|
||||
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}
|
||||
/>
|
||||
|
||||
<ExerciseGapFillPrepModal
|
||||
|
|
|
|||
|
|
@ -166,6 +166,24 @@ export function describeAiSkillRowForPreview(row, skillsCatalog) {
|
|||
}
|
||||
|
||||
/** KI-Vorschau → bearbeitbarer Entwurf (Rich-Text-Felder). */
|
||||
/** Rohes API-ai_suggestion oder bereits bearbeiteter Entwurf → Preview-Entwurf. */
|
||||
export function ensureQuickCreateDraftFromAiSuggestion(
|
||||
aiSuggestion,
|
||||
{ title = '', focusAreaId = '', sketchPlain = '' } = {},
|
||||
) {
|
||||
if (!aiSuggestion || typeof aiSuggestion !== 'object') return null
|
||||
if (aiSuggestion.instructionFields) return aiSuggestion
|
||||
const preview = buildQuickCreateAiPreview(aiSuggestion, { sketchPlain })
|
||||
if (
|
||||
!preview.hasSummaryProposal &&
|
||||
!preview.hasInstructionChoices &&
|
||||
!preview.hasSkillChoices
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return aiPreviewToQuickCreateDraft(preview, { title, focusAreaId, sketchPlain })
|
||||
}
|
||||
|
||||
export function aiPreviewToQuickCreateDraft(preview, { title, focusAreaId, sketchPlain }) {
|
||||
const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain)
|
||||
const instructionFields = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user