progression V2 #57
|
|
@ -2253,9 +2253,15 @@ def suggest_progression_path(
|
|||
baseline_body = body.model_copy(
|
||||
update={
|
||||
"evaluate_only": True,
|
||||
"evaluate_steps": list(body.slot_assignments or []),
|
||||
"evaluate_steps": list(
|
||||
body.evaluate_steps or body.slot_assignments or []
|
||||
),
|
||||
"compare_with_assignments": False,
|
||||
"preserve_slot_assignments": True,
|
||||
"preserve_slot_assignments": False,
|
||||
# Gleiche QS-Pipeline wie „Graph bewerten“ (kein Match/Rematch-Schönung)
|
||||
"include_llm_intent": False,
|
||||
"auto_rematch_after_qa": False,
|
||||
"include_roadmap_preview": False,
|
||||
}
|
||||
)
|
||||
baseline = suggest_progression_path(cur, tenant=tenant, body=baseline_body)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,124 @@ function severityStyle(pathQa) {
|
|||
}
|
||||
}
|
||||
|
||||
function PathQaPipelineDetails({ pathQa, draft, title, compact = false }) {
|
||||
const { fixHints: optimizationHints } = useMemo(
|
||||
() => splitPathQaHints(pathQa),
|
||||
[pathQa],
|
||||
)
|
||||
const rematchLog = Array.isArray(pathQa?.rematch_log) ? pathQa.rematch_log : []
|
||||
const refineLog = Array.isArray(pathQa?.refine_log) ? pathQa.refine_log : []
|
||||
const qaTiers = Array.isArray(pathQa?.qa_tiers) ? pathQa.qa_tiers : []
|
||||
const qualityPct = pathQaQualityPercent(pathQa)
|
||||
const hasContent =
|
||||
qaTiers.length > 0
|
||||
|| (pathQa?.rematch_applied && rematchLog.length > 0)
|
||||
|| (pathQa?.refine_applied && refineLog.length > 0)
|
||||
|| optimizationHints.length > 0
|
||||
|
||||
if (!pathQa || !hasContent) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: compact ? '10px' : '12px',
|
||||
padding: compact ? '8px 10px' : '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))',
|
||||
}}
|
||||
>
|
||||
<strong style={{ fontSize: compact ? '11px' : '12px' }}>
|
||||
{title}
|
||||
{qualityPct != null ? ` · ${pathQa.overall_ok ? 'OK' : 'Hinweise'} (${qualityPct} %)` : ''}
|
||||
</strong>
|
||||
{qaTiers.length > 0 ? (
|
||||
<ul style={{ margin: '6px 0 0', paddingLeft: '16px', color: 'var(--text2)', fontSize: '11px' }}>
|
||||
{qaTiers.map((tier) => (
|
||||
<li key={tier.id || tier.label}>
|
||||
{tier.label || tier.id}
|
||||
{tier.finding_count != null ? ` (${tier.finding_count})` : ''}
|
||||
{tier.applied === false ? ' · LLM aus' : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
{pathQa.rematch_applied && rematchLog.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)', fontSize: '11px' }}>
|
||||
Auto-Rematch
|
||||
{pathQa.rematch_rounds != null ? ` (${pathQa.rematch_rounds} Runde(n))` : ''}
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: '16px', color: 'var(--text2)', fontSize: '11px' }}>
|
||||
{rematchLog.map((entry, i) => (
|
||||
<li key={`rematch-${i}-${entry.roadmap_major_step_index}-${entry.action}`}>
|
||||
{formatRematchLogEntry(entry)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
{pathQa.refine_applied && refineLog.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)', fontSize: '11px' }}>
|
||||
Stufen-Spec verfeinert ({refineLog.length})
|
||||
</p>
|
||||
<ul style={{ margin: 0, paddingLeft: '16px', color: 'var(--text2)', fontSize: '11px' }}>
|
||||
{refineLog.map((entry, i) => (
|
||||
<li key={`refine-${i}-${entry.roadmap_major_step_index}`}>
|
||||
{formatRefineLogEntry(entry)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : null}
|
||||
{optimizationHints.length > 0 ? (
|
||||
<>
|
||||
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)', fontSize: '11px' }}>
|
||||
Handlungsbedarf ({optimizationHints.length})
|
||||
</p>
|
||||
<ul
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyle: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
{optimizationHints.slice(0, compact ? 4 : 8).map((hint, i) => {
|
||||
const slotIdx = resolveHintSlotIndex(hint, draft)
|
||||
return (
|
||||
<li
|
||||
key={`hint-${i}-${hint.action}-${hint.issue}-${slotIdx ?? 'x'}`}
|
||||
style={{
|
||||
padding: '6px 8px',
|
||||
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>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function GapOfferCard({
|
||||
offer,
|
||||
slotCount,
|
||||
|
|
@ -163,6 +281,7 @@ export default function ProgressionFindingsPanel({
|
|||
onGenerateGapAi,
|
||||
onRematchSlots = null,
|
||||
onOptimizeCompare = null,
|
||||
optimizationPreviewQa = null,
|
||||
canOptimizeCompare = false,
|
||||
optimizeCompareBusy = false,
|
||||
rematchBusy = false,
|
||||
|
|
@ -187,7 +306,8 @@ export default function ProgressionFindingsPanel({
|
|||
<div className="card" style={{ position: 'sticky', top: '12px' }}>
|
||||
<h3 style={{ marginTop: 0, fontSize: '1rem' }}>Graph-Bewertung</h3>
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: 0, lineHeight: 1.45 }}>
|
||||
Prüft den Slot-Stand und listet KI-Angebote für leere Stufen und Lücken.
|
||||
Bewertet den aktuellen Slot-Stand (3-Stufen-QS, ohne Auto-Rematch) — dieselbe Logik nach Match,
|
||||
solange keine Zuordnung geändert wurde.
|
||||
</p>
|
||||
|
||||
<button
|
||||
|
|
@ -222,8 +342,8 @@ export default function ProgressionFindingsPanel({
|
|||
</strong>
|
||||
{pathQa.assignments_preserved ? (
|
||||
<p style={{ margin: '6px 0 0', color: 'var(--text2)', fontSize: '11px' }}>
|
||||
Bestehende Slot-Zuordnungen beibehalten — QS wie „Graph bewerten“, ohne Auto-Rematch.
|
||||
{showOptimizeCompare ? ' „Übungen matchen“ oder „Optimierung vergleichen“ prüft Alternativen.' : ''}
|
||||
Bewertung des aktuellen Pfads — keine automatische Umzuordnung.
|
||||
{showOptimizeCompare ? ' Match prüft zusätzlich Optimierungsvorschläge im Vergleich.' : ''}
|
||||
</p>
|
||||
) : null}
|
||||
{strongResult ? (
|
||||
|
|
@ -386,6 +506,14 @@ export default function ProgressionFindingsPanel({
|
|||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{optimizationPreviewQa ? (
|
||||
<PathQaPipelineDetails
|
||||
pathQa={optimizationPreviewQa}
|
||||
draft={draft}
|
||||
title="3-Stufen-Optimierung (Vorschlag nach Match)"
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: 0 }}>
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
const [comparePayload, setComparePayload] = useState(null)
|
||||
const [comparing, setComparing] = useState(false)
|
||||
const [compareApplying, setCompareApplying] = useState(false)
|
||||
const [proposedPathQa, setProposedPathQa] = useState(null)
|
||||
|
||||
const loadGraph = useCallback(async () => {
|
||||
if (!graphId) return
|
||||
|
|
@ -435,6 +436,35 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
}
|
||||
}
|
||||
|
||||
const buildEvaluateRequest = (synced) => {
|
||||
const override = majorStepsToOverridePayload(synced.slots)
|
||||
return {
|
||||
query: (synced.goalQuery || '').trim(),
|
||||
max_steps: synced.slots.length || draft?.maxSteps || 5,
|
||||
include_path_qa: true,
|
||||
include_llm_path_qa: true,
|
||||
include_ai_gap_fill: true,
|
||||
include_path_reorder: false,
|
||||
include_llm_intent: false,
|
||||
evaluate_only: true,
|
||||
evaluate_steps: slotsToEvaluateSteps(synced),
|
||||
roadmap_override: override,
|
||||
slot_assignments: slotsToSlotAssignments(synced),
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(synced.startSituation, synced.targetState, synced.roadmapNotes),
|
||||
...catalogApiPayload,
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPathEvaluate = async (synced) => api.suggestProgressionPath(buildEvaluateRequest(synced))
|
||||
|
||||
const applyEvaluateResult = (synced, res) => {
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setPathQa(res?.path_qa || null)
|
||||
const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res)
|
||||
return { draft: { ...evaluated, lastFindings: res?.path_qa || null }, remainingOffers }
|
||||
}
|
||||
|
||||
const buildMatchRequestBase = (synced) => {
|
||||
const override = majorStepsToOverridePayload(synced.slots)
|
||||
return {
|
||||
|
|
@ -460,6 +490,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
const fetchOptimizeCompare = async (synced) => {
|
||||
const res = await api.suggestProgressionPath({
|
||||
...buildMatchRequestBase(synced),
|
||||
evaluate_steps: slotsToEvaluateSteps(synced),
|
||||
preserve_slot_assignments: false,
|
||||
compare_with_assignments: true,
|
||||
})
|
||||
|
|
@ -475,6 +506,7 @@ 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
|
||||
setPathQa(baselineQa)
|
||||
setProposedPathQa(proposedQa)
|
||||
|
||||
const openCompareDialog = (diffCount, noticePrefix) => {
|
||||
setComparePayload(res)
|
||||
|
|
@ -521,11 +553,16 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
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) return
|
||||
if (opened) {
|
||||
const evalRes = await fetchPathEvaluate(synced)
|
||||
setPathQa(evalRes?.path_qa || compareRes?.baseline_path_qa || null)
|
||||
return
|
||||
}
|
||||
|
||||
const { draft: matched, remainingOffers } = applyMatchResponseToDraft(
|
||||
{
|
||||
|
|
@ -536,19 +573,27 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
},
|
||||
compareRes,
|
||||
)
|
||||
setDraft(matched)
|
||||
setPathQa(compareRes?.proposed_path_qa || compareRes?.path_qa || null)
|
||||
setGapFillOffers(remainingOffers)
|
||||
const syncedMatched = syncProgressionRoadmapFromSlots(matched)
|
||||
const evalRes = await fetchPathEvaluate(syncedMatched)
|
||||
const { draft: evaluated, remainingOffers: evalOffers } = applyEvaluateResult(
|
||||
syncedMatched,
|
||||
evalRes,
|
||||
)
|
||||
setDraft(evaluated)
|
||||
setGapFillOffers(evalOffers.length ? evalOffers : remainingOffers)
|
||||
const evalPct = pathQaQualityPercent(evalRes?.path_qa)
|
||||
const ms = compareRes?.match_summary
|
||||
if (ms) {
|
||||
setMatchNotice(
|
||||
`Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote. Bestehende Zuordnungen unverändert.`,
|
||||
)
|
||||
let notice = `Match: ${ms.library_matches ?? 0}/${ms.slot_count ?? '?'} Slots aus Bibliothek, ${ms.gap_fill_offer_count ?? 0} KI-Angebote.`
|
||||
if (evalPct != null) {
|
||||
notice += ` Pfad-QS (Bewertung): ${evalPct} %.`
|
||||
}
|
||||
setMatchNotice(notice)
|
||||
}
|
||||
try {
|
||||
await saveProgressionGraphDraft(api, graphId, {
|
||||
...matched,
|
||||
lastFindings: compareRes?.proposed_path_qa || compareRes?.path_qa || null,
|
||||
...evaluated,
|
||||
lastFindings: evalRes?.path_qa || null,
|
||||
})
|
||||
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
|
||||
} catch (saveErr) {
|
||||
|
|
@ -625,6 +670,8 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
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)
|
||||
} catch (e) {
|
||||
setActionErr(e.message || 'Optimierungs-Vergleich fehlgeschlagen')
|
||||
} finally {
|
||||
|
|
@ -642,15 +689,18 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
comparePayload.proposed_steps || comparePayload.steps,
|
||||
selectedMajorIndices,
|
||||
)
|
||||
const proposedQa = comparePayload.proposed_path_qa || comparePayload.path_qa
|
||||
setDraft({ ...nextDraft, lastFindings: proposedQa || null })
|
||||
setPathQa(proposedQa || null)
|
||||
const syncedNext = syncProgressionRoadmapFromSlots(nextDraft)
|
||||
const evalRes = await fetchPathEvaluate(syncedNext)
|
||||
const { draft: evaluated, remainingOffers } = applyEvaluateResult(syncedNext, evalRes)
|
||||
setDraft({ ...evaluated, dirty: true })
|
||||
setGapFillOffers(remainingOffers)
|
||||
setProposedPathQa(null)
|
||||
setCompareOpen(false)
|
||||
setComparePayload(null)
|
||||
setMatchNotice('Ausgewählte Optimierungen übernommen.')
|
||||
setMatchNotice('Ausgewählte Optimierungen übernommen — Pfad-QS neu bewertet.')
|
||||
await saveProgressionGraphDraft(api, graphId, {
|
||||
...nextDraft,
|
||||
lastFindings: proposedQa || null,
|
||||
...evaluated,
|
||||
lastFindings: evalRes?.path_qa || null,
|
||||
})
|
||||
setDraft((prev) => (prev ? { ...prev, dirty: false } : prev))
|
||||
} catch (e) {
|
||||
|
|
@ -668,29 +718,12 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
}
|
||||
setEvaluating(true)
|
||||
setActionErr('')
|
||||
setProposedPathQa(null)
|
||||
try {
|
||||
const synced = syncProgressionRoadmapFromSlots(draft)
|
||||
const override =
|
||||
validMajorSteps.length >= 2 ? majorStepsToOverridePayload(synced.slots) : undefined
|
||||
const res = await api.suggestProgressionPath({
|
||||
query: q,
|
||||
max_steps: synced.slots.length || draft.maxSteps || 5,
|
||||
include_path_qa: true,
|
||||
include_llm_path_qa: true,
|
||||
include_ai_gap_fill: true,
|
||||
include_path_reorder: false,
|
||||
include_llm_intent: false,
|
||||
evaluate_only: true,
|
||||
evaluate_steps: slotsToEvaluateSteps(synced),
|
||||
roadmap_override: override,
|
||||
progression_graph_id: Number(graphId),
|
||||
...roadmapStructuredPayload(draft.startSituation, draft.targetState, draft.roadmapNotes),
|
||||
...catalogApiPayload,
|
||||
})
|
||||
setSemanticBrief(res?.semantic_brief_summary || null)
|
||||
setPathQa(res?.path_qa || null)
|
||||
const { draft: evaluated, remainingOffers } = applyEvaluateResponseToDraft(synced, res)
|
||||
setDraft({ ...evaluated, lastFindings: res?.path_qa || null })
|
||||
const res = await fetchPathEvaluate(synced)
|
||||
const { draft: evaluated, remainingOffers } = applyEvaluateResult(synced, res)
|
||||
setDraft(evaluated)
|
||||
setGapFillOffers(remainingOffers)
|
||||
} catch (e) {
|
||||
setActionErr(e.message || 'Bewertung fehlgeschlagen')
|
||||
|
|
@ -1187,6 +1220,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
onGenerateGapAi={openGapFillPrep}
|
||||
onRematchSlots={runMatch}
|
||||
onOptimizeCompare={runOptimizeCompare}
|
||||
optimizationPreviewQa={proposedPathQa}
|
||||
canOptimizeCompare={draftHasLibrarySlotAssignments(draft)}
|
||||
optimizeCompareBusy={comparing}
|
||||
rematchBusy={matching}
|
||||
|
|
@ -1235,6 +1269,7 @@ export default function ProgressionGraphEditor({ graphId, embedded = false, onSa
|
|||
if (compareApplying) return
|
||||
setCompareOpen(false)
|
||||
setComparePayload(null)
|
||||
setProposedPathQa(null)
|
||||
}}
|
||||
onApplySelected={applyOptimizeCompare}
|
||||
applying={compareApplying}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,11 @@ export default function ProgressionOptimizeCompareModal({
|
|||
const proposedQa = comparison.proposed_path_qa || comparison.path_qa
|
||||
const baselinePct = pathQaQualityPercent(baselineQa)
|
||||
const proposedPct = pathQaQualityPercent(proposedQa)
|
||||
const rematchRounds = proposedQa?.rematch_rounds
|
||||
const rematchCount = Array.isArray(proposedQa?.rematch_log) ? proposedQa.rematch_log.length : 0
|
||||
const refineCount = Array.isArray(proposedQa?.refine_log) ? proposedQa.refine_log.length : 0
|
||||
const hintCount = Number(proposedQa?.optimization_hint_count || 0)
|
||||
const tierCount = Array.isArray(proposedQa?.qa_tiers) ? proposedQa.qa_tiers.length : 0
|
||||
|
||||
const toggle = (midx) => {
|
||||
setSelected((prev) => {
|
||||
|
|
@ -120,6 +125,19 @@ export default function ProgressionOptimizeCompareModal({
|
|||
</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
|
||||
{tierCount > 0 ? ` · ${tierCount} QS-Stufen` : ''}
|
||||
{rematchCount > 0
|
||||
? ` · Auto-Rematch ${rematchRounds != null ? `(${rematchRounds} Runde(n))` : ''}: ${rematchCount} Anpassung(en)`
|
||||
: ''}
|
||||
{refineCount > 0 ? ` · ${refineCount} Stufen-Spec verfeinert` : ''}
|
||||
{hintCount > 0 ? ` · ${hintCount} Handlungshinweis(e)` : ''}
|
||||
. Details im Panel „Graph-Bewertung“.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{slotDiffs.length === 0 ? (
|
||||
<p style={{ fontSize: '12px', color: 'var(--text2)' }}>
|
||||
Keine abweichenden Slot-Zuordnungen — der optimierte Lauf liefert denselben Pfad.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user