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,
|
offerCanExpandSlots,
|
||||||
offerNeedsNewSlot,
|
offerNeedsNewSlot,
|
||||||
offerSourceLabel,
|
offerSourceLabel,
|
||||||
|
optimizationHintActionLabel,
|
||||||
|
formatRematchLogEntry,
|
||||||
|
hasRematchSlotHints,
|
||||||
|
resolveHintSlotIndex,
|
||||||
resolveOfferSlotIndex,
|
resolveOfferSlotIndex,
|
||||||
} from '../utils/progressionGraphDraft'
|
} from '../utils/progressionGraphDraft'
|
||||||
|
|
||||||
|
|
@ -153,10 +157,16 @@ export default function ProgressionFindingsPanel({
|
||||||
onApplyGapOffer,
|
onApplyGapOffer,
|
||||||
onInsertGapSlot,
|
onInsertGapSlot,
|
||||||
onGenerateGapAi,
|
onGenerateGapAi,
|
||||||
|
onRematchSlots = null,
|
||||||
|
rematchBusy = false,
|
||||||
generatingOfferId = null,
|
generatingOfferId = null,
|
||||||
aiBusy = false,
|
aiBusy = false,
|
||||||
evaluateDisabled = 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 (
|
return (
|
||||||
<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>
|
||||||
|
|
@ -221,6 +231,75 @@ export default function ProgressionFindingsPanel({
|
||||||
{pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema.
|
{pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : 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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: 0 }}>
|
<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)
|
setPathQa(res?.path_qa || null)
|
||||||
setGapFillOffers(remainingOffers)
|
setGapFillOffers(remainingOffers)
|
||||||
const ms = res?.match_summary
|
const ms = res?.match_summary
|
||||||
|
const rematchLog = res?.path_qa?.rematch_log
|
||||||
|
const rematchRounds = res?.path_qa?.rematch_rounds
|
||||||
if (ms) {
|
if (ms) {
|
||||||
setMatchNotice(
|
const parts = [
|
||||||
`Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote.`,
|
`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 {
|
try {
|
||||||
await saveProgressionGraphDraft(api, graphId, {
|
await saveProgressionGraphDraft(api, graphId, {
|
||||||
|
|
@ -954,6 +962,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
||||||
onApplyGapOffer={handleApplyGapOffer}
|
onApplyGapOffer={handleApplyGapOffer}
|
||||||
onInsertGapSlot={handleInsertGapSlot}
|
onInsertGapSlot={handleInsertGapSlot}
|
||||||
onGenerateGapAi={openGapFillPrep}
|
onGenerateGapAi={openGapFillPrep}
|
||||||
|
onRematchSlots={runMatch}
|
||||||
|
rematchBusy={matching}
|
||||||
generatingOfferId={generatingOfferId}
|
generatingOfferId={generatingOfferId}
|
||||||
aiBusy={gapAiBusy}
|
aiBusy={gapAiBusy}
|
||||||
evaluateDisabled={busy || !draft.goalQuery?.trim()}
|
evaluateDisabled={busy || !draft.goalQuery?.trim()}
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,50 @@ export function offerSourceLabel(source) {
|
||||||
return OFFER_SOURCE_LABELS[source] || source || 'Angebot'
|
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) {
|
function createEmptySlot(index) {
|
||||||
const phase = ROADMAP_PHASES[Math.min(index, ROADMAP_PHASES.length - 1)]
|
const phase = ROADMAP_PHASES[Math.min(index, ROADMAP_PHASES.length - 1)]
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user