From 5bca5ef9eb3e62d1a7d502ae6a04ca9bc8b42730 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 12 Jun 2026 13:33:36 +0200 Subject: [PATCH] Enhance Progression Path Evaluation and Optimization Features - Updated `suggest_progression_path` to include additional evaluation parameters, allowing for more comprehensive path assessments. - Introduced `PathQaPipelineDetails` component to display detailed quality assessment metrics, including rematch and refine logs, in the frontend. - Enhanced `ProgressionGraphEditor` to manage proposed path evaluations and integrate quality assessment results into the draft workflow. - Improved `ProgressionOptimizeCompareModal` to present optimization hints and quality tier information for proposed paths. - Bumped version to reflect the new features and improvements. --- backend/planning_exercise_path_builder.py | 10 +- .../components/ProgressionFindingsPanel.jsx | 134 +++++++++++++++++- .../src/components/ProgressionGraphEditor.jsx | 107 +++++++++----- .../ProgressionOptimizeCompareModal.jsx | 18 +++ 4 files changed, 228 insertions(+), 41 deletions(-) diff --git a/backend/planning_exercise_path_builder.py b/backend/planning_exercise_path_builder.py index b034dea..795360c 100644 --- a/backend/planning_exercise_path_builder.py +++ b/backend/planning_exercise_path_builder.py @@ -2253,9 +2253,15 @@ def suggest_progression_path( baseline_body = body.model_copy( update={ "evaluate_only": True, - "evaluate_steps": list(body.slot_assignments or []), + "evaluate_steps": list( + body.evaluate_steps or body.slot_assignments or [] + ), "compare_with_assignments": False, - "preserve_slot_assignments": True, + "preserve_slot_assignments": False, + # Gleiche QS-Pipeline wie „Graph bewerten“ (kein Match/Rematch-Schönung) + "include_llm_intent": False, + "auto_rematch_after_qa": False, + "include_roadmap_preview": False, } ) baseline = suggest_progression_path(cur, tenant=tenant, body=baseline_body) diff --git a/frontend/src/components/ProgressionFindingsPanel.jsx b/frontend/src/components/ProgressionFindingsPanel.jsx index 91ea61c..24dad48 100644 --- a/frontend/src/components/ProgressionFindingsPanel.jsx +++ b/frontend/src/components/ProgressionFindingsPanel.jsx @@ -26,6 +26,124 @@ function severityStyle(pathQa) { } } +function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) { + const { fixHints: optimizationHints } = useMemo( + () => splitPathQaHints(pathQa), + [pathQa], + ) + const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : [] + const refineLog = Array.isArray(pathQa?.refine_log) ? pathQa.refine_log : [] + const qaTiers = Array.isArray(pathQa?.qa_tiers) ? pathQa.qa_tiers : [] + const qualityPct = pathQaQualityPercent(pathQa) + const hasContent = + qaTiers.length > 0 + || (pathQa?.rematch_applied && rematchLog.length > 0) + || (pathQa?.refine_applied && refineLog.length > 0) + || optimizationHints.length > 0 + + if (!pathQa || !hasContent) return null + + return ( +
+ + {title} + {qualityPct != null ? ` · ${pathQa.overall_ok ? 'OK' : 'Hinweise'} (${qualityPct} %)` : ''} + + {qaTiers.length > 0 ? ( + + ) : null} + {pathQa.rematch_applied && rematchLog.length > 0 ? ( + <> +

+ Auto-Rematch + {pathQa.rematch_rounds != null ? ` (${pathQa.rematch_rounds} Runde(n))` : ''} +

+ + + ) : null} + {pathQa.refine_applied && refineLog.length > 0 ? ( + <> +

+ Stufen-Spec verfeinert ({refineLog.length}) +

+ + + ) : null} + {optimizationHints.length > 0 ? ( + <> +

+ Handlungsbedarf ({optimizationHints.length}) +

+ + + ) : null} +
+ ) +} + function GapOfferCard({ offer, slotCount, @@ -163,6 +281,7 @@ export default function ProgressionFindingsPanel({ onGenerateGapAi, onRematchSlots = null, onOptimizeCompare = null, + optimizationPreviewQa = null, canOptimizeCompare = false, optimizeCompareBusy = false, rematchBusy = false, @@ -187,7 +306,8 @@ export default function ProgressionFindingsPanel({

Graph-Bewertung

- Prüft den Slot-Stand und listet KI-Angebote für leere Stufen und Lücken. + Bewertet den aktuellen Slot-Stand (3-Stufen-QS, ohne Auto-Rematch) — dieselbe Logik nach Match, + solange keine Zuordnung geändert wurde.

) : (

diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index 6c12253..40caac1 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -122,6 +122,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const [comparePayload, setComparePayload] = useState(null) const [comparing, setComparing] = useState(false) const [compareApplying, setCompareApplying] = useState(false) + const [proposedPathQa, setProposedPathQa] = useState(null) const loadGraph = useCallback(async () => { if (!graphId) return @@ -435,6 +436,35 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa } } + const buildEvaluateRequest = (synced) => { + 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_path_reorder: false, + include_llm_intent: false, + evaluate_only: true, + evaluate_steps: slotsToEvaluateSteps(synced), + roadmap_override: override, + slot_assignments: slotsToSlotAssignments(synced), + progression_graph_id: Number(graphId), + ...roadmapStructuredPayload(synced.startSituation, synced.targetState, synced.roadmapNotes), + ...catalogApiPayload, + } + } + + const fetchPathEvaluate = async (synced) => api.suggestProgressionPath(buildEvaluateRequest(synced)) + + const applyEvaluateResult = (synced, res) => { + setSemanticBrief(res?.semantic_brief_summary || null) + setPathQa(res?.path_qa || null) + const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res) + return { draft: { ...evaluated, lastFindings: res?.path_qa || null }, remainingOffers } + } + const buildMatchRequestBase = (synced) => { const override = majorStepsToOverridePayload(synced.slots) return { @@ -460,6 +490,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const fetchOptimizeCompare = async (synced) => { const res = await api.suggestProgressionPath({ ...buildMatchRequestBase(synced), + evaluate_steps: slotsToEvaluateSteps(synced), preserve_slot_assignments: false, compare_with_assignments: true, }) @@ -475,6 +506,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const baselineQa = res?.baseline_path_qa || null const proposedQa = res?.proposed_path_qa || res?.path_qa || null setPathQa(baselineQa) + setProposedPathQa(proposedQa) const openCompareDialog = (diffCount, noticePrefix) => { setComparePayload(res) @@ -521,11 +553,16 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa try { const synced = syncProgressionRoadmapFromSlots(draft) const hasAssignments = draftHasLibrarySlotAssignments(synced) + setProposedPathQa(null) if (hasAssignments) { const res = await fetchOptimizeCompare(synced) const { opened, res: compareRes } = presentOptimizeCompare(res, { source: 'match' }) - if (opened) return + if (opened) { + const evalRes = await fetchPathEvaluate(synced) + setPathQa(evalRes?.path_qa || compareRes?.baseline_path_qa || null) + return + } const { draft: matched, remainingOffers } = applyMatchResponseToDraft( { @@ -536,19 +573,27 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa }, compareRes, ) - setDraft(matched) - setPathQa(compareRes?.proposed_path_qa || compareRes?.path_qa || null) - setGapFillOffers(remainingOffers) + const syncedMatched = syncProgressionRoadmapFromSlots(matched) + const evalRes = await fetchPathEvaluate(syncedMatched) + const { draft: evaluated, remainingOffers: evalOffers } = applyEvaluateResult( + syncedMatched, + evalRes, + ) + setDraft(evaluated) + setGapFillOffers(evalOffers.length ? evalOffers : remainingOffers) + const evalPct = pathQaQualityPercent(evalRes?.path_qa) const ms = compareRes?.match_summary if (ms) { - setMatchNotice( - `Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote. Bestehende Zuordnungen unverändert.`, - ) + let notice = `Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote.` + if (evalPct != null) { + notice += ` Pfad-QS (Bewertung): ${evalPct} %.` + } + setMatchNotice(notice) } try { await saveProgressionGraphDraft(api, graphId, { - ...matched, - lastFindings: compareRes?.proposed_path_qa || compareRes?.path_qa || null, + ...evaluated, + lastFindings: evalRes?.path_qa || null, }) setDraft((prev) => (prev ? { ...prev, dirty: false } : prev)) } catch (saveErr) { @@ -625,6 +670,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa const synced = syncProgressionRoadmapFromSlots(draft) const res = await fetchOptimizeCompare(synced) presentOptimizeCompare(res, { source: 'manual' }) + const evalRes = await fetchPathEvaluate(synced) + setPathQa(evalRes?.path_qa || res?.baseline_path_qa || null) } catch (e) { setActionErr(e.message || 'Optimierungs-Vergleich fehlgeschlagen') } finally { @@ -642,15 +689,18 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa comparePayload.proposed_steps || comparePayload.steps, selectedMajorIndices, ) - const proposedQa = comparePayload.proposed_path_qa || comparePayload.path_qa - setDraft({ ...nextDraft, lastFindings: proposedQa || null }) - setPathQa(proposedQa || null) + const syncedNext = syncProgressionRoadmapFromSlots(nextDraft) + const evalRes = await fetchPathEvaluate(syncedNext) + const { draft: evaluated, remainingOffers } = applyEvaluateResult(syncedNext, evalRes) + setDraft({ ...evaluated, dirty: true }) + setGapFillOffers(remainingOffers) + setProposedPathQa(null) setCompareOpen(false) setComparePayload(null) - setMatchNotice('Ausgewählte Optimierungen übernommen.') + setMatchNotice('Ausgewählte Optimierungen übernommen — Pfad-QS neu bewertet.') await saveProgressionGraphDraft(api, graphId, { - ...nextDraft, - lastFindings: proposedQa || null, + ...evaluated, + lastFindings: evalRes?.path_qa || null, }) setDraft((prev) => (prev ? { ...prev, dirty: false } : prev)) } catch (e) { @@ -668,29 +718,12 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa } setEvaluating(true) setActionErr('') + setProposedPathQa(null) try { const synced = syncProgressionRoadmapFromSlots(draft) - const override = - validMajorSteps.length >= 2 ? majorStepsToOverridePayload(synced.slots) : undefined - const res = await api.suggestProgressionPath({ - query: q, - max_steps: synced.slots.length || draft.maxSteps || 5, - include_path_qa: true, - include_llm_path_qa: true, - include_ai_gap_fill: true, - include_path_reorder: false, - include_llm_intent: false, - evaluate_only: true, - evaluate_steps: slotsToEvaluateSteps(synced), - roadmap_override: override, - progression_graph_id: Number(graphId), - ...roadmapStructuredPayload(draft.startSituation, draft.targetState, draft.roadmapNotes), - ...catalogApiPayload, - }) - setSemanticBrief(res?.semantic_brief_summary || null) - setPathQa(res?.path_qa || null) - const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res) - setDraft({ ...evaluated, lastFindings: res?.path_qa || null }) + const res = await fetchPathEvaluate(synced) + const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, res) + setDraft(evaluated) setGapFillOffers(remainingOffers) } catch (e) { setActionErr(e.message || 'Bewertung fehlgeschlagen') @@ -1187,6 +1220,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa onGenerateGapAi={openGapFillPrep} onRematchSlots={runMatch} onOptimizeCompare={runOptimizeCompare} + optimizationPreviewQa={proposedPathQa} canOptimizeCompare={draftHasLibrarySlotAssignments(draft)} optimizeCompareBusy={comparing} rematchBusy={matching} @@ -1235,6 +1269,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa if (compareApplying) return setCompareOpen(false) setComparePayload(null) + setProposedPathQa(null) }} onApplySelected={applyOptimizeCompare} applying={compareApplying} diff --git a/frontend/src/components/ProgressionOptimizeCompareModal.jsx b/frontend/src/components/ProgressionOptimizeCompareModal.jsx index ca66e10..864ebdf 100644 --- a/frontend/src/components/ProgressionOptimizeCompareModal.jsx +++ b/frontend/src/components/ProgressionOptimizeCompareModal.jsx @@ -37,6 +37,11 @@ export default function ProgressionOptimizeCompareModal({ const proposedQa = comparison.proposed_path_qa || comparison.path_qa const baselinePct = pathQaQualityPercent(baselineQa) const proposedPct = pathQaQualityPercent(proposedQa) + const rematchRounds = proposedQa?.rematch_rounds + const rematchCount = Array.isArray(proposedQa?.rematch_log) ? proposedQa.rematch_log.length : 0 + const refineCount = Array.isArray(proposedQa?.refine_log) ? proposedQa.refine_log.length : 0 + const hintCount = Number(proposedQa?.optimization_hint_count || 0) + const tierCount = Array.isArray(proposedQa?.qa_tiers) ? proposedQa.qa_tiers.length : 0 const toggle = (midx) => { setSelected((prev) => { @@ -120,6 +125,19 @@ export default function ProgressionOptimizeCompareModal({ + {tierCount > 0 || rematchCount > 0 || refineCount > 0 || hintCount > 0 ? ( +

+ 3-Stufen-Optimierung im Vorschlag + {tierCount > 0 ? ` · ${tierCount} QS-Stufen` : ''} + {rematchCount > 0 + ? ` · Auto-Rematch ${rematchRounds != null ? `(${rematchRounds} Runde(n))` : ''}: ${rematchCount} Anpassung(en)` + : ''} + {refineCount > 0 ? ` · ${refineCount} Stufen-Spec verfeinert` : ''} + {hintCount > 0 ? ` · ${hintCount} Handlungshinweis(e)` : ''} + . Details im Panel „Graph-Bewertung“. +

+ ) : null} + {slotDiffs.length === 0 ? (

Keine abweichenden Slot-Zuordnungen — der optimierte Lauf liefert denselben Pfad.