Refactor Progression Comparison Logic and Enhance UI Components
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 43s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m19s

- Introduced new utility functions for comparing slot differences, including `compareDiffKind`, `annotateCompareDiffKinds`, and various filtering functions to streamline the comparison process.
- Updated `ProgressionGraphEditor` to utilize the new comparison logic, improving the handling of slot differences and user notifications.
- Enhanced `ProgressionOptimizeCompareModal` to better manage proposed path suggestions, including clearer messaging and improved selection handling for optional replacements.
- Adjusted frontend components to reflect changes in comparison logic, ensuring a more intuitive user experience in managing progression paths.
This commit is contained in:
Lars 2026-06-13 09:02:15 +02:00
parent cec96ae473
commit 19bbcdaf50
3 changed files with 327 additions and 152 deletions

View File

@ -28,36 +28,37 @@ import {
applyEvaluateResponseToDraft,
applyGapOfferToDraft,
applySelectedCompareSteps,
buildProgressionComparePayload,
compareSlotDiffs,
collectGapOffersFromApiResponse,
dedupeGapOffersBySlot,
filterGapOffersForUnfilledSlots,
mergeGapOffersForDraft,
pathQaQualityPercent,
applyResolvedStructuredToDraft,
buildPlanningArtifactFromDraft,
buildProgressionComparePayload,
collectGapOffersFromApiResponse,
compareSlotDiffs,
dedupeGapOffersBySlot,
draftHasLibrarySlotAssignments,
draftRetrievalBoostExerciseIds,
EMPTY_PLANNING_CATALOG_CONTEXT,
filterGapOffersForUnfilledSlots,
hydrateProgressionGraphDraft,
SLOT_MIN,
insertSlotInDraft,
librarySlotExercise,
majorStepsToOverridePayload,
mergeGapOffersForDraft,
moveSlotInDraft,
patchSlotInDraft,
pathQaQualityPercent,
planningCatalogContextToApi,
recommendedCompareDiffs,
removeSlotFromDraft,
saveProgressionGraphDraft,
setCatalogSelectItems,
setSlotPrimaryLibrary,
SLOT_MAX,
SLOT_MIN,
slotsAsPathStepRows,
slotsToEvaluateSteps,
draftRetrievalBoostExerciseIds,
draftHasLibrarySlotAssignments,
slotsToSlotAssignments,
syncProgressionRoadmapFromSlots,
syncSlotPhasesFromRoadmap,
EMPTY_PLANNING_CATALOG_CONTEXT,
planningCatalogContextToApi,
setCatalogSelectItems,
} from '../utils/progressionGraphDraft'
function roadmapStructuredPayload(startSituation, targetState, roadmapNotes) {
@ -523,13 +524,22 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
const baselineQa = res?.baseline_path_qa || null
const proposedQa = res?.proposed_path_qa || res?.path_qa || null
const diffCount =
res?.slot_diff_count ?? compareSlotDiffs(res, { actionableOnly: true }).length
res?.slot_diff_count_recommended
?? recommendedCompareDiffs(res).length
?? res?.slot_diff_count
?? compareSlotDiffs(res, { actionableOnly: true }).length
const replaceCount = (res?.slot_diffs || []).filter(
(d) => d?.diff_kind === 'replace',
).length
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 Bibliotheks-Slots — Dialog zur Kontrolle geöffnet.'
? `Match: ${diffCount} Lückenfüllung(en) im Dialog — nur diese sind vorausgewählt.`
: 'Match: Keine Bibliotheks-Lückenfüllungen — Dialog zur Kontrolle geöffnet.'
if (replaceCount > 0) {
notice += ` ${replaceCount} optionale Ersetzung(en) bestehender Slots — standardmäßig abgewählt.`
}
const gapCount = collectGapOffersFromApiResponse(res).length
if (gapCount > 0) {
notice += ` ${gapCount} KI-Angebot(e) für leere Slots im Panel „Graph-Bewertung“.`

View File

@ -2,7 +2,14 @@
* Gegenüberstellung: bestehender Pfad vs. optimierter Match-Vorschlag.
*/
import React, { useMemo, useState } from 'react'
import { compareSlotDiffs, pathQaQualityPercent } from '../utils/progressionGraphDraft'
import {
compareDiffsForDialog,
defaultSelectedCompareDiffs,
gapOnlyCompareDiffs,
optionalReplaceCompareDiffs,
pathQaQualityPercent,
recommendedCompareDiffs,
} from '../utils/progressionGraphDraft'
function qaLabel(pathQa) {
const pct = pathQaQualityPercent(pathQa)
@ -11,6 +18,66 @@ function qaLabel(pathQa) {
return ok ? 'OK' : 'Hinweise'
}
function DiffRow({ diff, checked, onToggle, applying, tone = 'neutral' }) {
const midx = Number(diff.roadmap_major_step_index)
const border =
tone === 'warn'
? '1px solid color-mix(in srgb, var(--danger) 35%, var(--border))'
: '1px solid var(--border)'
const bg = checked
? tone === 'warn'
? 'color-mix(in srgb, var(--danger) 6%, var(--surface2))'
: 'var(--surface2)'
: 'var(--surface)'
return (
<li
style={{
padding: '10px 12px',
borderRadius: '8px',
border,
background: bg,
fontSize: '12px',
}}
>
<label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', cursor: 'pointer' }}>
<input
type="checkbox"
checked={checked}
onChange={() => onToggle(midx)}
disabled={applying}
style={{ marginTop: '3px' }}
/>
<span style={{ flex: 1 }}>
<strong>Slot {midx + 1}</strong>
{tone === 'warn' ? (
<span style={{ marginLeft: '6px', fontSize: '10px', color: 'var(--danger)' }}>
Ersetzt deine Zuordnung
</span>
) : null}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '8px',
marginTop: '6px',
}}
>
<span style={{ color: 'var(--text2)' }}>
Bisher: {diff.baseline_title || '— leer —'}
{diff.baseline_exercise_id != null ? ` (#${diff.baseline_exercise_id})` : ''}
</span>
<span style={{ color: tone === 'warn' ? 'var(--danger)' : 'var(--accent-dark)' }}>
Neu: {diff.proposed_title || '— leer —'}
{diff.proposed_exercise_id != null ? ` (#${diff.proposed_exercise_id})` : ''}
</span>
</div>
</span>
</label>
</li>
)
}
export default function ProgressionOptimizeCompareModal({
open,
comparison,
@ -19,37 +86,43 @@ export default function ProgressionOptimizeCompareModal({
onApplySelected,
applying = false,
}) {
const slotDiffs = compareSlotDiffs(comparison, { actionableOnly: true })
const trivialDiffs = (comparison?.slot_diffs || []).filter((d) => d?.trivial_id_swap)
const [selected, setSelected] = useState(() => new Set())
const allKeys = useMemo(
() => slotDiffs.map((d) => Number(d.roadmap_major_step_index)),
[slotDiffs],
const recommended = useMemo(
() => recommendedCompareDiffs(comparison),
[comparison],
)
const optionalReplace = useMemo(
() => optionalReplaceCompareDiffs(comparison),
[comparison],
)
const gapOnly = useMemo(() => gapOnlyCompareDiffs(comparison), [comparison])
const dialogDiffs = useMemo(() => compareDiffsForDialog(comparison), [comparison])
const defaultSelected = useMemo(
() => defaultSelectedCompareDiffs(comparison),
[comparison],
)
const [selected, setSelected] = useState(() => new Set())
React.useEffect(() => {
if (!open) return
setSelected(new Set(allKeys))
}, [open, allKeys])
setSelected(new Set(defaultSelected))
}, [open, defaultSelected])
if (!open || !comparison) return null
const baselineQa = comparison.baseline_path_qa
const proposedQa = comparison.proposed_path_qa || comparison.path_qa
const pipelineQa = comparison.proposed_path_qa_pipeline
const baselinePct = pathQaQualityPercent(baselineQa)
const proposedPct = pathQaQualityPercent(proposedQa)
const pipelinePct = pathQaQualityPercent(comparison?.proposed_path_qa_pipeline)
const rematchRounds = comparison?.proposed_path_qa_pipeline?.rematch_rounds
?? proposedQa?.rematch_rounds
const pipelineQa = comparison?.proposed_path_qa_pipeline
const pipelinePct = pathQaQualityPercent(pipelineQa)
const rematchRounds = pipelineQa?.rematch_rounds
const rematchCount = Array.isArray(pipelineQa?.rematch_log) ? pipelineQa.rematch_log.length : 0
const refineCount = Array.isArray(pipelineQa?.refine_log) ? pipelineQa.refine_log.length : 0
const hintCount = Number(pipelineQa?.optimization_hint_count || 0)
const tierCount = Array.isArray(pipelineQa?.qa_tiers) ? pipelineQa.qa_tiers.length : 0
const noMeaningfulDiffs = slotDiffs.length === 0
const proposedNotBetter =
proposedPct != null && baselinePct != null && proposedPct <= baselinePct
const selectedReplaceCount = optionalReplace.filter((d) =>
selected.has(Number(d.roadmap_major_step_index)),
).length
const toggle = (midx) => {
setSelected((prev) => {
@ -60,8 +133,16 @@ export default function ProgressionOptimizeCompareModal({
})
}
const toggleAll = (on) => {
setSelected(on ? new Set(allKeys) : new Set())
const toggleGroup = (diffs, on) => {
setSelected((prev) => {
const next = new Set(prev)
for (const d of diffs) {
const midx = Number(d.roadmap_major_step_index)
if (on) next.add(midx)
else next.delete(midx)
}
return next
})
}
const title =
@ -87,41 +168,30 @@ export default function ProgressionOptimizeCompareModal({
{title}
</h3>
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
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“.
Übernimm nur, was deinen Pfad verbessert. Leere Slots mit Bibliotheks-Treffer sind
vorausgewählt; Ersetzungen bestehender Übungen sind optional und oft schlechter.
KI-Entwürfe ohne Bibliotheks-ID gehören ins Panel „KI-Angebote“, nicht hierher.
</p>
{noMeaningfulDiffs || proposedNotBetter ? (
<div
style={{
marginBottom: '12px',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid color-mix(in srgb, var(--danger) 35%, var(--border))',
background: 'color-mix(in srgb, var(--danger) 8%, var(--surface2))',
fontSize: '12px',
lineHeight: 1.45,
}}
>
{noMeaningfulDiffs ? (
<strong>Keine inhaltlichen Slot-Änderungen</strong>
) : (
<strong>Vorschlag nicht besser als dein Pfad</strong>
)}
{noMeaningfulDiffs ? (
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>
Rematch hat höchstens dieselben Übungen unter anderen IDs getroffen — kein Grund zur
Übernahme. Bitte abbrechen.
</p>
) : (
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>
Fair bewertet liefert der Vorschlag keinen höheren Pfad-QS-Wert. Die frühere niedrigere
Pipeline-Zahl{pipelinePct != null ? ` (${pipelinePct} %)` : ''} stammte aus dem
Rematch-Lauf, nicht aus dem sichtbaren End-Pfad.
</p>
)}
</div>
) : null}
<div
style={{
marginBottom: '12px',
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid color-mix(in srgb, var(--accent) 30%, var(--border))',
background: 'color-mix(in srgb, var(--accent) 6%, var(--surface2))',
fontSize: '12px',
lineHeight: 1.45,
}}
>
<strong>Warum nicht einfach alles übernehmen?</strong>
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>
Der Match-Lauf optimiert den <em>gesamten</em> Pfad neu (inkl. Rematch). Das kann
bereits gute Slots verschlechtern. Die Prozentzahl rechts bezieht sich auf diesen
Ganzpfad — nicht darauf, dass deine Auswahl besser ist. Nimm deshalb standardmäßig
nur Lückenfüllungen an.
</p>
</div>
<div
style={{
@ -140,7 +210,7 @@ export default function ProgressionOptimizeCompareModal({
fontSize: '12px',
}}
>
<strong>Aktuell</strong>
<strong>Dein Pfad (bewertet)</strong>
<div style={{ marginTop: '6px' }}>{qaLabel(baselineQa)}</div>
{baselineQa?.topic_coverage ? (
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>{baselineQa.topic_coverage}</p>
@ -150,117 +220,158 @@ export default function ProgressionOptimizeCompareModal({
style={{
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid color-mix(in srgb, var(--accent) 35%, var(--border))',
background: 'color-mix(in srgb, var(--accent) 8%, var(--surface2))',
border: '1px solid color-mix(in srgb, var(--text3) 35%, var(--border))',
background: 'var(--surface2)',
fontSize: '12px',
}}
>
<strong>Vorschlag (Match + Optimierung)</strong>
<div style={{ marginTop: '6px' }}>{qaLabel(proposedQa)}</div>
{proposedPct != null && baselinePct != null && proposedPct !== baselinePct ? (
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>
Δ {proposedPct - baselinePct > 0 ? '+' : ''}
{proposedPct - baselinePct} Prozentpunkte
</p>
) : null}
{proposedQa?.topic_coverage ? (
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>{proposedQa.topic_coverage}</p>
) : null}
<strong>Match-Ganzpfad (nur Info)</strong>
<div style={{ marginTop: '6px', color: 'var(--text2)' }}>
{pipelineQa ? qaLabel(pipelineQa) : '—'}
</div>
<p style={{ margin: '6px 0 0', fontSize: '11px', color: 'var(--text3)' }}>
Rematch-Prozess — kein Versprechen für deine Checkbox-Auswahl.
{pipelinePct != null && baselinePct != null && pipelinePct < baselinePct
? ` (${pipelinePct} % < ${baselinePct} % bei voller Übernahme).`
: ''}
</p>
</div>
</div>
{tierCount > 0 || rematchCount > 0 || refineCount > 0 || hintCount > 0 ? (
<p style={{ fontSize: '11px', color: 'var(--text2)', margin: '0 0 14px', lineHeight: 1.45 }}>
3-Stufen-Optimierung im Vorschlag
Rematch-Protokoll
{tierCount > 0 ? ` · ${tierCount} QS-Stufen` : ''}
{rematchCount > 0
? ` · Auto-Rematch ${rematchRounds != null ? `(${rematchRounds} Runde(n))` : ''}: ${rematchCount} Anpassung(en)`
? ` · ${rematchRounds != null ? `${rematchRounds} Runde(n)` : ''}: ${rematchCount} Anpassung(en)`
: ''}
{refineCount > 0 ? ` · ${refineCount} Stufen-Spec verfeinert` : ''}
{hintCount > 0 ? ` · ${hintCount} Handlungshinweis(e)` : ''}
. Nur Prozessinfo — nicht für die Prozentzahl links/rechts.
</p>
) : null}
{trivialDiffs.length > 0 ? (
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '0 0 14px', lineHeight: 1.45 }}>
{trivialDiffs.length} reine ID-Tausche (gleicher Titel) — nicht übernehmbar, daher nicht gelistet.
{gapOnly.length > 0 ? (
<p style={{ fontSize: '11px', color: 'var(--text2)', margin: '0 0 14px', lineHeight: 1.45 }}>
Slot{gapOnly.length > 1 ? 's' : ''}{' '}
{gapOnly.map((d) => Number(d.roadmap_major_step_index) + 1).join(', ')}: kein
Bibliotheks-Treffer — bitte „KI-Angebote“ im Panel nutzen (eigenständig pro Slot).
</p>
) : null}
{slotDiffs.length === 0 ? (
{dialogDiffs.length === 0 ? (
<p style={{ fontSize: '12px', color: 'var(--text2)' }}>
Keine inhaltlichen Abweichungen — der End-Stand entspricht deinem Pfad.
Keine übernehmbaren Bibliotheks-Änderungen. Leere Slots ggf. über KI-Angebote im Panel
befüllen — nichts am Pfad ändern ist oft die richtige Wahl.
</p>
) : (
<>
<div style={{ display: 'flex', gap: '8px', marginBottom: '8px', flexWrap: 'wrap' }}>
<button type="button" className="btn btn-secondary" style={{ fontSize: '11px' }} onClick={() => toggleAll(true)}>
Alle wählen
</button>
<button type="button" className="btn btn-secondary" style={{ fontSize: '11px' }} onClick={() => toggleAll(false)}>
Keine
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px' }}>
{slotDiffs.map((diff) => {
const midx = Number(diff.roadmap_major_step_index)
const checked = selected.has(midx)
return (
<li
key={`diff-${midx}`}
style={{
padding: '10px 12px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: checked ? 'var(--surface2)' : 'var(--surface)',
fontSize: '12px',
}}
{recommended.length > 0 ? (
<>
<h4 style={{ margin: '0 0 8px', fontSize: '0.9rem' }}>
Lücken füllen (empfohlen)
</h4>
<div style={{ display: 'flex', gap: '8px', marginBottom: '8px', flexWrap: 'wrap' }}>
<button
type="button"
className="btn btn-secondary"
style={{ fontSize: '11px' }}
onClick={() => toggleGroup(recommended, true)}
>
<label style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', cursor: 'pointer' }}>
<input
type="checkbox"
checked={checked}
onChange={() => toggle(midx)}
disabled={applying}
style={{ marginTop: '3px' }}
/>
<span style={{ flex: 1 }}>
<strong>Slot {midx + 1}</strong>
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '8px',
marginTop: '6px',
}}
>
<span style={{ color: 'var(--text2)' }}>
Bisher: {diff.baseline_title || '— leer —'}
{diff.baseline_exercise_id != null ? ` (#${diff.baseline_exercise_id})` : ''}
</span>
<span style={{ color: 'var(--accent-dark)' }}>
Neu: {diff.proposed_title || '— leer —'}
{diff.proposed_exercise_id != null ? ` (#${diff.proposed_exercise_id})` : ''}
</span>
</div>
</span>
</label>
</li>
)
})}
</ul>
Alle Lücken wählen
</button>
<button
type="button"
className="btn btn-secondary"
style={{ fontSize: '11px' }}
onClick={() => toggleGroup(recommended, false)}
>
Keine
</button>
</div>
<ul
style={{
listStyle: 'none',
padding: 0,
margin: '0 0 16px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
{recommended.map((diff) => (
<DiffRow
key={`fill-${diff.roadmap_major_step_index}`}
diff={diff}
checked={selected.has(Number(diff.roadmap_major_step_index))}
onToggle={toggle}
applying={applying}
/>
))}
</ul>
</>
) : null}
{optionalReplace.length > 0 ? (
<>
<h4 style={{ margin: '0 0 8px', fontSize: '0.9rem', color: 'var(--danger)' }}>
Bestehende Slots ersetzen (optional — oft Verschlechterung)
</h4>
<p style={{ fontSize: '11px', color: 'var(--text2)', margin: '0 0 8px' }}>
Standard: abgewählt. Nur aktivieren, wenn du die konkrete Übung bewusst tauschen
willst.
</p>
<ul
style={{
listStyle: 'none',
padding: 0,
margin: 0,
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
{optionalReplace.map((diff) => (
<DiffRow
key={`replace-${diff.roadmap_major_step_index}`}
diff={diff}
checked={selected.has(Number(diff.roadmap_major_step_index))}
onToggle={toggle}
applying={applying}
tone="warn"
/>
))}
</ul>
</>
) : null}
</>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginTop: '16px', justifyContent: 'flex-end' }}>
{selectedReplaceCount > 0 ? (
<p
className="form-error"
style={{ marginTop: '12px', fontSize: '11px' }}
>
{selectedReplaceCount} Ersetzung(en) gewählt — kann Pfad-QS senken. Lückenfüllungen
sind unkritischer.
</p>
) : null}
<div
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '8px',
marginTop: '16px',
justifyContent: 'flex-end',
}}
>
<button type="button" className="btn btn-secondary" disabled={applying} onClick={onClose}>
Abbrechen
</button>
<button
type="button"
className="btn btn-primary"
disabled={applying || selected.size === 0 || slotDiffs.length === 0 || proposedNotBetter}
disabled={applying || selected.size === 0 || dialogDiffs.length === 0}
onClick={() => onApplySelected([...selected])}
>
{applying ? 'Übernehmen …' : `Auswahl übernehmen (${selected.size})`}

View File

@ -985,6 +985,51 @@ function actionableCompareSlotDiffs(diffs) {
return (diffs || []).filter((d) => !d.trivial_id_swap)
}
/** fill = leerer Slot + Bibliotheks-Treffer; replace = bestehende Übung tauschen; gap_only = nur KI-Angebot. */
export function compareDiffKind(diff) {
if (!diff || diff.trivial_id_swap) return 'skip'
const hasBase = diff.baseline_exercise_id != null
const hasProp = diff.proposed_exercise_id != null
if (!hasBase && hasProp) return 'fill'
if (hasBase && hasProp) return 'replace'
if (!hasBase && !hasProp) return 'gap_only'
if (hasBase && !hasProp) return 'replace'
return 'skip'
}
export function annotateCompareDiffKinds(diffs) {
return (diffs || []).map((d) => ({
...d,
diff_kind: compareDiffKind(d),
}))
}
/** Nur übernehmbare Bibliotheks-Diffs (kein reines Titel-/Gap-Geplänkel). */
export function compareDiffsForDialog(comparison) {
const diffs = annotateCompareDiffKinds(
compareSlotDiffs(comparison, { actionableOnly: true }),
)
return diffs.filter((d) => d.diff_kind === 'fill' || d.diff_kind === 'replace')
}
export function recommendedCompareDiffs(comparison) {
return compareDiffsForDialog(comparison).filter((d) => d.diff_kind === 'fill')
}
export function optionalReplaceCompareDiffs(comparison) {
return compareDiffsForDialog(comparison).filter((d) => d.diff_kind === 'replace')
}
export function gapOnlyCompareDiffs(comparison) {
return annotateCompareDiffKinds(
compareSlotDiffs(comparison, { actionableOnly: true }),
).filter((d) => d.diff_kind === 'gap_only')
}
export function defaultSelectedCompareDiffs(comparison) {
return recommendedCompareDiffs(comparison).map((d) => Number(d.roadmap_major_step_index))
}
function mergeGapFillOffersFromSteps(steps, offers) {
const merged = (offers || []).map((o) => ({ ...o }))
const seen = new Set(merged.map((o) => o.offer_id).filter(Boolean))
@ -1006,10 +1051,16 @@ export function buildProgressionComparePayload(baselineRes, proposedRes) {
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 slotDiffs = annotateCompareDiffKinds(
annotateCompareSlotDiffs(
buildProgressionSlotDiffs(baselineSteps, proposedSteps),
),
)
const actionableDiffs = actionableCompareSlotDiffs(slotDiffs)
const dialogDiffs = actionableDiffs.filter(
(d) => d.diff_kind === 'fill' || d.diff_kind === 'replace',
)
const recommendedDiffs = dialogDiffs.filter((d) => d.diff_kind === 'fill')
const gapFillOffers = mergeGapFillOffersFromSteps(
proposedSteps,
proposedRes?.gap_fill_offers || [],
@ -1029,7 +1080,10 @@ export function buildProgressionComparePayload(baselineRes, proposedRes) {
gap_fill_offers: gapFillOffers,
slot_diffs: slotDiffs,
slot_diffs_actionable: actionableDiffs,
slot_diff_count: actionableDiffs.length,
slot_diffs_dialog: dialogDiffs,
slot_diffs_recommended: recommendedDiffs,
slot_diff_count: dialogDiffs.length,
slot_diff_count_recommended: recommendedDiffs.length,
slot_diff_count_including_trivial: slotDiffs.length,
slot_diffs_source: 'steps',
path_qa: proposedQa,