Enhance Progression Findings and Graph Editor with Evaluation Staleness Handling
Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 47s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Failing after 2s
Test Suite / playwright-tests (push) Successful in 1m19s

- Added `evaluationStale` state to `ProgressionGraphEditor` and `ProgressionFindingsPanel` to track the freshness of evaluations.
- Updated UI to display a warning when evaluations are stale, prompting users to re-evaluate the graph.
- Modified loading and evaluation functions to manage the `evaluationStale` state effectively, ensuring accurate user feedback during the evaluation process.
- Improved user notifications regarding the need for re-evaluation after changes to the graph.
This commit is contained in:
Lars 2026-06-13 16:23:04 +02:00
parent f0e581a9f5
commit 5e5f4ca8d4
2 changed files with 49 additions and 20 deletions

View File

@ -296,6 +296,7 @@ export default function ProgressionFindingsPanel({
generatingOfferId = null,
aiBusy = false,
evaluateDisabled = false,
evaluationStale = false,
}) {
const { fixHints: optimizationHints, highlightTexts } = useMemo(
() => splitPathQaHints(pathQa),
@ -314,8 +315,8 @@ export default function ProgressionFindingsPanel({
<div className="card" style={{ position: 'sticky', top: '12px' }}>
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Graph-Bewertung</h3>
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
Bewertet den aktuellen Slot-Stand (3-Stufen-QS, ohne Auto-Rematch) dieselbe Logik nach Match,
solange keine Zuordnung geändert wurde.
Bewertet den aktuellen Slot-Stand (3-Stufen-QS, ohne Auto-Rematch). Nach Änderungen am Graphen
erscheint ein Hinweis dann erneut Graph bewerten.
</p>
<button
@ -334,6 +335,24 @@ export default function ProgressionFindingsPanel({
</p>
) : null}
{evaluationStale && pathQa ? (
<div
role="status"
style={{
marginBottom: '12px',
padding: '8px 10px',
borderRadius: '8px',
border: '1px solid color-mix(in srgb, var(--danger) 45%, var(--border))',
background: 'color-mix(in srgb, var(--danger) 12%, var(--surface2))',
fontSize: '12px',
lineHeight: 1.45,
color: 'var(--text1)',
}}
>
<strong>Bewertung veraltet, neue Bewertung notwendig.</strong>
</div>
) : null}
{pathQa ? (
<div
style={{

View File

@ -129,8 +129,9 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const [comparing, setComparing] = useState(false)
const [compareApplying, setCompareApplying] = useState(false)
const [proposedPathQa, setProposedPathQa] = useState(null)
const [evaluationStale, setEvaluationStale] = useState(false)
const loadGraph = useCallback(async () => {
const loadGraph = useCallback(async ({ preserveEvaluationStale = false } = {}) => {
if (!graphId) return
setBusy(true)
setLoadErr('')
@ -153,6 +154,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
)
const findings = graph?.planning_roadmap?.last_findings
if (findings) setPathQa(findings)
if (!preserveEvaluationStale) setEvaluationStale(false)
} catch (e) {
setLoadErr(e.message || 'Graph konnte nicht geladen werden')
setDraft(null)
@ -202,6 +204,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const next = patchFn(prev)
return { ...next, dirty: true }
})
setEvaluationStale(true)
}, [])
const gapContextParams = useMemo(() => {
@ -366,6 +369,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
)
return { ...structured, dirty: true }
})
setEvaluationStale(true)
setStartTargetReady(true)
setSemanticBrief(res?.semantic_brief_summary || null)
} catch (e) {
@ -434,6 +438,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
})
const withPhases = syncSlotPhasesFromRoadmap(hydrated, roadmap)
setDraft({ ...withPhases, goalQuery: q, maxSteps: majorCount || withPhases.maxSteps, dirty: true })
setEvaluationStale(true)
setSemanticBrief(res?.semantic_brief_summary || null)
} catch (e) {
setActionErr(e.message || 'Roadmap-Generierung fehlgeschlagen')
@ -442,14 +447,14 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
}
}
const buildEvaluateRequest = (synced) => {
const buildEvaluateRequest = (synced, { llmPathQa = true, aiGapFill = true } = {}) => {
const override = majorStepsToOverridePayload(synced.slots)
return {
query: (synced.goalQuery || '').trim(),
max_steps: synced.slots.length || draft?.maxSteps || 5,
include_path_qa: true,
include_llm_path_qa: true,
include_ai_gap_fill: true,
include_llm_path_qa: llmPathQa,
include_ai_gap_fill: aiGapFill,
include_path_reorder: false,
include_llm_intent: false,
evaluate_only: true,
@ -462,11 +467,13 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
}
}
const fetchPathEvaluate = async (synced) => api.suggestProgressionPath(buildEvaluateRequest(synced))
const fetchPathEvaluate = async (synced, options) =>
api.suggestProgressionPath(buildEvaluateRequest(synced, options))
const applyEvaluateResult = (synced, res) => {
setSemanticBrief(res?.semantic_brief_summary || null)
setPathQa(res?.path_qa || null)
setEvaluationStale(false)
const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res)
return { draft: { ...evaluated, lastFindings: res?.path_qa || null }, remainingOffers }
}
@ -632,6 +639,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const applyOptimizeCompare = async (selectedMajorIndices) => {
if (!comparePayload || !draft) return
setCompareApplying(true)
setMatchNotice('Übernahme: Slots aktualisieren …')
try {
const synced = syncProgressionRoadmapFromSlots(draft)
const nextDraft = comparePayload?.unified_slot_review
@ -642,20 +650,17 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
selectedMajorIndices,
)
const syncedNext = syncProgressionRoadmapFromSlots(nextDraft)
const evalRes = await fetchPathEvaluate(syncedNext)
const { draft: evaluated, remainingOffers } = applyEvaluateResult(syncedNext, evalRes)
setDraft({ ...evaluated, dirty: true })
const mergedOffers = mergeGapOffersForDraft(evaluated, comparePayload, evalRes)
setGapFillOffers(mergedOffers.length > 0 ? mergedOffers : remainingOffers)
setProposedPathQa(null)
setDraft({ ...syncedNext, dirty: false })
setEvaluationStale(true)
setCompareOpen(false)
setComparePayload(null)
setMatchNotice('Ausgewählte Optimierungen übernommen — Pfad-QS neu bewertet.')
await saveProgressionGraphDraft(api, graphId, {
...evaluated,
lastFindings: evalRes?.path_qa || null,
})
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
setProposedPathQa(null)
await saveProgressionGraphDraft(api, graphId, syncedNext)
setMatchNotice(
'Übernommen und gespeichert. Bewertung bezieht sich noch auf den vorherigen Stand — bitte „Graph bewerten“.',
)
} catch (e) {
setActionErr(e.message || 'Übernahme fehlgeschlagen')
} finally {
@ -692,7 +697,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
setActionErr('')
try {
await saveProgressionGraphDraft(api, graphId, { ...draft, lastFindings: pathQa })
await loadGraph()
await loadGraph({ preserveEvaluationStale: true })
if (typeof onSaved === 'function') await onSaved()
alert('Progressionsgraph gespeichert.')
} catch (e) {
@ -707,6 +712,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const next = applyGapOfferToDraft(prev, offer, { slotIndex })
return { ...next, dirty: true }
})
setEvaluationStale(true)
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
}
@ -719,6 +725,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const next = applyGapOfferToDraft(prev, offer, { insertNewSlot: true })
return { ...next, dirty: true }
})
setEvaluationStale(true)
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
}
@ -832,6 +839,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
if (resolvedSlot != null) {
setSlotQuickCreateIndex(resolvedSlot)
setDraft((prev) => applyGapOfferToDraft(prev, enrichedOffer, { slotIndex: resolvedSlot }))
setEvaluationStale(true)
}
setSlotQuickCreateDraft(aiDraft)
setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id))
@ -880,6 +888,7 @@ 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))
setEvaluationStale(true)
setSlotQuickCreateDraft(null)
setSlotQuickCreateIndex(null)
setActiveOffer(null)
@ -1168,6 +1177,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
slotCount={draft.slots.length}
loading={evaluating}
error=""
evaluationStale={evaluationStale}
onEvaluate={runEvaluate}
onApplyGapOffer={handleApplyGapOffer}
onInsertGapSlot={handleInsertGapSlot}