| import { useState, useEffect, useMemo } from "react"; |
| import { |
| BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, |
| RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, |
| ReferenceLine, |
| } from "recharts"; |
| import ConfidenceBadge from "../ConfidenceBadge.jsx"; |
|
|
| const SCENARIO_LABELS = { |
| revenue_shock: "Revenue Shock (β20%)", |
| rate_hike: "Interest Rate Hike (+200bps)", |
| gst_scrutiny: "GST Scrutiny (Γ1.5)", |
| }; |
|
|
| function decisionColorVal(decision) { |
| if (decision === "APPROVE") return "var(--success)"; |
| if (decision === "CONDITIONAL") return "var(--warning)"; |
| return "var(--danger)"; |
| } |
|
|
| |
| function AnimatedScoreRing({ score, maxScore = 100, size = 200 }) { |
| const [animated, setAnimated] = useState(0); |
| const radius = (size - 24) / 2; |
| const circumference = 2 * Math.PI * radius; |
| const offset = circumference * (1 - animated / maxScore); |
|
|
| useEffect(() => { |
| let raf; |
| const start = performance.now(); |
| const target = Math.min(score || 0, maxScore); |
| const duration = 1600; |
| function step(now) { |
| const t = Math.min((now - start) / duration, 1); |
| const ease = 1 - Math.pow(1 - t, 3); |
| setAnimated(Math.round(ease * target)); |
| if (t < 1) raf = requestAnimationFrame(step); |
| } |
| raf = requestAnimationFrame(step); |
| return () => cancelAnimationFrame(raf); |
| }, [score, maxScore]); |
|
|
| const color = score >= 70 ? "#22c55e" : score >= 45 ? "#eab308" : "#ef4444"; |
| const glow = score >= 70 ? "rgba(34,197,94,0.35)" : score >= 45 ? "rgba(234,179,8,0.35)" : "rgba(239,68,68,0.35)"; |
| const pct = animated / maxScore; |
| const angle = pct * 2 * Math.PI - Math.PI / 2; |
| const dotX = size / 2 + radius * Math.cos(angle); |
| const dotY = size / 2 + radius * Math.sin(angle); |
|
|
| return ( |
| <div style={{ position: "relative", width: size, height: size }}> |
| <svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}> |
| <defs> |
| <linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="100%"> |
| <stop offset="0%" stopColor={color} stopOpacity="1" /> |
| <stop offset="100%" stopColor={color} stopOpacity="0.3" /> |
| </linearGradient> |
| <filter id="ringGlow"> |
| <feGaussianBlur stdDeviation="5" result="b" /> |
| <feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge> |
| </filter> |
| <filter id="dotGlow"> |
| <feGaussianBlur stdDeviation="3" result="b" /> |
| <feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge> |
| </filter> |
| </defs> |
| <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="var(--bg-elevated)" strokeWidth="10" opacity="0.6" /> |
| <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="var(--bg-elevated)" strokeWidth="10" opacity="0.15" strokeDasharray="4 8" /> |
| <circle |
| cx={size / 2} cy={size / 2} r={radius} fill="none" |
| stroke="url(#ringGrad)" strokeWidth="10" strokeLinecap="round" |
| strokeDasharray={circumference} strokeDashoffset={offset} |
| filter="url(#ringGlow)" |
| /> |
| {animated > 0 && ( |
| <circle cx={dotX} cy={dotY} r="6" fill={color} filter="url(#dotGlow)" opacity="0.9" /> |
| )} |
| </svg> |
| <div style={{ |
| position: "absolute", inset: 0, display: "flex", flexDirection: "column", |
| alignItems: "center", justifyContent: "center", |
| }}> |
| <span style={{ fontFamily: "var(--font-heading)", fontSize: "3rem", fontWeight: 700, color, lineHeight: 1 }}> |
| {animated} |
| </span> |
| <span style={{ fontSize: "12px", color: "var(--text-muted)", marginTop: "4px" }}>/ {maxScore}</span> |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| const tooltipStyle = { |
| background: "rgba(30,30,38,0.92)", backdropFilter: "blur(16px)", WebkitBackdropFilter: "blur(16px)", |
| border: "1px solid rgba(255,255,255,0.1)", borderRadius: "12px", padding: "14px 16px", |
| fontSize: "12px", boxShadow: "0 8px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.05) inset", |
| }; |
|
|
| function ShapTooltip({ active, payload }) { |
| if (!active || !payload || !payload.length) return null; |
| const d = payload[0].payload; |
| const impactColor = d.impact >= 0 ? "#22c55e" : "#ef4444"; |
| const barW = Math.min(Math.abs(d.impact) * 8, 120); |
| return ( |
| <div style={{ ...tooltipStyle, maxWidth: "260px" }}> |
| <p style={{ fontWeight: 700, color: "var(--accent)", marginBottom: "8px", fontSize: "13px" }}>{d.feature}</p> |
| <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "6px" }}> |
| <div style={{ width: barW, height: "4px", borderRadius: "2px", background: impactColor, boxShadow: `0 0 8px ${impactColor}66`, transition: "width 0.3s" }} /> |
| <span style={{ color: impactColor, fontWeight: 700, fontFamily: "var(--font-mono)" }}> |
| {d.impact >= 0 ? "+" : ""}{d.impact} |
| </span> |
| </div> |
| <p style={{ color: "var(--text-muted)" }}>Value: <span style={{ color: "var(--text-primary)", fontFamily: "var(--font-mono)" }}>{d.value}</span></p> |
| <p style={{ color: "var(--text-muted)" }}>Source: <span style={{ color: "var(--text-secondary)" }}>{d.source}</span></p> |
| </div> |
| ); |
| } |
|
|
| function ModelTooltip({ active, payload }) { |
| if (!active || !payload || !payload.length) return null; |
| const d = payload[0].payload; |
| const pct = ((d.score || 0) / d.max * 100).toFixed(0); |
| const barColor = d.score / d.max > 0.75 ? "#22c55e" : d.score / d.max > 0.5 ? "#eab308" : "#ef4444"; |
| return ( |
| <div style={tooltipStyle}> |
| <p style={{ fontWeight: 700, marginBottom: "8px", fontSize: "13px" }}>{d.name}</p> |
| <div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "6px" }}> |
| <div style={{ flex: 1, height: "6px", borderRadius: "3px", background: "var(--bg-primary)" }}> |
| <div style={{ width: `${pct}%`, height: "100%", borderRadius: "3px", background: barColor, boxShadow: `0 0 10px ${barColor}55`, transition: "width 0.3s" }} /> |
| </div> |
| <span style={{ color: barColor, fontWeight: 700, fontFamily: "var(--font-mono)", minWidth: "50px", textAlign: "right" }}> |
| {d.score} / {d.max} |
| </span> |
| </div> |
| <p style={{ color: "var(--text-muted)", fontSize: "11px" }}>{pct}% utilization</p> |
| </div> |
| ); |
| } |
|
|
| function RadarTooltip({ active, payload }) { |
| if (!active || !payload || !payload.length) return null; |
| const d = payload[0].payload; |
| const pct = d.pct.toFixed(0); |
| const color = d.pct > 75 ? "#22c55e" : d.pct > 50 ? "#eab308" : "#ef4444"; |
| return ( |
| <div style={tooltipStyle}> |
| <p style={{ fontWeight: 700, marginBottom: "6px" }}>{d.subject}</p> |
| <p style={{ fontFamily: "var(--font-mono)", color, fontWeight: 700 }}>{d.raw} / {d.max} <span style={{ color: "var(--text-muted)", fontWeight: 400 }}>({pct}%)</span></p> |
| </div> |
| ); |
| } |
|
|
| |
| function GlowDot({ cx, cy, payload }) { |
| const color = payload.pct > 75 ? "#22c55e" : payload.pct > 50 ? "#eab308" : "#ef4444"; |
| return ( |
| <g> |
| <circle cx={cx} cy={cy} r="8" fill={color} opacity="0.15" /> |
| <circle cx={cx} cy={cy} r="4" fill={color} stroke="#fff" strokeWidth="1.5" /> |
| </g> |
| ); |
| } |
|
|
| |
| function GlowBar(props) { |
| const { x, y, width, height, fill } = props; |
| if (!width || !height) return null; |
| const w = Math.abs(width); |
| const h = Math.abs(height); |
| const rx = Math.min(4, w / 2); |
| return ( |
| <g> |
| <rect x={x} y={y} width={w} height={h} rx={rx} fill={fill} opacity="0.12" style={{ filter: `drop-shadow(0 0 12px ${fill})` }} /> |
| <rect x={x} y={y} width={w} height={h} rx={rx} fill={fill} /> |
| </g> |
| ); |
| } |
|
|
| |
| function StressMiniGauge({ original, stressed, maxScore = 100 }) { |
| const oPct = Math.min((original || 0) / maxScore * 100, 100); |
| const sPct = Math.min((stressed || 0) / maxScore * 100, 100); |
| const oColor = oPct > 70 ? "#22c55e" : oPct > 45 ? "#eab308" : "#ef4444"; |
| const sColor = sPct > 70 ? "#22c55e" : sPct > 45 ? "#eab308" : "#ef4444"; |
| return ( |
| <div style={{ display: "flex", flexDirection: "column", gap: "6px", marginTop: "4px" }}> |
| <div> |
| <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "3px" }}> |
| <span style={{ fontSize: "10px", color: "var(--text-muted)" }}>Original</span> |
| <span style={{ fontSize: "10px", fontFamily: "var(--font-mono)", color: oColor }}>{original ?? "β"}</span> |
| </div> |
| <div style={{ height: "4px", borderRadius: "2px", background: "rgba(255,255,255,0.06)" }}> |
| <div className="stress-bar-fill" style={{ width: `${oPct}%`, height: "100%", borderRadius: "2px", background: oColor, boxShadow: `0 0 8px ${oColor}44` }} /> |
| </div> |
| </div> |
| <div> |
| <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "3px" }}> |
| <span style={{ fontSize: "10px", color: "var(--text-muted)" }}>Stressed</span> |
| <span style={{ fontSize: "10px", fontFamily: "var(--font-mono)", color: sColor }}>{stressed ?? "β"}</span> |
| </div> |
| <div style={{ height: "4px", borderRadius: "2px", background: "rgba(255,255,255,0.06)" }}> |
| <div className="stress-bar-fill" style={{ width: `${sPct}%`, height: "100%", borderRadius: "2px", background: sColor, boxShadow: `0 0 8px ${sColor}44` }} /> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| |
| export default function ScoreTab({ scoreBreakdown, shapValues, stressResults }) { |
| const [hoveredModel, setHoveredModel] = useState(null); |
|
|
| if (!scoreBreakdown) { |
| return <div className="card" style={{ color: "var(--text-muted)", fontSize: "13px" }}>No score data available.</div>; |
| } |
|
|
| const modelData = [ |
| { name: "Financial Health", score: scoreBreakdown.model_1_financial_health, max: 40 }, |
| { name: "Credit Behaviour", score: scoreBreakdown.model_2_credit_behaviour, max: 30 }, |
| { name: "External Risk", score: scoreBreakdown.model_3_external_risk, max: 20 }, |
| { name: "Text Risk", score: scoreBreakdown.model_4_text_risk, max: 10 }, |
| ]; |
|
|
| const radarData = modelData.map((m) => ({ |
| subject: m.name.replace(" ", "\n"), |
| pct: ((m.score || 0) / m.max) * 100, |
| raw: m.score || 0, |
| max: m.max, |
| fullMark: 100, |
| })); |
|
|
| function barColor(score, max) { |
| const ratio = (score || 0) / max; |
| if (ratio > 0.75) return "#22c55e"; |
| if (ratio > 0.5) return "#eab308"; |
| return "#ef4444"; |
| } |
|
|
| const topShap = [...(shapValues || [])].sort((a, b) => Math.abs(b.impact) - Math.abs(a.impact)).slice(0, 10); |
| const anyFlipped = (stressResults || []).some((s) => s.flipped); |
| const layer2Delta = scoreBreakdown.layer2_ml_refinement; |
| const layer2Sign = layer2Delta >= 0 ? "+" : ""; |
|
|
| return ( |
| <div className="flex flex-col gap-lg score-tab-enter"> |
| |
| {/* βββ Two-Layer Architecture ββββββββββββββββββββββββββββββββββββββ */} |
| <div> |
| <span className="label" style={{ display: "block", marginBottom: "12px" }}>Two-Layer Scoring Architecture</span> |
| <div className="grid grid-3" style={{ gap: "16px", alignItems: "center" }}> |
| {/* Layer 1 */} |
| <div className="card score-card-hover" style={{ position: "relative", overflow: "hidden" }}> |
| <div className="card-shimmer" /> |
| <div className="flex justify-between items-center flex-wrap gap-sm" style={{ marginBottom: "8px" }}> |
| <span className="label">Layer 1</span> |
| <span className="badge badge-success">REGULATORY ANCHOR</span> |
| </div> |
| <p style={{ fontSize: "13px", fontWeight: 600, marginBottom: "4px" }}>RBI/CRISIL Rules</p> |
| <p style={{ fontFamily: "var(--font-heading)", fontSize: "1.5rem", fontWeight: 700, color: "var(--accent)" }}> |
| {scoreBreakdown.layer1_rule_based} pts |
| </p> |
| <p style={{ color: "var(--text-muted)", fontSize: "12px", lineHeight: 1.6, marginTop: "4px" }}> |
| Rule-based thresholds from RBI Prudential Norms. |
| </p> |
| </div> |
| |
| {/* Final Score β Animated Ring */} |
| <div className="card" style={{ |
| background: "var(--bg-elevated)", borderColor: "rgba(59,130,246,0.3)", |
| display: "flex", flexDirection: "column", alignItems: "center", padding: "28px 24px", |
| position: "relative", overflow: "hidden", |
| }}> |
| <div style={{ |
| position: "absolute", inset: 0, opacity: 0.06, |
| background: "radial-gradient(circle at 50% 40%, var(--accent) 0%, transparent 70%)", |
| }} /> |
| <span className="label" style={{ display: "block", marginBottom: "12px", position: "relative" }}>Final Score</span> |
| <AnimatedScoreRing score={scoreBreakdown.final_score} /> |
| {scoreBreakdown.confidence && ( |
| <div style={{ marginTop: "12px", position: "relative" }}> |
| <ConfidenceBadge confidence={scoreBreakdown.confidence} /> |
| </div> |
| )} |
| </div> |
| |
| {/* Layer 2 */} |
| <div className="card score-card-hover" style={{ position: "relative", overflow: "hidden" }}> |
| <div className="card-shimmer" /> |
| <div className="flex justify-between items-center flex-wrap gap-sm" style={{ marginBottom: "8px" }}> |
| <span className="label">Layer 2</span> |
| <span className="badge badge-accent">ML REFINEMENT</span> |
| </div> |
| <p style={{ fontSize: "13px", fontWeight: 600, marginBottom: "4px" }}>LightGBM ML Refinement</p> |
| <p style={{ fontFamily: "var(--font-heading)", fontSize: "1.5rem", fontWeight: 700, color: layer2Delta >= 0 ? "var(--success)" : "var(--danger)" }}> |
| {layer2Sign}{layer2Delta} pts |
| </p> |
| <p style={{ color: "var(--text-muted)", fontSize: "12px", lineHeight: 1.6, marginTop: "4px" }}> |
| ML model trained on CRISIL-calibrated synthetic data. |
| </p> |
| </div> |
| </div> |
| </div> |
| |
| {/* βββ 4-Model Breakdown: Radar + Bar ββββββββββββββββββββββββββββββ */} |
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> |
| <div style={{ |
| position: "absolute", inset: 0, opacity: 0.04, |
| background: "radial-gradient(circle at 30% 50%, #3b82f6 0%, transparent 60%)", |
| }} /> |
| <span className="label" style={{ display: "block", marginBottom: "16px", position: "relative" }}>4-Model Score Breakdown</span> |
| <div style={{ display: "grid", gridTemplateColumns: "280px 1fr", gap: "24px", alignItems: "center", position: "relative" }}> |
| {/* Radar Chart */} |
| <div style={{ display: "flex", justifyContent: "center" }}> |
| <ResponsiveContainer width={260} height={240}> |
| <RadarChart data={radarData} cx="50%" cy="50%" outerRadius="75%"> |
| <PolarGrid stroke="var(--border)" /> |
| <PolarAngleAxis |
| dataKey="subject" |
| tick={{ fill: "var(--text-primary)", fontSize: 10, fontWeight: 500 }} |
| /> |
| <PolarRadiusAxis |
| angle={90} domain={[0, 100]} tick={false} axisLine={false} |
| /> |
| <Tooltip content={<RadarTooltip />} /> |
| <Radar |
| name="Score" dataKey="pct" stroke="#3b82f6" fill="#3b82f6" |
| fillOpacity={0.15} strokeWidth={2} |
| dot={<GlowDot />} |
| animationBegin={200} animationDuration={1200} animationEasing="ease-out" |
| /> |
| </RadarChart> |
| </ResponsiveContainer> |
| </div> |
| |
| {/* Enhanced Bar Chart */} |
| <ResponsiveContainer width="100%" height={200}> |
| <BarChart data={modelData} layout="vertical" margin={{ top: 5, right: 40, left: 10, bottom: 5 }}> |
| <defs> |
| {modelData.map((entry, idx) => { |
| const c = barColor(entry.score, entry.max); |
| return ( |
| <linearGradient key={idx} id={`barGrad${idx}`} x1="0" y1="0" x2="1" y2="0"> |
| <stop offset="0%" stopColor={c} stopOpacity="0.6" /> |
| <stop offset="100%" stopColor={c} stopOpacity="1" /> |
| </linearGradient> |
| ); |
| })} |
| </defs> |
| <XAxis type="number" domain={[0, 45]} tick={{ fill: "var(--text-muted)", fontSize: 11 }} axisLine={{ stroke: "var(--border)" }} tickLine={false} /> |
| <YAxis type="category" dataKey="name" width={120} tick={{ fill: "var(--text-primary)", fontSize: 11, fontWeight: 500 }} axisLine={false} tickLine={false} /> |
| <Tooltip content={<ModelTooltip />} cursor={{ fill: "var(--bg-elevated)" }} /> |
| {/* Background max bars */} |
| <Bar dataKey="max" radius={[0, 4, 4, 0]} fill="var(--bg-elevated)" isAnimationActive={false} /> |
| <Bar dataKey="score" radius={[0, 4, 4, 0]} shape={<GlowBar />} animationBegin={400} animationDuration={1000} animationEasing="ease-out"> |
| {modelData.map((entry, idx) => <Cell key={idx} fill={`url(#barGrad${idx})`} />)} |
| </Bar> |
| </BarChart> |
| </ResponsiveContainer> |
| </div> |
| </div> |
| |
| {/* βββ SHAP Feature Attribution ββββββββββββββββββββββββββββββββββββ */} |
| <div className="card" style={{ position: "relative", overflow: "hidden" }}> |
| <div style={{ |
| position: "absolute", inset: 0, opacity: 0.03, |
| background: "linear-gradient(135deg, rgba(34,197,94,0.3) 0%, transparent 40%, rgba(239,68,68,0.3) 100%)", |
| }} /> |
| <span className="label" style={{ display: "block", marginBottom: "4px", position: "relative" }}>Why This Score? β SHAP Feature Attribution</span> |
| <p style={{ color: "var(--text-muted)", fontSize: "12px", marginBottom: "16px", position: "relative" }}>Every point deduction traced to its exact source</p> |
| {topShap.length === 0 ? ( |
| <p style={{ color: "var(--text-muted)", fontSize: "13px" }}>No SHAP data available.</p> |
| ) : ( |
| <div style={{ position: "relative" }}> |
| <ResponsiveContainer width="100%" height={topShap.length * 40 + 40}> |
| <BarChart data={topShap} layout="vertical" margin={{ top: 0, right: 40, left: 10, bottom: 0 }}> |
| <defs> |
| <linearGradient id="shapGreen" x1="0" y1="0" x2="1" y2="0"> |
| <stop offset="0%" stopColor="#22c55e" stopOpacity="0.3" /> |
| <stop offset="100%" stopColor="#22c55e" stopOpacity="1" /> |
| </linearGradient> |
| <linearGradient id="shapRed" x1="1" y1="0" x2="0" y2="0"> |
| <stop offset="0%" stopColor="#ef4444" stopOpacity="0.3" /> |
| <stop offset="100%" stopColor="#ef4444" stopOpacity="1" /> |
| </linearGradient> |
| <filter id="greenGlow"><feGaussianBlur stdDeviation="3" result="b" /><feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge></filter> |
| <filter id="redGlow"><feGaussianBlur stdDeviation="3" result="b" /><feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge></filter> |
| </defs> |
| <XAxis type="number" tick={{ fill: "var(--text-muted)", fontSize: 11 }} axisLine={{ stroke: "var(--border)" }} tickLine={false} /> |
| <YAxis type="category" dataKey="feature" width={160} tick={{ fill: "var(--text-primary)", fontSize: 11, fontWeight: 500 }} axisLine={false} tickLine={false} /> |
| <Tooltip content={<ShapTooltip />} cursor={{ fill: "var(--bg-elevated)" }} /> |
| <ReferenceLine x={0} stroke="var(--border)" strokeWidth={1} /> |
| <Bar dataKey="impact" radius={[4, 4, 4, 4]} shape={<GlowBar />} animationBegin={600} animationDuration={1200} animationEasing="ease-out"> |
| {topShap.map((entry, idx) => ( |
| <Cell key={idx} fill={entry.impact >= 0 ? "url(#shapGreen)" : "url(#shapRed)"} /> |
| ))} |
| </Bar> |
| </BarChart> |
| </ResponsiveContainer> |
| <div style={{ display: "flex", gap: "20px", marginTop: "12px", position: "relative" }}> |
| <div style={{ display: "flex", alignItems: "center", gap: "6px" }}> |
| <div style={{ width: "24px", height: "4px", borderRadius: "2px", background: "linear-gradient(90deg, rgba(34,197,94,0.3), #22c55e)", boxShadow: "0 0 6px rgba(34,197,94,0.4)" }} /> |
| <span style={{ color: "var(--text-muted)", fontSize: "12px" }}>Helped score</span> |
| </div> |
| <div style={{ display: "flex", alignItems: "center", gap: "6px" }}> |
| <div style={{ width: "24px", height: "4px", borderRadius: "2px", background: "linear-gradient(90deg, #ef4444, rgba(239,68,68,0.3))", boxShadow: "0 0 6px rgba(239,68,68,0.4)" }} /> |
| <span style={{ color: "var(--text-muted)", fontSize: "12px" }}>Hurt score</span> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* βββ Stress Scenario Analysis ββββββββββββββββββββββββββββββββββββ */} |
| <div className="flex flex-col gap-md"> |
| <span className="label">Stress Scenario Analysis β Loan Resilience</span> |
| {anyFlipped && ( |
| <div className="card" style={{ background: "var(--warning-subtle)", borderColor: "rgba(234,179,8,0.5)", position: "relative", overflow: "hidden" }}> |
| <div style={{ position: "absolute", inset: 0, background: "linear-gradient(90deg, rgba(234,179,8,0.06) 0%, transparent 100%)" }} /> |
| <p style={{ color: "var(--warning)", fontSize: "13px", fontWeight: 600, position: "relative" }}> |
| β STRUCTURALLY FRAGILE β This loan may not withstand economic stress. Additional protective covenants required. |
| </p> |
| </div> |
| )} |
| <div className="grid grid-3" style={{ gap: "16px" }}> |
| {(stressResults || []).map((sr) => { |
| const flipped = sr.flipped; |
| const borderCol = flipped ? "rgba(239,68,68,0.4)" : "rgba(34,197,94,0.3)"; |
| const bgCol = flipped ? "var(--danger-subtle)" : "var(--success-subtle)"; |
| return ( |
| <div key={sr.scenario} className="card score-card-hover" style={{ background: bgCol, borderColor: borderCol, display: "flex", flexDirection: "column", gap: "12px", position: "relative", overflow: "hidden" }}> |
| {flipped && <div className="stress-flipped-bg" />} |
| <div className="flex justify-between items-start gap-sm" style={{ position: "relative" }}> |
| <p style={{ fontSize: "13px", fontWeight: 600, lineHeight: 1.3 }}>{SCENARIO_LABELS[sr.scenario] || sr.scenario}</p> |
| <span className={`badge ${flipped ? "badge-danger animate-pulse" : "badge-success"}`} style={{ whiteSpace: "nowrap" }}> |
| {flipped ? "DECISION FLIPPED" : "RESILIENT"} |
| </span> |
| </div> |
| <div className="flex items-center gap-sm" style={{ position: "relative" }}> |
| <span style={{ fontSize: "14px", fontWeight: 700, color: decisionColorVal(sr.original_decision), fontFamily: "var(--font-mono)" }}>{sr.original_decision}</span> |
| <span style={{ color: "var(--text-muted)", fontSize: "18px" }}>β</span> |
| <span style={{ fontSize: "14px", fontWeight: 700, color: decisionColorVal(sr.stressed_decision), fontFamily: "var(--font-mono)" }}>{sr.stressed_decision}</span> |
| </div> |
| {(sr.original_score != null || sr.stressed_score != null) && ( |
| <StressMiniGauge original={sr.original_score} stressed={sr.stressed_score} /> |
| )} |
| {flipped && sr.recommendation && ( |
| <p style={{ color: "var(--warning)", fontSize: "12px", lineHeight: 1.6, position: "relative" }}>{sr.recommendation}</p> |
| )} |
| </div> |
| ); |
| })} |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|