Refactor Progression Path Evaluation and Comparison Logic
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m22s
- Updated `suggest_progression_path` to utilize `evaluate_steps` for improved validation, ensuring at least one evaluation step is provided. - Modified frontend components to enhance user experience in the comparison process, including clearer messaging and improved dialog handling. - Adjusted `ProgressionGraphEditor` to streamline the comparison flow and integrate new evaluation parameters. - Enhanced `ProgressionOptimizeCompareModal` to reflect changes in comparison logic, allowing for better user interaction with proposed path suggestions. - Bumped version to reflect the new features and improvements.
This commit is contained in:
parent
69ce3f6975
commit
3f130aa8ad
|
|
@ -2448,18 +2448,16 @@ def suggest_progression_path(
|
|||
raise HTTPException(status_code=403, detail="Nur Trainer dürfen Pfad-Vorschläge abrufen")
|
||||
|
||||
if body.compare_with_assignments:
|
||||
assignments = _slot_assignments_by_major_index(body.slot_assignments)
|
||||
if len(assignments) < 1:
|
||||
eval_source = list(body.evaluate_steps or body.slot_assignments or [])
|
||||
if len(eval_source) < 1:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="compare_with_assignments erfordert mindestens ein slot_assignment",
|
||||
detail="compare_with_assignments erfordert evaluate_steps",
|
||||
)
|
||||
baseline_body = body.model_copy(
|
||||
update={
|
||||
"evaluate_only": True,
|
||||
"evaluate_steps": list(
|
||||
body.evaluate_steps or body.slot_assignments or []
|
||||
),
|
||||
"evaluate_steps": eval_source,
|
||||
"compare_with_assignments": False,
|
||||
"preserve_slot_assignments": False,
|
||||
# Gleiche QS-Pipeline wie „Graph bewerten“ (kein Match/Rematch-Schönung)
|
||||
|
|
@ -2472,7 +2470,7 @@ def suggest_progression_path(
|
|||
proposed_body = body.model_copy(
|
||||
update={
|
||||
"compare_with_assignments": False,
|
||||
"preserve_slot_assignments": True,
|
||||
"preserve_slot_assignments": False,
|
||||
"evaluate_only": False,
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -350,8 +350,8 @@ export default function ProgressionFindingsPanel({
|
|||
</strong>
|
||||
{pathQa.assignments_preserved ? (
|
||||
<p style={{ margin: '6px 0 0', color: 'var(--text2)', fontSize: '11px' }}>
|
||||
Bewertung des aktuellen Pfads — keine automatische Umzuordnung.
|
||||
{showOptimizeCompare ? ' Match prüft zusätzlich Optimierungsvorschläge im Vergleich.' : ''}
|
||||
Bewertung des aktuellen Pfads. „Übungen matchen“ öffnet einen Dialog mit Vorschlägen für
|
||||
alle Slots — Übernahme nur nach deiner Auswahl.
|
||||
</p>
|
||||
) : null}
|
||||
{strongResult ? (
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ import {
|
|||
addSlotToDraft,
|
||||
applyEvaluateResponseToDraft,
|
||||
applyGapOfferToDraft,
|
||||
applyMatchResponseToDraft,
|
||||
applySelectedCompareSteps,
|
||||
compareResponseHasActionableSlotChanges,
|
||||
compareResponseHadRematchWithoutActionableDiffs,
|
||||
compareSlotDiffs,
|
||||
collectGapOffersFromApiResponse,
|
||||
dedupeGapOffersBySlot,
|
||||
filterGapOffersForUnfilledSlots,
|
||||
pathQaQualityPercent,
|
||||
applyResolvedStructuredToDraft,
|
||||
buildPlanningArtifactFromDraft,
|
||||
|
|
@ -120,6 +120,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
const [activePlanningContextLines, setActivePlanningContextLines] = useState([])
|
||||
const [compareOpen, setCompareOpen] = useState(false)
|
||||
const [comparePayload, setComparePayload] = useState(null)
|
||||
const [compareSource, setCompareSource] = useState('manual')
|
||||
const [comparing, setComparing] = useState(false)
|
||||
const [compareApplying, setCompareApplying] = useState(false)
|
||||
const [proposedPathQa, setProposedPathQa] = useState(null)
|
||||
|
|
@ -487,11 +488,10 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
}
|
||||
}
|
||||
|
||||
const fetchOptimizeCompare = async (synced) => {
|
||||
const fetchMatchCompare = async (synced) => {
|
||||
const res = await api.suggestProgressionPath({
|
||||
...buildMatchRequestBase(synced),
|
||||
evaluate_steps: slotsToEvaluateSteps(synced),
|
||||
preserve_slot_assignments: true,
|
||||
compare_with_assignments: true,
|
||||
})
|
||||
if (!res?.comparison_mode) {
|
||||
|
|
@ -500,44 +500,43 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
return res
|
||||
}
|
||||
|
||||
const presentOptimizeCompare = (res, { source = 'manual' } = {}) => {
|
||||
const gapOffersFromMatchResponse = (synced, res) =>
|
||||
filterGapOffersForUnfilledSlots(
|
||||
synced,
|
||||
dedupeGapOffersBySlot(collectGapOffersFromApiResponse(res), synced),
|
||||
)
|
||||
|
||||
const presentMatchCompare = (res, { source = 'manual' } = {}) => {
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setTargetSummary(res?.target_profile_summary || null)
|
||||
setComparePayload(res)
|
||||
setCompareSource(source)
|
||||
setProposedPathQa(res?.proposed_path_qa_pipeline || null)
|
||||
setCompareOpen(true)
|
||||
|
||||
const baselineQa = res?.baseline_path_qa || null
|
||||
const proposedQa = res?.proposed_path_qa || res?.path_qa || null
|
||||
const actionableCount =
|
||||
const diffCount =
|
||||
res?.slot_diff_count ?? compareSlotDiffs(res, { actionableOnly: true }).length
|
||||
|
||||
const openCompareDialog = (diffCount, noticePrefix) => {
|
||||
setComparePayload(res)
|
||||
setProposedPathQa(res?.proposed_path_qa_pipeline || null)
|
||||
setCompareOpen(true)
|
||||
const bPct = pathQaQualityPercent(baselineQa)
|
||||
const pPct = pathQaQualityPercent(proposedQa)
|
||||
let notice = diffCount > 0
|
||||
? `${noticePrefix}${diffCount} Slot-Anpassung(en) — Vergleichsdialog geöffnet.`
|
||||
: 'Vergleichsdialog geöffnet — keine übernehmbaren Slot-Änderungen im End-Pfad.'
|
||||
if (bPct != null && pPct != null && pPct !== bPct) {
|
||||
notice += ` Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
||||
}
|
||||
setMatchNotice(notice)
|
||||
const bPct = pathQaQualityPercent(baselineQa)
|
||||
const pPct = pathQaQualityPercent(proposedQa)
|
||||
let notice =
|
||||
diffCount > 0
|
||||
? `Match: ${diffCount} Slot-Vorschlag/Vorschläge — bitte im Dialog prüfen und auswählen.`
|
||||
: 'Match: Keine abweichenden Slot-Vorschläge — Dialog zur Kontrolle geöffnet.'
|
||||
if (bPct != null && pPct != null && pPct !== bPct) {
|
||||
notice += ` Pfad-QS Vorschlag fair bewertet: ${bPct} % → ${pPct} %.`
|
||||
}
|
||||
setMatchNotice(notice)
|
||||
}
|
||||
|
||||
if (source === 'match') {
|
||||
if (compareResponseHasActionableSlotChanges(res)) {
|
||||
openCompareDialog(actionableCount, 'KI schlägt ')
|
||||
return { opened: true, res }
|
||||
}
|
||||
setProposedPathQa(null)
|
||||
setComparePayload(null)
|
||||
return { opened: false, res }
|
||||
}
|
||||
|
||||
openCompareDialog(
|
||||
actionableCount,
|
||||
compareResponseHasActionableSlotChanges(res) ? 'KI schlägt ' : '',
|
||||
)
|
||||
return { opened: true, res }
|
||||
const runMatchCompareFlow = async (synced, { source = 'match' } = {}) => {
|
||||
const res = await fetchMatchCompare(synced)
|
||||
setGapFillOffers(gapOffersFromMatchResponse(synced, res))
|
||||
presentMatchCompare(res, { source })
|
||||
const evalRes = await fetchPathEvaluate(synced)
|
||||
setPathQa(evalRes?.path_qa || res?.baseline_path_qa || null)
|
||||
return res
|
||||
}
|
||||
|
||||
const runMatch = async () => {
|
||||
|
|
@ -555,94 +554,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
setMatchNotice('')
|
||||
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) {
|
||||
const evalRes = await fetchPathEvaluate(synced)
|
||||
setPathQa(evalRes?.path_qa || compareRes?.baseline_path_qa || null)
|
||||
return
|
||||
}
|
||||
|
||||
const evalRes = await fetchPathEvaluate(synced)
|
||||
const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, evalRes)
|
||||
setDraft(evaluated)
|
||||
setGapFillOffers(remainingOffers)
|
||||
setProposedPathQa(null)
|
||||
setComparePayload(null)
|
||||
const evalPct = pathQaQualityPercent(evalRes?.path_qa)
|
||||
if (compareResponseHadRematchWithoutActionableDiffs(compareRes)) {
|
||||
setMatchNotice(
|
||||
`Match: Auto-Rematch ohne übernehmbare End-Änderung — dein Pfad bleibt unverändert${
|
||||
evalPct != null ? ` (Pfad-QS ${evalPct} %).` : '.'
|
||||
}`,
|
||||
)
|
||||
} else {
|
||||
setMatchNotice(
|
||||
`Match: Kein inhaltlicher Optimierungsvorschlag — Pfad unverändert${
|
||||
evalPct != null ? ` (${evalPct} %).` : '.'
|
||||
}`,
|
||||
)
|
||||
}
|
||||
try {
|
||||
await saveProgressionGraphDraft(api, graphId, {
|
||||
...evaluated,
|
||||
lastFindings: evalRes?.path_qa || null,
|
||||
})
|
||||
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
|
||||
} catch (saveErr) {
|
||||
console.warn('Match-Artefakt konnte nicht gespeichert werden', saveErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const res = await api.suggestProgressionPath({
|
||||
...buildMatchRequestBase(synced),
|
||||
preserve_slot_assignments: false,
|
||||
})
|
||||
const { draft: matched, remainingOffers } = applyMatchResponseToDraft(
|
||||
{
|
||||
...synced,
|
||||
progressionRoadmap: res?.progression_roadmap || synced.progressionRoadmap,
|
||||
pathSkillExpectations: res?.path_skill_expectations || synced.pathSkillExpectations,
|
||||
},
|
||||
res,
|
||||
)
|
||||
setDraft(matched)
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setTargetSummary(res?.target_profile_summary || null)
|
||||
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) {
|
||||
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).`,
|
||||
)
|
||||
}
|
||||
const refineLog = res?.path_qa?.refine_log
|
||||
if (Array.isArray(refineLog) && refineLog.length > 0) {
|
||||
parts.push(`Stufen-Spec: ${refineLog.length} Slot(s) verfeinert.`)
|
||||
}
|
||||
setMatchNotice(parts.join(' '))
|
||||
}
|
||||
try {
|
||||
await saveProgressionGraphDraft(api, graphId, {
|
||||
...matched,
|
||||
lastFindings: res?.path_qa || null,
|
||||
})
|
||||
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
|
||||
} catch (saveErr) {
|
||||
console.warn('Match-Artefakt konnte nicht gespeichert werden', saveErr)
|
||||
}
|
||||
await runMatchCompareFlow(synced, { source: 'match' })
|
||||
} catch (e) {
|
||||
setActionErr(e.message || 'Übungs-Match fehlgeschlagen')
|
||||
} finally {
|
||||
|
|
@ -656,8 +569,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
alert('Ziel-Anfrage: mindestens 3 Zeichen.')
|
||||
return
|
||||
}
|
||||
if (!draftHasLibrarySlotAssignments(draft)) {
|
||||
alert('Mindestens ein Slot mit Bibliotheks-Übung nötig für einen Vergleich.')
|
||||
if (validMajorSteps.length < 2) {
|
||||
alert('Mindestens zwei Slots mit Lernziel (je 3+ Zeichen) nötig.')
|
||||
return
|
||||
}
|
||||
setComparing(true)
|
||||
|
|
@ -665,10 +578,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
setMatchNotice('')
|
||||
try {
|
||||
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)
|
||||
setProposedPathQa(null)
|
||||
await runMatchCompareFlow(synced, { source: 'manual' })
|
||||
} catch (e) {
|
||||
setActionErr(e.message || 'Optimierungs-Vergleich fehlgeschlagen')
|
||||
} finally {
|
||||
|
|
@ -1217,7 +1128,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
onGenerateGapAi={openGapFillPrep}
|
||||
onRematchSlots={runMatch}
|
||||
onOptimizeCompare={runOptimizeCompare}
|
||||
canOptimizeCompare={draftHasLibrarySlotAssignments(draft)}
|
||||
canOptimizeCompare={validMajorSteps.length >= 2}
|
||||
optimizeCompareBusy={comparing}
|
||||
rematchBusy={matching}
|
||||
generatingOfferId={generatingOfferId}
|
||||
|
|
@ -1261,6 +1172,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
<ProgressionOptimizeCompareModal
|
||||
open={compareOpen}
|
||||
comparison={comparePayload}
|
||||
mode={compareSource}
|
||||
onClose={() => {
|
||||
if (compareApplying) return
|
||||
setCompareOpen(false)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ function qaLabel(pathQa) {
|
|||
export default function ProgressionOptimizeCompareModal({
|
||||
open,
|
||||
comparison,
|
||||
mode = 'manual',
|
||||
onClose,
|
||||
onApplySelected,
|
||||
applying = false,
|
||||
|
|
@ -63,12 +64,16 @@ export default function ProgressionOptimizeCompareModal({
|
|||
setSelected(on ? new Set(allKeys) : new Set())
|
||||
}
|
||||
|
||||
const title =
|
||||
mode === 'match' ? 'Übungs-Match — Vorschläge prüfen' : 'Optimierung vergleichen'
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="optimize-compare-title"
|
||||
style={{ zIndex: 2200 }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !applying) onClose()
|
||||
}}
|
||||
|
|
@ -79,12 +84,11 @@ export default function ProgressionOptimizeCompareModal({
|
|||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 id="optimize-compare-title" style={{ marginTop: 0 }}>
|
||||
Optimierung vergleichen
|
||||
{title}
|
||||
</h3>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
||||
Vergleicht deinen Pfad mit dem Match-Vorschlag — beide Seiten mit derselben Bewertungslogik
|
||||
wie „Graph bewerten“. Prozentwerte beziehen sich auf den übernehmbaren End-Stand, nicht auf
|
||||
das Auto-Rematch-Protokoll.
|
||||
KI matcht alle Lernziel-Slots neu (auch bereits belegte). Du wählst im Dialog, welche
|
||||
Vorschläge übernommen werden — der Graph ändert sich erst nach „Auswahl übernehmen“.
|
||||
</p>
|
||||
|
||||
{noMeaningfulDiffs || proposedNotBetter ? (
|
||||
|
|
|
|||
|
|
@ -906,7 +906,7 @@ export function slotsToSlotAssignments(draft) {
|
|||
}))
|
||||
}
|
||||
|
||||
/** Mindestens ein Bibliotheks-Slot belegt → kuratierter Stand, Match prüft Abweichungen. */
|
||||
/** Mindestens ein Bibliotheks-Slot belegt. */
|
||||
export function draftHasLibrarySlotAssignments(draft) {
|
||||
return slotsToSlotAssignments(draft).length >= 1
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user