arshenoy commited on
Commit
84b6aa6
·
verified ·
1 Parent(s): 744989e

create somAI

Browse files
components/Chat.tsx ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useRef, useEffect, useState } from 'react';
3
+ import { MessageSquare, Send, ClipboardList, Activity, Mic, Volume2, Camera, X, ArrowLeft, Sparkles, ScanEye, Info } from 'lucide-react';
4
+ import { ChatMessage, AppMode } from '../types';
5
+ import { generateQuickReplies } from '../services/geminiService';
6
+
7
+ interface ChatProps {
8
+ chatHistory: ChatMessage[];
9
+ currentInput: string;
10
+ setCurrentInput: (val: string) => void;
11
+ onSendMessage: (image?: string) => void;
12
+ isProcessing: boolean;
13
+ mode: AppMode;
14
+ setMode: (mode: AppMode) => void;
15
+ onSummarize: () => void;
16
+ isSummarizing: boolean;
17
+ chatSummary?: string;
18
+ }
19
+
20
+ const Chat: React.FC<ChatProps> = ({
21
+ chatHistory,
22
+ currentInput,
23
+ setCurrentInput,
24
+ onSendMessage,
25
+ isProcessing,
26
+ mode,
27
+ setMode,
28
+ onSummarize,
29
+ isSummarizing,
30
+ chatSummary
31
+ }) => {
32
+ const scrollRef = useRef<HTMLDivElement>(null);
33
+ const fileInputRef = useRef<HTMLInputElement>(null);
34
+
35
+ const [isListening, setIsListening] = useState(false);
36
+ const [speakingId, setSpeakingId] = useState<string | null>(null);
37
+ const [viewMode, setViewMode] = useState<'chat' | 'summary'>('chat');
38
+ const [selectedImage, setSelectedImage] = useState<string | null>(null);
39
+ const [quickReplies, setQuickReplies] = useState<string[]>([]);
40
+ const [showVisionTip, setShowVisionTip] = useState(true);
41
+
42
+ useEffect(() => {
43
+ if (scrollRef.current) {
44
+ scrollRef.current.scrollIntoView({ behavior: 'smooth' });
45
+ }
46
+ }, [chatHistory, isProcessing, quickReplies]);
47
+
48
+ // Generate quick replies when AI responds
49
+ useEffect(() => {
50
+ const lastMsg = chatHistory[chatHistory.length - 1];
51
+ if (lastMsg && lastMsg.role === 'model') {
52
+ generateQuickReplies(lastMsg.text).then(setQuickReplies);
53
+ } else {
54
+ setQuickReplies([]);
55
+ }
56
+ }, [chatHistory]);
57
+
58
+ const handleSummarizeClick = async () => {
59
+ await onSummarize();
60
+ setViewMode('summary');
61
+ };
62
+
63
+ const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
64
+ const file = e.target.files?.[0];
65
+ if (file) {
66
+ const reader = new FileReader();
67
+ reader.onloadend = () => {
68
+ setSelectedImage(reader.result as string);
69
+ };
70
+ reader.readAsDataURL(file);
71
+ // Automatically hide tip if user uses feature
72
+ setShowVisionTip(false);
73
+ }
74
+ };
75
+
76
+ const handleSend = () => {
77
+ if (!currentInput.trim() && !selectedImage) return;
78
+ onSendMessage(selectedImage || undefined);
79
+ setSelectedImage(null);
80
+ setQuickReplies([]);
81
+ };
82
+
83
+ const speakText = (text: string, id: string) => {
84
+ if ('speechSynthesis' in window) {
85
+ if (speakingId === id) {
86
+ window.speechSynthesis.cancel();
87
+ setSpeakingId(null);
88
+ return;
89
+ }
90
+ window.speechSynthesis.cancel();
91
+ const utterance = new SpeechSynthesisUtterance(text);
92
+ utterance.onend = () => setSpeakingId(null);
93
+ setSpeakingId(id);
94
+ window.speechSynthesis.speak(utterance);
95
+ }
96
+ };
97
+
98
+ const toggleListening = () => {
99
+ if (!('webkitSpeechRecognition' in window)) {
100
+ alert("Voice input is not supported in this browser.");
101
+ return;
102
+ }
103
+
104
+ if (isListening) {
105
+ setIsListening(false);
106
+ return;
107
+ }
108
+
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
+ const recognition = new (window as any).webkitSpeechRecognition();
111
+ recognition.continuous = false;
112
+ recognition.interimResults = false;
113
+ recognition.lang = 'en-US';
114
+
115
+ recognition.onstart = () => setIsListening(true);
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ recognition.onresult = (event: any) => {
118
+ const transcript = event.results[0][0].transcript;
119
+ setCurrentInput(transcript);
120
+ setIsListening(false);
121
+ };
122
+ recognition.onerror = () => setIsListening(false);
123
+ recognition.onend = () => setIsListening(false);
124
+ recognition.start();
125
+ };
126
+
127
+ if (viewMode === 'summary') {
128
+ return (
129
+ <div className="animate-in fade-in slide-in-from-bottom-4 duration-500 h-full">
130
+ <div className="glass-panel rounded-2xl h-[calc(100vh-150px)] p-8 flex flex-col relative overflow-hidden">
131
+ <div className="absolute top-0 right-0 w-96 h-96 bg-neon-green/5 rounded-full blur-3xl -z-10 pointer-events-none"></div>
132
+
133
+ <div className="flex justify-between items-center mb-6 border-b border-white/10 pb-4">
134
+ <button
135
+ onClick={() => setViewMode('chat')}
136
+ className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors text-sm font-mono font-bold uppercase"
137
+ >
138
+ <ArrowLeft size={16} /> Back to Chat
139
+ </button>
140
+ <h2 className="text-xl font-bold text-white flex items-center gap-2">
141
+ <ClipboardList className="text-neon-yellow" /> Session Brief
142
+ </h2>
143
+ </div>
144
+
145
+ <div className="flex-1 overflow-y-auto">
146
+ {isSummarizing ? (
147
+ <div className="flex flex-col items-center justify-center h-64 text-gray-500 gap-4">
148
+ <Activity className="animate-spin text-neon-green" size={32} />
149
+ <p className="font-mono text-xs animate-pulse">ANALYZING CONVERSATION...</p>
150
+ </div>
151
+ ) : (
152
+ <div className="bg-white/5 p-6 rounded-xl border border-white/10 leading-relaxed text-gray-200 text-base whitespace-pre-wrap">
153
+ {chatSummary || "No summary available."}
154
+ </div>
155
+ )}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ return (
163
+ <div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
164
+ <div className="glass-panel rounded-2xl flex flex-col h-[calc(100vh-150px)] relative overflow-hidden">
165
+ {/* Header */}
166
+ <div className="p-4 bg-white/5 border-b border-white/5 flex justify-between items-center z-10">
167
+ <div className="flex gap-2">
168
+ <button
169
+ onClick={() => setMode(AppMode.GENERAL)}
170
+ className={`px-3 py-1.5 rounded-lg text-[10px] md:text-xs font-mono font-bold uppercase tracking-wider transition-all ${mode === AppMode.GENERAL ? 'bg-neon-blue text-black' : 'text-gray-400 hover:text-white'}`}
171
+ >
172
+ Medical Guide
173
+ </button>
174
+ <button
175
+ onClick={() => setMode(AppMode.THERAPY)}
176
+ className={`px-3 py-1.5 rounded-lg text-[10px] md:text-xs font-mono font-bold uppercase tracking-wider transition-all ${mode === AppMode.THERAPY ? 'bg-purple-500 text-white' : 'text-gray-400 hover:text-white'}`}
177
+ >
178
+ Therapist (CBT)
179
+ </button>
180
+ </div>
181
+ <button
182
+ onClick={handleSummarizeClick}
183
+ disabled={chatHistory.length === 0}
184
+ className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-mono font-bold transition-all border border-white/10 text-gray-300 hover:text-white hover:border-white/30 bg-white/5 disabled:opacity-50"
185
+ >
186
+ <ClipboardList size={16} /> <span className="hidden md:inline">BRIEF</span>
187
+ </button>
188
+ </div>
189
+
190
+ {/* Messages */}
191
+ <div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 relative">
192
+ {chatHistory.length === 0 && (
193
+ <div className="flex flex-col items-center justify-center h-full text-gray-600">
194
+ <div className="p-4 rounded-full bg-white/5 mb-4 relative">
195
+ <div className="absolute -top-1 -right-1 w-3 h-3 bg-neon-green rounded-full animate-pulse"></div>
196
+ <MessageSquare size={32} className="opacity-50" />
197
+ </div>
198
+ <p className="font-medium text-lg">Start a session with SomAI.</p>
199
+ <p className="text-xs mt-2 opacity-50 flex items-center gap-2">
200
+ <span className="flex items-center gap-1"><ScanEye size={12}/> Vision Enabled</span> •
201
+ <span>Secure</span> •
202
+ <span>Private</span>
203
+ </p>
204
+ </div>
205
+ )}
206
+
207
+ {chatHistory.map((msg) => (
208
+ <div key={msg.id} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
209
+ <div className={`max-w-[85%] md:max-w-[75%] rounded-2xl px-5 py-3 shadow-lg relative group ${
210
+ msg.role === 'user'
211
+ ? 'bg-neon-green/10 text-white border border-neon-green/20 rounded-tr-none'
212
+ : 'bg-black/40 text-gray-200 border border-white/10 rounded-tl-none'
213
+ }`}>
214
+ {msg.image && (
215
+ <div className="mb-3 rounded-lg overflow-hidden border border-white/10">
216
+ <img src={msg.image} alt="Upload" className="max-w-full h-auto max-h-64 object-cover" />
217
+ </div>
218
+ )}
219
+ <p className="text-sm leading-relaxed whitespace-pre-wrap">{msg.text}</p>
220
+
221
+ {msg.role === 'model' && (
222
+ <button
223
+ onClick={() => speakText(msg.text, msg.id)}
224
+ className={`absolute -right-10 top-2 p-2 rounded-full hover:bg-white/10 transition-all ${speakingId === msg.id ? 'text-neon-blue opacity-100' : 'text-gray-500 opacity-0 group-hover:opacity-100'}`}
225
+ >
226
+ <Volume2 size={16} className={speakingId === msg.id ? "animate-pulse" : ""} />
227
+ </button>
228
+ )}
229
+ </div>
230
+ </div>
231
+ ))}
232
+
233
+ {isProcessing && (
234
+ <div className="flex justify-start">
235
+ <div className="bg-black/20 px-4 py-3 rounded-2xl rounded-tl-none border border-white/5 flex gap-1.5 items-center">
236
+ <span className="w-1.5 h-1.5 bg-neon-green rounded-full animate-bounce"></span>
237
+ <span className="w-1.5 h-1.5 bg-neon-green rounded-full animate-bounce delay-100"></span>
238
+ <span className="w-1.5 h-1.5 bg-neon-green rounded-full animate-bounce delay-200"></span>
239
+ </div>
240
+ </div>
241
+ )}
242
+ <div ref={scrollRef}></div>
243
+ </div>
244
+
245
+ {/* Quick Replies */}
246
+ {!isProcessing && quickReplies.length > 0 && (
247
+ <div className="px-4 pb-2 flex gap-2 overflow-x-auto scrollbar-hide">
248
+ {quickReplies.map((reply, i) => (
249
+ <button
250
+ key={i}
251
+ onClick={() => { setCurrentInput(reply); handleSend(); }}
252
+ className="whitespace-nowrap px-3 py-1.5 rounded-full bg-white/5 border border-white/10 text-xs text-gray-300 hover:bg-neon-blue/20 hover:text-neon-blue hover:border-neon-blue/30 transition-all flex items-center gap-1"
253
+ >
254
+ <Sparkles size={10} /> {reply}
255
+ </button>
256
+ ))}
257
+ </div>
258
+ )}
259
+
260
+ {/* Input Area */}
261
+ <div className="p-4 bg-black/50 border-t border-white/5 z-20 relative">
262
+
263
+ {/* SomAI Vision Tip */}
264
+ {showVisionTip && !selectedImage && (
265
+ <div className="absolute -top-12 left-4 z-30 animate-in fade-in slide-in-from-bottom-2">
266
+ <div className="bg-neon-blue/10 backdrop-blur-md border border-neon-blue/30 text-neon-blue text-[10px] md:text-xs px-3 py-2 rounded-lg shadow-lg flex items-center gap-2">
267
+ <ScanEye size={14} />
268
+ <span><span className="font-bold">SomAI Vision:</span> Upload nutrition labels, skin symptoms, or reports for analysis.</span>
269
+ <button onClick={() => setShowVisionTip(false)} className="hover:text-white ml-2"><X size={12}/></button>
270
+ </div>
271
+ {/* Arrow */}
272
+ <div className="absolute -bottom-1 left-4 w-2 h-2 bg-neon-blue/10 border-b border-r border-neon-blue/30 transform rotate-45"></div>
273
+ </div>
274
+ )}
275
+
276
+ {selectedImage && (
277
+ <div className="flex items-center gap-2 mb-3 bg-white/5 p-2 rounded-lg w-fit border border-white/10">
278
+ <div className="w-8 h-8 rounded bg-gray-800 overflow-hidden">
279
+ <img src={selectedImage} alt="Selected" className="w-full h-full object-cover" />
280
+ </div>
281
+ <span className="text-xs text-gray-400">Image attached</span>
282
+ <button onClick={() => setSelectedImage(null)} className="p-1 hover:bg-white/10 rounded-full text-gray-500 hover:text-white ml-2">
283
+ <X size={14} />
284
+ </button>
285
+ </div>
286
+ )}
287
+ <div className="flex items-center gap-3">
288
+ <input
289
+ type="file"
290
+ accept="image/*"
291
+ className="hidden"
292
+ ref={fileInputRef}
293
+ onChange={handleImageSelect}
294
+ />
295
+ <button
296
+ onClick={() => fileInputRef.current?.click()}
297
+ className={`p-3 rounded-xl transition-all relative ${selectedImage ? 'bg-neon-blue/20 text-neon-blue' : 'bg-white/5 text-gray-400 hover:text-white hover:bg-white/10'}`}
298
+ title="SomAI Vision"
299
+ >
300
+ <Camera size={20} />
301
+ </button>
302
+
303
+ <div className="flex-1 relative">
304
+ <input
305
+ value={currentInput}
306
+ onChange={(e) => setCurrentInput(e.target.value)}
307
+ onKeyDown={(e) => e.key === 'Enter' && handleSend()}
308
+ placeholder={isListening ? "Listening..." : "Message SomAI..."}
309
+ className="w-full bg-black/40 border border-white/10 rounded-xl py-3 pl-4 pr-10 text-white placeholder-gray-600 focus:border-neon-blue outline-none transition-all"
310
+ />
311
+ <button
312
+ onClick={toggleListening}
313
+ className={`absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-lg transition-all ${isListening ? 'text-red-500 animate-pulse' : 'text-gray-500 hover:text-white'}`}
314
+ >
315
+ <Mic size={18} />
316
+ </button>
317
+ </div>
318
+
319
+ <button
320
+ onClick={handleSend}
321
+ disabled={(!currentInput.trim() && !selectedImage) || isProcessing}
322
+ className="p-3 rounded-xl bg-neon-blue text-black font-bold hover:bg-neon-blue/90 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_15px_rgba(0,204,255,0.3)]"
323
+ >
324
+ <Send size={20} />
325
+ </button>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ );
331
+ };
332
+
333
+ export default Chat;
components/Dashboard.tsx ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { Activity, HeartPulse, FileText, ShieldCheck } from 'lucide-react';
4
+ import { ClinicalVitals, RiskAnalysisResult, PatientProfile, Medication, ChatMessage } from '../types';
5
+ import GaugeChart from './GaugeChart';
6
+ import ReportView from './ReportView';
7
+
8
+ interface DashboardProps {
9
+ vitals: ClinicalVitals;
10
+ setVitals: React.Dispatch<React.SetStateAction<ClinicalVitals>>;
11
+ riskResult: RiskAnalysisResult | null;
12
+ chatSummary: string;
13
+ handleRunAnalysis: () => void;
14
+ isAnalyzing: boolean;
15
+ onPrint: () => void;
16
+ // New props for full report
17
+ profile: PatientProfile;
18
+ medications: Medication[];
19
+ chatHistory: ChatMessage[];
20
+ }
21
+
22
+ const Dashboard: React.FC<DashboardProps> = ({
23
+ vitals,
24
+ setVitals,
25
+ riskResult,
26
+ chatSummary,
27
+ handleRunAnalysis,
28
+ isAnalyzing,
29
+ onPrint,
30
+ profile,
31
+ medications,
32
+ chatHistory
33
+ }) => {
34
+ const handleVitalChange = (key: keyof ClinicalVitals, val: number) => {
35
+ setVitals(prev => ({ ...prev, [key]: val }));
36
+ };
37
+
38
+ return (
39
+ <div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
40
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
41
+ {/* LEFT COLUMN: CONTROLS */}
42
+ <div className="space-y-6">
43
+ <div className="glass-panel p-6 rounded-2xl border-t border-neon-green">
44
+ <h2 className="text-lg font-bold mb-6 flex items-center gap-2 text-white">
45
+ <Activity className="text-neon-green" size={20} /> Clinical Vitals Input
46
+ </h2>
47
+
48
+ <div className="space-y-6">
49
+ <div>
50
+ <div className="flex justify-between text-sm mb-2">
51
+ <span className="text-gray-400 font-medium">Systolic BP</span>
52
+ <span className="font-mono text-neon-green font-bold">{vitals.systolicBp} mmHg</span>
53
+ </div>
54
+ <input
55
+ type="range" min="90" max="200"
56
+ value={vitals.systolicBp}
57
+ onChange={e => handleVitalChange('systolicBp', parseInt(e.target.value))}
58
+ className="w-full h-2 bg-black rounded-lg appearance-none cursor-pointer accent-neon-green"
59
+ />
60
+ </div>
61
+
62
+ <div>
63
+ <div className="flex justify-between text-sm mb-2">
64
+ <span className="text-gray-400 font-medium">Glucose</span>
65
+ <span className="font-mono text-neon-yellow font-bold">{vitals.glucose} mg/dL</span>
66
+ </div>
67
+ <input
68
+ type="range" min="70" max="300"
69
+ value={vitals.glucose}
70
+ onChange={e => handleVitalChange('glucose', parseInt(e.target.value))}
71
+ className="w-full h-2 bg-black rounded-lg appearance-none cursor-pointer accent-neon-yellow"
72
+ />
73
+ </div>
74
+
75
+ <div>
76
+ <div className="flex justify-between text-sm mb-2">
77
+ <span className="text-gray-400 font-medium">Sleep Quality</span>
78
+ <span className="font-mono text-neon-blue font-bold">{vitals.sleepQuality}/10</span>
79
+ </div>
80
+ <input
81
+ type="range" min="0" max="10"
82
+ value={vitals.sleepQuality}
83
+ onChange={e => handleVitalChange('sleepQuality', parseInt(e.target.value))}
84
+ className="w-full h-2 bg-black rounded-lg appearance-none cursor-pointer accent-neon-blue"
85
+ />
86
+ </div>
87
+
88
+ <div>
89
+ <div className="flex justify-between text-sm mb-2">
90
+ <span className="text-gray-400 font-medium">Missed Doses (7d)</span>
91
+ <span className="font-mono text-neon-red font-bold">{vitals.missedDoses}</span>
92
+ </div>
93
+ <input
94
+ type="range" min="0" max="14"
95
+ value={vitals.missedDoses}
96
+ onChange={e => handleVitalChange('missedDoses', parseInt(e.target.value))}
97
+ className="w-full h-2 bg-black rounded-lg appearance-none cursor-pointer accent-neon-red"
98
+ />
99
+ </div>
100
+
101
+ <button
102
+ onClick={handleRunAnalysis}
103
+ disabled={isAnalyzing}
104
+ className="w-full mt-4 bg-neon-green hover:bg-neon-green/90 text-black py-4 rounded-xl font-mono font-bold transition-all flex justify-center items-center gap-2 shadow-[0_0_20px_rgba(0,255,128,0.2)] disabled:opacity-50"
105
+ >
106
+ {isAnalyzing ? <span className="animate-spin">⏳</span> : <HeartPulse size={20} />}
107
+ {isAnalyzing ? 'PROCESSING...' : 'RUN ANALYSIS'}
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ {/* RIGHT COLUMN: ANALYSIS */}
114
+ <div className="space-y-6">
115
+ <div className="glass-panel p-6 rounded-2xl min-h-[500px] flex flex-col">
116
+ <div className="flex justify-between items-center mb-4">
117
+ <h3 className="text-xl font-bold text-white">SomAI Analysis</h3>
118
+ <button
119
+ onClick={onPrint}
120
+ className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-mono font-bold transition-all border border-white/10 text-gray-300 hover:text-white hover:border-white/30 bg-white/5"
121
+ >
122
+ <FileText size={14} />
123
+ EXPORT PDF
124
+ </button>
125
+ </div>
126
+
127
+ {riskResult ? (
128
+ <div className="flex flex-col gap-6 animate-in fade-in">
129
+ <div className="flex flex-col md:flex-row items-center gap-6">
130
+ <div className="w-full md:w-56">
131
+ <GaugeChart value={riskResult.numericScore} />
132
+ </div>
133
+ <div className="flex-1">
134
+ <h4 className="text-xs text-neon-blue font-bold uppercase mb-2">Clinical Summary</h4>
135
+ <p className="text-base text-gray-200 leading-relaxed">{riskResult.summary}</p>
136
+ </div>
137
+ </div>
138
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
139
+ <div className="glass-card p-4 rounded-xl">
140
+ <h4 className="text-xs font-mono text-gray-500 uppercase mb-3 font-bold">Recommended Actions</h4>
141
+ <ul className="space-y-2">
142
+ {riskResult.actionItems.map((item, i) => (
143
+ <li key={i} className="flex items-start gap-2 text-sm text-gray-300">
144
+ <span className="text-neon-green mt-1">✓</span> {item}
145
+ </li>
146
+ ))}
147
+ </ul>
148
+ </div>
149
+ <div className="glass-card p-4 rounded-xl">
150
+ <h4 className="text-xs font-mono text-gray-500 uppercase mb-3 font-bold flex items-center gap-2">
151
+ <ShieldCheck size={14} className="text-purple-400"/> Medical Coding Pipeline
152
+ </h4>
153
+ <div className="space-y-2">
154
+ {riskResult.codingPipeline?.map((code, i) => (
155
+ <div key={i} className="flex justify-between items-center text-xs border-b border-white/5 pb-1 last:border-0">
156
+ <span className="text-gray-400">{code.description}</span>
157
+ <span className="font-mono text-purple-300 bg-purple-500/10 px-1.5 py-0.5 rounded">{code.code}</span>
158
+ </div>
159
+ ))}
160
+ {(!riskResult.codingPipeline || riskResult.codingPipeline.length === 0) && (
161
+ <div className="flex flex-wrap gap-2">
162
+ {riskResult.icd10Codes.map((code, i) => (
163
+ <span key={i} className="px-2 py-1 bg-white/5 rounded text-xs text-purple-300 font-mono">
164
+ {code}
165
+ </span>
166
+ ))}
167
+ </div>
168
+ )}
169
+ </div>
170
+ {riskResult.insuranceNote && (
171
+ <div className="mt-3 pt-3 border-t border-white/5">
172
+ <p className="text-[10px] text-gray-500 uppercase font-bold mb-1">Insurance Justification</p>
173
+ <p className="text-xs text-gray-300 italic">"{riskResult.insuranceNote}"</p>
174
+ </div>
175
+ )}
176
+ </div>
177
+ </div>
178
+ </div>
179
+ ) : (
180
+ <div className="flex-1 flex flex-col items-center justify-center text-gray-600 border-2 border-dashed border-white/10 rounded-xl">
181
+ <Activity size={48} className="mb-4 opacity-30" />
182
+ <p className="font-medium text-lg text-gray-500">Awaiting Clinical Data</p>
183
+ <p className="text-sm mt-2 opacity-50">Input vitals on the left to generate analysis</p>
184
+ </div>
185
+ )}
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ {/* FULL REPORT PREVIEW SECTION */}
191
+ <ReportView
192
+ profile={profile}
193
+ vitals={vitals}
194
+ riskResult={riskResult}
195
+ chatHistory={chatHistory}
196
+ chatSummary={chatSummary}
197
+ medications={medications}
198
+ />
199
+ </div>
200
+ );
201
+ };
202
+
203
+ export default Dashboard;
components/GaugeChart.tsx ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
3
+
4
+ interface GaugeChartProps {
5
+ value: number; // 0 to 100
6
+ }
7
+
8
+ const GaugeChart: React.FC<GaugeChartProps> = ({ value }) => {
9
+ const data = [
10
+ { name: 'Low', value: 40, color: '#00ff80' },
11
+ { name: 'Medium', value: 40, color: '#ffc300' },
12
+ { name: 'High', value: 20, color: '#ff3300' },
13
+ ];
14
+
15
+ const cx = "50%";
16
+ const cy = "70%";
17
+ const iR = 60;
18
+ const oR = 80;
19
+
20
+ const needleValue = Math.min(Math.max(value, 0), 100);
21
+ // Calculate needle rotation
22
+ const needleAngle = 180 - (needleValue / 100) * 180;
23
+
24
+ return (
25
+ // Added min-w-[200px] and min-h to prevent Recharts -1 width calculation error
26
+ <div className="relative h-48 w-full min-w-[200px] flex flex-col items-center justify-center overflow-hidden">
27
+ <ResponsiveContainer width="100%" height="100%">
28
+ <PieChart>
29
+ <Pie
30
+ dataKey="value"
31
+ startAngle={180}
32
+ endAngle={0}
33
+ data={data}
34
+ cx={cx}
35
+ cy={cy}
36
+ innerRadius={iR}
37
+ outerRadius={oR}
38
+ paddingAngle={2}
39
+ stroke="none"
40
+ >
41
+ {data.map((entry, index) => (
42
+ <Cell key={`cell-${index}`} fill={entry.color} fillOpacity={0.3} stroke={entry.color} strokeWidth={1} />
43
+ ))}
44
+ </Pie>
45
+ </PieChart>
46
+ </ResponsiveContainer>
47
+
48
+ {/* Needle Overlay */}
49
+ <div className="absolute top-0 left-0 w-full h-full pointer-events-none flex items-end justify-center pb-[30%]">
50
+ <div
51
+ className="w-1 h-16 bg-white origin-bottom transition-transform duration-1000 ease-out shadow-[0_0_10px_rgba(255,255,255,0.8)]"
52
+ style={{ transform: `rotate(${needleValue * 1.8 - 90}deg)` }}
53
+ />
54
+ <div className="absolute w-4 h-4 bg-white rounded-full shadow-[0_0_10px_rgba(255,255,255,0.8)]" />
55
+ </div>
56
+
57
+ <div className="absolute bottom-4 flex flex-col items-center">
58
+ <span className="text-4xl font-mono font-bold text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.3)]">
59
+ {Math.round(value)}
60
+ </span>
61
+ <span className="text-xs text-gray-400 uppercase tracking-widest mt-1">Risk Score</span>
62
+ </div>
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default GaugeChart;
components/MedicationTracker.tsx ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useCallback } from 'react';
3
+ import { Plus, Trash2, CheckCircle, Trophy, AlertTriangle, Quote, Calendar, Edit2, X, Save } from 'lucide-react';
4
+ import { Medication, PatientProfile } from '../types';
5
+
6
+ interface MedicationTrackerProps {
7
+ medications: Medication[];
8
+ setMedications: React.Dispatch<React.SetStateAction<Medication[]>>;
9
+ profile: PatientProfile;
10
+ setProfile: React.Dispatch<React.SetStateAction<PatientProfile>>;
11
+ }
12
+
13
+ const MOTIVATIONAL_QUOTES = [
14
+ "Success is stumbling from failure to failure with no loss of enthusiasm. – Winston Churchill",
15
+ "Fall seven times, stand up eight. – Japanese Proverb",
16
+ "The only real mistake is the one from which we learn nothing. – Henry Ford",
17
+ "It does not matter how slowly you go as long as you do not stop. – Confucius"
18
+ ];
19
+
20
+ // Memoized item to prevent lag when typing in form
21
+ const MedicationItem = React.memo(({ med, editingId, onToggle, onEdit, onDelete }: {
22
+ med: Medication;
23
+ editingId: string | null;
24
+ onToggle: (id: string) => void;
25
+ onEdit: (med: Medication) => void;
26
+ onDelete: (id: string) => void;
27
+ }) => (
28
+ <div className={`glass-card p-4 rounded-xl flex justify-between items-center group transition-all relative ${editingId === med.id ? 'border-neon-yellow/50 bg-neon-yellow/5' : (med.taken ? 'border-green-500/30 bg-green-500/5' : 'border-white/5')}`}>
29
+ <div className="flex items-center gap-4">
30
+ <button
31
+ onClick={() => onToggle(med.id)}
32
+ className={`w-8 h-8 rounded-full flex items-center justify-center border transition-all ${
33
+ med.taken
34
+ ? 'bg-green-500 border-green-500 text-black'
35
+ : 'border-gray-500 text-transparent hover:border-green-400'
36
+ }`}
37
+ >
38
+ <CheckCircle size={16} />
39
+ </button>
40
+ <div>
41
+ <p className={`font-bold ${med.taken ? 'text-green-400 line-through' : 'text-white'}`}>{med.name}</p>
42
+ <div className="flex flex-col gap-0.5 mt-1">
43
+ <span className="text-xs text-gray-400">{med.dosage} • {med.time}</span>
44
+ {(med.startDate || med.endDate) && (
45
+ <span className="text-[10px] text-gray-500 flex items-center gap-1">
46
+ <Calendar size={10} /> {med.startDate || 'Now'} → {med.endDate || 'Ongoing'}
47
+ </span>
48
+ )}
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
54
+ <button
55
+ onClick={() => onEdit(med)}
56
+ className="p-2 rounded-lg text-gray-400 hover:text-neon-yellow hover:bg-neon-yellow/10 transition-colors"
57
+ title="Edit"
58
+ >
59
+ <Edit2 size={16} />
60
+ </button>
61
+ <button
62
+ onClick={() => onDelete(med.id)}
63
+ className="p-2 rounded-lg text-gray-400 hover:text-red-400 hover:bg-red-400/10 transition-colors"
64
+ title="Delete"
65
+ >
66
+ <Trash2 size={16} />
67
+ </button>
68
+ </div>
69
+ </div>
70
+ ));
71
+
72
+ const MedicationTracker: React.FC<MedicationTrackerProps> = ({
73
+ medications,
74
+ setMedications,
75
+ profile,
76
+ setProfile
77
+ }) => {
78
+ const [newMedName, setNewMedName] = useState('');
79
+ const [newMedDosage, setNewMedDosage] = useState('');
80
+ const [newMedTime, setNewMedTime] = useState('09:00');
81
+ const [startDate, setStartDate] = useState('');
82
+ const [endDate, setEndDate] = useState('');
83
+
84
+ // Edit Mode State
85
+ const [editingId, setEditingId] = useState<string | null>(null);
86
+
87
+ const resetForm = () => {
88
+ setNewMedName('');
89
+ setNewMedDosage('');
90
+ setNewMedTime('09:00');
91
+ setStartDate('');
92
+ setEndDate('');
93
+ setEditingId(null);
94
+ };
95
+
96
+ const handleEdit = useCallback((med: Medication) => {
97
+ setNewMedName(med.name);
98
+ setNewMedDosage(med.dosage);
99
+ setNewMedTime(med.time);
100
+ setStartDate(med.startDate || '');
101
+ setEndDate(med.endDate || '');
102
+ setEditingId(med.id);
103
+ }, []);
104
+
105
+ const handleUpdate = () => {
106
+ if (!editingId || !newMedName || !newMedDosage) return;
107
+
108
+ setMedications(prev => prev.map(med => {
109
+ if (med.id === editingId) {
110
+ return {
111
+ ...med,
112
+ name: newMedName,
113
+ dosage: newMedDosage,
114
+ time: newMedTime,
115
+ startDate: startDate || undefined,
116
+ endDate: endDate || undefined
117
+ };
118
+ }
119
+ return med;
120
+ }));
121
+ resetForm();
122
+ };
123
+
124
+ const addMedication = () => {
125
+ if (!newMedName || !newMedDosage) return;
126
+ const newMed: Medication = {
127
+ id: Date.now().toString(),
128
+ name: newMedName,
129
+ dosage: newMedDosage,
130
+ time: newMedTime,
131
+ taken: false,
132
+ startDate: startDate || undefined,
133
+ endDate: endDate || undefined
134
+ };
135
+ setMedications(prev => [...prev, newMed]);
136
+ resetForm();
137
+ };
138
+
139
+ const removeMedication = useCallback((id: string) => {
140
+ setMedications(prev => prev.filter(m => m.id !== id));
141
+ if (editingId === id) resetForm();
142
+ }, [editingId, setMedications]);
143
+
144
+ const toggleTaken = useCallback((id: string) => {
145
+ // 1. Calculate new state immediately
146
+ const updatedMeds = medications.map(m => {
147
+ if (m.id === id) {
148
+ return { ...m, taken: !m.taken };
149
+ }
150
+ return m;
151
+ });
152
+
153
+ // 2. Determine Streak Changes
154
+ const allTakenNow = updatedMeds.length > 0 && updatedMeds.every(m => m.taken);
155
+
156
+ const today = new Date();
157
+ today.setHours(0,0,0,0);
158
+ const todayStr = today.toISOString();
159
+
160
+ const lastUpdate = new Date(profile.lastStreakUpdate);
161
+ lastUpdate.setHours(0,0,0,0);
162
+ const lastUpdateStr = lastUpdate.toISOString();
163
+
164
+ const isToday = today.getTime() === lastUpdate.getTime();
165
+
166
+ let newStreak = profile.streak;
167
+ let newLastUpdate = profile.lastStreakUpdate;
168
+
169
+ if (allTakenNow) {
170
+ if (!isToday) {
171
+ // First time finishing today -> Increment
172
+ newStreak += 1;
173
+ newLastUpdate = new Date().toISOString(); // store full timestamp for exactness
174
+ }
175
+ } else {
176
+ // Not all taken
177
+ if (isToday) {
178
+ // If we previously marked today as done, we need to revert it.
179
+ // Decrement streak (min 0)
180
+ newStreak = Math.max(0, newStreak - 1);
181
+ // Set update date to yesterday so if they re-complete it, it counts as "new" for today
182
+ const yesterday = new Date(today);
183
+ yesterday.setDate(yesterday.getDate() - 1);
184
+ newLastUpdate = yesterday.toISOString();
185
+ }
186
+ }
187
+
188
+ // 3. Batch Updates
189
+ setMedications(updatedMeds);
190
+ if (newStreak !== profile.streak || newLastUpdate !== profile.lastStreakUpdate) {
191
+ setProfile(prev => ({
192
+ ...prev,
193
+ streak: newStreak,
194
+ lastStreakUpdate: newLastUpdate
195
+ }));
196
+ }
197
+ }, [medications, profile.streak, profile.lastStreakUpdate, setMedications, setProfile]);
198
+
199
+ const pendingMeds = medications.filter(m => !m.taken).length;
200
+ const randomQuote = MOTIVATIONAL_QUOTES[Math.floor(Math.random() * MOTIVATIONAL_QUOTES.length)];
201
+
202
+ return (
203
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
204
+
205
+ <div className="space-y-6">
206
+ <div className={`glass-panel p-6 rounded-2xl border-t ${editingId ? 'border-neon-yellow' : 'border-neon-blue'} transition-colors`}>
207
+ <div className="flex justify-between items-center mb-4">
208
+ <h2 className={`text-xl font-bold transition-colors flex items-center gap-2 ${editingId ? 'text-neon-yellow' : 'text-white'}`}>
209
+ {editingId ? <Edit2 size={20} /> : <Plus className="text-neon-blue" size={20} />}
210
+ {editingId ? 'Edit Medication' : 'Add Medication'}
211
+ </h2>
212
+ {editingId && (
213
+ <button onClick={resetForm} className="text-xs text-gray-400 hover:text-white flex items-center gap-1">
214
+ <X size={14} /> Cancel
215
+ </button>
216
+ )}
217
+ </div>
218
+
219
+ <div className="grid grid-cols-2 gap-4 mb-4">
220
+ <input
221
+ value={newMedName} onChange={e => setNewMedName(e.target.value)}
222
+ placeholder="Medication Name"
223
+ className="bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:border-neon-blue outline-none col-span-2"
224
+ />
225
+ <input
226
+ value={newMedDosage} onChange={e => setNewMedDosage(e.target.value)}
227
+ placeholder="Dosage (e.g. 10mg)"
228
+ className="bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:border-neon-blue outline-none"
229
+ />
230
+ <input
231
+ type="time"
232
+ value={newMedTime} onChange={e => setNewMedTime(e.target.value)}
233
+ className="bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:border-neon-blue outline-none"
234
+ />
235
+
236
+ <div className="col-span-2 grid grid-cols-2 gap-4">
237
+ <div>
238
+ <label className="text-[10px] text-gray-500 uppercase font-bold mb-1 block">Start Date</label>
239
+ <input
240
+ type="date"
241
+ value={startDate} onChange={e => setStartDate(e.target.value)}
242
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:border-neon-blue outline-none"
243
+ />
244
+ </div>
245
+ <div>
246
+ <label className="text-[10px] text-gray-500 uppercase font-bold mb-1 block">Finish Date (Opt)</label>
247
+ <input
248
+ type="date"
249
+ value={endDate} onChange={e => setEndDate(e.target.value)}
250
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:border-neon-blue outline-none"
251
+ />
252
+ </div>
253
+ </div>
254
+
255
+ {editingId ? (
256
+ <button
257
+ onClick={handleUpdate}
258
+ disabled={!newMedName}
259
+ className="bg-neon-yellow text-black font-bold rounded-lg py-3 hover:bg-neon-yellow/90 transition-colors flex items-center justify-center gap-2 disabled:opacity-50 col-span-2 mt-2 shadow-[0_0_15px_rgba(255,195,0,0.2)]"
260
+ >
261
+ <Save size={18} /> Update Medication
262
+ </button>
263
+ ) : (
264
+ <button
265
+ onClick={addMedication}
266
+ disabled={!newMedName}
267
+ className="bg-neon-blue text-black font-bold rounded-lg py-3 hover:bg-neon-blue/90 transition-colors flex items-center justify-center gap-2 disabled:opacity-50 col-span-2 mt-2 shadow-[0_0_15px_rgba(0,204,255,0.2)]"
268
+ >
269
+ <Plus size={18} /> Add to Schedule
270
+ </button>
271
+ )}
272
+ </div>
273
+ </div>
274
+
275
+ <div className="space-y-3">
276
+ {medications.map(med => (
277
+ <MedicationItem
278
+ key={med.id}
279
+ med={med}
280
+ editingId={editingId}
281
+ onToggle={toggleTaken}
282
+ onEdit={handleEdit}
283
+ onDelete={removeMedication}
284
+ />
285
+ ))}
286
+ {medications.length === 0 && (
287
+ <p className="text-center text-gray-500 text-sm py-4 italic">No medications tracked.</p>
288
+ )}
289
+ </div>
290
+ </div>
291
+
292
+ <div className="space-y-6">
293
+ <div className="glass-panel p-8 rounded-2xl text-center relative overflow-hidden flex flex-col items-center justify-center min-h-[300px]">
294
+ <div className={`absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent ${profile.streak > 0 ? 'via-yellow-500' : 'via-gray-500'} to-transparent`}></div>
295
+
296
+ <Trophy size={64} className={`${profile.streak > 0 ? 'text-yellow-400 drop-shadow-[0_0_10px_rgba(234,179,8,0.5)]' : 'text-gray-700'} mb-4 transition-colors duration-500`} />
297
+
298
+ <h3 className={`text-4xl font-mono font-bold ${profile.streak > 0 ? 'text-white' : 'text-gray-500'} mb-2`}>{profile.streak} Days</h3>
299
+ <p className="text-gray-400/80 font-bold uppercase tracking-widest text-xs">Current Streak</p>
300
+
301
+ {profile.streak === 0 && (
302
+ <div className="mt-8 bg-white/5 p-4 rounded-xl border border-white/5">
303
+ <Quote size={16} className="text-gray-500 mx-auto mb-2" />
304
+ <p className="text-sm text-gray-300 italic">"{randomQuote}"</p>
305
+ </div>
306
+ )}
307
+
308
+ {profile.streak > 0 && (
309
+ <p className="mt-6 text-gray-400 text-sm max-w-xs">
310
+ Keep going! You are building a healthier future.
311
+ </p>
312
+ )}
313
+ </div>
314
+
315
+ {pendingMeds > 0 && (
316
+ <div className="glass-card p-4 rounded-xl border-l-4 border-l-neon-red flex items-start gap-4">
317
+ <div className="p-2 bg-neon-red/10 rounded-lg text-neon-red">
318
+ <AlertTriangle size={24} />
319
+ </div>
320
+ <div>
321
+ <h4 className="font-bold text-white">Missed Doses Pending</h4>
322
+ <p className="text-sm text-gray-400 mt-1">You have <span className="text-neon-red font-bold">{pendingMeds}</span> medication(s) pending for today.</p>
323
+ </div>
324
+ </div>
325
+ )}
326
+ </div>
327
+
328
+ </div>
329
+ );
330
+ };
331
+
332
+ export default MedicationTracker;
components/PrintReport.tsx ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { PatientProfile, ClinicalVitals, RiskAnalysisResult, ChatMessage, Medication } from '../types';
4
+ import { Activity, Pill, AlertTriangle, CheckCircle, MessageSquare, ClipboardList, User, ShieldCheck } from 'lucide-react';
5
+
6
+ interface PrintReportProps {
7
+ profile: PatientProfile;
8
+ vitals: ClinicalVitals;
9
+ riskResult: RiskAnalysisResult | null;
10
+ chatHistory: ChatMessage[];
11
+ chatSummary: string;
12
+ medications?: Medication[];
13
+ }
14
+
15
+ const PrintReport: React.FC<PrintReportProps> = ({
16
+ profile,
17
+ vitals,
18
+ riskResult,
19
+ chatHistory,
20
+ chatSummary,
21
+ medications = []
22
+ }) => {
23
+ return (
24
+ <div className="p-8 max-w-[210mm] mx-auto bg-white text-black font-sans leading-normal">
25
+ {/* Header */}
26
+ <header className="flex justify-between items-start border-b-4 border-black pb-6 mb-8">
27
+ <div>
28
+ <h1 className="text-4xl font-black text-black tracking-tight flex items-center gap-3">
29
+ <Activity className="text-black" size={32} />
30
+ SomAI <span className="text-gray-400 font-light">Report</span>
31
+ </h1>
32
+ <p className="text-gray-600 text-sm mt-1 font-medium tracking-wide uppercase">Patient Education & Clinical Risk Assessment</p>
33
+ </div>
34
+ <div className="text-right">
35
+ <div className="inline-block bg-black text-white px-3 py-1 text-xs font-bold uppercase tracking-wider mb-2">Confidential</div>
36
+ <p className="text-gray-900 font-mono text-sm">{new Date().toLocaleDateString()}</p>
37
+ <p className="text-gray-500 text-xs">{new Date().toLocaleTimeString()}</p>
38
+ </div>
39
+ </header>
40
+
41
+ {/* Patient Profile & Vitals Grid */}
42
+ <div className="grid grid-cols-2 gap-8 mb-8">
43
+ {/* Patient Info */}
44
+ <section className="break-inside-avoid">
45
+ <h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wider text-black border-b border-gray-200 pb-2 mb-4">
46
+ <User size={16} /> Patient Demographics
47
+ </h2>
48
+ <div className="bg-gray-50 p-4 rounded-xl border border-gray-100 space-y-3">
49
+ <div className="grid grid-cols-3 text-sm">
50
+ <span className="text-gray-500 font-medium">Name</span>
51
+ <span className="col-span-2 font-bold text-gray-900">{profile.name}</span>
52
+ </div>
53
+ <div className="grid grid-cols-3 text-sm">
54
+ <span className="text-gray-500 font-medium">Age</span>
55
+ <span className="col-span-2 font-bold text-gray-900">{profile.age} years</span>
56
+ </div>
57
+ <div className="grid grid-cols-3 text-sm">
58
+ <span className="text-gray-500 font-medium">Condition</span>
59
+ <span className="col-span-2 font-bold text-gray-900">{profile.condition}</span>
60
+ </div>
61
+ <div className="grid grid-cols-3 text-sm">
62
+ <span className="text-gray-500 font-medium">Allergies</span>
63
+ <span className="col-span-2 font-bold text-red-600">{profile.allergies}</span>
64
+ </div>
65
+ <div className="pt-2 border-t border-gray-200 mt-2">
66
+ <span className="block text-xs font-bold text-gray-400 uppercase mb-1">History</span>
67
+ <p className="text-sm text-gray-700 leading-snug">{profile.history}</p>
68
+ </div>
69
+ </div>
70
+ </section>
71
+
72
+ {/* Clinical Snapshot */}
73
+ <section className="break-inside-avoid">
74
+ <h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wider text-black border-b border-gray-200 pb-2 mb-4">
75
+ <Activity size={16} /> Clinical Vitals
76
+ </h2>
77
+ <div className="grid grid-cols-2 gap-3">
78
+ <div className="bg-gray-50 p-3 rounded-lg border border-gray-100 flex flex-col items-center justify-center text-center">
79
+ <span className="text-[10px] font-bold text-gray-400 uppercase">Systolic BP</span>
80
+ <span className={`text-2xl font-black ${vitals.systolicBp > 140 ? 'text-red-600' : 'text-gray-900'}`}>{vitals.systolicBp}</span>
81
+ <span className="text-[10px] text-gray-500">mmHg</span>
82
+ </div>
83
+ <div className="bg-gray-50 p-3 rounded-lg border border-gray-100 flex flex-col items-center justify-center text-center">
84
+ <span className="text-[10px] font-bold text-gray-400 uppercase">Glucose</span>
85
+ <span className={`text-2xl font-black ${vitals.glucose > 180 ? 'text-red-600' : 'text-gray-900'}`}>{vitals.glucose}</span>
86
+ <span className="text-[10px] text-gray-500">mg/dL</span>
87
+ </div>
88
+ <div className="bg-gray-50 p-3 rounded-lg border border-gray-100 flex flex-col items-center justify-center text-center">
89
+ <span className="text-[10px] font-bold text-gray-400 uppercase">Sleep Quality</span>
90
+ <span className="text-2xl font-black text-gray-900">{vitals.sleepQuality}<span className="text-sm text-gray-400">/10</span></span>
91
+ </div>
92
+ <div className="bg-gray-50 p-3 rounded-lg border border-gray-100 flex flex-col items-center justify-center text-center">
93
+ <span className="text-[10px] font-bold text-gray-400 uppercase">Risk Score</span>
94
+ <span className={`text-2xl font-black ${riskResult && riskResult.numericScore > 60 ? 'text-red-600' : 'text-green-600'}`}>
95
+ {riskResult ? riskResult.numericScore : '--'}
96
+ </span>
97
+ <span className="text-[10px] text-gray-500">/100</span>
98
+ </div>
99
+ </div>
100
+ </section>
101
+ </div>
102
+
103
+ {/* AI Analysis */}
104
+ <section className="mb-8 break-inside-avoid">
105
+ <h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wider text-black border-b border-gray-200 pb-2 mb-4">
106
+ <AlertTriangle size={16} /> AI Clinical Analysis
107
+ </h2>
108
+ {riskResult ? (
109
+ <div className="bg-blue-50/50 rounded-xl border border-blue-100 p-6">
110
+ <div className="mb-6">
111
+ <h3 className="text-xs font-bold text-blue-800 uppercase tracking-wide mb-2">Clinical Summary</h3>
112
+ <p className="text-sm text-blue-950 leading-relaxed font-medium">{riskResult.summary}</p>
113
+ </div>
114
+
115
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
116
+ <div>
117
+ <h3 className="text-xs font-bold text-gray-500 uppercase tracking-wide mb-3">Action Items</h3>
118
+ <ul className="space-y-2">
119
+ {riskResult.actionItems.map((item, i) => (
120
+ <li key={i} className="flex items-start gap-2 text-sm text-gray-800">
121
+ <CheckCircle size={14} className="text-green-600 mt-0.5 flex-shrink-0" />
122
+ {item}
123
+ </li>
124
+ ))}
125
+ </ul>
126
+ </div>
127
+ {/* Medical Coding Table */}
128
+ <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
129
+ <div className="bg-gray-50 px-3 py-2 border-b border-gray-200 text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
130
+ <ShieldCheck size={14} /> Coding Pipeline
131
+ </div>
132
+ <table className="w-full text-xs text-left">
133
+ <thead>
134
+ <tr className="bg-gray-50 text-gray-400">
135
+ <th className="px-3 py-1 font-medium">Type</th>
136
+ <th className="px-3 py-1 font-medium">Code</th>
137
+ <th className="px-3 py-1 font-medium">Desc</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody className="divide-y divide-gray-100">
141
+ {riskResult.codingPipeline?.map((code, i) => (
142
+ <tr key={i}>
143
+ <td className="px-3 py-1.5 text-gray-500">{code.type}</td>
144
+ <td className="px-3 py-1.5 font-bold font-mono text-blue-600">{code.code}</td>
145
+ <td className="px-3 py-1.5 text-gray-800">{code.description}</td>
146
+ </tr>
147
+ ))}
148
+ {(!riskResult.codingPipeline || riskResult.codingPipeline.length === 0) && (
149
+ <tr><td colSpan={3} className="px-3 py-2 text-gray-400 italic">No codes generated.</td></tr>
150
+ )}
151
+ </tbody>
152
+ </table>
153
+ {riskResult.insuranceNote && (
154
+ <div className="p-3 bg-gray-50 border-t border-gray-200">
155
+ <p className="text-[10px] text-gray-400 font-bold uppercase mb-0.5">Insurance Note</p>
156
+ <p className="text-xs text-gray-600 italic">"{riskResult.insuranceNote}"</p>
157
+ </div>
158
+ )}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ ) : (
163
+ <div className="bg-gray-100 p-6 rounded-lg border-2 border-dashed border-gray-300 text-center">
164
+ <p className="text-gray-600 font-bold text-sm">Analysis Not Run</p>
165
+ <p className="text-gray-500 text-xs">Run analysis in dashboard before printing.</p>
166
+ </div>
167
+ )}
168
+ </section>
169
+
170
+ {/* Medication Adherence */}
171
+ <section className="mb-8 break-inside-avoid">
172
+ <h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wider text-black border-b border-gray-200 pb-2 mb-4">
173
+ <Pill size={16} /> Medication Schedule
174
+ </h2>
175
+ <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
176
+ <table className="w-full text-sm text-left">
177
+ <thead className="bg-gray-50 text-gray-500 font-bold text-xs uppercase">
178
+ <tr>
179
+ <th className="px-4 py-3">Medication</th>
180
+ <th className="px-4 py-3">Dosage</th>
181
+ <th className="px-4 py-3">Time</th>
182
+ <th className="px-4 py-3">Duration</th>
183
+ <th className="px-4 py-3 text-right">Status</th>
184
+ </tr>
185
+ </thead>
186
+ <tbody className="divide-y divide-gray-100">
187
+ {medications.length > 0 ? medications.map(med => (
188
+ <tr key={med.id}>
189
+ <td className="px-4 py-3 font-bold text-gray-900">{med.name}</td>
190
+ <td className="px-4 py-3 text-gray-600">{med.dosage}</td>
191
+ <td className="px-4 py-3 font-mono text-gray-600">{med.time}</td>
192
+ <td className="px-4 py-3 text-gray-500 text-xs">
193
+ {med.startDate ? `${med.startDate} → ${med.endDate || 'Ongoing'}` : 'Continuous'}
194
+ </td>
195
+ <td className="px-4 py-3 text-right">
196
+ {med.taken ? (
197
+ <span className="inline-flex items-center gap-1 text-xs font-bold text-green-700 bg-green-50 px-2 py-1 rounded-full">
198
+ <CheckCircle size={10} /> Taken
199
+ </span>
200
+ ) : (
201
+ <span className="inline-flex items-center gap-1 text-xs font-bold text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
202
+ Pending
203
+ </span>
204
+ )}
205
+ </td>
206
+ </tr>
207
+ )) : (
208
+ <tr>
209
+ <td colSpan={5} className="px-4 py-6 text-center text-gray-500 italic">No medications recorded.</td>
210
+ </tr>
211
+ )}
212
+ </tbody>
213
+ </table>
214
+ {medications.length > 0 && (
215
+ <div className="bg-gray-50 px-4 py-3 border-t border-gray-200 flex justify-end items-center gap-3">
216
+ <span className="text-xs font-bold text-gray-500 uppercase">Current Streak</span>
217
+ <span className="text-lg font-black text-gray-900">{profile.streak} Days</span>
218
+ </div>
219
+ )}
220
+ </div>
221
+ </section>
222
+
223
+ {/* Chat Summary */}
224
+ <section className="mb-8 break-inside-avoid">
225
+ <h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wider text-black border-b border-gray-200 pb-2 mb-4">
226
+ <ClipboardList size={16} /> Consultation Brief
227
+ </h2>
228
+ <div className="bg-yellow-50/50 p-6 rounded-xl border border-yellow-100">
229
+ {chatSummary ? (
230
+ <p className="text-sm text-gray-800 leading-relaxed whitespace-pre-wrap font-medium">{chatSummary}</p>
231
+ ) : (
232
+ <p className="text-gray-500 italic text-sm">No session summary available.</p>
233
+ )}
234
+ </div>
235
+ </section>
236
+
237
+ {/* Full Transcript */}
238
+ <section>
239
+ <h2 className="flex items-center gap-2 text-sm font-black uppercase tracking-wider text-black border-b border-gray-200 pb-2 mb-4">
240
+ <MessageSquare size={16} /> Conversation Transcript
241
+ </h2>
242
+ <div className="space-y-1">
243
+ {chatHistory.map((m) => (
244
+ <div key={m.id} className="grid grid-cols-12 gap-4 py-2 border-b border-gray-100 break-inside-avoid text-xs">
245
+ <div className="col-span-2 font-bold text-gray-500 uppercase tracking-wider pt-1">
246
+ {m.role === 'user' ? 'Patient' : 'SomAI'}
247
+ </div>
248
+ <div className="col-span-10 text-gray-800 leading-relaxed">
249
+ {m.image && <div className="text-[10px] text-blue-600 mb-1 font-bold">[Image Attachment]</div>}
250
+ {m.text}
251
+ </div>
252
+ </div>
253
+ ))}
254
+ {chatHistory.length === 0 && <p className="text-gray-400 italic text-xs">No messages logged.</p>}
255
+ </div>
256
+ </section>
257
+
258
+ {/* Footer */}
259
+ <footer className="mt-12 pt-6 border-t-2 border-gray-900 flex justify-between items-center text-[10px] text-gray-500 break-inside-avoid">
260
+ <div className="max-w-[60%]">
261
+ <p className="font-bold text-gray-900 mb-1 uppercase tracking-wider">Disclaimer</p>
262
+ <p>This report is generated by an AI assistant for educational purposes only. It is not a medical device. Always consult a qualified healthcare provider for diagnosis and treatment.</p>
263
+ </div>
264
+ <div className="text-right">
265
+ <p className="font-bold text-gray-900">SomAI</p>
266
+ <p>Powered by Google Gemini</p>
267
+ </div>
268
+ </footer>
269
+ </div>
270
+ );
271
+ };
272
+
273
+ export default PrintReport;
components/Profile.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Save, Check, FileText } from 'lucide-react';
3
+ import { PatientProfile, RiskAnalysisResult, ClinicalVitals, ChatMessage, Medication } from '../types';
4
+ import ReportView from './ReportView';
5
+
6
+ interface ProfileProps {
7
+ profile: PatientProfile;
8
+ setProfile: React.Dispatch<React.SetStateAction<PatientProfile>>;
9
+ onProfileUpdate?: () => void;
10
+ riskResult: RiskAnalysisResult | null;
11
+ chatSummary: string;
12
+ onPrint: () => void;
13
+ // New props for full report
14
+ vitals: ClinicalVitals;
15
+ medications: Medication[];
16
+ chatHistory: ChatMessage[];
17
+ }
18
+
19
+ const Profile: React.FC<ProfileProps> = ({
20
+ profile,
21
+ setProfile,
22
+ onProfileUpdate,
23
+ riskResult,
24
+ chatSummary,
25
+ onPrint,
26
+ vitals,
27
+ medications,
28
+ chatHistory
29
+ }) => {
30
+ const [isSaved, setIsSaved] = useState(false);
31
+
32
+ const handleChange = (field: keyof PatientProfile, value: string | number) => {
33
+ setProfile(prev => ({ ...prev, [field]: value }));
34
+ setIsSaved(false);
35
+ };
36
+
37
+ const handleManualSave = () => {
38
+ try {
39
+ localStorage.setItem('somai_profile', JSON.stringify(profile));
40
+ } catch(e) {
41
+ console.error("Failed to save profile:", e)
42
+ }
43
+
44
+ if (onProfileUpdate) {
45
+ onProfileUpdate();
46
+ }
47
+
48
+ setIsSaved(true);
49
+ setTimeout(() => setIsSaved(false), 3000);
50
+ };
51
+
52
+ return (
53
+ <div className="max-w-4xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
54
+ <div>
55
+ <div className="flex justify-between items-center mb-2">
56
+ <div>
57
+ <h2 className="text-2xl font-bold text-white mb-1">Patient Identity</h2>
58
+ <p className="text-gray-400 text-sm">Manage core patient demographics and history.</p>
59
+ </div>
60
+ <button
61
+ onClick={onPrint}
62
+ className="flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-mono font-bold transition-all border border-white/10 text-gray-300 hover:text-white hover:border-white/30 bg-white/5"
63
+ >
64
+ <FileText size={14} />
65
+ EXPORT PDF
66
+ </button>
67
+ </div>
68
+
69
+ <div className="glass-panel p-8 rounded-2xl border-t border-neon-border relative overflow-hidden">
70
+ <div className="absolute top-0 right-0 w-64 h-64 bg-purple-500/5 rounded-full blur-3xl -z-10 pointer-events-none"></div>
71
+
72
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
73
+ <div className="space-y-4">
74
+ <div>
75
+ <label className="block text-xs font-mono font-bold text-gray-500 mb-2 uppercase tracking-wider">Full Name</label>
76
+ <input
77
+ value={profile.name}
78
+ onChange={e => handleChange('name', e.target.value)}
79
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-4 text-white placeholder-gray-600 focus:border-purple-500 focus:ring-1 focus:ring-purple-500/50 outline-none transition-all"
80
+ placeholder="Enter patient name"
81
+ />
82
+ </div>
83
+
84
+ <div>
85
+ <label className="block text-xs font-mono font-bold text-gray-500 mb-2 uppercase tracking-wider">Age</label>
86
+ <input
87
+ type="number"
88
+ value={profile.age}
89
+ onChange={e => handleChange('age', parseInt(e.target.value) || 0)}
90
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-4 text-white placeholder-gray-600 focus:border-purple-500 focus:ring-1 focus:ring-purple-500/50 outline-none transition-all"
91
+ />
92
+ </div>
93
+
94
+ <div>
95
+ <label className="block text-xs font-mono font-bold text-gray-500 mb-2 uppercase tracking-wider">Primary Condition</label>
96
+ <input
97
+ value={profile.condition}
98
+ onChange={e => handleChange('condition', e.target.value)}
99
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-4 text-white placeholder-gray-600 focus:border-purple-500 focus:ring-1 focus:ring-purple-500/50 outline-none transition-all"
100
+ placeholder="e.g. Type 2 Diabetes"
101
+ />
102
+ </div>
103
+ </div>
104
+
105
+ <div className="space-y-4">
106
+ <div>
107
+ <label className="block text-xs font-mono font-bold text-gray-500 mb-2 uppercase tracking-wider">Medical History</label>
108
+ <textarea
109
+ value={profile.history}
110
+ onChange={e => handleChange('history', e.target.value)}
111
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-4 text-white placeholder-gray-600 focus:border-purple-500 focus:ring-1 focus:ring-purple-500/50 outline-none transition-all h-32 resize-none"
112
+ placeholder="Brief medical history..."
113
+ />
114
+ </div>
115
+ <div>
116
+ <label className="block text-xs font-mono font-bold text-gray-500 mb-2 uppercase tracking-wider">Allergies & Sensitivities</label>
117
+ <textarea
118
+ value={profile.allergies}
119
+ onChange={e => handleChange('allergies', e.target.value)}
120
+ className="w-full bg-black/40 border border-white/10 rounded-lg p-4 text-white placeholder-gray-600 focus:border-purple-500 focus:ring-1 focus:ring-purple-500/50 outline-none transition-all h-32 resize-none"
121
+ placeholder="e.g. Penicillin, Peanuts, Latex"
122
+ />
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <div className="mt-8 flex justify-end pt-6 border-t border-white/5">
128
+ <button
129
+ type="button"
130
+ onClick={handleManualSave}
131
+ className={`flex items-center gap-2 px-6 py-3 rounded-lg font-mono font-bold transition-all ${
132
+ isSaved
133
+ ? 'bg-green-500/20 text-green-400 border border-green-500/50'
134
+ : 'bg-neon-green text-black hover:bg-neon-green/90 shadow-[0_0_15px_rgba(0,255,128,0.3)]'
135
+ }`}
136
+ >
137
+ {isSaved ? <Check size={18} /> : <Save size={18} />}
138
+ {isSaved ? 'PROFILE SAVED' : 'SAVE CHANGES'}
139
+ </button>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ {/* FULL REPORT PREVIEW SECTION */}
145
+ <ReportView
146
+ profile={profile}
147
+ vitals={vitals}
148
+ riskResult={riskResult}
149
+ chatHistory={chatHistory}
150
+ chatSummary={chatSummary}
151
+ medications={medications}
152
+ />
153
+ </div>
154
+ );
155
+ };
156
+
157
+ export default Profile;
components/ReportView.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { PatientProfile, ClinicalVitals, RiskAnalysisResult, ChatMessage, Medication } from '../types';
4
+ import { FileText, User, Activity, Pill, MessageSquare, ShieldCheck } from 'lucide-react';
5
+
6
+ interface ReportViewProps {
7
+ profile: PatientProfile;
8
+ vitals: ClinicalVitals;
9
+ riskResult: RiskAnalysisResult | null;
10
+ chatHistory: ChatMessage[];
11
+ chatSummary: string;
12
+ medications: Medication[];
13
+ }
14
+
15
+ const ReportView: React.FC<ReportViewProps> = ({
16
+ profile,
17
+ vitals,
18
+ riskResult,
19
+ chatHistory,
20
+ chatSummary,
21
+ medications
22
+ }) => {
23
+ return (
24
+ <div className="space-y-6 mt-12 pt-8 border-t border-white/10">
25
+ <h2 className="text-2xl font-bold text-white flex items-center gap-2">
26
+ <FileText className="text-neon-green" /> Full Clinical Report
27
+ </h2>
28
+
29
+ <div className="glass-panel p-6 rounded-2xl space-y-8">
30
+ {/* Demographics */}
31
+ <section>
32
+ <h3 className="text-sm font-mono font-bold text-gray-400 uppercase mb-4 border-b border-white/5 pb-2 flex items-center gap-2">
33
+ <User size={16} /> Patient Demographics
34
+ </h3>
35
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
36
+ <div><span className="text-gray-500 block text-xs uppercase tracking-wide">Name</span><span className="text-white">{profile.name}</span></div>
37
+ <div><span className="text-gray-500 block text-xs uppercase tracking-wide">Age</span><span className="text-white">{profile.age}</span></div>
38
+ <div className="col-span-2"><span className="text-gray-500 block text-xs uppercase tracking-wide">Condition</span><span className="text-white">{profile.condition}</span></div>
39
+ <div className="col-span-4"><span className="text-gray-500 block text-xs uppercase tracking-wide">Medical History</span><span className="text-white">{profile.history || 'N/A'}</span></div>
40
+ </div>
41
+ </section>
42
+
43
+ {/* Vitals & Risk */}
44
+ <section>
45
+ <h3 className="text-sm font-mono font-bold text-gray-400 uppercase mb-4 border-b border-white/5 pb-2 flex items-center gap-2">
46
+ <Activity size={16} /> Clinical Analysis
47
+ </h3>
48
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
49
+ <div className="bg-white/5 p-3 rounded-lg text-center border border-white/5">
50
+ <span className="text-gray-500 text-xs block">Systolic BP</span>
51
+ <span className="font-mono font-bold text-neon-green text-lg">{vitals.systolicBp}</span>
52
+ </div>
53
+ <div className="bg-white/5 p-3 rounded-lg text-center border border-white/5">
54
+ <span className="text-gray-500 text-xs block">Glucose</span>
55
+ <span className="font-mono font-bold text-neon-yellow text-lg">{vitals.glucose}</span>
56
+ </div>
57
+ <div className="bg-white/5 p-3 rounded-lg text-center border border-white/5">
58
+ <span className="text-gray-500 text-xs block">Risk Score</span>
59
+ <span className="font-mono font-bold text-neon-red text-lg">{riskResult?.numericScore ?? 'N/A'}</span>
60
+ </div>
61
+ </div>
62
+ {riskResult ? (
63
+ <div className="space-y-4">
64
+ <div className="text-sm text-gray-300 bg-black/20 p-4 rounded-lg border border-white/5">
65
+ <p><strong className="text-neon-blue font-mono text-xs uppercase block mb-1">Summary</strong> {riskResult.summary}</p>
66
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
67
+ <div>
68
+ <strong className="text-neon-green font-mono text-xs uppercase block mb-1">Recommended Actions</strong>
69
+ <ul className="list-disc list-inside text-gray-400">
70
+ {riskResult.actionItems.map((item, idx) => <li key={idx}>{item}</li>)}
71
+ </ul>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ {/* Medical Coding Pipeline Table */}
77
+ <div className="bg-black/20 rounded-lg border border-white/5 overflow-hidden">
78
+ <div className="px-4 py-2 bg-purple-500/10 border-b border-white/5 flex items-center gap-2 text-purple-300 font-bold text-xs uppercase tracking-wide">
79
+ <ShieldCheck size={14} /> Insurance & Coding Pipeline
80
+ </div>
81
+ <table className="w-full text-xs text-left">
82
+ <thead className="text-gray-500 border-b border-white/5">
83
+ <tr>
84
+ <th className="px-4 py-2">Diagnosis Type</th>
85
+ <th className="px-4 py-2">ICD-10 Code</th>
86
+ <th className="px-4 py-2">Description</th>
87
+ </tr>
88
+ </thead>
89
+ <tbody className="divide-y divide-white/5 text-gray-300">
90
+ {riskResult.codingPipeline?.map((code, idx) => (
91
+ <tr key={idx} className="hover:bg-white/5">
92
+ <td className="px-4 py-2 text-gray-400">{code.type}</td>
93
+ <td className="px-4 py-2 font-mono text-purple-400 font-bold">{code.code}</td>
94
+ <td className="px-4 py-2">{code.description}</td>
95
+ </tr>
96
+ ))}
97
+ {(!riskResult.codingPipeline || riskResult.codingPipeline.length === 0) && (
98
+ <tr>
99
+ <td colSpan={3} className="px-4 py-2 italic text-gray-500">Pipeline empty. Run analysis to generate.</td>
100
+ </tr>
101
+ )}
102
+ </tbody>
103
+ </table>
104
+ {riskResult.insuranceNote && (
105
+ <div className="px-4 py-2 border-t border-white/5 bg-white/5">
106
+ <p className="text-[10px] text-gray-500 uppercase font-bold">Medical Necessity Note</p>
107
+ <p className="text-xs text-gray-300 italic">{riskResult.insuranceNote}</p>
108
+ </div>
109
+ )}
110
+ </div>
111
+ </div>
112
+ ) : (
113
+ <p className="text-gray-500 text-sm italic">Analysis not yet performed.</p>
114
+ )}
115
+ </section>
116
+
117
+ {/* Meds */}
118
+ <section>
119
+ <h3 className="text-sm font-mono font-bold text-gray-400 uppercase mb-4 border-b border-white/5 pb-2 flex items-center gap-2">
120
+ <Pill size={16} /> Medication Schedule
121
+ </h3>
122
+ <ul className="grid grid-cols-1 md:grid-cols-2 gap-2">
123
+ {medications.length > 0 ? medications.map(med => (
124
+ <li key={med.id} className="flex justify-between items-center text-sm bg-white/5 p-3 rounded-lg border border-white/5">
125
+ <div>
126
+ <span className="font-bold text-white block">{med.name}</span>
127
+ <span className="text-xs text-gray-500">{med.dosage} • {med.time}</span>
128
+ </div>
129
+ <span className={`px-2 py-1 rounded text-xs font-bold ${med.taken ? "bg-green-500/20 text-green-400" : "bg-yellow-500/20 text-yellow-400"}`}>
130
+ {med.taken ? 'Taken' : 'Pending'}
131
+ </span>
132
+ </li>
133
+ )) : <li className="text-gray-500 text-sm italic">No medications recorded.</li>}
134
+ </ul>
135
+ </section>
136
+
137
+ {/* Chat Summary */}
138
+ <section>
139
+ <h3 className="text-sm font-mono font-bold text-gray-400 uppercase mb-4 border-b border-white/5 pb-2 flex items-center gap-2">
140
+ <MessageSquare size={16} /> Consultation Brief
141
+ </h3>
142
+ {chatSummary ? (
143
+ <div className="bg-black/20 p-4 rounded-lg border border-white/5">
144
+ <p className="text-sm text-gray-300 whitespace-pre-wrap leading-relaxed">
145
+ {chatSummary}
146
+ </p>
147
+ </div>
148
+ ) : (
149
+ <p className="text-gray-500 text-sm italic">No consultation summary available.</p>
150
+ )}
151
+ </section>
152
+ </div>
153
+ </div>
154
+ );
155
+ };
156
+
157
+ export default ReportView;
components/Sidebar.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import { Activity, MessageSquare, User, Pill } from 'lucide-react';
4
+
5
+ interface SidebarProps {
6
+ activeTab: 'dashboard' | 'chat' | 'profile' | 'medication';
7
+ setActiveTab: (tab: 'dashboard' | 'chat' | 'profile' | 'medication') => void;
8
+ }
9
+
10
+ const Sidebar: React.FC<SidebarProps> = ({ activeTab, setActiveTab }) => {
11
+ const navItems = [
12
+ { id: 'dashboard', label: 'Dashboard', icon: Activity },
13
+ { id: 'chat', label: 'AI Companion', icon: MessageSquare },
14
+ { id: 'medication', label: 'Meds & Rewards', icon: Pill },
15
+ { id: 'profile', label: 'Patient Profile', icon: User },
16
+ ] as const;
17
+
18
+ return (
19
+ <aside className="hidden md:flex flex-col w-64 h-screen fixed glass-panel border-r border-neon-border z-20 top-0 left-0">
20
+ <div className="p-8">
21
+ <h1 className="text-2xl font-mono font-bold text-white tracking-tighter flex items-center gap-3">
22
+ <div className="p-2 bg-neon-green/10 rounded-lg">
23
+ <Activity className="text-neon-green" size={24} />
24
+ </div>
25
+ SomAI
26
+ </h1>
27
+ <p className="text-xs text-gray-400 mt-3 font-medium tracking-wide">CLINICAL COMPANION v1.0</p>
28
+ </div>
29
+
30
+ <nav className="flex-1 px-4 space-y-2 mt-4">
31
+ {navItems.map((item) => (
32
+ <button
33
+ key={item.id}
34
+ onClick={() => setActiveTab(item.id)}
35
+ className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200 group ${
36
+ activeTab === item.id
37
+ ? 'bg-white/10 text-white border border-white/10 shadow-lg shadow-black/20'
38
+ : 'text-gray-400 hover:bg-white/5 hover:text-white'
39
+ }`}
40
+ >
41
+ <item.icon
42
+ size={20}
43
+ className={`transition-colors ${activeTab === item.id ? 'text-neon-green' : 'text-gray-500 group-hover:text-gray-300'}`}
44
+ />
45
+ <span className="font-medium text-sm">{item.label}</span>
46
+ </button>
47
+ ))}
48
+ </nav>
49
+
50
+ <div className="p-6 border-t border-white/5">
51
+ <div className="flex items-center gap-3 text-xs text-gray-500 font-mono">
52
+ <div className="w-2 h-2 rounded-full bg-neon-green animate-pulse-slow"></div>
53
+ SYSTEM ONLINE
54
+ </div>
55
+ </div>
56
+ </aside>
57
+ );
58
+ };
59
+
60
+ export default Sidebar;