Spaces:
Sleeping
Sleeping
Deploy v1 — single-Docker FastAPI + Next.js + RAG + voice + faithfulness
Browse files- backend/premium_calculator.py +9 -2
- frontend/src/app/page.tsx +59 -10
backend/premium_calculator.py
CHANGED
|
@@ -54,8 +54,15 @@ FALLBACK_SI = {
|
|
| 54 |
"2500000": 3.1, "5000000": 4.6, "10000000": 7.2,
|
| 55 |
}
|
| 56 |
FALLBACK_CITY = {"metro": 1.0, "tier1": 0.92, "tier2": 0.82}
|
| 57 |
-
# family_size=
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
# Pre-existing-disease loading factors. Sources: Acko + PolicyBazaar coverage
|
| 61 |
# articles on PED loading (typical 25-50% premium uplift depending on severity).
|
|
|
|
| 54 |
"2500000": 3.1, "5000000": 4.6, "10000000": 7.2,
|
| 55 |
}
|
| 56 |
FALLBACK_CITY = {"metro": 1.0, "tier1": 0.92, "tier2": 0.82}
|
| 57 |
+
# family_size = NUMBER OF DEPENDENTS COVERED (in addition to self).
|
| 58 |
+
# 0 = self only (individual policy, no floater premium uplift)
|
| 59 |
+
# 1 = self + 1 dependent (couple cover)
|
| 60 |
+
# 2 = self + 2 dependents (small family)
|
| 61 |
+
# ...
|
| 62 |
+
# Source: typical retail family-floater rate cards from PolicyBazaar +
|
| 63 |
+
# InsuranceDekho — individual base, ~1.5× for couple, ~2× for family of 3,
|
| 64 |
+
# ~2.4× for family of 4, scaling thereafter.
|
| 65 |
+
FALLBACK_FLOATER = {0: 1.0, 1: 1.50, 2: 1.85, 3: 2.20, 4: 2.55, 5: 2.85, 6: 3.10}
|
| 66 |
|
| 67 |
# Pre-existing-disease loading factors. Sources: Acko + PolicyBazaar coverage
|
| 68 |
# articles on PED loading (typical 25-50% premium uplift depending on severity).
|
frontend/src/app/page.tsx
CHANGED
|
@@ -440,20 +440,22 @@ function PremiumCalculatorPanel({ onClose }: { onClose: () => void }) {
|
|
| 440 |
const [sumInsured, setSumInsured] = useState(1000000);
|
| 441 |
const [cityTier, setCityTier] = useState<"metro" | "tier1" | "tier2">("metro");
|
| 442 |
const [smoker, setSmoker] = useState(false);
|
| 443 |
-
const [familySize, setFamilySize] = useState(
|
|
|
|
|
|
|
| 444 |
const [estimate, setEstimate] = useState<PremiumEstimateResponse | null>(null);
|
| 445 |
const [busy, setBusy] = useState(false);
|
| 446 |
|
| 447 |
useEffect(() => {
|
| 448 |
const handler = setTimeout(() => {
|
| 449 |
setBusy(true);
|
| 450 |
-
postPremiumEstimate({ age, sum_insured_inr: sumInsured, city_tier: cityTier, smoker, family_size: familySize })
|
| 451 |
.then(setEstimate)
|
| 452 |
.catch(() => setEstimate(null))
|
| 453 |
.finally(() => setBusy(false));
|
| 454 |
}, 200); // debounce
|
| 455 |
return () => clearTimeout(handler);
|
| 456 |
-
}, [age, sumInsured, cityTier, smoker, familySize]);
|
| 457 |
|
| 458 |
const fmtINR = (v: number) => `₹${v.toLocaleString("en-IN")}`;
|
| 459 |
const siDisplay = sumInsured >= 10000000 ? `${sumInsured / 10000000} cr` : `${sumInsured / 100000} L`;
|
|
@@ -506,15 +508,41 @@ function PremiumCalculatorPanel({ onClose }: { onClose: () => void }) {
|
|
| 506 |
<div>
|
| 507 |
<label className="flex items-center justify-between text-xs mb-1">
|
| 508 |
<span className="font-medium">Family covered</span>
|
| 509 |
-
<span className="font-mono">{familySize} {familySize === 1 ? "
|
| 510 |
</label>
|
| 511 |
<input
|
| 512 |
-
type="range" min={
|
| 513 |
value={familySize}
|
| 514 |
onChange={(e) => setFamilySize(parseInt(e.target.value))}
|
| 515 |
className="w-full accent-[var(--primary)]"
|
| 516 |
/>
|
| 517 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
<div className="flex items-center gap-3 flex-wrap text-xs">
|
| 519 |
<span className="font-medium">City tier:</span>
|
| 520 |
{(["metro", "tier1", "tier2"] as const).map((t) => (
|
|
@@ -1131,17 +1159,19 @@ function PerPolicyPremiumEstimator({ policy }: { policy: MarketplacePolicy }) {
|
|
| 1131 |
const [si, setSI] = useState(defaultSI);
|
| 1132 |
const [city, setCity] = useState<"metro" | "tier1" | "tier2">("metro");
|
| 1133 |
const [smoker, setSmoker] = useState(false);
|
| 1134 |
-
const [fam, setFam] = useState(
|
|
|
|
|
|
|
| 1135 |
const [est, setEst] = useState<PremiumEstimateResponse | null>(null);
|
| 1136 |
const [busy, setBusy] = useState(false);
|
| 1137 |
useEffect(() => {
|
| 1138 |
const t = setTimeout(() => {
|
| 1139 |
setBusy(true);
|
| 1140 |
-
postPremiumEstimate({ age, sum_insured_inr: si, city_tier: city, smoker, family_size: fam, policy_id: policy.policy_id })
|
| 1141 |
.then(setEst).catch(() => setEst(null)).finally(() => setBusy(false));
|
| 1142 |
}, 150);
|
| 1143 |
return () => clearTimeout(t);
|
| 1144 |
-
}, [age, si, city, smoker, fam, policy.policy_id]);
|
| 1145 |
const fmt = (v: number) => `₹${v.toLocaleString("en-IN")}`;
|
| 1146 |
const siDisp = si >= 10000000 ? `${si / 10000000} cr` : `${si / 100000} L`;
|
| 1147 |
return (
|
|
@@ -1161,9 +1191,28 @@ function PerPolicyPremiumEstimator({ policy }: { policy: MarketplacePolicy }) {
|
|
| 1161 |
</div>
|
| 1162 |
<div>
|
| 1163 |
<div className="flex items-center justify-between text-[10px] uppercase tracking-wide text-[var(--muted-foreground)] font-semibold">
|
| 1164 |
-
<span>Family
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1165 |
</div>
|
| 1166 |
-
<input type="range" min={
|
| 1167 |
</div>
|
| 1168 |
<div className="flex items-center gap-2 flex-wrap text-[11px] pt-1">
|
| 1169 |
{(["metro", "tier1", "tier2"] as const).map((t) => (
|
|
|
|
| 440 |
const [sumInsured, setSumInsured] = useState(1000000);
|
| 441 |
const [cityTier, setCityTier] = useState<"metro" | "tier1" | "tier2">("metro");
|
| 442 |
const [smoker, setSmoker] = useState(false);
|
| 443 |
+
const [familySize, setFamilySize] = useState(0);
|
| 444 |
+
const [ped, setPed] = useState<"none" | "diabetes_or_hypertension" | "heart_disease" | "multiple">("none");
|
| 445 |
+
const [copay, setCopay] = useState(0);
|
| 446 |
const [estimate, setEstimate] = useState<PremiumEstimateResponse | null>(null);
|
| 447 |
const [busy, setBusy] = useState(false);
|
| 448 |
|
| 449 |
useEffect(() => {
|
| 450 |
const handler = setTimeout(() => {
|
| 451 |
setBusy(true);
|
| 452 |
+
postPremiumEstimate({ age, sum_insured_inr: sumInsured, city_tier: cityTier, smoker, family_size: familySize, pre_existing_conditions: ped, copayment_pct: copay })
|
| 453 |
.then(setEstimate)
|
| 454 |
.catch(() => setEstimate(null))
|
| 455 |
.finally(() => setBusy(false));
|
| 456 |
}, 200); // debounce
|
| 457 |
return () => clearTimeout(handler);
|
| 458 |
+
}, [age, sumInsured, cityTier, smoker, familySize, ped, copay]);
|
| 459 |
|
| 460 |
const fmtINR = (v: number) => `₹${v.toLocaleString("en-IN")}`;
|
| 461 |
const siDisplay = sumInsured >= 10000000 ? `${sumInsured / 10000000} cr` : `${sumInsured / 100000} L`;
|
|
|
|
| 508 |
<div>
|
| 509 |
<label className="flex items-center justify-between text-xs mb-1">
|
| 510 |
<span className="font-medium">Family covered</span>
|
| 511 |
+
<span className="font-mono">{familySize === 0 ? "Self only" : `Self + ${familySize} dependent${familySize === 1 ? "" : "s"}`}</span>
|
| 512 |
</label>
|
| 513 |
<input
|
| 514 |
+
type="range" min={0} max={6} step={1}
|
| 515 |
value={familySize}
|
| 516 |
onChange={(e) => setFamilySize(parseInt(e.target.value))}
|
| 517 |
className="w-full accent-[var(--primary)]"
|
| 518 |
/>
|
| 519 |
</div>
|
| 520 |
+
<div>
|
| 521 |
+
<label className="block text-xs mb-1 font-medium">Pre-existing conditions</label>
|
| 522 |
+
<select
|
| 523 |
+
value={ped}
|
| 524 |
+
onChange={(e) => setPed(e.target.value as typeof ped)}
|
| 525 |
+
className="w-full text-xs bg-transparent border border-[var(--border)] rounded-md px-2 py-1.5 outline-none focus:border-[var(--primary)]"
|
| 526 |
+
>
|
| 527 |
+
<option value="none">None</option>
|
| 528 |
+
<option value="diabetes_or_hypertension">Diabetes or hypertension</option>
|
| 529 |
+
<option value="heart_disease">Heart disease</option>
|
| 530 |
+
<option value="multiple">Multiple conditions</option>
|
| 531 |
+
</select>
|
| 532 |
+
</div>
|
| 533 |
+
<div>
|
| 534 |
+
<label className="flex items-center justify-between text-xs mb-1">
|
| 535 |
+
<span className="font-medium">Voluntary co-pay</span>
|
| 536 |
+
<span className="font-mono">{copay}%</span>
|
| 537 |
+
</label>
|
| 538 |
+
<input
|
| 539 |
+
type="range" min={0} max={40} step={5}
|
| 540 |
+
value={copay}
|
| 541 |
+
onChange={(e) => setCopay(parseInt(e.target.value))}
|
| 542 |
+
className="w-full accent-[var(--primary)]"
|
| 543 |
+
/>
|
| 544 |
+
<p className="text-[10px] text-[var(--muted-foreground)] mt-0.5">Higher co-pay = lower premium; you pay this % of each claim</p>
|
| 545 |
+
</div>
|
| 546 |
<div className="flex items-center gap-3 flex-wrap text-xs">
|
| 547 |
<span className="font-medium">City tier:</span>
|
| 548 |
{(["metro", "tier1", "tier2"] as const).map((t) => (
|
|
|
|
| 1159 |
const [si, setSI] = useState(defaultSI);
|
| 1160 |
const [city, setCity] = useState<"metro" | "tier1" | "tier2">("metro");
|
| 1161 |
const [smoker, setSmoker] = useState(false);
|
| 1162 |
+
const [fam, setFam] = useState(0);
|
| 1163 |
+
const [ped, setPed] = useState<"none" | "diabetes_or_hypertension" | "heart_disease" | "multiple">("none");
|
| 1164 |
+
const [copay, setCopay] = useState(0);
|
| 1165 |
const [est, setEst] = useState<PremiumEstimateResponse | null>(null);
|
| 1166 |
const [busy, setBusy] = useState(false);
|
| 1167 |
useEffect(() => {
|
| 1168 |
const t = setTimeout(() => {
|
| 1169 |
setBusy(true);
|
| 1170 |
+
postPremiumEstimate({ age, sum_insured_inr: si, city_tier: city, smoker, family_size: fam, policy_id: policy.policy_id, pre_existing_conditions: ped, copayment_pct: copay })
|
| 1171 |
.then(setEst).catch(() => setEst(null)).finally(() => setBusy(false));
|
| 1172 |
}, 150);
|
| 1173 |
return () => clearTimeout(t);
|
| 1174 |
+
}, [age, si, city, smoker, fam, ped, copay, policy.policy_id]);
|
| 1175 |
const fmt = (v: number) => `₹${v.toLocaleString("en-IN")}`;
|
| 1176 |
const siDisp = si >= 10000000 ? `${si / 10000000} cr` : `${si / 100000} L`;
|
| 1177 |
return (
|
|
|
|
| 1191 |
</div>
|
| 1192 |
<div>
|
| 1193 |
<div className="flex items-center justify-between text-[10px] uppercase tracking-wide text-[var(--muted-foreground)] font-semibold">
|
| 1194 |
+
<span>Family covered</span><span className="font-mono">{fam === 0 ? "Self only" : `Self + ${fam}`}</span>
|
| 1195 |
+
</div>
|
| 1196 |
+
<input type="range" min={0} max={6} value={fam} onChange={(e) => setFam(parseInt(e.target.value))} className="w-full accent-[var(--primary)]" />
|
| 1197 |
+
</div>
|
| 1198 |
+
<div>
|
| 1199 |
+
<div className="text-[10px] uppercase tracking-wide text-[var(--muted-foreground)] font-semibold mb-0.5">Pre-existing conditions</div>
|
| 1200 |
+
<select
|
| 1201 |
+
value={ped}
|
| 1202 |
+
onChange={(e) => setPed(e.target.value as typeof ped)}
|
| 1203 |
+
className="w-full text-[11px] bg-transparent border border-[var(--border)] rounded px-1.5 py-1"
|
| 1204 |
+
>
|
| 1205 |
+
<option value="none">None</option>
|
| 1206 |
+
<option value="diabetes_or_hypertension">Diabetes / hypertension</option>
|
| 1207 |
+
<option value="heart_disease">Heart disease</option>
|
| 1208 |
+
<option value="multiple">Multiple</option>
|
| 1209 |
+
</select>
|
| 1210 |
+
</div>
|
| 1211 |
+
<div>
|
| 1212 |
+
<div className="flex items-center justify-between text-[10px] uppercase tracking-wide text-[var(--muted-foreground)] font-semibold">
|
| 1213 |
+
<span>Co-pay</span><span className="font-mono">{copay}%</span>
|
| 1214 |
</div>
|
| 1215 |
+
<input type="range" min={0} max={40} step={5} value={copay} onChange={(e) => setCopay(parseInt(e.target.value))} className="w-full accent-[var(--primary)]" />
|
| 1216 |
</div>
|
| 1217 |
<div className="flex items-center gap-2 flex-wrap text-[11px] pt-1">
|
| 1218 |
{(["metro", "tier1", "tier2"] as const).map((t) => (
|