datboyalex commited on
Commit
1d5edd0
·
verified ·
1 Parent(s): f70959d

Upload 2 files

Browse files
Files changed (2) hide show
  1. app (1).py +687 -0
  2. requirements (3).txt +3 -0
app (1).py ADDED
@@ -0,0 +1,687 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Connecticut Hospital Financial Assistance Screener v4
4
+ Deployed on Hugging Face Spaces
5
+ """
6
+
7
+ import os
8
+ import gradio as gr
9
+ from openai import OpenAI
10
+ from geopy.geocoders import Nominatim
11
+ from geopy.distance import geodesic
12
+ import time
13
+
14
+ # Load API key from environment variable (set in HF Spaces secrets)
15
+ OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
16
+ client = OpenAI(api_key=OPENAI_API_KEY)
17
+ DEMO_PASSWORD = "ct2026"
18
+
19
+ geolocator = Nominatim(user_agent="ct_hospital_screener")
20
+
21
+ HOSPITALS = {
22
+ "Yale New Haven Health": {
23
+ "free_care_threshold": 250,
24
+ "sliding_scale_max": 550,
25
+ "asset_limit": None,
26
+ "contact": "877-442-2455",
27
+ "special_notes": "No asset limit",
28
+ "location": (41.3083, -72.9279),
29
+ "city": "New Haven",
30
+ "fap_url": "https://www.ynhhs.org/patient-care/billing-insurance/FAP-guidelines"
31
+ },
32
+ "Hartford HealthCare": {
33
+ "free_care_threshold": 250,
34
+ "sliding_scale_max": 550,
35
+ "asset_limit": "Liquid asset review",
36
+ "contact": "877-442-2455",
37
+ "special_notes": "Asset review required",
38
+ "location": (41.7658, -72.6734),
39
+ "city": "Hartford",
40
+ "fap_url": "https://hartfordhealthcare.org/patients-visitors/patients/billing-insurance/financial-assistance"
41
+ },
42
+ "Trinity Health of New England": {
43
+ "free_care_threshold": 200,
44
+ "sliding_scale_max": 400,
45
+ "asset_limit": None,
46
+ "contact": "860-714-1657",
47
+ "special_notes": "Medicaid exhaustion required",
48
+ "location": (41.7658, -72.6734),
49
+ "city": "Hartford",
50
+ "fap_url": "https://www.trinityhealthofne.org/for-patients/billing-and-financial-resources"
51
+ },
52
+ "Stamford Health": {
53
+ "free_care_threshold": 250,
54
+ "sliding_scale_max": 400,
55
+ "asset_limit": None,
56
+ "contact": "203-276-7572",
57
+ "special_notes": "No carve-outs",
58
+ "location": (41.0534, -73.5387),
59
+ "city": "Stamford",
60
+ "fap_url": "https://www.stamfordhealth.org/patients/fap/"
61
+ },
62
+ "Bristol Hospital": {
63
+ "free_care_threshold": 250,
64
+ "sliding_scale_max": 400,
65
+ "asset_limit": "$7,500 / $15,000",
66
+ "contact": "860-585-3035",
67
+ "special_notes": "Strict asset limits",
68
+ "location": (41.6718, -72.9493),
69
+ "city": "Bristol",
70
+ "fap_url": "https://www.bristolhealth.org/patients-and-visitors/cost-care-and-financial-assistance"
71
+ },
72
+ "Day Kimball Healthcare": {
73
+ "free_care_threshold": 200,
74
+ "sliding_scale_max": 400,
75
+ "asset_limit": "$100,000",
76
+ "contact": "860-928-7024",
77
+ "special_notes": "240-day window",
78
+ "location": (41.8528, -71.9004),
79
+ "city": "Putnam",
80
+ "fap_url": "https://www.daykimball.org/resources/financial-services/"
81
+ },
82
+ "UConn Health": {
83
+ "free_care_threshold": 400,
84
+ "sliding_scale_max": 400,
85
+ "asset_limit": None,
86
+ "contact": "860-679-4120",
87
+ "special_notes": "Most generous in CT",
88
+ "location": (41.7295, -72.7935),
89
+ "city": "Farmington",
90
+ "fap_url": "https://www.uconnhealth.org/patients-visitors/patient-resources/billing-costs-insurance"
91
+ },
92
+ "Nuvance Health": {
93
+ "free_care_threshold": 250,
94
+ "sliding_scale_max": 400,
95
+ "asset_limit": "Not specified",
96
+ "contact": "845-788-9012",
97
+ "special_notes": "Multi-location",
98
+ "location": (41.3948, -73.4540),
99
+ "city": "Danbury",
100
+ "fap_url": "https://www.nuvancehealth.org/patients-and-visitors/billing-and-insurance/patient-financial-assistance"
101
+ },
102
+ "Middlesex Health": {
103
+ "free_care_threshold": 200,
104
+ "sliding_scale_max": 400,
105
+ "asset_limit": "Not specified",
106
+ "contact": "860-358-6150",
107
+ "special_notes": "Limited docs",
108
+ "location": (41.5623, -72.6506),
109
+ "city": "Middletown",
110
+ "fap_url": "https://middlesexhealth.org/patients-and-visitors/financial-assistance-services"
111
+ }
112
+ }
113
+
114
+ # 2025 Federal Poverty Level Guidelines
115
+ # Source: https://aspe.hhs.gov/topics/poverty-economic-mobility/poverty-guidelines
116
+ # Updated: January 2025. Check annually for new guidelines.
117
+ FPL_2025 = {1: 15650, 2: 21150, 3: 26650, 4: 32150, 5: 37650, 6: 43150, 7: 48650, 8: 54150}
118
+
119
+ TRANSLATIONS = {
120
+ "en": {
121
+ "title": "Connecticut Hospital Financial Assistance Screener",
122
+ "beta_tag": "BETA",
123
+ "subtitle": "Check your eligibility for financial assistance",
124
+ "help_choose": "Help me find a hospital",
125
+ "know_hospital": "I know my hospital",
126
+ "zip_label": "Enter your ZIP code",
127
+ "search_button": "Search Hospitals",
128
+ "select_hospital": "Select Hospital",
129
+ "income_label": "Annual Household Income ($)",
130
+ "household_label": "Number of People in Household",
131
+ "snap_label": "Enrolled in SNAP or WIC?",
132
+ "yes": "Yes",
133
+ "no": "No",
134
+ "check_button": "Check Eligibility",
135
+ "continue_button": "Continue",
136
+ "back_button": "Back",
137
+ "miles_away": "miles away",
138
+ "free_care": "Free care up to",
139
+ "sliding_scale": "Sliding scale up to",
140
+ "status": "Status",
141
+ "contact": "Contact",
142
+ "sources": "Source Documents",
143
+ "disclaimer": "⚠️ This result was generated by AI and is for informational purposes only. It does not guarantee accuracy or constitute legal or financial advice. Please contact the hospital directly to confirm eligibility and complete the formal application process.",
144
+ "eligible": "LIKELY ELIGIBLE",
145
+ "not_eligible": "MAY NOT QUALIFY"
146
+ },
147
+ "es": {
148
+ "title": "Evaluador de Asistencia Financiera Hospitalaria de Connecticut",
149
+ "beta_tag": "BETA",
150
+ "subtitle": "Verifique su elegibilidad para asistencia financiera",
151
+ "help_choose": "Ayúdame a encontrar un hospital",
152
+ "know_hospital": "Conozco mi hospital",
153
+ "zip_label": "Ingrese su código postal",
154
+ "search_button": "Buscar Hospitales",
155
+ "select_hospital": "Seleccione Hospital",
156
+ "income_label": "Ingreso Anual del Hogar ($)",
157
+ "household_label": "Número de Personas en el Hogar",
158
+ "snap_label": "¿Inscrito en SNAP o WIC?",
159
+ "yes": "Sí",
160
+ "no": "No",
161
+ "check_button": "Verificar Elegibilidad",
162
+ "continue_button": "Continuar",
163
+ "back_button": "Atrás",
164
+ "miles_away": "millas de distancia",
165
+ "free_care": "Atención gratuita hasta",
166
+ "sliding_scale": "Escala móvil hasta",
167
+ "status": "Estado",
168
+ "contact": "Contacto",
169
+ "sources": "Documentos Fuente",
170
+ "disclaimer": "⚠️ Este resultado fue generado por IA y es solo para fines informativos. No garantiza precisión ni constituye asesoramiento legal o financiero. Comuníquese directamente con el hospital para confirmar la elegibilidad y completar el proceso de solicitud formal.",
171
+ "eligible": "PROBABLEMENTE ELEGIBLE",
172
+ "not_eligible": "PUEDE NO CALIFICAR"
173
+ }
174
+ }
175
+
176
+ def calculate_fpl_percentage(income, household_size):
177
+ if household_size <= 8:
178
+ fpl_base = FPL_2025[household_size]
179
+ else:
180
+ fpl_base = FPL_2025[8] + (5500 * (household_size - 8))
181
+ return round((income / fpl_base) * 100, 1)
182
+
183
+ def find_nearby_hospitals(zip_code, lang):
184
+ t = TRANSLATIONS[lang]
185
+ try:
186
+ location = geolocator.geocode(f"{zip_code}, Connecticut, USA")
187
+ if not location:
188
+ return "Invalid ZIP code" if lang == "en" else "Código postal inválido", []
189
+
190
+ user_coords = (location.latitude, location.longitude)
191
+ distances = []
192
+ for name, data in HOSPITALS.items():
193
+ dist = geodesic(user_coords, data["location"]).miles
194
+ distances.append((name, dist, data))
195
+
196
+ distances.sort(key=lambda x: x[1])
197
+ nearest = distances[:3]
198
+
199
+ choices = []
200
+ cards = ""
201
+ for name, dist, data in nearest:
202
+ choice_label = f"{name} ({data['city']}, {dist:.1f} mi)"
203
+ choices.append(choice_label)
204
+ cards += f"""### {name}
205
+ 📍 {data['city']} — {dist:.1f} {t['miles_away']}
206
+ 💰 {t['free_care']} {data['free_care_threshold']}% FPL | {t['sliding_scale']} {data['sliding_scale_max']}% FPL
207
+ 📞 {data['contact']}
208
+
209
+ ---
210
+ """
211
+
212
+ return cards, choices
213
+ except Exception as e:
214
+ return f"Error: {str(e)}", []
215
+
216
+ def determine_eligibility(hospital_name, income, household_size, has_snap_wic, lang):
217
+ if "(" in hospital_name:
218
+ hospital_name = hospital_name.split(" (")[0]
219
+
220
+ t = TRANSLATIONS[lang]
221
+ hospital = HOSPITALS[hospital_name]
222
+ fpl_percentage = calculate_fpl_percentage(income, household_size)
223
+
224
+ result = {
225
+ "hospital": hospital_name,
226
+ "income": income,
227
+ "household_size": household_size,
228
+ "fpl_percentage": fpl_percentage,
229
+ "has_snap_wic": has_snap_wic,
230
+ "pa_24_81_eligible": False,
231
+ "contact": hospital["contact"],
232
+ "special_notes": hospital["special_notes"],
233
+ "asset_limit": hospital["asset_limit"],
234
+ "fap_url": hospital["fap_url"],
235
+ "lang": lang
236
+ }
237
+
238
+ if has_snap_wic and fpl_percentage <= 250:
239
+ result["pa_24_81_eligible"] = True
240
+ result["eligibility_status"] = t["eligible"]
241
+ result["discount_level"] = "Presumptive Eligibility (PA 24-81)" if lang == "en" else "Elegibilidad Presuntiva (PA 24-81)"
242
+ elif fpl_percentage <= hospital["free_care_threshold"]:
243
+ result["eligibility_status"] = t["eligible"]
244
+ result["discount_level"] = "100% Discount" if lang == "en" else "100% de Descuento"
245
+ elif fpl_percentage <= hospital["sliding_scale_max"]:
246
+ result["eligibility_status"] = t["eligible"]
247
+ result["discount_level"] = "Partial Discount" if lang == "en" else "Descuento Parcial"
248
+ else:
249
+ result["eligibility_status"] = t["not_eligible"]
250
+ result["discount_level"] = "Income exceeds thresholds" if lang == "en" else "Ingresos superan umbrales"
251
+
252
+ return result
253
+
254
+ def generate_explanation_streaming(data):
255
+ """
256
+ Streaming version - yields text as it's generated
257
+ Uses confidence-based language tiers
258
+ """
259
+ lang = data["lang"]
260
+ t = TRANSLATIONS[lang]
261
+
262
+ # Determine confidence level for language calibration
263
+ is_pa_24_81 = data["pa_24_81_eligible"]
264
+ is_eligible = data["eligibility_status"] == t["eligible"]
265
+ has_asset_limit = data["asset_limit"] is not None
266
+ fpl_pct = data["fpl_percentage"]
267
+
268
+ # Confidence-based language selection
269
+ if is_pa_24_81:
270
+ confidence = "statutory" # Law-based, highest confidence
271
+ elif is_eligible and not has_asset_limit and fpl_pct < data.get("threshold", 200) * 0.8:
272
+ confidence = "strong" # Clear match, no complications
273
+ elif is_eligible and has_asset_limit:
274
+ confidence = "moderate" # Additional requirements exist
275
+ elif is_eligible:
276
+ confidence = "strong" # Generally eligible
277
+ else:
278
+ confidence = "negative" # Not eligible
279
+
280
+ # Build the system message with appropriate language guidance
281
+ if lang == "es":
282
+ if confidence == "statutory":
283
+ lang_guide = """LENGUAJE DEFINITIVO - Este es un derecho estatutario:
284
+ - Usa "califica" o "es elegible" con confianza
285
+ - Enfatiza que es una ley estatal (PA 24-81)
286
+ - No uses "puede" - esto es definitivo"""
287
+ elif confidence == "strong":
288
+ lang_guide = """LENGUAJE CONFIADO:
289
+ - Usa "parece ser elegible" o "aparentemente califica"
290
+ - Sé positivo pero incluye "basado en la información proporcionada"
291
+ - Anima a confirmar con el hospital"""
292
+ elif confidence == "moderate":
293
+ lang_guide = """LENGUAJE CAUTELOSO:
294
+ - Usa "puede calificar" o "podría ser elegible"
295
+ - IMPORTANTE: Menciona los requisitos adicionales (límites de activos, etc.)
296
+ - Enfatiza la necesidad de contactar al hospital"""
297
+ else:
298
+ lang_guide = """LENGUAJE AMABLE PERO DIRECTO:
299
+ - Sé claro: "sus ingresos superan los umbrales"
300
+ - Sugiere alternativas (otros hospitales, otros programas)
301
+ - Mantén un tono esperanzador"""
302
+
303
+ system_msg = f"""Eres un defensor de la salud que ayuda a explicar la elegibilidad para asistencia financiera.
304
+
305
+ {lang_guide}
306
+
307
+ Explica en 2 párrafos claros y cálidos. Máximo 150 palabras."""
308
+ else:
309
+ if confidence == "statutory":
310
+ lang_guide = """DEFINITIVE LANGUAGE - This is a statutory right:
311
+ - Use "you qualify" or "you are eligible" confidently
312
+ - Emphasize this is Connecticut state law (PA 24-81)
313
+ - Don't use "may" - this is definitive"""
314
+ elif confidence == "strong":
315
+ lang_guide = """CONFIDENT LANGUAGE:
316
+ - Use "you appear to be eligible" or "you likely qualify"
317
+ - Be positive but include "based on the information provided"
318
+ - Encourage hospital confirmation"""
319
+ elif confidence == "moderate":
320
+ lang_guide = """CAUTIOUS LANGUAGE:
321
+ - Use "you may qualify" or "you could be eligible"
322
+ - IMPORTANT: Mention additional requirements (asset limits, etc.)
323
+ - Emphasize need to contact hospital"""
324
+ else:
325
+ lang_guide = """GENTLE BUT DIRECT LANGUAGE:
326
+ - Be clear: "your income exceeds the eligibility thresholds"
327
+ - Suggest alternatives (other hospitals, other programs)
328
+ - Keep tone hopeful"""
329
+
330
+ system_msg = f"""You are a healthcare advocate helping explain financial assistance eligibility.
331
+
332
+ {lang_guide}
333
+
334
+ Explain in 2 clear, warm paragraphs. Maximum 150 words."""
335
+
336
+ user_prompt = f"""Hospital: {data['hospital']}
337
+ Income: ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
338
+ Household: {data['household_size']} people
339
+ SNAP/WIC: {'Yes' if data['has_snap_wic'] else 'No'}
340
+ Status: {data['eligibility_status']}
341
+ Discount: {data['discount_level']}
342
+ Contact: {data['contact']}
343
+ Asset Limit: {data['asset_limit'] or 'None'}
344
+ PA 24-81 Eligible: {'Yes' if data['pa_24_81_eligible'] else 'No'}"""
345
+
346
+ try:
347
+ stream = client.chat.completions.create(
348
+ model="gpt-4o",
349
+ messages=[
350
+ {"role": "system", "content": system_msg},
351
+ {"role": "user", "content": user_prompt}
352
+ ],
353
+ temperature=0.7,
354
+ max_tokens=300,
355
+ stream=True
356
+ )
357
+
358
+ collected_text = ""
359
+ for chunk in stream:
360
+ if chunk.choices[0].delta.content:
361
+ collected_text += chunk.choices[0].delta.content
362
+ yield collected_text
363
+
364
+ except Exception as e:
365
+ yield f"Error: {str(e)}"
366
+
367
+ def format_results_streaming(data, lang):
368
+ """
369
+ Generator that yields progressive updates to the results
370
+ """
371
+ t = TRANSLATIONS[lang]
372
+
373
+ # Start with header (instant)
374
+ header = f"""## {data['hospital']}
375
+
376
+ ### {t['status']}: {data['eligibility_status']}
377
+
378
+ 💵 ${data['income']:,.0f} ({data['fpl_percentage']}% FPL)
379
+
380
+ ---
381
+
382
+ """
383
+
384
+ # Stream the explanation
385
+ partial_explanation = ""
386
+ for partial_text in generate_explanation_streaming(data):
387
+ partial_explanation = partial_text
388
+ full_output = header + partial_explanation
389
+ yield full_output
390
+
391
+ # Add footer after streaming completes
392
+ footer = f"""
393
+
394
+ ---
395
+
396
+ 📞 **{t['contact']}:** {data['contact']}
397
+
398
+ ---
399
+
400
+ ### {t['sources']}
401
+
402
+ 📄 [**{data['hospital']} Financial Assistance Policy**]({data['fap_url']})
403
+
404
+ 📄 [**Connecticut Public Act 24-81**](https://www.cga.ct.gov/2024/ba/pdf/2024HB-05320-R000149-BA.pdf)
405
+
406
+ 📄 [**2025 Federal Poverty Level Guidelines**](https://www.healthcare.gov/glossary/federal-poverty-level-fpl/)
407
+
408
+ 📄 [**CT Office of the Healthcare Advocate**](https://portal.ct.gov/oha)
409
+
410
+ ---
411
+
412
+ {t['disclaimer']}
413
+ """
414
+
415
+ final_output = header + partial_explanation + footer
416
+ yield final_output
417
+
418
+ # Custom CSS for CT.gov styling
419
+ custom_css = """
420
+ /* CT.gov inspired styling */
421
+ .gradio-container {
422
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
423
+ max-width: 1200px !important;
424
+ margin: auto !important;
425
+ }
426
+
427
+ /* CT Blue header */
428
+ h1 {
429
+ color: #0d6efd !important;
430
+ font-weight: 600 !important;
431
+ border-bottom: 3px solid #0d6efd;
432
+ padding-bottom: 1rem;
433
+ }
434
+
435
+ /* Buttons - CT Blue */
436
+ .gr-button-primary {
437
+ background: #0d6efd !important;
438
+ border: none !important;
439
+ color: white !important;
440
+ }
441
+
442
+ .gr-button-primary:hover {
443
+ background: #0a58ca !important;
444
+ }
445
+
446
+ .gr-button-secondary {
447
+ background: #6c757d !important;
448
+ border: none !important;
449
+ color: white !important;
450
+ }
451
+
452
+ /* Form inputs */
453
+ .gr-box, .gr-input, .gr-dropdown {
454
+ border: 1px solid #ced4da !important;
455
+ border-radius: 0.375rem !important;
456
+ }
457
+
458
+ /* Card-like sections */
459
+ .gr-group {
460
+ background: #f8f9fa !important;
461
+ padding: 1.5rem !important;
462
+ border-radius: 0.5rem !important;
463
+ border: 1px solid #dee2e6 !important;
464
+ }
465
+
466
+ /* Labels */
467
+ label {
468
+ color: #212529 !important;
469
+ font-weight: 500 !important;
470
+ margin-bottom: 0.5rem !important;
471
+ }
472
+
473
+ /* Results cards */
474
+ .markdown-text h2 {
475
+ color: #0d6efd !important;
476
+ border-bottom: 2px solid #e9ecef;
477
+ padding-bottom: 0.5rem;
478
+ }
479
+
480
+ .markdown-text h3 {
481
+ color: #495057 !important;
482
+ font-size: 1.1rem;
483
+ }
484
+
485
+ /* Source links section */
486
+ .markdown-text a {
487
+ color: #0d6efd !important;
488
+ text-decoration: none;
489
+ }
490
+
491
+ .markdown-text a:hover {
492
+ text-decoration: underline;
493
+ }
494
+ """
495
+
496
+ def create_interface():
497
+ with gr.Blocks(
498
+ theme=gr.themes.Default(primary_hue="blue", neutral_hue="slate"),
499
+ css=custom_css
500
+ ) as demo:
501
+
502
+ lang_state = gr.State("en")
503
+ selected_hospital = gr.State("")
504
+
505
+ # Header with Beta tag
506
+ with gr.Row():
507
+ with gr.Column(scale=4):
508
+ gr.Markdown("# 🏥 Connecticut Hospital Financial Assistance <span style='background-color: #ffc107; color: #664d03; padding: 4px 12px; border-radius: 4px; font-size: 0.75em; font-weight: bold; margin-left: 0.5rem;'>BETA</span>")
509
+ with gr.Column(scale=1):
510
+ lang_toggle = gr.Radio(
511
+ choices=["English", "Español"],
512
+ value="English",
513
+ label="Language",
514
+ container=False
515
+ )
516
+
517
+ # Step 1: Choose path
518
+ step1 = gr.Group(visible=True)
519
+ with step1:
520
+ gr.Markdown("### How would you like to start?")
521
+ with gr.Row():
522
+ help_btn = gr.Button("🔍 Help me find a hospital", variant="primary", size="lg")
523
+ know_btn = gr.Button("🏥 I know my hospital", variant="secondary", size="lg")
524
+
525
+ # Step 2a: Find hospital by ZIP
526
+ step2a = gr.Group(visible=False)
527
+ with step2a:
528
+ gr.Markdown("### Enter your ZIP code")
529
+ zip_input = gr.Textbox(label="ZIP Code", placeholder="e.g., 06511", max_lines=1)
530
+ search_btn = gr.Button("Search", variant="primary")
531
+
532
+ nearby_cards = gr.Markdown()
533
+ hospital_radio = gr.Radio(label="Select a hospital", choices=[], visible=False)
534
+ continue_btn_a = gr.Button("Continue", variant="primary", visible=False)
535
+ back_btn_a = gr.Button("← Back", size="sm")
536
+
537
+ # Step 2b: Select known hospital
538
+ step2b = gr.Group(visible=False)
539
+ with step2b:
540
+ gr.Markdown("### Select your hospital")
541
+ hospital_dropdown = gr.Dropdown(
542
+ choices=list(HOSPITALS.keys()),
543
+ label="Hospital"
544
+ )
545
+ continue_btn_b = gr.Button("Continue", variant="primary")
546
+ back_btn_b = gr.Button("← Back", size="sm")
547
+
548
+ # Step 3: Eligibility form
549
+ step3 = gr.Group(visible=False)
550
+ with step3:
551
+ selected_hospital_display = gr.Markdown()
552
+
553
+ income_input = gr.Number(label="Annual Household Income ($)", minimum=0, value=35000)
554
+ household_input = gr.Number(label="Household Size", minimum=1, value=3, precision=0)
555
+ snap_radio = gr.Radio(choices=["Yes", "No"], label="Enrolled in SNAP or WIC?", value="No")
556
+
557
+ check_btn = gr.Button("Check Eligibility", variant="primary", size="lg")
558
+ back_btn_c = gr.Button("← Back", size="sm")
559
+
560
+ # Step 4: Results (with streaming)
561
+ step4 = gr.Group(visible=False)
562
+ with step4:
563
+ results_output = gr.Markdown()
564
+ restart_btn = gr.Button("Check Another Hospital", variant="secondary")
565
+
566
+ # Navigation functions
567
+ def show_find_path():
568
+ return [
569
+ gr.update(visible=False),
570
+ gr.update(visible=True),
571
+ gr.update(visible=False),
572
+ gr.update(visible=False),
573
+ gr.update(visible=False)
574
+ ]
575
+
576
+ def show_know_path():
577
+ return [
578
+ gr.update(visible=False),
579
+ gr.update(visible=False),
580
+ gr.update(visible=True),
581
+ gr.update(visible=False),
582
+ gr.update(visible=False)
583
+ ]
584
+
585
+ def back_to_start():
586
+ return [
587
+ gr.update(visible=True),
588
+ gr.update(visible=False),
589
+ gr.update(visible=False),
590
+ gr.update(visible=False),
591
+ gr.update(visible=False)
592
+ ]
593
+
594
+ def search_hospitals_wrapper(zip_code, lang):
595
+ cards, choices = find_nearby_hospitals(zip_code, lang)
596
+ if choices:
597
+ return cards, gr.update(choices=choices, visible=True, value=choices[0]), gr.update(visible=True)
598
+ return cards, gr.update(visible=False), gr.update(visible=False)
599
+
600
+ def continue_from_search(hospital_choice, lang):
601
+ t = TRANSLATIONS[lang]
602
+ hospital_name = hospital_choice.split(" (")[0] if "(" in hospital_choice else hospital_choice
603
+ return [
604
+ hospital_name,
605
+ f"### Selected: {hospital_name}",
606
+ gr.update(visible=False),
607
+ gr.update(visible=False),
608
+ gr.update(visible=True),
609
+ gr.update(visible=False),
610
+ gr.update(choices=[t['yes'], t['no']], value=t['no'])
611
+ ]
612
+
613
+ def continue_from_dropdown(hospital_name, lang):
614
+ t = TRANSLATIONS[lang]
615
+ return [
616
+ hospital_name,
617
+ f"### Selected: {hospital_name}",
618
+ gr.update(visible=False),
619
+ gr.update(visible=False),
620
+ gr.update(visible=True),
621
+ gr.update(visible=False),
622
+ gr.update(choices=[t['yes'], t['no']], value=t['no'])
623
+ ]
624
+
625
+ def check_eligibility_wrapper(hospital, income, household, snap, lang):
626
+ t = TRANSLATIONS[lang]
627
+ has_snap = (snap == t['yes'])
628
+ data = determine_eligibility(hospital, income, int(household), has_snap, lang)
629
+
630
+ yield [
631
+ "",
632
+ gr.update(visible=False),
633
+ gr.update(visible=False),
634
+ gr.update(visible=False),
635
+ gr.update(visible=True)
636
+ ]
637
+
638
+ for partial_result in format_results_streaming(data, lang):
639
+ yield [
640
+ partial_result,
641
+ gr.update(),
642
+ gr.update(),
643
+ gr.update(),
644
+ gr.update()
645
+ ]
646
+
647
+ def update_lang(choice):
648
+ return "es" if choice == "Español" else "en"
649
+
650
+ # Wire up events
651
+ lang_toggle.change(fn=update_lang, inputs=[lang_toggle], outputs=[lang_state])
652
+
653
+ help_btn.click(fn=show_find_path, outputs=[step1, step2a, step2b, step3, step4])
654
+ know_btn.click(fn=show_know_path, outputs=[step1, step2a, step2b, step3, step4])
655
+
656
+ back_btn_a.click(fn=back_to_start, outputs=[step1, step2a, step2b, step3, step4])
657
+ back_btn_b.click(fn=back_to_start, outputs=[step1, step2a, step2b, step3, step4])
658
+ restart_btn.click(fn=back_to_start, outputs=[step1, step2a, step2b, step3, step4])
659
+
660
+ search_btn.click(
661
+ fn=search_hospitals_wrapper,
662
+ inputs=[zip_input, lang_state],
663
+ outputs=[nearby_cards, hospital_radio, continue_btn_a]
664
+ )
665
+
666
+ continue_btn_a.click(
667
+ fn=continue_from_search,
668
+ inputs=[hospital_radio, lang_state],
669
+ outputs=[selected_hospital, selected_hospital_display, step1, step2a, step3, step4, snap_radio]
670
+ )
671
+
672
+ continue_btn_b.click(
673
+ fn=continue_from_dropdown,
674
+ inputs=[hospital_dropdown, lang_state],
675
+ outputs=[selected_hospital, selected_hospital_display, step1, step2b, step3, step4, snap_radio]
676
+ )
677
+
678
+ check_btn.click(
679
+ fn=check_eligibility_wrapper,
680
+ inputs=[selected_hospital, income_input, household_input, snap_radio, lang_state],
681
+ outputs=[results_output, step1, step2a, step3, step4]
682
+ )
683
+
684
+ return demo
685
+
686
+ demo = create_interface()
687
+ demo.launch(auth=("oha", DEMO_PASSWORD))
requirements (3).txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.0
2
+ openai>=1.0
3
+ geopy>=2.4