From 7265cd5a0142c4c53921d0000d3b49c9e4879808 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 13 Jun 2026 16:29:17 +0200 Subject: [PATCH] Add findings_stale field to GraphPlanningRoadmapArtifact and update ProgressionGraphEditor for state management - Introduced `findings_stale` field in `GraphPlanningRoadmapArtifact` to track the freshness of findings. - Updated `ProgressionGraphEditor` to manage `findingsStale` state across various functions, ensuring accurate representation of evaluation status. - Modified related utility functions and tests to accommodate the new state, enhancing overall functionality and user feedback in the progression graph management process. --- .../progression_graph_planning_artifact.py | 1 + ...est_progression_graph_planning_artifact.py | 12 +++++ .../src/components/ProgressionGraphEditor.jsx | 47 +++++++++---------- frontend/src/utils/progressionGraphDraft.js | 2 + 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/backend/progression_graph_planning_artifact.py b/backend/progression_graph_planning_artifact.py index 3cffdaf..296446a 100644 --- a/backend/progression_graph_planning_artifact.py +++ b/backend/progression_graph_planning_artifact.py @@ -37,6 +37,7 @@ class GraphPlanningRoadmapArtifact(BaseModel): path_skill_expectations: Optional[Dict[str, Any]] = None slot_contents: Optional[List[SlotContentEntry]] = None last_findings: Optional[Dict[str, Any]] = None + findings_stale: bool = Field(default=False) planning_catalog_context: Optional[Dict[str, Any]] = None @field_validator("progression_roadmap", "path_skill_expectations", "last_findings", "planning_catalog_context", mode="before") diff --git a/backend/tests/test_progression_graph_planning_artifact.py b/backend/tests/test_progression_graph_planning_artifact.py index 1ae1932..4898070 100644 --- a/backend/tests/test_progression_graph_planning_artifact.py +++ b/backend/tests/test_progression_graph_planning_artifact.py @@ -57,3 +57,15 @@ def test_normalize_slot_contents(): ) assert len(out["slot_contents"]) == 2 assert out["slot_contents"][1]["primary"]["kind"] == "proposal" + + +def test_normalize_planning_roadmap_with_findings_stale(): + out = normalize_planning_roadmap_payload( + { + "goal_query": "Mae Geri", + "last_findings": {"overall_ok": False}, + "findings_stale": True, + } + ) + assert out["findings_stale"] is True + assert out["last_findings"]["overall_ok"] is False diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index 6e2a8c3..59875ad 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -129,9 +129,8 @@ 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 ({ preserveEvaluationStale = false } = {}) => { + const loadGraph = useCallback(async () => { if (!graphId) return setBusy(true) setLoadErr('') @@ -154,7 +153,6 @@ 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,9 +200,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa setDraft((prev) => { if (!prev) return prev const next = patchFn(prev) - return { ...next, dirty: true } + return { ...next, dirty: true, findingsStale: true } }) - setEvaluationStale(true) }, []) const gapContextParams = useMemo(() => { @@ -367,9 +364,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa { ...prev, progressionRoadmap: roadmap }, roadmap, ) - return { ...structured, dirty: true } + return { ...structured, dirty: true, findingsStale: true } }) - setEvaluationStale(true) setStartTargetReady(true) setSemanticBrief(res?.semantic_brief_summary || null) } catch (e) { @@ -437,8 +433,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa graphName: draft.graphName, }) const withPhases = syncSlotPhasesFromRoadmap(hydrated, roadmap) - setDraft({ ...withPhases, goalQuery: q, maxSteps: majorCount || withPhases.maxSteps, dirty: true }) - setEvaluationStale(true) + setDraft({ ...withPhases, goalQuery: q, maxSteps: majorCount || withPhases.maxSteps, dirty: true, findingsStale: true }) setSemanticBrief(res?.semantic_brief_summary || null) } catch (e) { setActionErr(e.message || 'Roadmap-Generierung fehlgeschlagen') @@ -473,9 +468,11 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa 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 } + return { + draft: { ...evaluated, lastFindings: res?.path_qa || null, findingsStale: false }, + remainingOffers, + } } const buildMatchRequestBase = (synced) => { @@ -651,13 +648,12 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa ) const syncedNext = syncProgressionRoadmapFromSlots(nextDraft) - setDraft({ ...syncedNext, dirty: false }) - setEvaluationStale(true) + setDraft({ ...syncedNext, dirty: false, findingsStale: true }) setCompareOpen(false) setComparePayload(null) setProposedPathQa(null) - await saveProgressionGraphDraft(api, graphId, syncedNext) + await saveProgressionGraphDraft(api, graphId, { ...syncedNext, findingsStale: true }) setMatchNotice( 'Übernommen und gespeichert. Bewertung bezieht sich noch auf den vorherigen Stand — bitte „Graph bewerten“.', ) @@ -697,7 +693,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa setActionErr('') try { await saveProgressionGraphDraft(api, graphId, { ...draft, lastFindings: pathQa }) - await loadGraph({ preserveEvaluationStale: true }) + await loadGraph() if (typeof onSaved === 'function') await onSaved() alert('Progressionsgraph gespeichert.') } catch (e) { @@ -710,9 +706,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const handleApplyGapOffer = (offer, slotIndex) => { setDraft((prev) => { const next = applyGapOfferToDraft(prev, offer, { slotIndex }) - return { ...next, dirty: true } + return { ...next, dirty: true, findingsStale: true } }) - setEvaluationStale(true) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) } @@ -723,9 +718,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa } setDraft((prev) => { const next = applyGapOfferToDraft(prev, offer, { insertNewSlot: true }) - return { ...next, dirty: true } + return { ...next, dirty: true, findingsStale: true } }) - setEvaluationStale(true) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) } @@ -838,8 +832,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa : null if (resolvedSlot != null) { setSlotQuickCreateIndex(resolvedSlot) - setDraft((prev) => applyGapOfferToDraft(prev, enrichedOffer, { slotIndex: resolvedSlot })) - setEvaluationStale(true) + setDraft((prev) => ({ + ...applyGapOfferToDraft(prev, enrichedOffer, { slotIndex: resolvedSlot }), + findingsStale: true, + })) } setSlotQuickCreateDraft(aiDraft) setGapFillOffers((prev) => prev.filter((o) => o.offer_id !== offer?.offer_id)) @@ -887,8 +883,11 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const payload = buildQuickCreateExercisePayloadFromDraft(slotQuickCreateDraft) const created = await api.createExercise(payload) if (!created?.id) throw new Error('Anlegen fehlgeschlagen') - setDraft((prev) => setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created)) - setEvaluationStale(true) + setDraft((prev) => ({ + ...setSlotPrimaryLibrary(prev, slotQuickCreateIndex, created), + dirty: true, + findingsStale: true, + })) setSlotQuickCreateDraft(null) setSlotQuickCreateIndex(null) setActiveOffer(null) @@ -1177,7 +1176,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa slotCount={draft.slots.length} loading={evaluating} error="" - evaluationStale={evaluationStale} + evaluationStale={Boolean(draft?.findingsStale)} onEvaluate={runEvaluate} onApplyGapOffer={handleApplyGapOffer} onInsertGapSlot={handleInsertGapSlot} diff --git a/frontend/src/utils/progressionGraphDraft.js b/frontend/src/utils/progressionGraphDraft.js index 7360c89..eec0907 100644 --- a/frontend/src/utils/progressionGraphDraft.js +++ b/frontend/src/utils/progressionGraphDraft.js @@ -835,6 +835,7 @@ export function hydrateProgressionGraphDraft({ progressionRoadmap: artifact?.progression_roadmap || null, planningCatalogContext: parsePlanningCatalogContextFromArtifact(artifact), lastFindings: artifact?.last_findings || null, + findingsStale: Boolean(artifact?.findings_stale), primaryChainEdgeIds: primaryChain?.edges?.map((e) => e.id) || [], siblingEdgeIds: siblingEdges.map((e) => e.id), dirty: false, @@ -871,6 +872,7 @@ export function buildPlanningArtifactFromDraft(draft, { lastFindings = undefined const findings = lastFindings !== undefined ? lastFindings : draft.lastFindings if (findings) artifact.last_findings = findings + artifact.findings_stale = Boolean(draft.findingsStale) return artifact }