rajkhanke commited on
Commit
ccbb8a0
·
verified ·
1 Parent(s): 79aeab6

Create templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +861 -0
templates/index.html ADDED
@@ -0,0 +1,861 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Healthcare Risk Prediction Dashboard</title>
7
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css" rel="stylesheet">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.18.2/plotly.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
11
+ <style>
12
+ /* Custom Styles */
13
+ :root {
14
+ --brand-blue: #0073e6;
15
+ --brand-dark-blue: #00437c;
16
+ --text-primary: #1f2937; /* gray-800 */
17
+ --text-secondary: #4b5563; /* gray-600 */
18
+ --border-color: #e5e7eb; /* gray-200 */
19
+ --card-bg: #ffffff;
20
+ --body-bg: #f9fafb; /* gray-50 */
21
+ }
22
+ body { background-color: var(--body-bg); color: var(--text-primary); font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }
23
+ .gradient-bg { background: linear-gradient(135deg, var(--brand-blue) 0%, var(--brand-dark-blue) 100%); }
24
+ .card { background-color: var(--card-bg); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px 0 rgba(0, 0, 0, 0.06); border: 1px solid var(--border-color); border-radius: 0.5rem; /* rounded-lg */ transition: all 0.3s ease-in-out; }
25
+ .card:hover { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.07), 0 4px 6px -2px rgba(0, 0, 0, 0.05); transform: translateY(-2px) scale(1.01); }
26
+ .feature-row:nth-child(odd) { background-color: #f9fafb; /* gray-50 */ }
27
+ .feature-row:hover { background-color: #eff6ff; /* blue-50 */ }
28
+ /* Recommendation Card Styles */
29
+ .recommendation-card { border-left-width: 4px; border-left-style: solid; transition: all 0.2s ease-out; }
30
+ .recommendation-card.critical { border-left-color: #ef4444; } /* red-500 */
31
+ .recommendation-card.high { border-left-color: #f97316; } /* orange-500 */
32
+ .recommendation-card.medium { border-left-color: #eab308; } /* yellow-500 */
33
+ .recommendation-card.standard { border-left-color: #22c55e; } /* green-500 */
34
+ .recommendation-card:hover { background-color: #f8fafc; /* cool-gray-50 */ }
35
+ /* Priority Indicator Styles */
36
+ .priority-indicator { border-radius: 9999px; height: 1.5rem; width: 1.5rem; display: flex; align-items: center; justify-content: center; margin-right: 0.75rem; flex-shrink: 0; transition: transform 0.2s ease; }
37
+ .recommendation-card:hover .priority-indicator { transform: scale(1.1); }
38
+ .priority-indicator.critical { background-color: #fee2e2; } /* red-100 */
39
+ .priority-indicator.high { background-color: #ffedd5; } /* orange-100 */
40
+ .priority-indicator.medium { background-color: #fef9c3; } /* yellow-100 */
41
+ .priority-indicator.standard { background-color: #dcfce7; } /* green-100 */
42
+ .priority-indicator i { font-size: 0.8rem; }
43
+ .priority-indicator.critical i { color: #dc2626; } /* red-600 */
44
+ .priority-indicator.high i { color: #ea580c; } /* orange-600 */
45
+ .priority-indicator.medium i { color: #ca8a04; } /* yellow-600 */
46
+ .priority-indicator.standard i { color: #16a34a; } /* green-600 */
47
+ /* Risk Badge Colors */
48
+ .risk-badge.green { background-color: #10b981; color: white; } /* emerald-500 */
49
+ .risk-badge.yellow { background-color: #f59e0b; color: white; } /* amber-500 */
50
+ .risk-badge.red { background-color: #ef4444; color: white; } /* red-500 */
51
+ .risk-badge.gray { background-color: #6b7280; color: white; } /* gray-500 */
52
+ /* SHAP Bar Styles */
53
+ .shap-bar-container { position: relative; width: 100%; background-color: #e5e7eb; border-radius: 0.25rem; height: 0.75rem; margin-top: 0.25rem; margin-bottom: 0.1rem; overflow: hidden; }
54
+ .shap-bar { position: absolute; top: 0; left: 0; height: 100%; border-radius: 0.25rem; transition: width 0.4s ease-out;}
55
+ .shap-positive { background-color: #fca5a5; } /* red-300 */
56
+ .shap-negative { background-color: #86efac; } /* green-300 */
57
+ /* Loading Spinner */
58
+ .spinner { border: 4px solid rgba(0, 0, 0, 0.1); border-left-color: var(--brand-blue); border-radius: 50%; width: 36px; height: 36px; animation: spin 1.2s linear infinite; }
59
+ .spinner-pulse { animation: spin 1.2s linear infinite, pulse 1.5s ease-in-out infinite alternate; }
60
+ @keyframes spin { to { transform: rotate(360deg); } }
61
+ @keyframes pulse { from { opacity: 0.7; } to { opacity: 1; } }
62
+ /* Tooltip Styles */
63
+ .tooltip { position: relative; display: inline-block; cursor: help; }
64
+ .tooltip .tooltip-text { visibility: hidden; width: 240px; background-color: #374151; color: #fff; text-align: center; border-radius: 6px; padding: 8px 10px; position: absolute; z-index: 10; bottom: 130%; left: 50%; margin-left: -120px; opacity: 0; transition: opacity 0.3s, visibility 0s 0.3s; font-size: 0.75rem; line-height: 1.2; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
65
+ .tooltip:hover .tooltip-text { visibility: visible; opacity: 1; transition: opacity 0.3s; }
66
+ /* Result Section Initial State & Animation */
67
+ #result-section { display: none; animation: fadeIn 0.8s ease-out forwards; }
68
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
69
+ /* Tab Styling */
70
+ .tab-btn, .factor-tab-btn { padding: 0.75rem 1.5rem; font-weight: 500; color: var(--text-secondary); border-bottom: 3px solid transparent; transition: color 0.2s ease, border-color 0.3s ease; white-space: nowrap; }
71
+ .tab-btn:hover, .factor-tab-btn:hover { color: var(--brand-blue); }
72
+ .tab-btn.active, .factor-tab-btn.active { color: var(--brand-blue); border-bottom-color: var(--brand-blue); font-weight: 600; }
73
+ /* Smaller padding for factor tabs */
74
+ .factor-tab-btn { padding: 0.5rem 1rem; }
75
+ /* Chart Containers */
76
+ .chart-container { width: 100%; /* Ensure container takes full width */ /* resize: vertical; Optional */ overflow: hidden; /* Helps contain chart */ }
77
+ /* Plotly specific adjustments if needed */
78
+ .plotly-graph-div { min-width: 100%; min-height: 100%; }
79
+ </style>
80
+ </head>
81
+ <body class="bg-gray-50 min-h-screen">
82
+ <!-- Header -->
83
+ <header class="gradient-bg text-white shadow-lg sticky top-0 z-50">
84
+ <div class="container mx-auto px-4 py-4">
85
+ <h1 class="text-2xl sm:text-3xl font-bold">Healthcare Risk Prediction with XAI</h1>
86
+ <p class="opacity-90 text-xs sm:text-sm mt-1">Explainable AI Enabled Risk Assessment Dashboard</p>
87
+ </div>
88
+ </header>
89
+ <main class="container mx-auto px-4 py-6">
90
+ <!-- Input Form Card -->
91
+ <section id="input-section" class="card bg-white rounded-lg p-5 sm:p-6 mb-6">
92
+ <h2 class="text-xl font-bold mb-4 text-gray-800 border-b border-gray-200 pb-2">Patient Information Input</h2>
93
+ <form id="patient-form">
94
+ <div id="form-fields" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-4">
95
+ <!-- Form fields dynamically generated -->
96
+ <p class="text-gray-500 italic">Loading form fields...</p>
97
+ </div>
98
+ <div class="mt-6 pt-4 border-t border-gray-200 flex items-center gap-4 flex-wrap">
99
+ <button type="submit" class="gradient-bg text-white px-5 py-2 sm:px-6 sm:py-2.5 rounded-md hover:opacity-90 transition-all duration-300 ease-in-out shadow hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 transform hover:-translate-y-0.5">
100
+ <i class="fas fa-chart-line mr-2"></i>Analyze Patient Risk
101
+ </button>
102
+ <button type="button" id="reset-defaults-btn" class="text-sm text-gray-600 hover:text-blue-600 underline transition-colors duration-200">
103
+ Reset to Defaults
104
+ </button>
105
+ </div>
106
+ </form>
107
+ </section>
108
+ <!-- Loading Spinner -->
109
+ <div id="loading" class="hidden flex justify-center items-center my-10 py-8">
110
+ <div class="spinner spinner-pulse"></div>
111
+ <span class="ml-4 text-lg text-gray-600 font-medium">Analyzing patient data... Please wait.</span>
112
+ </div>
113
+ <!-- Error Message Area -->
114
+ <div id="error-message" class="hidden bg-red-100 border-l-4 border-red-500 text-red-700 px-4 py-3 rounded relative mb-6 shadow-md" role="alert">
115
+ <strong class="font-bold mr-2"><i class="fas fa-exclamation-triangle mr-1"></i>Error:</strong>
116
+ <span class="block sm:inline" id="error-text"></span>
117
+ </div>
118
+ <!-- Results Section -->
119
+ <section id="result-section">
120
+ <!-- Risk Overview Cards -->
121
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
122
+ <!-- Non-Adherence Risk Card -->
123
+ <div class="card bg-white rounded-lg p-5 flex flex-col">
124
+ <div class="flex items-center justify-between mb-3">
125
+ <h2 class="text-lg font-bold text-gray-800">Medication Non-Adherence</h2>
126
+ <span id="na-risk-badge" class="risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider flex-shrink-0"></span>
127
+ </div>
128
+ <div id="gauge-na" class="chart-container min-h-[220px] mb-4 flex-grow"></div>
129
+ <div class="border-t border-gray-200 pt-3 mt-auto">
130
+ <h3 class="font-semibold text-gray-700 mb-1 text-sm">Interpretation:</h3>
131
+ <p id="na-explanation" class="text-gray-600 text-sm"></p>
132
+ </div>
133
+ </div>
134
+ <!-- Readmission Risk Card -->
135
+ <div class="card bg-white rounded-lg p-5 flex flex-col">
136
+ <div class="flex items-center justify-between mb-3">
137
+ <h2 class="text-lg font-bold text-gray-800">Hospital Readmission</h2>
138
+ <span id="r-risk-badge" class="risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider flex-shrink-0"></span>
139
+ </div>
140
+ <div id="gauge-r" class="chart-container min-h-[220px] mb-4 flex-grow"></div>
141
+ <div class="border-t border-gray-200 pt-3 mt-auto">
142
+ <h3 class="font-semibold text-gray-700 mb-1 text-sm">Interpretation:</h3>
143
+ <p id="r-explanation" class="text-gray-600 text-sm"></p>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ <!-- Tabs Navigation Card -->
148
+ <div class="card bg-white rounded-lg mb-6">
149
+ <div class="border-b border-gray-200">
150
+ <nav class="-mb-px flex space-x-1 sm:space-x-4 overflow-x-auto px-4" aria-label="Tabs">
151
+ <button class="tab-btn active" data-tab="recommendations">
152
+ <i class="fas fa-clipboard-list mr-1.5 opacity-80 text-sm"></i>Recommendations
153
+ </button>
154
+ <button class="tab-btn" data-tab="factors">
155
+ <i class="fas fa-chart-bar mr-1.5 opacity-80 text-sm"></i>Risk Factors
156
+ </button>
157
+ <button class="tab-btn" data-tab="whatif">
158
+ <i class="fas fa-wand-magic-sparkles mr-1.5 opacity-80 text-sm"></i>Interventions
159
+ </button>
160
+ <button class="tab-btn" data-tab="insights">
161
+ <i class="fas fa-search-plus mr-1.5 opacity-80 text-sm"></i>Insights
162
+ </button>
163
+ </nav>
164
+ </div>
165
+ <!-- Tab Content Area -->
166
+ <div class="p-5 sm:p-6">
167
+ <!-- Recommendations Tab -->
168
+ <div class="tab-content" id="recommendations-tab">
169
+ <h2 class="text-xl font-bold mb-4 text-gray-800">Personalized Recommendations</h2>
170
+ <div id="recommendations-container" class="space-y-4">
171
+ <p class="text-gray-500 italic">Loading recommendations...</p>
172
+ </div>
173
+ </div>
174
+ <!-- Risk Factors Tab -->
175
+ <div class="tab-content hidden" id="factors-tab">
176
+ <h2 class="text-xl font-bold mb-4 text-gray-800">Risk Factors Analysis</h2>
177
+ <!-- Risk Factor Sub-Tabs -->
178
+ <div class="mb-5 border-b border-gray-200">
179
+ <nav class="-mb-px flex space-x-4 sm:space-x-6" aria-label="Factor Tabs">
180
+ <button class="factor-tab-btn active" data-factor="na">
181
+ Non-Adherence Factors
182
+ </button>
183
+ <button class="factor-tab-btn" data-factor="r">
184
+ Readmission Factors
185
+ </button>
186
+ </nav>
187
+ </div>
188
+ <!-- Non-Adherence Factors Content -->
189
+ <div id="na-factors" class="factor-content space-y-6">
190
+ <div class="card border p-4 rounded-md shadow-sm">
191
+ <h3 class="font-semibold text-gray-700 text-lg mb-1">Non-Adherence Waterfall</h3>
192
+ <p class="text-sm text-gray-600 mb-3">Shows how top factors contribute to the non-adherence risk score vs. the average.</p>
193
+ <div id="waterfall-na" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
194
+ </div>
195
+ <div class="card border p-4 rounded-md shadow-sm">
196
+ <h4 class="font-semibold text-gray-700 mb-2 text-md">Detailed Factor Impact (SHAP):</h4>
197
+ <div id="shap-table-na" class="border-t border-gray-100 pt-3">
198
+ <p class="text-gray-500 italic">Loading factor details...</p>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ <!-- Readmission Factors Content -->
203
+ <div id="r-factors" class="factor-content hidden space-y-6">
204
+ <div class="card border p-4 rounded-md shadow-sm">
205
+ <h3 class="font-semibold text-gray-700 text-lg mb-1">Readmission Waterfall</h3>
206
+ <p class="text-sm text-gray-600 mb-3">Shows how top factors contribute to the readmission risk score vs. the average.</p>
207
+ <div id="waterfall-r" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
208
+ </div>
209
+ <div class="card border p-4 rounded-md shadow-sm">
210
+ <h4 class="font-semibold text-gray-700 mb-2 text-md">Detailed Factor Impact (SHAP):</h4>
211
+ <div id="shap-table-r" class="border-t border-gray-100 pt-3">
212
+ <p class="text-gray-500 italic">Loading factor details...</p>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ <!-- What-If / Intervention Tab -->
218
+ <div class="tab-content hidden" id="whatif-tab">
219
+ <h2 class="text-xl font-bold mb-4 text-gray-800">Intervention Impact Analysis</h2>
220
+ <div class="space-y-6">
221
+ <div class="card border p-4 rounded-md shadow-sm">
222
+ <h3 class="font-semibold text-gray-700 text-lg mb-2">Potential Modifications</h3>
223
+ <p class="text-sm text-gray-600 mb-4">Suggests changes to modifiable factors that could potentially lower risk, based on the model. The estimated impact shows the simulated risk change.</p>
224
+ <div id="cf-table" class="border-t border-gray-100 pt-3">
225
+ <p class="text-gray-500 italic">Loading potential modifications...</p>
226
+ </div>
227
+ </div>
228
+ <div class="card border p-4 rounded-md shadow-sm">
229
+ <h3 class="font-semibold text-gray-700 text-lg mb-2">Simulated Intervention Impact</h3>
230
+ <p class="text-sm text-gray-600 mb-4">Visualizes the estimated risk score *reduction* by optimizing the most impactful modifiable factors (SHAP-based estimate).</p>
231
+ <div id="intervention-chart" class="chart-container mt-4 min-h-[400px] md:min-h-[500px]"></div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+ <!-- Advanced Insights Tab -->
236
+ <div class="tab-content hidden" id="insights-tab">
237
+ <h2 class="text-xl font-bold mb-4 text-gray-800">Advanced Analytical Insights</h2>
238
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
239
+ <div class="card border p-4 rounded-md shadow-sm">
240
+ <h3 class="font-semibold text-gray-700 mb-2 text-md">Risk Factor Heatmap</h3>
241
+ <p class="text-sm text-gray-600 mb-3">Compares factor impacts (SHAP value) on Non-Adherence vs. Readmission. Red increases risk, Blue decreases risk.</p>
242
+ <div id="risk-heatmap" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
243
+ </div>
244
+ <div class="card border p-4 rounded-md shadow-sm">
245
+ <h3 class="font-semibold text-gray-700 mb-2 text-md">Feature Importance</h3>
246
+ <p class="text-sm text-gray-600 mb-3">Shows overall importance (absolute SHAP value) of top features for each risk type.</p>
247
+ <div id="feature-comparison" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
248
+ </div>
249
+ </div>
250
+ <!-- Cleaner Placeholder for Network Graph -->
251
+ <div class="card border p-4 rounded-md shadow-sm mt-6">
252
+ <h3 class="font-semibold text-gray-700 mb-2 text-md">Risk Factor Network (Conceptual)</h3>
253
+ <p class="text-sm text-gray-600 mb-3">Visualizing factor interconnections could reveal complex relationships. (Requires dedicated data & libraries).</p>
254
+ <div id="network-graph-placeholder"
255
+ class="text-center text-gray-400 py-10 border-2 border-dashed border-gray-300 rounded-md bg-gray-50/50">
256
+ <i class="fas fa-project-diagram fa-3x mb-3 opacity-50"></i>
257
+ <p class="font-medium">Network Graph Placeholder</p>
258
+ <p class="text-xs mt-1">(Visualization not implemented)</p>
259
+ </div>
260
+ <div id="network-graph" class="chart-container min-h-[450px] md:min-h-[500px]"></div>
261
+ <!-- Removed the pre tag for network data preview -->
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ <!-- End Tabs Card -->
267
+ </section>
268
+ <!-- End Result Section -->
269
+ </main>
270
+ <script>
271
+ // Feature information and explanations from backend
272
+ const featureInfo = JSON.parse('{{ feature_info|tojson|safe }}');
273
+ const riskExplanations = JSON.parse('{{ risk_explanations|tojson|safe }}');
274
+ let currentResultsData = null; // Store latest results
275
+
276
+ // --- Robust ID Generation ---
277
+ function getFieldId(fieldName) {
278
+ return fieldName.toLowerCase()
279
+ .replace(/₹/g, 'rupees')
280
+ .replace(/[^\w\s-]/g, '')
281
+ .trim()
282
+ .replace(/\s+/g, '-');
283
+ }
284
+
285
+ // --- Form Generation ---
286
+ function generateFormFields() {
287
+ const formFieldsContainer = document.getElementById('form-fields');
288
+ if (!formFieldsContainer) return;
289
+ formFieldsContainer.innerHTML = '';
290
+ const order = JSON.parse('{{ feature_order|tojson|safe }}') || [];
291
+
292
+ order.forEach(field => {
293
+ const info = featureInfo[field];
294
+ if (!info) { console.warn(`Info missing for: ${field}`); return; }
295
+ const fieldId = getFieldId(field);
296
+ let inputHTML = '';
297
+ const commonClasses = "w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 text-sm shadow-sm transition-colors duration-200";
298
+
299
+ if (info.options) {
300
+ const optionsHTML = info.options.map(opt => `<option value="${opt}">${opt}</option>`).join('');
301
+ inputHTML = `<select id="${fieldId}" name="${fieldId}" class="${commonClasses}">${optionsHTML}</select>`;
302
+ } else {
303
+ const range = info.ideal_range ? info.ideal_range.split('-') : [];
304
+ const min = range.length > 0 ? range[0] : '';
305
+ const max = range.length > 1 ? range[1] : '';
306
+ const step = info.step || (field === 'Medicine Availability (0-1)' ? '0.01' : '1');
307
+ const placeholder = info.ideal_range ? `Range: ${info.ideal_range}` : 'Enter value';
308
+ inputHTML = `<input type="number" id="${fieldId}" name="${fieldId}" step="${step}" ${min !== '' ? `min="${min}"` : ''} ${max !== '' ? `max="${max}"` : ''} class="${commonClasses}" placeholder="${placeholder}">`;
309
+ }
310
+
311
+ const fieldHTML = `
312
+ <div class="mb-1">
313
+ <label for="${fieldId}" class="block text-sm font-medium text-gray-700 mb-1 flex justify-between items-center">
314
+ <span>${info.question}</span>
315
+ <span class="tooltip text-gray-400 hover:text-blue-500 transition-colors duration-200">
316
+ <i class="far fa-question-circle"></i>
317
+ <span class="tooltip-text">${info.help_text || info.description}</span>
318
+ </span>
319
+ </label>
320
+ ${inputHTML}
321
+ </div>`;
322
+ formFieldsContainer.innerHTML += fieldHTML;
323
+ });
324
+
325
+ // Add listeners for ICU days visibility
326
+ const wasIcuSelect = document.getElementById(getFieldId('Was in ICU (1=Yes)'));
327
+ const icuDaysInput = document.getElementById(getFieldId('ICU Days'));
328
+ if (wasIcuSelect && icuDaysInput) {
329
+ const icuDaysContainer = icuDaysInput.closest('div');
330
+ const updateIcuVisibility = () => {
331
+ if (!icuDaysContainer) return;
332
+ if (wasIcuSelect.value === 'No') {
333
+ icuDaysInput.value = '0';
334
+ icuDaysContainer.style.display = 'none';
335
+ } else {
336
+ icuDaysContainer.style.display = '';
337
+ // Optional: Clear ICU days if previously hidden, or keep value
338
+ // if (icuDaysInput.value === '0') icuDaysInput.value = '';
339
+ }
340
+ };
341
+ wasIcuSelect.addEventListener('change', updateIcuVisibility);
342
+ updateIcuVisibility(); // Initial check
343
+ } else {
344
+ console.warn("ICU related form fields not found for dynamic visibility.");
345
+ }
346
+ }
347
+
348
+ // --- Default Values ---
349
+ function setDefaultValues() {
350
+ hideError();
351
+ try {
352
+ document.getElementById(getFieldId('Age')).value = '65';
353
+ document.getElementById(getFieldId('Gender')).value = 'Male';
354
+ document.getElementById(getFieldId('Why in Hospital')).value = 'Heart';
355
+ document.getElementById(getFieldId('Hospital Days')).value = '7';
356
+ const wasIcuSelect = document.getElementById(getFieldId('Was in ICU (1=Yes)'));
357
+ wasIcuSelect.value = 'Yes';
358
+ document.getElementById(getFieldId('ICU Days')).value = '2';
359
+ document.getElementById(getFieldId('Number of Medicines')).value = '5';
360
+ document.getElementById(getFieldId('Cost per Medicine (₹)')).value = '200';
361
+ document.getElementById(getFieldId('Days Medicine Lasts')).value = '30';
362
+ document.getElementById(getFieldId('Total Dosage per Day (mg)')).value = '100';
363
+ document.getElementById(getFieldId('Total Pills Given')).value = '90';
364
+ document.getElementById(getFieldId('Medicine Availability (0-1)')).value = '0.8';
365
+ document.getElementById(getFieldId('Took Medicine Day 1 (1=Yes)')).value = 'Yes';
366
+ document.getElementById(getFieldId('Took Medicine Day 2 (1=Yes)')).value = 'Yes';
367
+ document.getElementById(getFieldId('Took Medicine Day 3 (1=Yes)')).value = 'No';
368
+ if (wasIcuSelect) { wasIcuSelect.dispatchEvent(new Event('change')); } // Trigger visibility update
369
+ console.log("Default values reset.");
370
+ } catch (error) {
371
+ console.error("Error setting default values:", error);
372
+ showError("Could not reset all default values. Please check the form fields.");
373
+ }
374
+ }
375
+
376
+ // --- Error Handling ---
377
+ function showError(message) {
378
+ const errorDiv = document.getElementById('error-message');
379
+ const errorText = document.getElementById('error-text');
380
+ if(errorDiv && errorText) {
381
+ errorText.textContent = message;
382
+ errorDiv.classList.remove('hidden');
383
+ errorDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
384
+ }
385
+ }
386
+ function hideError() {
387
+ const errorDiv = document.getElementById('error-message');
388
+ if(errorDiv) errorDiv.classList.add('hidden');
389
+ }
390
+
391
+ // --- Form Submission Logic ---
392
+ function setupFormSubmission() {
393
+ const form = document.getElementById('patient-form');
394
+ if (!form) return;
395
+
396
+ form.addEventListener('submit', function(e) {
397
+ e.preventDefault();
398
+ hideError();
399
+
400
+ document.getElementById('loading').classList.remove('hidden');
401
+ document.getElementById('result-section').style.display = 'none';
402
+ const loadingElement = document.getElementById('loading');
403
+ if (loadingElement) loadingElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
404
+
405
+ const jsKeyMap = {
406
+ 'age': 'Age', 'gender': 'Gender', 'why-in-hospital': 'Why in Hospital',
407
+ 'hospital-days': 'Hospital Days', 'was-in-icu-1yes': 'Was in ICU (1=Yes)',
408
+ 'icu-days': 'ICU Days', 'number-of-medicines': 'Number of Medicines',
409
+ 'cost-per-medicine-rupees': 'Cost per Medicine (₹)', 'days-medicine-lasts': 'Days Medicine Lasts',
410
+ 'total-dosage-per-day-mg': 'Total Dosage per Day (mg)', 'total-pills-given': 'Total Pills Given',
411
+ 'medicine-availability-0-1': 'Medicine Availability (0-1)',
412
+ 'took-medicine-day-1-1yes': 'Took Medicine Day 1 (1=Yes)',
413
+ 'took-medicine-day-2-1yes': 'Took Medicine Day 2 (1=Yes)',
414
+ 'took-medicine-day-3-1yes': 'Took Medicine Day 3 (1=Yes)'
415
+ };
416
+ const formData = {};
417
+ let hasValidationError = false;
418
+
419
+ for (const [jsKey, featureName] of Object.entries(jsKeyMap)) {
420
+ const element = document.getElementById(getFieldId(featureName));
421
+ if (!element) {
422
+ showError(`Internal error: Form field for ${featureName} not found.`);
423
+ hasValidationError = true; break;
424
+ }
425
+ // Check visibility for ICU Days based on its container div
426
+ const isIcuDays = featureName === 'ICU Days';
427
+ const icuContainer = element.closest('div');
428
+ const isIcuHidden = isIcuDays && icuContainer && icuContainer.style.display === 'none';
429
+
430
+ // Validate if element is visible OR if it's ICU Days and should be 0
431
+ if (!isIcuHidden) {
432
+ if (element.value === null || element.value.trim() === '') {
433
+ showError(`Missing value for: ${featureName}`);
434
+ element.classList.add('border-red-500', 'ring-red-500');
435
+ hasValidationError = true; break;
436
+ } else {
437
+ formData[jsKey] = element.value;
438
+ element.classList.remove('border-red-500', 'ring-red-500');
439
+ }
440
+ } else if (isIcuDays) {
441
+ formData[jsKey] = '0'; // Send 0 if ICU container is hidden
442
+ }
443
+ }
444
+
445
+ if (hasValidationError) {
446
+ document.getElementById('loading').classList.add('hidden');
447
+ return;
448
+ }
449
+
450
+ fetch('/predict', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) })
451
+ .then(response => {
452
+ if (!response.ok) {
453
+ return response.json().then(errData => { throw new Error(errData.error || `Server error: ${response.status}`); })
454
+ .catch(() => { throw new Error(`Server error: ${response.status}`); });
455
+ } return response.json(); })
456
+ .then(data => {
457
+ if (data.success) {
458
+ currentResultsData = data;
459
+ displayResults(data);
460
+ const resultElement = document.getElementById('result-section');
461
+ if (resultElement) {
462
+ resultElement.style.display = 'block'; // Ensure it's displayed before scrolling
463
+ // Timeout ensures animation can start
464
+ setTimeout(() => resultElement.scrollIntoView({ behavior: 'smooth', block: 'start' }), 100);
465
+ }
466
+ } else { showError('Prediction Error: ' + data.error); }})
467
+ .catch(error => { showError('Request Error: ' + error.message); })
468
+ .finally(() => { document.getElementById('loading').classList.add('hidden'); });
469
+ });
470
+ // Reset defaults button listener
471
+ const resetButton = document.getElementById('reset-defaults-btn');
472
+ if(resetButton) resetButton.addEventListener('click', setDefaultValues);
473
+ }
474
+
475
+ // --- Display Results ---
476
+ function displayResults(data) {
477
+ // Check for essential data presence
478
+ if (!data || !data.predictions || !data.visualizations || !data.explanations || !data.recommendations) {
479
+ showError("Incomplete data received from server. Cannot display results.");
480
+ return;
481
+ }
482
+
483
+ // 1. Risk Badges & Explanations
484
+ const naRisk = data.predictions.risk_level_na;
485
+ const rRisk = data.predictions.risk_level_r;
486
+ const naRiskBadge = document.getElementById('na-risk-badge');
487
+ const rRiskBadge = document.getElementById('r-risk-badge');
488
+ if(naRisk && naRiskBadge) {
489
+ naRiskBadge.textContent = `${naRisk.level} (${naRisk.percentage})`;
490
+ naRiskBadge.className = `risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider ${naRisk.color || 'gray'}`;
491
+ document.getElementById('na-explanation').textContent = riskExplanations.non_adherence.levels[naRisk.level] || 'N/A';
492
+ }
493
+ if(rRisk && rRiskBadge) {
494
+ rRiskBadge.textContent = `${rRisk.level} (${rRisk.percentage})`;
495
+ rRiskBadge.className = `risk-badge px-3 py-1 rounded-full font-semibold text-xs uppercase tracking-wider ${rRisk.color || 'gray'}`;
496
+ document.getElementById('r-explanation').textContent = riskExplanations.readmission.levels[rRisk.level] || 'N/A';
497
+ }
498
+
499
+ // 2. Plot Gauges
500
+ plotGauge('gauge-na', data.visualizations?.gauges?.non_adherence, 'Non-Adherence Gauge');
501
+ plotGauge('gauge-r', data.visualizations?.gauges?.readmission, 'Readmission Gauge');
502
+
503
+ // 3. Recommendations
504
+ displayRecommendations(data.recommendations);
505
+
506
+ // 4. Risk Factors (Waterfall & SHAP)
507
+ displayRiskFactors(data.explanations, data.visualizations?.additional);
508
+
509
+ // 5. Interventions (CF Table & Chart)
510
+ displayCounterfactuals(data.explanations);
511
+ plotInterventionChart(data.visualizations?.additional?.intervention_impact);
512
+
513
+ // 6. Advanced Insights
514
+ displayAdditionalVisualizations(data.visualizations?.additional);
515
+
516
+ // 7. Setup Tabs
517
+ setupTabs(true); // Force reset to default tabs
518
+ }
519
+
520
+ // --- Plotting Functions (with Error Handling) ---
521
+ function plotGauge(elementId, gaugeJson, chartName) { /* ... Keep as is ... */
522
+ const container = document.getElementById(elementId);
523
+ if (!container) { console.error(`Container not found: ${elementId}`); return; }
524
+ container.innerHTML = '';
525
+ try {
526
+ if (gaugeJson) {
527
+ const gaugeData = JSON.parse(gaugeJson);
528
+ Plotly.newPlot(elementId, gaugeData.data, gaugeData.layout, {responsive: true, displaylogo: false, modeBarButtonsToRemove: ['toImage', 'sendDataToCloud', 'select2d', 'lasso2d']});
529
+ } else { container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error: ${chartName} data not available.</p>`; }
530
+ } catch (e) { console.error(`Plotting Error ${chartName}:`, e); container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error displaying ${chartName}.</p>`; }
531
+ }
532
+ function plotGenericChart(elementId, chartJson, chartName) { /* ... Keep as is ... */
533
+ const container = document.getElementById(elementId);
534
+ if (!container) { console.error(`Container not found: ${elementId}`); return; }
535
+ container.innerHTML = '';
536
+ try {
537
+ if (chartJson) {
538
+ const plotData = JSON.parse(chartJson);
539
+ if (plotData.data && plotData.data.length > 0 && plotData.layout) {
540
+ Plotly.newPlot(elementId, plotData.data, plotData.layout, {responsive: true, displaylogo: false, modeBarButtonsToRemove: ['toImage', 'sendDataToCloud', 'select2d', 'lasso2d']});
541
+ } else if (plotData.layout?.annotations?.[0]?.text) {
542
+ container.innerHTML = `<div class="text-center text-gray-500 p-6 italic h-full flex items-center justify-center">${plotData.layout.annotations[0].text}</div>`;
543
+ } else { container.innerHTML = `<p class="text-sm text-gray-500 text-center p-4">${chartName}: No data to display.</p>`; }
544
+ } else { container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error: ${chartName} data not available.</p>`; }
545
+ } catch (e) { console.error(`Plotting Error ${chartName}:`, e); container.innerHTML = `<p class="text-sm text-red-500 text-center p-4">Error displaying ${chartName}.</p>`; }
546
+ }
547
+ function plotInterventionChart(interventionJson) {
548
+ plotGenericChart('intervention-chart', interventionJson, 'Intervention Impact Chart');
549
+ }
550
+
551
+ // --- Display Recommendations ---
552
+ function displayRecommendations(recommendations) {
553
+ const container = document.getElementById('recommendations-container');
554
+ if (!container) return;
555
+ container.innerHTML = ''; // Clear previous
556
+
557
+ if (!recommendations || recommendations.length === 0 || (recommendations[0] && recommendations[0].error)) {
558
+ container.innerHTML = `<p class="text-gray-500 italic p-4">${recommendations?.[0]?.error || 'No specific recommendations generated.'}</p>`;
559
+ return;
560
+ }
561
+
562
+ recommendations.forEach(rec => {
563
+ const priorityClass = rec.priority ? rec.priority.toLowerCase() : 'standard';
564
+ const priorityColor = getPriorityColor(rec.priority);
565
+ const priorityIcon = getPriorityIcon(rec.priority);
566
+ const indicatorClasses = `priority-indicator ${priorityClass}`;
567
+ const iconClasses = `fas ${priorityIcon}`;
568
+
569
+ // !! REMOVED the invalid comment here !!
570
+ const recHTML = `
571
+ <div class="recommendation-card card p-4 ${priorityClass} bg-white shadow-sm border-l-4">
572
+ <div class="flex items-start">
573
+ <div class="${indicatorClasses}">
574
+ <i class="${iconClasses}"></i>
575
+ </div>
576
+ <div class="flex-1 min-w-0">
577
+ <div class="flex justify-between items-center flex-wrap gap-x-2 mb-1">
578
+ <h3 class="font-semibold text-gray-800 text-base">${rec.category || 'Recommendation'}</h3>
579
+ <span class="px-2 py-0.5 rounded-full text-xs font-medium whitespace-nowrap bg-${priorityColor}-100 text-${priorityColor}-800">
580
+ ${rec.priority || 'Standard'} Priority
581
+ </span>
582
+ </div>
583
+ <p class="text-gray-700 mt-1 text-sm">${rec.recommendation || 'Details not available.'}</p>
584
+ <div class="mt-3 p-3 bg-blue-50 rounded border border-blue-100">
585
+ <p class="text-sm">
586
+ <strong class="font-medium text-blue-800">Suggested Action:</strong>
587
+ <span class="text-blue-700 ml-1">${rec.action || 'No specific action suggested.'}</span>
588
+ </p>
589
+ </div>
590
+ </div>
591
+ </div>
592
+ </div>`;
593
+ container.innerHTML += recHTML;
594
+ });
595
+ }
596
+ function getPriorityIcon(priority) { /* ... Keep as is ... */
597
+ switch (priority ? priority.toLowerCase() : '') {
598
+ case 'critical': return 'fa-circle-exclamation'; // Updated icon
599
+ case 'high': return 'fa-triangle-exclamation'; // Updated icon
600
+ case 'medium': return 'fa-arrow-up';
601
+ case 'standard': return 'fa-circle-check'; // Updated icon
602
+ default: return 'fa-circle-info'; // Updated icon
603
+ }
604
+ }
605
+ function getPriorityColor(priority) { /* ... Keep as is ... */
606
+ switch (priority ? priority.toLowerCase() : '') {
607
+ case 'critical': return 'red';
608
+ case 'high': return 'orange';
609
+ case 'medium': return 'yellow';
610
+ case 'standard': return 'green';
611
+ default: return 'blue';
612
+ }
613
+ }
614
+
615
+
616
+ // --- Display Risk Factors ---
617
+ function displayRiskFactors(explanations, additionalViz) {
618
+ if (!explanations) return;
619
+ // Plot Waterfalls
620
+ plotGenericChart('waterfall-na', additionalViz?.waterfall_na, 'Non-Adherence Waterfall');
621
+ plotGenericChart('waterfall-r', additionalViz?.waterfall_r, 'Readmission Waterfall');
622
+ // Display SHAP Tables
623
+ displayShapTable(explanations.shap_values_na, 'na', explanations.shap_error_na);
624
+ displayShapTable(explanations.shap_values_r, 'r', explanations.shap_error_r);
625
+ }
626
+
627
+ // --- Display SHAP Table ---
628
+ function displayShapTable(shapData, riskType, shapError) { /* ... Keep as is ... */
629
+ const tableId = `shap-table-${riskType}`;
630
+ const tableContainer = document.getElementById(tableId);
631
+ if (!tableContainer) return;
632
+ tableContainer.innerHTML = '';
633
+
634
+ if (shapError || !shapData || shapData.length === 0 || (shapData[0] && shapData[0].error)) {
635
+ const errorMsg = (shapData && shapData[0] && shapData[0].error) ? shapData[0].error : `SHAP data calculation failed or is unavailable for ${riskType === 'na' ? 'Non-Adherence' : 'Readmission'}.`;
636
+ tableContainer.innerHTML = `<p class="text-red-600 italic p-4">${errorMsg}</p>`;
637
+ return;
638
+ }
639
+ const validShapData = shapData.filter(item => !(item && item.error));
640
+ if (validShapData.length === 0) { tableContainer.innerHTML = `<p class="text-gray-500 italic p-4">No valid factor data to display.</p>`; return; }
641
+
642
+ const maxAbsShap = Math.max(...validShapData.map(item => Math.abs(item.shap_value || 0)), 0.01);
643
+ let tableHTML = `
644
+ <div class="overflow-x-auto rounded border border-gray-200">
645
+ <table class="min-w-full divide-y divide-gray-200 text-sm">
646
+ <thead class="bg-gray-50">
647
+ <tr>
648
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Feature</th>
649
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Patient Value</th>
650
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider w-1/3">Impact on Risk Score (SHAP Value)</th>
651
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Effect</th>
652
+ </tr>
653
+ </thead>
654
+ <tbody class="bg-white divide-y divide-gray-200">`;
655
+
656
+ validShapData.forEach(item => {
657
+ const shapValue = item.shap_value || 0;
658
+ const absValue = Math.abs(shapValue);
659
+ const widthPercent = Math.min(Math.max((absValue / maxAbsShap) * 100, 1), 100);
660
+ const direction = shapValue > 0.001 ? 'Increases Risk' : (shapValue < -0.001 ? 'Decreases Risk' : 'Neutral');
661
+ const barClass = shapValue > 0.001 ? 'shap-positive' : (shapValue < -0.001 ? 'shap-negative' : 'bg-gray-300');
662
+ const directionClass = shapValue > 0.001 ? 'bg-red-100 text-red-800' : (shapValue < -0.001 ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800');
663
+ const shapTextClass = shapValue > 0.001 ? 'text-red-600' : (shapValue < -0.001 ? 'text-green-600' : 'text-gray-600');
664
+ tableHTML += `
665
+ <tr class="feature-row">
666
+ <td class="px-4 py-2 whitespace-nowrap">
667
+ <span class="font-medium text-gray-900">${item.feature || 'Unknown'}</span>
668
+ <span class="tooltip ml-1 text-gray-400 hover:text-blue-500"> <i class="far fa-question-circle"></i> <span class="tooltip-text">${item.help_text || item.description || 'No details'}</span> </span>
669
+ </td>
670
+ <td class="px-4 py-2 text-gray-700 whitespace-nowrap">${item.feature_value !== null ? item.feature_value : 'N/A'}</td>
671
+ <td class="px-4 py-2">
672
+ <div class="flex items-center"> <span class="mr-2 ${shapTextClass} font-mono w-12 text-right">${shapValue.toFixed(3)}</span> <div class="shap-bar-container flex-1"> <div class="shap-bar ${barClass}" style="width: ${widthPercent}%"></div> </div> </div>
673
+ </td>
674
+ <td class="px-4 py-2 whitespace-nowrap"> <span class="inline-flex rounded-full px-2 py-0.5 text-xs font-semibold leading-5 ${directionClass}"> ${direction} </span> </td>
675
+ </tr>`;
676
+ });
677
+ tableHTML += `</tbody></table></div> <p class="text-xs text-gray-500 mt-2 italic">SHAP values estimate the contribution of each feature to the model's risk score prediction relative to the average.</p>`;
678
+ tableContainer.innerHTML = tableHTML;
679
+ }
680
+
681
+ // --- Display Counterfactuals Table ---
682
+ function displayCounterfactuals(explanations) { /* ... Keep as is ... */
683
+ const cfTableContainer = document.getElementById('cf-table');
684
+ if (!cfTableContainer || !explanations) return;
685
+ cfTableContainer.innerHTML = '';
686
+
687
+ let allCfs = [];
688
+ if (explanations.counterfactuals_na && !explanations.counterfactuals_na[0]?.error) allCfs = allCfs.concat(explanations.counterfactuals_na);
689
+ if (explanations.counterfactuals_r && !explanations.counterfactuals_r[0]?.error) allCfs = allCfs.concat(explanations.counterfactuals_r);
690
+
691
+ const modifiableCfs = {};
692
+ allCfs.forEach(cf => {
693
+ if (cf.feature && cf.suggested_change && !cf.suggested_change.toLowerCase().includes("maintain")) {
694
+ if (!modifiableCfs[cf.feature]) modifiableCfs[cf.feature] = [];
695
+ modifiableCfs[cf.feature].push(cf);
696
+ }
697
+ });
698
+
699
+ if (Object.keys(modifiableCfs).length === 0) {
700
+ cfTableContainer.innerHTML = '<p class="text-gray-500 italic p-4">No specific modifications identified with significant potential for risk reduction.</p>';
701
+ return;
702
+ }
703
+ let tableHTML = `
704
+ <div class="overflow-x-auto rounded border border-gray-200">
705
+ <table class="min-w-full divide-y divide-gray-200 text-sm"> <thead class="bg-gray-50"> <tr>
706
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Factor</th>
707
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Current</th>
708
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Suggestion</th>
709
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Affects</th>
710
+ <th scope="col" class="px-4 py-2 text-left font-medium text-gray-500 uppercase tracking-wider">Est. Outcome</th>
711
+ </tr> </thead> <tbody class="bg-white divide-y divide-gray-200">`;
712
+
713
+ const sortedFeatures = Object.keys(modifiableCfs).sort();
714
+ sortedFeatures.forEach(feature => {
715
+ const cfsForFeature = modifiableCfs[feature];
716
+ const firstCf = cfsForFeature[0];
717
+ const affectedRisks = [...new Set(cfsForFeature.map(cf => cf.risk_type))].map(rt => rt.split('-')[0]).join(' & ');
718
+ const bestOutcomeCf = cfsForFeature.reduce((best, current) => {
719
+ const bestMag = ['Minor', 'Moderate', 'Significant'].indexOf(best.impact_magnitude);
720
+ const currentMag = ['Minor', 'Moderate', 'Significant'].indexOf(current.impact_magnitude);
721
+ return currentMag > bestMag ? current : best;
722
+ }, firstCf);
723
+ const suggestedChange = bestOutcomeCf.suggested_change || 'Optimize';
724
+ const potentialOutcome = bestOutcomeCf.potential_outcome || 'N/A';
725
+ const impactMagnitude = bestOutcomeCf.impact_magnitude || 'Minor';
726
+ let magnitudeClass = 'bg-blue-100 text-blue-800';
727
+ if (impactMagnitude === 'Significant') magnitudeClass = 'bg-yellow-100 text-yellow-800';
728
+ else if (impactMagnitude === 'Moderate') magnitudeClass = 'bg-orange-100 text-orange-800';
729
+ tableHTML += `
730
+ <tr class="feature-row">
731
+ <td class="px-4 py-2 font-medium text-gray-900">${feature}</td>
732
+ <td class="px-4 py-2 text-gray-700">${firstCf.current_value}</td>
733
+ <td class="px-4 py-2 text-blue-600 font-medium">${suggestedChange}</td>
734
+ <td class="px-4 py-2 text-gray-700">${affectedRisks}</td>
735
+ <td class="px-4 py-2 text-gray-700"> ${potentialOutcome !== 'N/A' ? potentialOutcome : ''} <span class="ml-1 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${magnitudeClass}"> ${impactMagnitude} Potential </span> </td>
736
+ </tr>`;
737
+ });
738
+ tableHTML += `</tbody></table></div>`;
739
+ cfTableContainer.innerHTML = tableHTML;
740
+ }
741
+
742
+
743
+ // --- Display Additional Visualizations ---
744
+ function displayAdditionalVisualizations(additionalViz) {
745
+ if (!additionalViz) return;
746
+
747
+ // 1. Risk Factor Heatmap
748
+ plotGenericChart(
749
+ 'risk-heatmap',
750
+ additionalViz.risk_heatmap,
751
+ 'Risk Factor Heatmap'
752
+ );
753
+
754
+ // 2. Feature Importance Comparison
755
+ plotGenericChart(
756
+ 'feature-comparison',
757
+ additionalViz.feature_comparison,
758
+ 'Feature Importance Comparison'
759
+ );
760
+
761
+ // 3. Intervention Impact Chart
762
+ plotGenericChart(
763
+ 'intervention-chart',
764
+ additionalViz.intervention_impact,
765
+ 'Intervention Impact Chart'
766
+ );
767
+
768
+ // 4. Network Graph
769
+ const networkContainer = document.getElementById('network-graph');
770
+ const networkPlaceholder = document.getElementById('network-graph-placeholder');
771
+
772
+ if (additionalViz.network_graph) {
773
+ // Hide the old placeholder if present
774
+ if (networkPlaceholder) networkPlaceholder.classList.add('hidden');
775
+
776
+ // Render the Plotly network graph
777
+ if (networkContainer) {
778
+ networkContainer.innerHTML = '';
779
+ try {
780
+ const netData = JSON.parse(additionalViz.network_graph);
781
+ Plotly.newPlot(
782
+ 'network-graph',
783
+ netData.data,
784
+ netData.layout,
785
+ {
786
+ responsive: true,
787
+ displaylogo: false,
788
+ modeBarButtonsToRemove: ['toImage','sendDataToCloud','select2d','lasso2d']
789
+ }
790
+ );
791
+ } catch (e) {
792
+ console.error('Error rendering network graph:', e);
793
+ networkContainer.innerHTML = `
794
+ <p class="text-sm text-red-500 text-center p-4">
795
+ Could not display network graph.
796
+ </p>`;
797
+ }
798
+ }
799
+ } else {
800
+ // No network data → show placeholder
801
+ if (networkContainer) networkContainer.innerHTML = '';
802
+ if (networkPlaceholder) networkPlaceholder.classList.remove('hidden');
803
+ }
804
+ }
805
+
806
+
807
+ // --- Tab Switching Logic ---
808
+ function setupTabs(forceDefault = false) { /* ... Keep as is ... */
809
+ const mainTabs = document.querySelectorAll('.tab-btn');
810
+ const mainTabContents = document.querySelectorAll('.tab-content');
811
+ const factorTabs = document.querySelectorAll('.factor-tab-btn');
812
+ const factorTabContents = document.querySelectorAll('.factor-content');
813
+
814
+ const activateTab = (tabs, contents, tabToActivate) => {
815
+ if (!tabToActivate) return;
816
+ tabs.forEach(t => t.classList.remove('active'));
817
+ contents.forEach(c => c.classList.add('hidden'));
818
+ tabToActivate.classList.add('active');
819
+ const targetContentId = tabToActivate.getAttribute('data-tab') || tabToActivate.getAttribute('data-factor');
820
+ const targetContent = document.getElementById(`${targetContentId}-tab`) || document.getElementById(`${targetContentId}-factors`);
821
+ if (targetContent) {
822
+ targetContent.classList.remove('hidden');
823
+ setTimeout(() => {
824
+ const charts = targetContent.querySelectorAll('.js-plotly-plot');
825
+ charts.forEach(chart => { try { if (chart.layout && chart.data) Plotly.Plots.resize(chart); } catch(e) { console.warn("Resize err:", e); } });
826
+ }, 150); // Slightly longer delay for complex tabs maybe
827
+ } else { console.error(`Target content not found for tab: ${targetContentId}`); }
828
+ };
829
+
830
+ mainTabs.forEach(tab => tab.addEventListener('click', () => activateTab(mainTabs, mainTabContents, tab)));
831
+ factorTabs.forEach(tab => tab.addEventListener('click', () => activateTab(factorTabs, factorTabContents, tab)));
832
+
833
+ if (forceDefault || !document.querySelector('.tab-btn.active')) {
834
+ const defaultMainTab = document.querySelector('.tab-btn[data-tab="recommendations"]');
835
+ activateTab(mainTabs, mainTabContents, defaultMainTab);
836
+ const defaultFactorTab = document.querySelector('.factor-tab-btn[data-factor="na"]');
837
+ activateTab(factorTabs, factorTabContents, defaultFactorTab);
838
+ // Ensure other factor content is hidden
839
+ const rFactorContent = document.getElementById('r-factors');
840
+ if (rFactorContent) rFactorContent.classList.add('hidden');
841
+ }
842
+ }
843
+
844
+ // --- Initialization ---
845
+ document.addEventListener('DOMContentLoaded', () => {
846
+ console.log("DOM Ready. Initializing...");
847
+ try {
848
+ generateFormFields();
849
+ setDefaultValues();
850
+ setupFormSubmission();
851
+ setupTabs();
852
+ console.log("Initialization complete.");
853
+ } catch (initError) {
854
+ console.error("Initialization Error:", initError);
855
+ showError("Failed to initialize the dashboard interface. Please refresh the page.");
856
+ }
857
+ });
858
+
859
+ </script>
860
+ </body>
861
+ </html>