arshenoy commited on
Commit
04c9599
·
verified ·
1 Parent(s): a4b5979

Update App.tsx

Browse files
Files changed (1) hide show
  1. App.tsx +335 -268
App.tsx CHANGED
@@ -1,290 +1,357 @@
1
- import React, { useState, useEffect, Component, ErrorInfo } from 'react';
2
  import { createPortal } from 'react-dom';
3
- import { Settings, X, Trash2, MessageSquare, FileText, AlertTriangle, RefreshCw } from 'lucide-react';
4
- import {
5
- PatientProfile,
6
- ClinicalVitals,
7
- INITIAL_PROFILE,
8
- INITIAL_VITALS,
9
- AppMode,
10
- RiskAnalysisResult,
11
- ChatMessage,
12
- Medication,
13
- HealthInsights,
14
- ChatSession
15
  } from './types';
16
  import { analyzeRisk, generateChatResponse, summarizeConversation, generateHealthInsights, wakeUpBackend, generateSessionName } from './services/geminiService';
 
17
  import Sidebar from './components/Sidebar';
18
  import Dashboard from './components/Dashboard';
19
  import Chat from './components/Chat';
20
  import PrintReport from './components/PrintReport';
21
  import MedicationTracker from './components/MedicationTracker';
22
 
23
- // --- ERROR BOUNDARY TO PREVENT BLACK SCREEN ---
24
- class ErrorBoundary extends Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
25
- constructor(props: {children: React.ReactNode}) {
26
- super(props);
27
- this.state = { hasError: false, error: null };
28
- }
29
- static getDerivedStateFromError(error: Error) {
30
- return { hasError: true, error };
31
- }
32
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
33
- console.error("Uncaught error:", error, errorInfo);
34
- }
35
- render() {
36
- if (this.state.hasError) {
37
- return (
38
- <div className="min-h-screen flex flex-col items-center justify-center bg-black text-white p-6 text-center">
39
- <AlertTriangle size={48} className="text-red-500 mb-4" />
40
- <h1 className="text-2xl font-bold mb-2">Something went wrong</h1>
41
- <p className="text-gray-400 mb-6 max-w-md">{this.state.error?.message || "An unexpected error occurred."}</p>
42
- <button
43
- onClick={() => { localStorage.clear(); window.location.reload(); }}
44
- className="flex items-center gap-2 px-6 py-3 bg-red-600 rounded-lg hover:bg-red-700 transition-colors font-bold"
45
- >
46
- <RefreshCw size={18} /> Reset App Data
47
- </button>
48
- <p className="text-xs text-gray-600 mt-4">This clears local storage which might be corrupted.</p>
49
- </div>
50
- );
51
- }
52
- return this.props.children;
53
- }
54
- }
55
-
56
- const MainApp: React.FC = () => {
57
- const [activeTab, setActiveTab] = useState<'dashboard' | 'chat' | 'medication'>('dashboard');
58
- const [showSettings, setShowSettings] = useState(false);
59
-
60
- // --- STATE INITIALIZATION (SAFE PARSING) ---
61
- const safeParse = <T,>(key: string, fallback: T): T => {
62
- try {
63
- const item = localStorage.getItem(key);
64
- return item ? JSON.parse(item) : fallback;
65
- } catch (e) {
66
- console.warn(`Failed to parse ${key}, using fallback.`);
67
- return fallback;
68
- }
69
- };
70
 
71
- const [profile, setProfile] = useState<PatientProfile>(() => safeParse('somai_profile', INITIAL_PROFILE));
72
- const [medications, setMedications] = useState<Medication[]>(() => safeParse('somai_medications', []));
73
- const [sessions, setSessions] = useState<ChatSession[]>(() => {
74
- try {
75
- const stored = localStorage.getItem('somai_chat_sessions');
76
- if (stored) return JSON.parse(stored);
77
- const legacyHistory = localStorage.getItem('somai_chat_history');
78
- if (legacyHistory) {
79
- const history = JSON.parse(legacyHistory);
80
- if (Array.isArray(history) && history.length > 0) {
81
- return [{ id: Date.now().toString(), name: 'Previous Session', messages: history, createdAt: Date.now(), lastModified: Date.now() }];
82
- }
83
- }
84
- return [];
85
- } catch { return []; }
86
- });
87
- const [currentSessionId, setCurrentSessionId] = useState<string>('');
88
- const [vitals, setVitals] = useState<ClinicalVitals>(INITIAL_VITALS);
89
- const [riskResult, setRiskResult] = useState<RiskAnalysisResult | null>(null);
90
- const [insights, setInsights] = useState<HealthInsights | null>(null);
91
- const [chatSummary, setChatSummary] = useState('');
92
- const [mode, setMode] = useState<AppMode>(AppMode.GENERAL);
93
- const [isProcessing, setIsProcessing] = useState(false);
94
- const [isAnalyzing, setIsAnalyzing] = useState(false);
95
- const [isSummarizing, setIsSummarizing] = useState(false);
96
- const [statusMessage, setStatusMessage] = useState('');
97
-
98
- // --- EFFECTS ---
99
- useEffect(() => { wakeUpBackend(); }, []);
100
-
101
- useEffect(() => {
102
- if (sessions.length === 0) {
103
- const newId = Date.now().toString();
104
- const newSession: ChatSession = { id: newId, name: 'New Consultation', messages: [], createdAt: Date.now(), lastModified: Date.now() };
105
- setSessions([newSession]);
106
- setCurrentSessionId(newId);
107
- } else if (!currentSessionId || !sessions.find(s => s.id === currentSessionId)) {
108
- setCurrentSessionId(sessions[0].id);
109
- }
110
- }, [sessions, currentSessionId]);
111
-
112
- useEffect(() => {
113
- const checkStreak = () => {
114
- if (!profile?.lastStreakUpdate) return;
115
- const last = new Date(profile.lastStreakUpdate);
116
- const today = new Date();
117
- last.setHours(0,0,0,0); today.setHours(0,0,0,0);
118
- const diff = Math.round((today.getTime() - last.getTime()) / (1000 * 60 * 60 * 24));
119
-
120
- if (diff > 1 && profile.streak > 0) setProfile(p => ({ ...p, streak: 0 }));
121
- };
122
- checkStreak();
123
- }, [profile.lastStreakUpdate, profile.streak]);
124
-
125
- useEffect(() => localStorage.setItem('somai_profile', JSON.stringify(profile)), [profile]);
126
- useEffect(() => localStorage.setItem('somai_medications', JSON.stringify(medications)), [medications]);
127
- useEffect(() => localStorage.setItem('somai_chat_sessions', JSON.stringify(sessions)), [sessions]);
128
-
129
- useEffect(() => {
130
- const m = Number(vitals.systolicBpMorning) || 0;
131
- const e = Number(vitals.systolicBpEvening) || 0;
132
- const avg = (m > 0 && e > 0) ? Math.round((m + e) / 2) : Math.max(m, e);
133
- if (avg !== vitals.systolicBp) {
134
- setVitals(v => ({ ...v, systolicBp: avg }));
135
- }
136
- }, [vitals.systolicBpMorning, vitals.systolicBpEvening]);
137
-
138
- // --- LOGIC ---
139
- const activeSession = sessions.find(s => s.id === currentSessionId) || sessions[0];
140
- const chatHistory = activeSession?.messages || [];
141
-
142
- const createNewSession = () => {
143
- const newId = Date.now().toString();
144
- const newSession: ChatSession = { id: newId, name: `Consultation ${new Date().toLocaleDateString()}`, messages: [], createdAt: Date.now(), lastModified: Date.now() };
145
- setSessions(prev => [newSession, ...prev]);
146
- setCurrentSessionId(newId);
147
- };
148
 
149
- const deleteSession = (id: string) => {
150
- if (sessions.length <= 1) {
151
- setSessions(prev => prev.map(s => s.id === id ? { ...s, messages: [], name: 'New Consultation' } : s));
152
- return;
153
- }
154
- const newSessions = sessions.filter(s => s.id !== id);
155
- setSessions(newSessions);
156
- if (currentSessionId === id) setCurrentSessionId(newSessions[0].id);
157
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- const renameSession = (id: string, newName: string) => {
160
- setSessions(prev => prev.map(s => s.id === id ? { ...s, name: newName } : s));
161
- };
162
 
163
- const calculateRiskScore = (): number => {
164
- let score = 10;
165
- const bpM = Number(vitals.systolicBpMorning) || 0;
166
- const bpE = Number(vitals.systolicBpEvening) || 0;
167
- const maxBp = Math.max(bpM, bpE);
168
- const gluc = Number(vitals.glucose) || 0;
169
- const spo2 = Number(vitals.spo2) || 98;
170
- const hr = Number(vitals.heartRate) || 72;
171
- const temp = Number(vitals.temperature) || 98.6;
172
-
173
- if (maxBp >= 180) score += 50; else if (maxBp >= 140) score += 25; else if (maxBp >= 130) score += 10;
174
- if (gluc >= 250) score += 40; else if (gluc >= 180) score += 20;
175
- if (spo2 < 90) score += 30; else if (spo2 < 95) score += 15;
176
- if (hr > 100 || hr < 50) score += 15;
177
- if (temp > 99.5) score += 20;
178
- if (vitals.missedDoses > 0) score += (vitals.missedDoses * 5);
179
- if (profile.smokingStatus === 'Current') score += 15;
180
- if (profile.exerciseFrequency === 'Sedentary') score += 10;
181
- return Math.min(Math.max(Math.round(score), 1), 100);
182
- };
183
 
184
- const handleRunAnalysis = async () => {
185
- setIsAnalyzing(true);
186
- setStatusMessage("Analyzing Vitals...");
187
- try {
188
- const score = calculateRiskScore();
189
- const [rResult, iResult] = await Promise.all([
190
- analyzeRisk(profile, vitals, score, setStatusMessage),
191
- generateHealthInsights(profile, vitals)
192
- ]);
193
- setRiskResult(rResult);
194
- setInsights(iResult);
195
- setProfile(p => ({ ...p, lastCheckup: new Date().toISOString() }));
196
- } catch (e) {
197
- alert("Analysis failed. Please check connection.");
198
- } finally {
199
- setIsAnalyzing(false);
200
- setStatusMessage('');
201
- }
202
- };
203
 
204
- const handleSendMessage = async (input: string, image?: string) => {
205
- if (!input.trim() && !image) return;
206
 
207
- const newUserMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: input, timestamp: Date.now(), image };
208
- const updatedMessages = [...chatHistory, newUserMsg];
209
- setSessions(prev => prev.map(s => s.id === currentSessionId ? { ...s, messages: updatedMessages, lastModified: Date.now() } : s));
210
- setIsProcessing(true);
211
- setStatusMessage("Thinking...");
 
 
 
 
 
 
212
 
213
- try {
214
- let activeSource = '';
215
- const responseText = await generateChatResponse(updatedMessages, input, image, profile, mode, (source) => { activeSource = source; }, setStatusMessage);
216
- const aiMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'model', text: responseText, timestamp: Date.now(), modelUsed: activeSource };
217
- setSessions(prev => prev.map(s => s.id === currentSessionId ? { ...s, messages: [...updatedMessages, aiMsg], lastModified: Date.now() } : s));
218
-
219
- if (updatedMessages.length === 1) {
220
- generateSessionName(input, responseText).then(newName => {
221
- setSessions(prev => prev.map(s => s.id === currentSessionId ? { ...s, name: newName } : s));
222
- });
223
- }
224
- } catch (e) { console.error(e); } finally { setIsProcessing(false); setStatusMessage(''); }
225
- };
226
 
