Skip to content

Commit fb28401

Browse files
committed
Modularize risk console UI components
1 parent a4911d8 commit fb28401

14 files changed

Lines changed: 1081 additions & 997 deletions

web/components/risk-console.tsx

Lines changed: 22 additions & 997 deletions
Large diffs are not rendered by default.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { AlertTriangle, BarChart3, Brain, CheckCircle2 } from "lucide-react";
2+
import { useMemo } from "react";
3+
import {
4+
Bar,
5+
BarChart,
6+
CartesianGrid,
7+
Cell,
8+
ReferenceLine,
9+
ResponsiveContainer,
10+
Tooltip,
11+
XAxis,
12+
YAxis,
13+
} from "recharts";
14+
15+
import { featureLabel, formatFeatureValue } from "@/lib/labels";
16+
import type { PredictionResult } from "@/types/healthcare";
17+
18+
import { DataTable, NarrativePanel, PanelHeading } from "./shared-ui";
19+
import type { ChartFeature } from "./types";
20+
21+
export function Assessment({ result }: { result: PredictionResult }) {
22+
const topFeatures = useMemo(() => {
23+
return result.feature_explanation.features
24+
.slice()
25+
.sort((a, b) => b.magnitude - a.magnitude)
26+
.slice(0, 8)
27+
.map((feature) => ({
28+
...feature,
29+
label: featureLabel(feature.feature),
30+
valueLabel: formatFeatureValue(feature.feature, feature.value),
31+
directionLabel: feature.impact > 0 ? "Raises risk" : "Lowers risk",
32+
}));
33+
}, [result.feature_explanation.features]);
34+
35+
return (
36+
<section className="grid gap-5 xl:grid-cols-[0.95fr_1.05fr]">
37+
<div className="space-y-5">
38+
<NarrativePanel title="Explanation" icon={<Brain className="h-4 w-4" />} text={result.explanation} />
39+
<NarrativePanel
40+
title="Recommendation"
41+
icon={<CheckCircle2 className="h-4 w-4" />}
42+
text={result.recommendation}
43+
/>
44+
{result.safety.alerts.length > 0 ? (
45+
<div className="rounded-lg border border-danger/40 bg-danger/10 p-4 text-sm text-[#ffc1bc]">
46+
<div className="mb-2 flex items-center gap-2 font-semibold text-ink">
47+
<AlertTriangle className="h-4 w-4 text-danger" />
48+
Safety Alerts
49+
</div>
50+
<ul className="space-y-1">
51+
{result.safety.alerts.map((alert) => (
52+
<li key={alert}>{alert}</li>
53+
))}
54+
</ul>
55+
</div>
56+
) : null}
57+
</div>
58+
<FeatureImpactPanel features={topFeatures} method={result.feature_explanation.method} />
59+
</section>
60+
);
61+
}
62+
63+
function FeatureImpactPanel({ features, method }: { features: ChartFeature[]; method: string }) {
64+
return (
65+
<div className="console-panel rounded-lg p-5">
66+
<PanelHeading icon={<BarChart3 className="h-4 w-4" />} title="Feature Impact" />
67+
<div className="mt-4 h-80">
68+
<ResponsiveContainer width="100%" height="100%">
69+
<BarChart data={features} layout="vertical" margin={{ top: 8, right: 18, bottom: 8, left: 28 }}>
70+
<CartesianGrid stroke="rgba(255,255,255,0.08)" horizontal={false} />
71+
<XAxis type="number" stroke="#9aa4b2" tickLine={false} axisLine={false} />
72+
<YAxis
73+
type="category"
74+
dataKey="label"
75+
width={135}
76+
stroke="#9aa4b2"
77+
tickLine={false}
78+
axisLine={false}
79+
tick={{ fontSize: 12 }}
80+
/>
81+
<Tooltip
82+
cursor={{ fill: "rgba(255,255,255,0.04)" }}
83+
formatter={(value) => [Number(value).toFixed(4), "Impact"]}
84+
contentStyle={{
85+
background: "#101318",
86+
border: "1px solid rgba(255,255,255,0.1)",
87+
borderRadius: 8,
88+
}}
89+
/>
90+
<ReferenceLine x={0} stroke="rgba(244,246,248,0.45)" strokeDasharray="3 3" />
91+
<Bar dataKey="impact" radius={[0, 4, 4, 0]}>
92+
{features.map((feature) => (
93+
<Cell key={feature.feature} fill={feature.impact >= 0 ? "#d05245" : "#2f9b6a"} />
94+
))}
95+
</Bar>
96+
</BarChart>
97+
</ResponsiveContainer>
98+
</div>
99+
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-muted">
100+
<span>Method: {method}</span>
101+
<span>Top {features.length} by absolute impact</span>
102+
</div>
103+
<DataTable
104+
rows={features.map((feature) => ({
105+
Feature: feature.label,
106+
Value: feature.valueLabel,
107+
Direction: feature.directionLabel,
108+
Impact: feature.impact.toFixed(4),
109+
}))}
110+
compact
111+
/>
112+
</div>
113+
);
114+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { PatientInput } from "@/types/healthcare";
2+
3+
export const DEFAULT_INPUT: PatientInput = {
4+
age: 45,
5+
height_cm: 170,
6+
weight_kg: 82.4,
7+
bp: 130,
8+
glucose: 180,
9+
high_chol: false,
10+
chol_check: true,
11+
smoker: false,
12+
stroke: false,
13+
heart_disease_or_attack: false,
14+
phys_activity: true,
15+
fruits: true,
16+
veggies: true,
17+
heavy_alcohol_consump: false,
18+
any_healthcare: true,
19+
no_doc_bc_cost: false,
20+
general_health: 3,
21+
mental_health_days: 2,
22+
physical_health_days: 2,
23+
diff_walk: false,
24+
sex: "female",
25+
education: 5,
26+
income: 5,
27+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Sparkles } from "lucide-react";
2+
3+
export function EmptyState() {
4+
return (
5+
<div className="console-panel rounded-lg p-8">
6+
<div className="flex max-w-2xl items-start gap-4">
7+
<div className="rounded-md border border-borderSoft bg-info/10 p-3 text-info">
8+
<Sparkles className="h-5 w-5" />
9+
</div>
10+
<div>
11+
<h3 className="text-xl font-semibold">Ready for assessment</h3>
12+
<p className="mt-2 text-sm leading-6 text-muted">
13+
Fill in the patient profile and run the trained Kaggle artifact through the FastAPI
14+
inference service.
15+
</p>
16+
</div>
17+
</div>
18+
</div>
19+
);
20+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Activity, Database } from "lucide-react";
2+
3+
import type { PredictionResult } from "@/types/healthcare";
4+
5+
import { DataTable, PanelHeading } from "./shared-ui";
6+
7+
export function Evidence({ result }: { result: PredictionResult }) {
8+
const contexts = result.retrieved_context ?? [];
9+
10+
return (
11+
<section className="grid gap-5 xl:grid-cols-[1fr_0.9fr]">
12+
<div className="console-panel rounded-lg p-5">
13+
<PanelHeading icon={<Database className="h-4 w-4" />} title="Retrieved Medical Context" />
14+
<div className="mt-4 space-y-3">
15+
{contexts.length > 0 ? (
16+
contexts.map((context) => (
17+
<div key={context.title} className="rounded-md border border-borderSoft bg-canvas/40 p-4">
18+
<div className="font-semibold text-ink">{context.title}</div>
19+
<p className="mt-2 text-sm leading-6 text-muted">{context.text}</p>
20+
</div>
21+
))
22+
) : (
23+
<p className="text-sm text-muted">No medical context retrieved for this case.</p>
24+
)}
25+
</div>
26+
</div>
27+
<div className="console-panel rounded-lg p-5">
28+
<PanelHeading icon={<Activity className="h-4 w-4" />} title="Similar Prior Cases" />
29+
<DataTable
30+
rows={result.similar_cases.map((item) => ({
31+
Risk: item.metadata.risk ?? "—",
32+
Probability:
33+
typeof item.metadata.probability === "number"
34+
? `${(item.metadata.probability * 100).toFixed(1)}%`
35+
: "—",
36+
Age: item.metadata.age ?? "—",
37+
BMI: item.metadata.bmi ?? "—",
38+
BP: item.metadata.bp ?? "—",
39+
Distance: item.distance.toFixed(3),
40+
}))}
41+
/>
42+
</div>
43+
</section>
44+
);
45+
}

0 commit comments

Comments
 (0)