Enhance Progression Findings Panel with Rematch and Optimization Hints
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- 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.
This commit is contained in:
parent
df93da9a03
commit
de9fdf3ac0
|
|
@ -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 (
|
||||
<div className="card" style={{ position: 'sticky', top: '12px' }}>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Graph-Bewertung</h3>
|
||||
|
|
@ -221,6 +231,75 @@ export default function ProgressionFindingsPanel({
|
|||
{pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema.
|
||||
</p>
|
||||
) : null}
|
||||
{pathQa.rematch_applied && rematchLog.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)' }}>
|
||||
Auto-Rematch
|
||||
{pathQa.rematch_rounds != null ? ` (${pathQa.rematch_rounds} Runde(n))` : ''}
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: '16px', color: 'var(--text2)' }}>
|
||||
{rematchLog.map((entry, i) => (
|
||||
<li key={`rematch-${i}-${entry.roadmap_major_step_index}-${entry.action}`}>
|
||||
{formatRematchLogEntry(entry)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
{optimizationHints.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)' }}>
|
||||
Optimierungspotenziale ({optimizationHints.length})
|
||||
</p>
|
||||
<ul
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
{optimizationHints.slice(0, 8).map((hint, i) => {
|
||||
const slotIdx = resolveHintSlotIndex(hint, draft)
|
||||
return (
|
||||
<li
|
||||
key={`hint-${i}-${hint.action}-${hint.issue}-${slotIdx ?? 'x'}`}
|
||||
style={{
|
||||
padding: '8px 10px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
fontSize: '11px',
|
||||
color: 'var(--text2)',
|
||||
}}
|
||||
>
|
||||
<span className="exercise-tag" style={{ marginBottom: '4px', display: 'inline-block' }}>
|
||||
{optimizationHintActionLabel(hint.action)}
|
||||
{slotIdx != null ? ` · Slot ${slotIdx + 1}` : ''}
|
||||
</span>
|
||||
{hint.title ? (
|
||||
<div style={{ fontWeight: 600, color: 'var(--text1)' }}>{hint.title}</div>
|
||||
) : null}
|
||||
{hint.reason ? <p style={{ margin: '4px 0 0' }}>{hint.reason}</p> : null}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
{showRematchAction ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-full"
|
||||
style={{ marginTop: '8px', fontSize: '12px' }}
|
||||
disabled={rematchBusy || evaluateDisabled}
|
||||
onClick={onRematchSlots}
|
||||
>
|
||||
{rematchBusy ? 'Match läuft…' : 'Betroffene Slots neu matchen'}
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: 0 }}>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user