142 lines
5.0 KiB
React
142 lines
5.0 KiB
React
|
|
/**
|
||
|
|
* Zentrales Findings-Panel für Progressionsgraph-QA (Phase B.2).
|
||
|
|
*/
|
||
|
|
import React from 'react'
|
||
|
|
|
||
|
|
function severityStyle(pathQa) {
|
||
|
|
if (!pathQa) return {}
|
||
|
|
return {
|
||
|
|
background: pathQa.overall_ok
|
||
|
|
? 'color-mix(in srgb, var(--accent) 8%, var(--surface2))'
|
||
|
|
: 'color-mix(in srgb, var(--danger) 8%, var(--surface2))',
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ProgressionFindingsPanel({
|
||
|
|
pathQa = null,
|
||
|
|
gapFillOffers = [],
|
||
|
|
loading = false,
|
||
|
|
error = '',
|
||
|
|
onEvaluate,
|
||
|
|
onApplyGapOffer,
|
||
|
|
evaluateDisabled = false,
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<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 aktuellen Slot-Stand (inkl. KI-Entwürfe) ohne erneutes Übungs-Matching.
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="btn btn-primary btn-full"
|
||
|
|
disabled={loading || evaluateDisabled}
|
||
|
|
onClick={onEvaluate}
|
||
|
|
style={{ marginBottom: '12px' }}
|
||
|
|
>
|
||
|
|
{loading ? 'Bewertung läuft…' : 'Graph bewerten'}
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{error ? (
|
||
|
|
<p className="form-error" style={{ marginTop: 0 }}>
|
||
|
|
{error}
|
||
|
|
</p>
|
||
|
|
) : null}
|
||
|
|
|
||
|
|
{pathQa ? (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
padding: '10px 12px',
|
||
|
|
borderRadius: '8px',
|
||
|
|
fontSize: '12px',
|
||
|
|
lineHeight: 1.45,
|
||
|
|
...severityStyle(pathQa),
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<strong>
|
||
|
|
Pfad-QS: {pathQa.overall_ok ? 'OK' : 'Hinweise'}
|
||
|
|
{pathQa.quality_score != null
|
||
|
|
? ` (${Math.round(Number(pathQa.quality_score) * 100)} %)`
|
||
|
|
: ''}
|
||
|
|
</strong>
|
||
|
|
{pathQa.topic_coverage ? (
|
||
|
|
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>{pathQa.topic_coverage}</p>
|
||
|
|
) : null}
|
||
|
|
{Array.isArray(pathQa.issues) && pathQa.issues.length > 0 ? (
|
||
|
|
<ul style={{ margin: '6px 0 0', paddingLeft: '16px', color: 'var(--text2)' }}>
|
||
|
|
{pathQa.issues.map((issue) => (
|
||
|
|
<li key={issue}>{issue}</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
) : null}
|
||
|
|
{Array.isArray(pathQa.recommendations) && pathQa.recommendations.length > 0 ? (
|
||
|
|
<>
|
||
|
|
<p style={{ margin: '8px 0 4px', fontWeight: 600, color: 'var(--text2)' }}>Empfehlungen</p>
|
||
|
|
<ul style={{ margin: 0, paddingLeft: '16px', color: 'var(--text2)' }}>
|
||
|
|
{pathQa.recommendations.map((rec) => (
|
||
|
|
<li key={rec}>{rec}</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</>
|
||
|
|
) : null}
|
||
|
|
{Number(pathQa.off_topic_count) > 0 ? (
|
||
|
|
<p style={{ margin: '6px 0 0', color: 'var(--danger)' }}>
|
||
|
|
{pathQa.off_topic_count} Schritt(e) ohne Bezug zum Pfad-Thema.
|
||
|
|
</p>
|
||
|
|
) : null}
|
||
|
|
{pathQa.roadmap_qa_mode === 'roadmap_first_lite' ? (
|
||
|
|
<p style={{ margin: '6px 0 0', color: 'var(--text2)' }}>
|
||
|
|
QS an Roadmap gekoppelt (keine Brücken zwischen Major Steps).
|
||
|
|
</p>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: 0 }}>
|
||
|
|
Noch keine Bewertung. Slots befüllen und „Graph bewerten“ ausführen.
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{Array.isArray(gapFillOffers) && gapFillOffers.length > 0 ? (
|
||
|
|
<div style={{ marginTop: '14px' }}>
|
||
|
|
<h4 style={{ margin: '0 0 8px', fontSize: '0.9rem' }}>Lücken-Angebote</h4>
|
||
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||
|
|
{gapFillOffers.slice(0, 6).map((offer) => (
|
||
|
|
<li
|
||
|
|
key={offer.offer_id || offer.title_hint}
|
||
|
|
style={{
|
||
|
|
padding: '8px 10px',
|
||
|
|
borderRadius: '8px',
|
||
|
|
border: '1px solid var(--border)',
|
||
|
|
background: 'var(--surface2)',
|
||
|
|
fontSize: '12px',
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ fontWeight: 600 }}>
|
||
|
|
{offer.title_hint || 'Übungsvorschlag'}
|
||
|
|
{offer.roadmap_major_step_index != null
|
||
|
|
? ` · Slot ${Number(offer.roadmap_major_step_index) + 1}`
|
||
|
|
: ''}
|
||
|
|
</div>
|
||
|
|
{offer.rationale ? (
|
||
|
|
<p style={{ margin: '4px 0 0', color: 'var(--text2)' }}>{offer.rationale}</p>
|
||
|
|
) : null}
|
||
|
|
{typeof onApplyGapOffer === 'function' ? (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="btn btn-secondary"
|
||
|
|
style={{ marginTop: '6px', fontSize: '11px', padding: '4px 8px' }}
|
||
|
|
onClick={() => onApplyGapOffer(offer)}
|
||
|
|
>
|
||
|
|
Als Entwurf in Slot
|
||
|
|
</button>
|
||
|
|
) : null}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|