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,
|
aiPreviewToQuickCreateDraft,
|
||||||
buildQuickCreateAiPreview,
|
buildQuickCreateAiPreview,
|
||||||
buildQuickCreateExercisePayloadFromDraft,
|
buildQuickCreateExercisePayloadFromDraft,
|
||||||
|
ensureQuickCreateDraftFromAiSuggestion,
|
||||||
} from '../utils/exerciseAiQuickCreate'
|
} from '../utils/exerciseAiQuickCreate'
|
||||||
import {
|
import {
|
||||||
buildPathGapPlanningContextForAi,
|
buildPathGapPlanningContextForAi,
|
||||||
gapOfferContextDisplayLines,
|
gapOfferContextDisplayLines,
|
||||||
initialStageLearningGoalFromOffer,
|
initialStageLearningGoalFromOffer,
|
||||||
} from '../utils/planningContextForExerciseAi'
|
} from '../utils/planningContextForExerciseAi'
|
||||||
import ExerciseAiQuickCreateModal from './exercises/ExerciseAiQuickCreateModal'
|
import ExerciseAiSuggestPreviewModal from './ExerciseAiSuggestPreviewModal'
|
||||||
import {
|
import {
|
||||||
addSlotToDraft,
|
addSlotToDraft,
|
||||||
applyEvaluateResponseToDraft,
|
applyEvaluateResponseToDraft,
|
||||||
|
|
@ -77,6 +78,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
const [semanticBrief, setSemanticBrief] = useState(null)
|
const [semanticBrief, setSemanticBrief] = useState(null)
|
||||||
const [targetSummary, setTargetSummary] = useState(null)
|
const [targetSummary, setTargetSummary] = useState(null)
|
||||||
const [focusAreas, setFocusAreas] = useState([])
|
const [focusAreas, setFocusAreas] = useState([])
|
||||||
|
const [skillsCatalog, setSkillsCatalog] = useState([])
|
||||||
|
|
||||||
const [activeOffer, setActiveOffer] = useState(null)
|
const [activeOffer, setActiveOffer] = useState(null)
|
||||||
const [activeOfferSlotIndex, setActiveOfferSlotIndex] = 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 [generatingOfferId, setGeneratingOfferId] = useState(null)
|
||||||
const [gapAiBusy, setGapAiBusy] = useState(false)
|
const [gapAiBusy, setGapAiBusy] = useState(false)
|
||||||
const [currentEdges, setCurrentEdges] = useState([])
|
const [currentEdges, setCurrentEdges] = useState([])
|
||||||
const [slotQuickCreateOpen, setSlotQuickCreateOpen] = useState(false)
|
|
||||||
const [slotQuickCreateIndex, setSlotQuickCreateIndex] = useState(null)
|
const [slotQuickCreateIndex, setSlotQuickCreateIndex] = useState(null)
|
||||||
const [slotQuickCreateDraft, setSlotQuickCreateDraft] = useState(null)
|
const [slotQuickCreateDraft, setSlotQuickCreateDraft] = useState(null)
|
||||||
const [slotQuickSaving, setSlotQuickSaving] = useState(false)
|
const [slotQuickSaving, setSlotQuickSaving] = useState(false)
|
||||||
const [slotQuickError, setSlotQuickError] = useState('')
|
const [slotQuickError, setSlotQuickError] = useState('')
|
||||||
|
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
||||||
|
|
||||||
const loadGraph = useCallback(async () => {
|
const loadGraph = useCallback(async () => {
|
||||||
if (!graphId) return
|
if (!graphId) return
|
||||||
|
|
@ -130,13 +132,20 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
api
|
Promise.all([
|
||||||
.listFocusAreas({ status: 'active' })
|
api.listFocusAreas({ status: 'active' }),
|
||||||
.then((fa) => {
|
api.listSkillsCatalog({ status: 'active' }),
|
||||||
if (!cancelled) setFocusAreas(Array.isArray(fa) ? fa : [])
|
])
|
||||||
|
.then(([fa, sk]) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setFocusAreas(Array.isArray(fa) ? fa : [])
|
||||||
|
setSkillsCatalog(Array.isArray(sk) ? sk : [])
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (!cancelled) setFocusAreas([])
|
if (!cancelled) {
|
||||||
|
setFocusAreas([])
|
||||||
|
setSkillsCatalog([])
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
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))
|
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 openGapFillPrep = (offer, slotIndex = null) => {
|
||||||
const defaultFocus = resolveDefaultFocusAreaId(targetSummary, focusAreas)
|
const defaultFocus = resolveDefaultFocusAreaId(targetSummary, focusAreas)
|
||||||
setActiveOffer(offer)
|
setActiveOffer(offer)
|
||||||
|
|
@ -442,6 +465,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
setGapPrepStageGoal(initialStageLearningGoalFromOffer(offer, gapContextParams))
|
setGapPrepStageGoal(initialStageLearningGoalFromOffer(offer, gapContextParams))
|
||||||
setGapPrepSupplements('')
|
setGapPrepSupplements('')
|
||||||
setGapPrepFocusAreaId(defaultFocus ? String(defaultFocus) : '')
|
setGapPrepFocusAreaId(defaultFocus ? String(defaultFocus) : '')
|
||||||
|
setActivePlanningContextLines(gapOfferContextDisplayLines(offer, gapContextParams))
|
||||||
setGapPrepError('')
|
setGapPrepError('')
|
||||||
setGapPrepOpen(true)
|
setGapPrepOpen(true)
|
||||||
}
|
}
|
||||||
|
|
@ -472,12 +496,17 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
setGapAiBusy(true)
|
setGapAiBusy(true)
|
||||||
setGeneratingOfferId(offer?.offer_id || null)
|
setGeneratingOfferId(offer?.offer_id || null)
|
||||||
setGapPrepError('')
|
setGapPrepError('')
|
||||||
|
setSlotQuickError('')
|
||||||
|
const contextParams = {
|
||||||
|
...gapContextParams,
|
||||||
|
stageLearningGoalOverride: stageGoal,
|
||||||
|
gapTrainerSupplements: supplements,
|
||||||
|
}
|
||||||
|
setActivePlanningContextLines(gapOfferContextDisplayLines(offer, contextParams))
|
||||||
try {
|
try {
|
||||||
const planningContext = buildPathGapPlanningContextForAi({
|
const planningContext = buildPathGapPlanningContextForAi({
|
||||||
offer,
|
offer,
|
||||||
...gapContextParams,
|
...contextParams,
|
||||||
stageLearningGoalOverride: stageGoal,
|
|
||||||
gapTrainerSupplements: supplements,
|
|
||||||
})
|
})
|
||||||
const aiRes = await api.suggestExerciseAi({
|
const aiRes = await api.suggestExerciseAi({
|
||||||
title,
|
title,
|
||||||
|
|
@ -507,15 +536,19 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
ai_suggestion: aiDraft,
|
ai_suggestion: aiDraft,
|
||||||
has_ai_payload: true,
|
has_ai_payload: true,
|
||||||
}
|
}
|
||||||
setDraft((prev) => applyGapOfferToDraft(prev, enrichedOffer, { slotIndex }))
|
const resolvedSlot =
|
||||||
if (slotIndex != null && Number.isFinite(slotIndex)) {
|
slotIndex != null && Number.isFinite(slotIndex)
|
||||||
setSlotQuickCreateIndex(slotIndex)
|
? slotIndex
|
||||||
setSlotQuickCreateDraft(aiDraft)
|
: activeOfferSlotIndex != null && Number.isFinite(activeOfferSlotIndex)
|
||||||
setSlotQuickCreateOpen(true)
|
? 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))
|
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
|
||||||
setGapPrepOpen(false)
|
setGapPrepOpen(false)
|
||||||
setActiveOffer(null)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setGapPrepError(e.message || 'KI-Anlage fehlgeschlagen')
|
setGapPrepError(e.message || 'KI-Anlage fehlgeschlagen')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -528,25 +561,27 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
const slot = draft?.slots?.[slotIndex]
|
const slot = draft?.slots?.[slotIndex]
|
||||||
if (!slot) return
|
if (!slot) return
|
||||||
const primary = slot.primary
|
const primary = slot.primary
|
||||||
|
const offer = slotOfferContext(slotIndex)
|
||||||
setSlotQuickCreateIndex(slotIndex)
|
setSlotQuickCreateIndex(slotIndex)
|
||||||
setSlotQuickError('')
|
setSlotQuickError('')
|
||||||
if (primary?.kind === 'proposal' && primary.aiSuggestion) {
|
setActiveOffer(offer)
|
||||||
setSlotQuickCreateDraft(primary.aiSuggestion)
|
|
||||||
setSlotQuickCreateOpen(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setActiveOfferSlotIndex(slotIndex)
|
setActiveOfferSlotIndex(slotIndex)
|
||||||
openGapFillPrep(
|
setActivePlanningContextLines(gapOfferContextDisplayLines(offer, gapContextParams))
|
||||||
{
|
|
||||||
offer_id: `slot-${slotIndex}`,
|
if (primary?.kind === 'proposal' && primary.aiSuggestion) {
|
||||||
title_hint: primary?.exerciseTitle || slot.learning_goal,
|
const focusId = resolveDefaultFocusAreaId(targetSummary, focusAreas)
|
||||||
roadmap_major_step_index: slot.majorStepIndex,
|
const draftReady = ensureQuickCreateDraftFromAiSuggestion(primary.aiSuggestion, {
|
||||||
phase: slot.phase,
|
title: primary.exerciseTitle || slot.learning_goal,
|
||||||
source: 'roadmap_unfilled',
|
focusAreaId: focusId,
|
||||||
goal_for_ai: slot.learning_goal,
|
sketchPlain: (offer?.goal_for_ai || slot.learning_goal || '').trim(),
|
||||||
},
|
})
|
||||||
slotIndex,
|
if (draftReady) {
|
||||||
)
|
setSlotQuickCreateDraft(draftReady)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openGapFillPrep(offer, slotIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
const applySlotQuickCreate = async () => {
|
const applySlotQuickCreate = async () => {
|
||||||
|
|
@ -558,11 +593,15 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
const created = await api.createExercise(payload)
|
const created = await api.createExercise(payload)
|
||||||
if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
|
if (!created?.id) throw new Error('Anlegen fehlgeschlagen')
|
||||||
setDraft((prev) => setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created))
|
setDraft((prev) => setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created))
|
||||||
setSlotQuickCreateOpen(false)
|
|
||||||
setSlotQuickCreateDraft(null)
|
setSlotQuickCreateDraft(null)
|
||||||
setSlotQuickCreateIndex(null)
|
setSlotQuickCreateIndex(null)
|
||||||
|
setActiveOffer(null)
|
||||||
|
setActiveOfferSlotIndex(null)
|
||||||
|
setActivePlanningContextLines([])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSlotQuickError(e.message || 'Übung konnte nicht angelegt werden')
|
const msg = e.message || 'Übung konnte nicht angelegt werden'
|
||||||
|
setSlotQuickError(msg)
|
||||||
|
alert(msg)
|
||||||
} finally {
|
} finally {
|
||||||
setSlotQuickSaving(false)
|
setSlotQuickSaving(false)
|
||||||
}
|
}
|
||||||
|
|
@ -766,20 +805,28 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ExerciseAiQuickCreateModal
|
<ExerciseAiSuggestPreviewModal
|
||||||
open={slotQuickCreateOpen}
|
|
||||||
onClose={() => {
|
|
||||||
if (slotQuickSaving) return
|
|
||||||
setSlotQuickCreateOpen(false)
|
|
||||||
setSlotQuickCreateDraft(null)
|
|
||||||
setSlotQuickError('')
|
|
||||||
}}
|
|
||||||
title={draft?.slots?.[slotQuickCreateIndex]?.primary?.exerciseTitle || ''}
|
|
||||||
draft={slotQuickCreateDraft}
|
draft={slotQuickCreateDraft}
|
||||||
onDraftChange={setSlotQuickCreateDraft}
|
onDraftChange={setSlotQuickCreateDraft}
|
||||||
busy={slotQuickSaving}
|
onDiscard={() => {
|
||||||
error={slotQuickError}
|
if (slotQuickSaving) return
|
||||||
onSubmit={applySlotQuickCreate}
|
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
|
<ExerciseGapFillPrepModal
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,24 @@ export function describeAiSkillRowForPreview(row, skillsCatalog) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** KI-Vorschau → bearbeitbarer Entwurf (Rich-Text-Felder). */
|
/** 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 }) {
|
export function aiPreviewToQuickCreateDraft(preview, { title, focusAreaId, sketchPlain }) {
|
||||||
const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain)
|
const sketchHtml = aiPlainTextToMinimalHtml(sketchPlain)
|
||||||
const instructionFields = {
|
const instructionFields = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user