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, generatingOfferId = null,
aiBusy = false, aiBusy = false,
evaluateDisabled = false, evaluateDisabled = false,
evaluationStale = false,
}) { }) {
const { fixHints: optimizationHints, highlightTexts } = useMemo( const { fixHints: optimizationHints, highlightTexts } = useMemo(
() => splitPathQaHints(pathQa), () => splitPathQaHints(pathQa),
@ -314,8 +315,8 @@ export default function ProgressionFindingsPanel({
<div className="card" style={{ position: 'sticky', top: '12px' }}> <div className="card" style={{ position: 'sticky', top: '12px' }}>
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Graph-Bewertung</h3> <h3 style={{ marginTop: 0, fontSize: '1rem' }}>Graph-Bewertung</h3>
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}> <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, Bewertet den aktuellen Slot-Stand (3-Stufen-QS, ohne Auto-Rematch). Nach Änderungen am Graphen
solange keine Zuordnung geändert wurde. erscheint ein Hinweis dann erneut Graph bewerten.
</p> </p>
<button <button
@ -334,6 +335,24 @@ export default function ProgressionFindingsPanel({
</p> </p>
) : null} ) : 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 ? ( {pathQa ? (
<div <div
style={{ style={{

View File

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