227
- const handleSummarizeChat = async () => {
228
- if (chatHistory.length === 0) return;
229
- setIsSummarizing(true);
230
- try { setChatSummary(await summarizeConversation(chatHistory)); } catch (e) { console.error(e); } finally { setIsSummarizing(false); }
231
- };
232
 
233
- const handlePrintReport = () => window.print();
234
- const handleResetData = () => { if(confirm('Reset all data?')) { localStorage.clear(); window.location.reload(); } };
235
-
236
- const printableRoot = document.getElementById('printable-root');
237
-
238
- return (
239
- <div className="min-h-screen bg-black text-white selection:bg-neon-green selection:text-black font-sans overflow-x-hidden flex flex-col md:block">
240
- <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
241
- <div className="md:hidden p-4 border-b border-white/10 flex justify-between items-center bg-black/80 backdrop-blur-md sticky top-0 z-50">
242
- <span className="font-mono font-bold text-white tracking-wider">SomAI</span>
243
- <button onClick={() => setShowSettings(true)}><Settings size={20} className="text-gray-400" /></button>
244
- </div>
245
- <button onClick={() => setShowSettings(true)} className="hidden md:block fixed top-6 right-6 z-30 p-2 text-gray-500 hover:text-white transition-colors bg-black/20 rounded-full hover:bg-white/10">
246
- <Settings size={20} />
247
- </button>
248
- <main className="flex-1 md:ml-64 p-3 md:p-8 pt-4 md:pt-6 max-w-7xl mx-auto w-full md:min-h-screen flex flex-col">
249
- <div className="md:hidden flex gap-2 mb-4 overflow-x-auto pb-2 scrollbar-hide shrink-0">
250
- {[{ id: 'dashboard', label: 'Dashboard' }, { id: 'chat', label: 'Chat' }, { id: 'medication', label: 'Meds' }].map(t => (
251
- <button key={t.id} onClick={() => setActiveTab(t.id as any)} className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-wide whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-neon-green text-black' : 'bg-gray-900 text-gray-400'}`}>{t.label}</button>
252
- ))}
253
- </div>
254
- <div className="flex-1 min-h-0 flex flex-col">
255
- {activeTab === 'dashboard' && (
256
- <Dashboard vitals={vitals} setVitals={setVitals} riskResult={riskResult} chatSummary={chatSummary} handleRunAnalysis={handleRunAnalysis} isAnalyzing={isAnalyzing} onPrint={handlePrintReport} profile={profile} setProfile={setProfile} medications={medications} chatHistory={chatHistory} insights={insights} statusMessage={statusMessage} setStatusMessage={setStatusMessage} />
257
- )}
258
- {activeTab === 'chat' && (
259
- <Chat sessions={sessions} currentSessionId={currentSessionId} onSwitchSession={setCurrentSessionId} onCreateSession={createNewSession} onRenameSession={renameSession} onDeleteSession={deleteSession} onSendMessage={handleSendMessage} isProcessing={isProcessing} statusMessage={statusMessage} mode={mode} setMode={setMode} onSummarize={handleSummarizeChat} isSummarizing={isSummarizing} chatSummary={chatSummary} />
260
- )}
261
- {activeTab === 'medication' && (
262
- <MedicationTracker medications={medications} setMedications={setMedications} profile={profile} setProfile={setProfile} />
263
- )}
264
- </div>
265
- </main>
266
- {showSettings && (
267
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
268
- <div className="glass-panel w-full max-w-md p-6 rounded-2xl border border-white/10 relative animate-in zoom-in-95">
269
- <button onClick={() => setShowSettings(false)} className="absolute top-4 right-4 text-gray-500 hover:text-white"><X size={20} /></button>
270
- <h2 className="text-xl font-bold text-white mb-6 flex items-center gap-2"><Settings className="text-neon-green" size={20} /> Settings</h2>
271
- <div className="space-y-4">
272
- <button onClick={handlePrintReport} className="w-full p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/5 flex items-center gap-3 text-left transition-all"><div className="p-2 bg-green-500/10 rounded-lg text-green-400"><FileText size={18} /></div><div><h3 className="font-bold text-sm text-gray-200">Download PDF Report</h3><p className="text-xs text-gray-500">Save full analysis & chat history</p></div></button>
273
- <button onClick={handleResetData} className="w-full p-4 rounded-xl bg-red-500/10 hover:bg-red-500/20 border border-red-500/10 flex items-center gap-3 text-left transition-all"><div className="p-2 bg-red-500/10 rounded-lg text-red-400"><Trash2 size={18} /></div><div><h3 className="font-bold text-sm text-red-200">Reset Application</h3><p className="text-xs text-red-400/60">Delete all local data & profile</p></div></button>
274
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  </div>
276
- </div>
277
- )}
278
- {printableRoot && createPortal(<PrintReport profile={profile} vitals={vitals} riskResult={riskResult} chatHistory={chatHistory} chatSummary={chatSummary} medications={medications} />, printableRoot)}
279
- </div>
280
- );
281
- };
282
 
