From cec96ae473116804f9094681d6ef335db780572d Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 13 Jun 2026 08:46:10 +0200 Subject: [PATCH] Implement Progression Comparison Logic and Refactor Fetching Methods - Introduced `buildProgressionComparePayload` to create a structured comparison response from baseline and proposed evaluation results, enhancing clarity in slot differences. - Refactored `fetchMatchCompare` to `fetchFullMatch` for improved clarity and functionality in fetching progression paths. - Updated `runMatchCompareFlow` to streamline the evaluation process, integrating baseline and match results for a comprehensive comparison. - Enhanced utility functions for managing slot differences and gap fill offers, improving overall data handling in the progression graph editor. - Adjusted frontend components to reflect these changes, ensuring a more intuitive user experience in managing progression paths. --- .../src/components/ProgressionGraphEditor.jsx | 29 ++--- frontend/src/utils/progressionGraphDraft.js | 109 ++++++++++++++++++ 2 files changed, 124 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/ProgressionGraphEditor.jsx b/frontend/src/components/ProgressionGraphEditor.jsx index 5b2da7d..5f0467a 100644 --- a/frontend/src/components/ProgressionGraphEditor.jsx +++ b/frontend/src/components/ProgressionGraphEditor.jsx @@ -28,6 +28,7 @@ import { applyEvaluateResponseToDraft, applyGapOfferToDraft, applySelectedCompareSteps, + buildProgressionComparePayload, compareSlotDiffs, collectGapOffersFromApiResponse, dedupeGapOffersBySlot, @@ -489,26 +490,26 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa } } - const fetchMatchCompare = async (synced) => { - const res = await api.suggestProgressionPath({ + const fetchFullMatch = async (synced) => + api.suggestProgressionPath({ ...buildMatchRequestBase(synced), - evaluate_steps: slotsToEvaluateSteps(synced), - compare_with_assignments: true, + preserve_slot_assignments: false, include_llm_intent: false, include_llm_path_qa: false, }) - if (!res?.comparison_mode) { - throw new Error('Kein Vergleich in der Antwort') - } - return res - } const runMatchCompareFlow = async (synced, { source = 'match' } = {}) => { - const res = await fetchMatchCompare(synced) - setGapFillOffers(mergeGapOffersForDraft(synced, res)) - presentMatchCompare(res, { source }) - setPathQa(res?.baseline_path_qa || null) - return res + setMatchNotice('Schritt 1/2: Aktuellen Pfad bewerten…') + const baselineRes = await fetchPathEvaluate(synced) + setPathQa(baselineRes?.path_qa || null) + + setMatchNotice('Schritt 2/2: Match für alle Slots (Bibliothek + Lücken)…') + const matchRes = await fetchFullMatch(synced) + + const compareRes = buildProgressionComparePayload(baselineRes, matchRes) + setGapFillOffers(mergeGapOffersForDraft(synced, baselineRes, matchRes)) + presentMatchCompare(compareRes, { source }) + return compareRes } const presentMatchCompare = (res, { source = 'manual' } = {}) => { diff --git a/frontend/src/utils/progressionGraphDraft.js b/frontend/src/utils/progressionGraphDraft.js index 747590f..c985a95 100644 --- a/frontend/src/utils/progressionGraphDraft.js +++ b/frontend/src/utils/progressionGraphDraft.js @@ -928,6 +928,115 @@ export function draftHasLibrarySlotAssignments(draft) { return slotsToSlotAssignments(draft).length >= 1 } +function normalizeCompareSlotTitle(title) { + return (title || '').trim().toLowerCase() +} + +function stepsByMajorIndex(steps) { + const out = new Map() + for (const step of steps || []) { + if (step?.roadmap_major_step_index == null || !Number.isFinite(Number(step.roadmap_major_step_index))) { + continue + } + out.set(Number(step.roadmap_major_step_index), step) + } + return out +} + +function buildProgressionSlotDiffs(baselineSteps, proposedSteps) { + const baseBy = stepsByMajorIndex(baselineSteps) + const propBy = stepsByMajorIndex(proposedSteps) + const indices = new Set([...baseBy.keys(), ...propBy.keys()]) + const diffs = [] + for (const midx of [...indices].sort((a, b) => a - b)) { + const base = baseBy.get(midx) || {} + const prop = propBy.get(midx) || {} + const baseId = base.exercise_id + const propId = prop.exercise_id + if (baseId != null && propId != null && Number(baseId) === Number(propId)) continue + const baseTitle = (base.title || '').trim() || null + const propTitle = (prop.title || '').trim() || null + diffs.push({ + roadmap_major_step_index: midx, + baseline_exercise_id: baseId != null ? Number(baseId) : null, + baseline_title: baseTitle, + proposed_exercise_id: propId != null ? Number(propId) : null, + proposed_title: propTitle, + baseline_slot_status: base.slot_status, + proposed_slot_status: prop.slot_status, + changed: baseId !== propId || baseTitle !== propTitle, + }) + } + return diffs +} + +function annotateCompareSlotDiffs(diffs) { + return (diffs || []).map((raw) => { + const bt = normalizeCompareSlotTitle(raw.baseline_title) + const pt = normalizeCompareSlotTitle(raw.proposed_title) + return { + ...raw, + trivial_id_swap: Boolean(bt && pt && bt === pt), + } + }) +} + +function actionableCompareSlotDiffs(diffs) { + return (diffs || []).filter((d) => !d.trivial_id_swap) +} + +function mergeGapFillOffersFromSteps(steps, offers) { + const merged = (offers || []).map((o) => ({ ...o })) + const seen = new Set(merged.map((o) => o.offer_id).filter(Boolean)) + for (const step of steps || []) { + const go = step?.gap_offer + if (!go || typeof go !== 'object') continue + if (go.offer_id && seen.has(go.offer_id)) continue + if (go.offer_id) seen.add(go.offer_id) + merged.push({ ...go }) + } + return merged +} + +/** + * Vergleich aus zwei kaskadierten Antworten (Evaluate → Match) — spiegelt Backend-Compare. + */ +export function buildProgressionComparePayload(baselineRes, proposedRes) { + const baselineSteps = Array.isArray(baselineRes?.steps) ? baselineRes.steps : [] + const proposedSteps = Array.isArray(proposedRes?.steps) ? proposedRes.steps : [] + const baselineQa = baselineRes?.path_qa || null + const pipelineQa = proposedRes?.path_qa || null + const slotDiffs = annotateCompareSlotDiffs( + buildProgressionSlotDiffs(baselineSteps, proposedSteps), + ) + const actionableDiffs = actionableCompareSlotDiffs(slotDiffs) + const gapFillOffers = mergeGapFillOffersFromSteps( + proposedSteps, + proposedRes?.gap_fill_offers || [], + ) + const proposedQa = + actionableDiffs.length === 0 && baselineQa ? baselineQa : pipelineQa + + return { + ...proposedRes, + comparison_mode: true, + baseline_steps: baselineSteps, + baseline_path_qa: baselineQa, + proposed_steps: proposedSteps, + proposed_steps_pipeline: proposedSteps, + proposed_path_qa: proposedQa, + proposed_path_qa_pipeline: pipelineQa, + gap_fill_offers: gapFillOffers, + slot_diffs: slotDiffs, + slot_diffs_actionable: actionableDiffs, + slot_diff_count: actionableDiffs.length, + slot_diff_count_including_trivial: slotDiffs.length, + slot_diffs_source: 'steps', + path_qa: proposedQa, + steps: proposedSteps, + } +} + /** Alle Slot-Diffs inkl. reiner ID-Tausche (gleicher Titel). */ export function compareSlotDiffs(comparison, { actionableOnly = false } = {}) { if (actionableOnly && Array.isArray(comparison?.slot_diffs_actionable)) {