From de9fdf3ac0c0d2ba09cdd339efe74786dd7cb0f3 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 11 Jun 2026 21:39:16 +0200 Subject: [PATCH] Enhance Progression Findings Panel with Rematch and Optimization Hints - Added support for displaying optimization hints and rematch logs in the Progression Findings Panel, improving user feedback on potential enhancements. - Introduced new utility functions for formatting rematch log entries and resolving hint slot indices, enhancing clarity in displayed information. - Updated the Progression Graph Editor to handle rematch actions and display relevant match summaries, ensuring comprehensive insights during progression analysis. - Enhanced the utility functions to support the new features, ensuring robust handling of optimization actions and rematch logic. --- .../components/ProgressionFindingsPanel.jsx | 79 +++++++++++++++++++ .../src/components/ProgressionGraphEditor.jsx | 14 +++- frontend/src/utils/progressionGraphDraft.js | 44 +++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProgressionFindingsPanel.jsx b/frontend/src/components/ProgressionFindingsPanel.jsx index ebfc1cb..53c3df7 100644 --- a/frontend/src/components/ProgressionFindingsPanel.jsx +++ b/frontend/src/components/ProgressionFindingsPanel.jsx @@ -6,6 +6,10 @@ import { offerCanExpandSlots, offerNeedsNewSlot, offerSourceLabel, + optimizationHintActionLabel, + formatRematchLogEntry, + hasRematchSlotHints, + resolveHintSlotIndex, resolveOfferSlotIndex, } from '../utils/progressionGraphDraft' @@ -153,10 +157,16 @@ export default function ProgressionFindingsPanel({ onApplyGapOffer, onInsertGapSlot, onGenerateGapAi, + onRematchSlots = null, + rematchBusy = false, generatingOfferId = null, aiBusy = false, evaluateDisabled = false, }) { + const optimizationHints = Array.isArray(pathQa?.optimization_hints) ? pathQa.optimization_hints : [] + const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : [] + const showRematchAction = hasRematchSlotHints(pathQa) && typeof onRematchSlots === 'function' + return (

Graph-Bewertung

@@ -221,6 +231,75 @@ export default function ProgressionFindingsPanel({ {pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema.

) : null} + {pathQa.rematch_applied && rematchLog.length > 0 ? ( + <> +

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

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

+ Optimierungspotenziale ({optimizationHints.length}) +

+ + {showRematchAction ? ( + + ) : null} + + ) : null}
) : (

diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index ecb7fd4..9aa5ea0 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -435,10 +435,18 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa setPathQa(res?.path_qa || null) setGapFillOffers(remainingOffers) const ms = res?.match_summary + const rematchLog = res?.path_qa?.rematch_log + const rematchRounds = res?.path_qa?.rematch_rounds if (ms) { - setMatchNotice( + const parts = [ `Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote.`, - ) + ] + if (rematchRounds > 0 && Array.isArray(rematchLog) && rematchLog.length > 0) { + parts.push( + `Auto-Rematch: ${rematchLog.length} Anpassung(en) in ${rematchRounds} Runde(n).`, + ) + } + setMatchNotice(parts.join(' ')) } try { await saveProgressionGraphDraft(api, graphId, { @@ -954,6 +962,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa onApplyGapOffer={handleApplyGapOffer} onInsertGapSlot={handleInsertGapSlot} onGenerateGapAi={openGapFillPrep} + onRematchSlots={runMatch} + rematchBusy={matching} generatingOfferId={generatingOfferId} aiBusy={gapAiBusy} evaluateDisabled={busy || !draft.goalQuery?.trim()} diff --git a/frontend/src/utils/progressionGraphDraft.js b/frontend/src/utils/progressionGraphDraft.js index 4c57960..19d1fe7 100644 --- a/frontend/src/utils/progressionGraphDraft.js +++ b/frontend/src/utils/progressionGraphDraft.js @@ -62,6 +62,50 @@ export function offerSourceLabel(source) { return OFFER_SOURCE_LABELS[source] || source || 'Angebot' } +const OPTIMIZATION_ACTION_LABELS = { + rematch_slot: 'Slot neu matchen', + bridge_or_gap_fill: 'Brücke / KI-Angebot', + refine_stage_spec: 'Stufen-Spec verfeinern', + review_roadmap: 'Roadmap prüfen', + review: 'Prüfen', +} + +export function optimizationHintActionLabel(action) { + return OPTIMIZATION_ACTION_LABELS[action] || action || 'Hinweis' +} + +/** Slot-Index aus optimization_hint (roadmap_major_step_index oder step_index). */ +export function resolveHintSlotIndex(hint, draft = null) { + if (!hint || typeof hint !== 'object') return null + const raw = hint.roadmap_major_step_index ?? hint.step_index + if (raw == null || !Number.isFinite(Number(raw))) return null + const idx = Number(raw) + const slotCount = draft?.slots?.length + if (slotCount != null && (idx < 0 || idx >= slotCount)) return null + return idx +} + +export function formatRematchLogEntry(entry) { + if (!entry || typeof entry !== 'object') return '' + const slot = Number.isFinite(Number(entry.roadmap_major_step_index)) + ? `Slot ${Number(entry.roadmap_major_step_index) + 1}` + : 'Slot' + const round = entry.round != null ? ` (Runde ${entry.round})` : '' + if (entry.action === 'replaced') { + const from = entry.replaced_title || (entry.replaced_exercise_id ? `#${entry.replaced_exercise_id}` : '—') + const to = entry.new_title || (entry.new_exercise_id ? `#${entry.new_exercise_id}` : '—') + return `${slot}${round}: „${from}“ → „${to}“` + } + if (entry.action === 'rematch_unfilled') { + return `${slot}${round}: kein passender Ersatz (${entry.reason || 'unfilled'})` + } + return `${slot}${round}: ${entry.reason || entry.action || 'Rematch'}` +} + +export function hasRematchSlotHints(pathQa) { + return (pathQa?.optimization_hints || []).some((h) => h?.action === 'rematch_slot') +} + function createEmptySlot(index) { const phase = ROADMAP_PHASES[Math.min(index, ROADMAP_PHASES.length - 1)] return {