283
- // Wrap App in Error Boundary
284
- const AppWithBoundary = () => (
285
- <ErrorBoundary>
286
- <MainApp />
287
- </ErrorBoundary>
288
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
- export default AppWithBoundary;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
  import { createPortal } from 'react-dom';
3
+ import { Settings, X, Trash2, MessageSquare, FileText, Download } from 'lucide-react';
4
+ import {
5
+ PatientProfile,
6
+ ClinicalVitals,
7
+ INITIAL_PROFILE,
8
+ INITIAL_VITALS,
9
+ AppMode,
10
+ RiskAnalysisResult,
11
+ ChatMessage,
12
+ Medication,
13
+ HealthInsights,
14
+ ChatSession
15
  } from './types';
16
  import { analyzeRisk, generateChatResponse, summarizeConversation, generateHealthInsights, wakeUpBackend, generateSessionName } from './services/geminiService';
17
+
18
  import Sidebar from './components/Sidebar';
19
  import Dashboard from './components/Dashboard';
20
  import Chat from './components/Chat';
21
  import PrintReport from './components/PrintReport';
22
  import MedicationTracker from './components/MedicationTracker';
23
 
24
+ const App: React.FC = () => {
25
+ const [activeTab, setActiveTab] = useState<'dashboard' | 'chat' | 'medication'>('dashboard');
26
+ const [showSettings, setShowSettings] = useState(false);
27
+
28
+ // --- STATE ---
29
+ const [profile, setProfile] = useState<PatientProfile>(() => {
30
+ try { return JSON.parse(localStorage.getItem('somai_profile') || '') || INITIAL_PROFILE; } catch { return INITIAL_PROFILE; }
31
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
+ const [medications, setMedications] = useState<Medication[]>(() => {
34
+ try { return JSON.parse(localStorage.getItem('somai_medications') || '') || []; } catch { return []; }
35
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ // Chat Sessions State
38
+ const [sessions, setSessions] = useState<ChatSession[]>(() => {
39
+ try {
40
+ const stored = localStorage.getItem('somai_chat_sessions');
41
+ if (stored) return JSON.parse(stored);
42
+
43
+ const legacyHistory = localStorage.getItem('somai_chat_history');
44
+ if (legacyHistory) {
45
+ const history = JSON.parse(legacyHistory);
46
+ if (history.length > 0) {
47
+ return [{
48
+ id: Date.now().toString(),
49
+ name: 'Previous Session',
50
+ messages: history,
51
+ createdAt: Date.now(),
52
+ lastModified: Date.now()
53
+ }];
54
+ }
55
+ }
56
+ return [];
57
+ } catch { return []; }
58
+ });
59
 
60
+ const [currentSessionId, setCurrentSessionId] = useState<string>('');
 
 
61
 
62
+ const [vitals, setVitals] = useState<ClinicalVitals>(INITIAL_VITALS);
63
+ const [riskResult, setRiskResult] = useState<RiskAnalysisResult | null>(null);
64
+ const [insights, setInsights] = useState<HealthInsights | null>(null);
65
+ const [chatSummary, setChatSummary] = useState('');
66
+ const [mode, setMode] = useState<AppMode>(AppMode.GENERAL);
67
+
68
+ const [isProcessing, setIsProcessing] = useState(false);
69
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
70
+ const [isSummarizing, setIsSummarizing] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ // --- GLOBAL STATUS MESSAGE (For Transparency) ---
73
+ const [statusMessage, setStatusMessage] = useState('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ // --- EFFECTS ---
76
+ useEffect(() => { wakeUpBackend(); }, []);
77
 
78
+ // Ensure there's always at least one session
79
+ useEffect(() => {
80
+ if (sessions.length === 0) {
81
+ const newId = Date.now().toString();
82
+ const newSession: ChatSession = { id: newId, name: 'New Consultation', messages: [], createdAt: Date.now(), lastModified: Date.now() };
83
+ setSessions([newSession]);
84
+ setCurrentSessionId(newId);
85
+ } else if (!currentSessionId || !sessions.find(s => s.id === currentSessionId)) {
86
+ setCurrentSessionId(sessions[0].id);
87
+ }
88
+ }, [sessions, currentSessionId]);
89
 
90
+ useEffect(() => {
91
+ const checkStreak = () => {
92
+ const last = new Date(profile.lastStreakUpdate);
93
+ const today = new Date();
94
+ last.setHours(0,0,0,0); today.setHours(0,0,0,0);
95
+ const diff = Math.round((today.getTime() - last.getTime()) / (1000 * 60 * 60 * 24));
96
+ if (diff > 1 && profile.streak > 0) setProfile(p => ({ ...p, streak: 0 }));
97
+ };
98
+ checkStreak();
99
+ }, []);
 
 
 
100
 
101
+ useEffect(() => localStorage.setItem('somai_profile', JSON.stringify(profile)), [profile]);
102
+ useEffect(() => localStorage.setItem('somai_medications', JSON.stringify(medications)), [medications]);
103
+ useEffect(() => localStorage.setItem('somai_chat_sessions', JSON.stringify(sessions)), [sessions]);
 
 
104
 
105
+ // Derived BP Sync
106
+ useEffect(() => {
107
+ const m = Number(vitals.systolicBpMorning) || 0;
108
+ const e = Number(vitals.systolicBpEvening) || 0;
109
+ const avg = (m > 0 && e > 0) ? Math.round((m + e) / 2) : Math.max(m, e);
110
+
111
+ if (avg !== vitals.systolicBp) {
112
+ setVitals(v => ({ ...v, systolicBp: avg }));
113
+ }
114
+ }, [vitals.systolicBpMorning, vitals.systolicBpEvening]);
115
+
116
+ // --- LOGIC ---
117
+ const activeSession = sessions.find(s => s.id === currentSessionId) || sessions[0];
118
+ const chatHistory = activeSession?.messages || [];
119
+
120
+ const createNewSession = () => {
121
+ const newId = Date.now().toString();
122
+ const newSession: ChatSession = {
123
+ id: newId,
124
+ name: `Consultation ${new Date().toLocaleDateString()}`,
125
+ messages: [],
126
+ createdAt: Date.now(),
127
+ lastModified: Date.now()
128
+ };
129
+ setSessions(prev => [newSession, ...prev]);
130
+ setCurrentSessionId(newId);
131
+ };
132
+
133
+ const deleteSession = (id: string) => {
134
+ if (sessions.length <= 1) {
135
+ setSessions(prev => prev.map(s => s.id === id ? { ...s, messages: [], name: 'New Consultation' } : s));
136
+ return;
137
+ }
138
+ const newSessions = sessions.filter(s => s.id !== id);
139
+ setSessions(newSessions);
140
+ if (currentSessionId === id) {
141
+ setCurrentSessionId(newSessions[0].id);
142
+ }
143
+ };
144
+
145
+ const renameSession = (id: string, newName: string) => {
146
+ setSessions(prev => prev.map(s => s.id === id ? { ...s, name: newName } : s));
147
+ };
148
+
149
+ const calculateRiskScore = (): number => {
150
+ let score = 10;
151
+ const bpM = Number(vitals.systolicBpMorning) || 0;
152
+ const bpE = Number(vitals.systolicBpEvening) || 0;
153
+ const maxBp = Math.max(bpM, bpE);
154
+
155
+ const gluc = Number(vitals.glucose) || 0;
156
+ const spo2 = Number(vitals.spo2) || 98;
157
+ const hr = Number(vitals.heartRate) || 72;
158
+ const temp = Number(vitals.temperature) || 98.6;
159
+
160
+ if (maxBp >= 180) score += 50; else if (maxBp >= 140) score += 25; else if (maxBp >= 130) score += 10;
161
+ if (gluc >= 250) score += 40; else if (gluc >= 180) score += 20;
162
+
163
+ if (spo2 < 90) score += 30; else if (spo2 < 95) score += 15;
164
+
165
+ if (hr > 100 || hr < 50) score += 15;
166
+ if (temp > 99.5) score += 20;
167
+ if (vitals.missedDoses > 0) score += (vitals.missedDoses * 5);
168
+
169
+ if (profile.smokingStatus === 'Current') score += 15;
170
+ if (profile.exerciseFrequency === 'Sedentary') score += 10;
171
+
172
+ return Math.min(Math.max(Math.round(score), 1), 100);
173
+ };
174
+
175
+ const handleRunAnalysis = async () => {
176
+ setIsAnalyzing(true);
177
+ setStatusMessage("Analyzing Vitals...");
178
+ try {
179
+ const score = calculateRiskScore();
180
+ const [rResult, iResult] = await Promise.all([
181
+ analyzeRisk(profile, vitals, score, setStatusMessage),
182
+ generateHealthInsights(profile, vitals)
183
+ ]);
184
+ setRiskResult(rResult);
185
+ setInsights(iResult);
186
+ setProfile(p => ({ ...p, lastCheckup: new Date().toISOString() }));
187
+ } catch (e) {
188
+ alert("Analysis failed. Please check connection.");
189
+ } finally {
190
+ setIsAnalyzing(false);
191
+ setStatusMessage('');
192
+ }
193
+ };
194
+
195
+ const handleSendMessage = async (input: string, image?: string) => {
196
+ if (!input.trim() && !image) return;
197
+
198
+ const newUserMsg: ChatMessage = { id: Date.now().toString(), role: 'user', text: input, timestamp: Date.now(), image };
199
+ const updatedMessages = [...chatHistory, newUserMsg];
200
+
201
+ setSessions(prev => prev.map(s =>
202
+ s.id === currentSessionId
203
+ ? { ...s, messages: updatedMessages, lastModified: Date.now() }
204
+ : s
205
+ ));
206
+
207
+ setIsProcessing(true);
208
+ setStatusMessage("Thinking...");
209
+
210
+ try {
211
+ let activeSource = '';
212
+
213
+ const responseText = await generateChatResponse(
214
+ updatedMessages,
215
+ input,
216
+ image,
217
+ profile,
218
+ mode,
219
+ (source) => { activeSource = source; },
220
+ setStatusMessage
221
+ );
222
+
223
+ const aiMsg: ChatMessage = {
224
+ id: (Date.now() + 1).toString(),
225
+ role: 'model',
226
+ text: responseText,
227
+ timestamp: Date.now(),
228
+ modelUsed: activeSource
229
+ };
230
+
231
+ setSessions(prev => prev.map(s =>
232
+ s.id === currentSessionId
233
+ ? { ...s, messages: [...updatedMessages, aiMsg], lastModified: Date.now() }
234
+ : s
235
+ ));
236
+
237
+ if (updatedMessages.length === 1) {
238
+ generateSessionName(input, responseText).then(newName => {
239
+ setSessions(prev => prev.map(s =>
240
+ s.id === currentSessionId ? { ...s, name: newName } : s
241
+ ));
242
+ });
243
+ }
244
+
245
+ } catch (e) {
246
+ console.error(e);
247
+ } finally {
248
+ setIsProcessing(false);
249
+ setStatusMessage('');
250
+ }
251
+ };
252
+
253
+ const handleSummarizeChat = async () => {
254
+ if (chatHistory.length === 0) return;
255
+ setIsSummarizing(true);
256
+ try { setChatSummary(await summarizeConversation(chatHistory)); } catch (e) { console.error(e); } finally { setIsSummarizing(false); }
257
+ };
258
+
259
+ const handlePrintReport = () => window.print();
260
+ const handleResetData = () => { if(confirm('Reset all data?')) { localStorage.clear(); window.location.reload(); } };
261
+
262
+ const handleExportData = () => {
263
+ const data = { profile, medications, sessions, vitals, riskResult };
264
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
265
+ const url = URL.createObjectURL(blob);
266
+ const a = document.createElement('a');
267
+ a.href = url;
268
+ a.download = `somai_backup_${new Date().toISOString().split('T')[0]}.json`;
269
+ document.body.appendChild(a);
270
+ a.click();
271
+ document.body.removeChild(a);
272
+ URL.revokeObjectURL(url);
273
+ };
274
+
275
+ const printableRoot = document.getElementById('printable-root');
276
+
277
+ return (
278
+ <div className="min-h-screen bg-black text-white selection:bg-neon-green selection:text-black font-sans overflow-x-hidden flex flex-col md:block">
279
+ <Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
280
+
281
+ <div className="md:hidden p-4 border-b border-white/10 flex justify-between items-center bg-black/80 backdrop-blur-md sticky top-0 z-50">
282
+ <span className="font-mono font-bold text-white tracking-wider">SomAI</span>
283
+ <button onClick={() => setShowSettings(true)}><Settings size={20} className="text-gray-400" /></button>
284
  </div>
 
 
 
 
 
 
285
 
286
+ <button onClick={() => setShowSettings(true)} className="hidden md:block fixed top-6 right-6 z-30 p-2 text-gray-500 hover:text-white transition-colors bg-black/20 rounded-full hover:bg-white/10">
287
+ <Settings size={20} />
288
+ </button>
289
+
290
+ <main className="flex-1 md:ml-64 p-3 md:p-8 pt-4 md:pt-6 max-w-7xl mx-auto w-full md:min-h-screen flex flex-col">
291
+ <div className="md:hidden flex gap-2 mb-4 overflow-x-auto pb-2 scrollbar-hide shrink-0">
292
+ {[{ id: 'dashboard', label: 'Dashboard' }, { id: 'chat', label: 'Chat' }, { id: 'medication', label: 'Meds' }].map(t => (
293
+ <button key={t.id} onClick={() => setActiveTab(t.id as any)} className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-wide whitespace-nowrap transition-colors ${activeTab === t.id ? 'bg-neon-green text-black' : 'bg-gray-900 text-gray-400'}`}>{t.label}</button>
294
+ ))}
295
+ </div>
296
+
297
+ <div className="flex-1 min-h-0 flex flex-col">
298
+ {activeTab === 'dashboard' && (
299
+ <Dashboard
300
+ vitals={vitals}
301
+ setVitals={setVitals}
302
+ riskResult={riskResult}
303
+ chatSummary={chatSummary}
304
+ handleRunAnalysis={handleRunAnalysis}
305
+ isAnalyzing={isAnalyzing}
306
+ onPrint={handlePrintReport}
307
+ profile={profile}
308
+ setProfile={setProfile}
309
+ medications={medications}
310
+ chatHistory={chatHistory}
311
+ insights={insights}
312
+ statusMessage={statusMessage}
313
+ setStatusMessage={setStatusMessage}
314
+ />
315
+ )}
316
+ {activeTab === 'chat' && (
317
+ <Chat
318
+ sessions={sessions}
319
+ currentSessionId={currentSessionId}
320
+ onSwitchSession={setCurrentSessionId}
321
+ onCreateSession={createNewSession}
322
+ onRenameSession={renameSession}
323
+ onDeleteSession={deleteSession}
324
+ onSendMessage={handleSendMessage}
325
+ isProcessing={isProcessing}
326
+ statusMessage={statusMessage}
327
+ mode={mode}
328
+ setMode={setMode}
329
+ onSummarize={handleSummarizeChat}
330
+ isSummarizing={isSummarizing}
331
+ chatSummary={chatSummary}
332
+ />
333
+ )}
334
+ {activeTab === 'medication' && (
335
+ <MedicationTracker medications={medications} setMedications={setMedications} profile={profile} setProfile={setProfile} />
336
+ )}
337
+ </div>
338
+ </main>
339
 
340
+ {showSettings && (
341
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
342
+ <div className="glass-panel w-full max-w-md p-6 rounded-2xl border border-white/10 relative animate-in zoom-in-95">
343
+ <button onClick={() => setShowSettings(false)} className="absolute top-4 right-4 text-gray-500 hover:text-white"><X size={20} /></button>
344
+ <h2 className="text-xl font-bold text-white mb-6 flex items-center gap-2"><Settings className="text-neon-green" size={20} /> Settings</h2>
345
+ <div className="space-y-4">
346
+ <button onClick={handleExportData} className="w-full p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/5 flex items-center gap-3 text-left transition-all"><div className="p-2 bg-blue-500/10 rounded-lg text-blue-400"><Download size={18} /></div><div><h3 className="font-bold text-sm text-gray-200">Backup Data</h3><p className="text-xs text-gray-500">Export profile to JSON</p></div></button>
347
+ <button onClick={handlePrintReport} className="w-full p-4 rounded-xl bg-white/5 hover:bg-white/10 border border-white/5 flex items-center gap-3 text-left transition-all"><div className="p-2 bg-green-500/10 rounded-lg text-green-400"><FileText size={18} /></div><div><h3 className="font-bold text-sm text-gray-200">Download PDF Report</h3><p className="text-xs text-gray-500">Save full analysis & chat history</p></div></button>
348
+ <button onClick={handleResetData} className="w-full p-4 rounded-xl bg-red-500/10 hover:bg-red-500/20 border border-red-500/10 flex items-center gap-3 text-left transition-all"><div className="p-2 bg-red-500/10 rounded-lg text-red-400"><Trash2 size={18} /></div><div><h3 className="font-bold text-sm text-red-200">Reset Application</h3><p className="text-xs text-red-400/60">Delete all local data & profile</p></div></button>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ )}
353
+ {printableRoot && createPortal(<PrintReport profile={profile} vitals={vitals} riskResult={riskResult} chatHistory={chatHistory} chatSummary={chatSummary} medications={medications} />, printableRoot)}
354
+ </div>
355
+ );
356
+ };
357
+ export default App;