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
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:
parent
f0e581a9f5
commit
5e5f4ca8d4
|
|
@ -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={{
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user