Sivaneshakumar commited on
Commit
49f45e4
·
1 Parent(s): 76dd92e
.gitignore CHANGED
@@ -30,7 +30,6 @@ datasets/
30
  datasets/*
31
 
32
  # Legacy Prototype Folders
33
- app/
34
  clinical_engine/
35
  evaluation/
36
  rag/
 
30
  datasets/*
31
 
32
  # Legacy Prototype Folders
 
33
  clinical_engine/
34
  evaluation/
35
  rag/
frontend/src/app/admin/page.tsx ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { Sidebar } from "@/components/Sidebar";
5
+ import { MetricCard } from "@/components/MetricCard";
6
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
7
+ import { useAuth } from "@/lib/auth-context";
8
+ import { adminApi } from "@/lib/api";
9
+ import {
10
+ ShieldAlert,
11
+ Users,
12
+ FileText,
13
+ Cpu,
14
+ Lock,
15
+ Clock,
16
+ Sparkles,
17
+ CheckCircle,
18
+ } from "lucide-react";
19
+
20
+ export default function AdminPage() {
21
+ const { user, quickLogin } = useAuth();
22
+ const [stats, setStats] = useState<any>(null);
23
+ const [auditLogs, setAuditLogs] = useState<any[]>([]);
24
+ const [isLoading, setIsLoading] = useState(true);
25
+
26
+ useEffect(() => {
27
+ if (user?.role === "ADMIN") {
28
+ Promise.allSettled([adminApi.getStats(), adminApi.getAuditLogs(50)])
29
+ .then(([statsRes, logsRes]) => {
30
+ if (statsRes.status === "fulfilled") setStats(statsRes.value.data.data);
31
+ if (logsRes.status === "fulfilled") setAuditLogs(logsRes.value.data.data || []);
32
+ })
33
+ .finally(() => setIsLoading(false));
34
+ } else {
35
+ setIsLoading(false);
36
+ }
37
+ }, [user]);
38
+
39
+ if (user?.role !== "ADMIN") {
40
+ return (
41
+ <div className="flex-1 flex bg-slate-50">
42
+ <Sidebar />
43
+ <div className="flex-1 p-8 max-w-2xl mx-auto my-auto space-y-4 text-center">
44
+ <div className="w-12 h-12 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center mx-auto shadow-xs">
45
+ <Lock className="w-6 h-6" />
46
+ </div>
47
+ <h1 className="text-xl font-bold text-slate-900">Administrator Access Required</h1>
48
+ <p className="text-xs text-slate-500 max-w-md mx-auto">
49
+ This module displays privileged platform health metrics, neural model telemetry, and immutable security audit logs.
50
+ </p>
51
+ <button
52
+ onClick={() => quickLogin("admin")}
53
+ className="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-semibold shadow-xs transition-colors"
54
+ >
55
+ <Sparkles className="w-4 h-4" />
56
+ <span>Switch to Demo Admin Account</span>
57
+ </button>
58
+ </div>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <div className="flex-1 flex bg-slate-50">
65
+ <Sidebar />
66
+
67
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-7xl mx-auto w-full">
68
+ {/* Header */}
69
+ <div>
70
+ <div className="inline-flex items-center gap-2 px-2.5 py-0.5 rounded-md bg-purple-100 text-purple-800 text-[10px] font-extrabold uppercase tracking-wider mb-1">
71
+ System Administration
72
+ </div>
73
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
74
+ <ShieldAlert className="w-6 h-6 text-purple-600" />
75
+ <span>Platform Operations & Security Audit</span>
76
+ </h1>
77
+ <p className="text-xs text-slate-500 mt-0.5">
78
+ Monitor model telemetry, aggregate clinical entities, and inspect access logs.
79
+ </p>
80
+ </div>
81
+
82
+ <MedicalDisclaimer />
83
+
84
+ {/* Metric Cards */}
85
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
86
+ <MetricCard
87
+ title="Total Users"
88
+ value={stats?.total_users ?? "--"}
89
+ subtitle="Registered accounts"
90
+ icon={Users}
91
+ iconBg="bg-blue-50"
92
+ iconColor="text-blue-600"
93
+ />
94
+ <MetricCard
95
+ title="Processed Documents"
96
+ value={stats?.total_documents ?? "--"}
97
+ subtitle="Clinical reports in DB"
98
+ icon={FileText}
99
+ iconBg="bg-teal-50"
100
+ iconColor="text-teal-600"
101
+ />
102
+ <MetricCard
103
+ title="Biomedical Entities"
104
+ value={stats?.total_entities_extracted ?? "--"}
105
+ subtitle="BC5CDR extractions"
106
+ icon={Cpu}
107
+ iconBg="bg-emerald-50"
108
+ iconColor="text-emerald-600"
109
+ />
110
+ <MetricCard
111
+ title="Model Status"
112
+ value="RoBERTa-large"
113
+ subtitle={stats?.model_status?.device ? `Device: ${stats.model_status.device}` : "Active"}
114
+ icon={ShieldAlert}
115
+ iconBg="bg-purple-50"
116
+ iconColor="text-purple-600"
117
+ />
118
+ </div>
119
+
120
+ {/* Security Audit Table */}
121
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs space-y-4">
122
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
123
+ <Clock className="w-4 h-4 text-purple-600" />
124
+ <span>Security & Access Audit Trail</span>
125
+ </h2>
126
+
127
+ {auditLogs.length === 0 ? (
128
+ <p className="text-xs text-slate-400 py-6 text-center">No audit logs recorded.</p>
129
+ ) : (
130
+ <div className="overflow-x-auto border border-slate-200 rounded-xl">
131
+ <table className="w-full text-left text-xs">
132
+ <thead className="bg-slate-50 text-slate-500 border-b border-slate-200">
133
+ <tr>
134
+ <th className="py-2.5 px-3 font-semibold">Action</th>
135
+ <th className="py-2.5 px-3 font-semibold">Status</th>
136
+ <th className="py-2.5 px-3 font-semibold">Details</th>
137
+ <th className="py-2.5 px-3 font-semibold">Timestamp</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody className="divide-y divide-slate-100 font-mono">
141
+ {auditLogs.map((log) => (
142
+ <tr key={log.id} className="hover:bg-slate-50/80 transition-colors">
143
+ <td className="py-2.5 px-3 font-bold text-slate-900">{log.action}</td>
144
+ <td className="py-2.5 px-3">
145
+ <span
146
+ className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${
147
+ log.status === "SUCCESS"
148
+ ? "bg-emerald-100 text-emerald-800"
149
+ : "bg-rose-100 text-rose-800"
150
+ }`}
151
+ >
152
+ {log.status}
153
+ </span>
154
+ </td>
155
+ <td className="py-2.5 px-3 text-slate-700 font-sans text-xs">{log.details || "--"}</td>
156
+ <td className="py-2.5 px-3 text-slate-400 text-[11px]">
157
+ {new Date(log.created_at).toLocaleString()}
158
+ </td>
159
+ </tr>
160
+ ))}
161
+ </tbody>
162
+ </table>
163
+ </div>
164
+ )}
165
+ </div>
166
+ </div>
167
+ </div>
168
+ );
169
+ }
frontend/src/app/assistant/page.tsx ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect, useRef } from "react";
4
+ import { Sidebar } from "@/components/Sidebar";
5
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
6
+ import { chatApi } from "@/lib/api";
7
+ import {
8
+ MessageSquareHeart,
9
+ Send,
10
+ Plus,
11
+ Trash2,
12
+ AlertTriangle,
13
+ HelpCircle,
14
+ Sparkles,
15
+ Bot,
16
+ User as UserIcon,
17
+ ShieldAlert,
18
+ Info,
19
+ } from "lucide-react";
20
+
21
+ interface StructuredData {
22
+ summary: string;
23
+ possible_considerations?: string[];
24
+ relevant_medical_info?: string[];
25
+ questions_for_doctor?: string[];
26
+ safety_warning?: string;
27
+ is_emergency?: boolean;
28
+ emergency_instructions?: string;
29
+ }
30
+
31
+ interface Message {
32
+ id?: string;
33
+ role: "user" | "assistant" | "system";
34
+ content: string;
35
+ structured_data?: StructuredData | null;
36
+ model_provider?: string;
37
+ created_at?: string;
38
+ }
39
+
40
+ interface Conversation {
41
+ id: string;
42
+ title: string;
43
+ messages: Message[];
44
+ created_at: string;
45
+ }
46
+
47
+ const SAMPLE_PROMPTS = [
48
+ "What are typical dietary recommendations for managing Type 2 Diabetes?",
49
+ "Can you explain why metformin is taken with meals and its gastrointestinal profile?",
50
+ "I am having severe crushing chest pain, shortness of breath, and left arm numbness.",
51
+ ];
52
+
53
+ export default function AssistantPage() {
54
+ const [conversations, setConversations] = useState<Conversation[]>([]);
55
+ const [activeConvId, setActiveConvId] = useState<string | null>(null);
56
+ const [messages, setMessages] = useState<Message[]>([]);
57
+ const [inputText, setInputText] = useState<string>("");
58
+ const [isSending, setIsSending] = useState<boolean>(false);
59
+ const messagesEndRef = useRef<HTMLDivElement>(null);
60
+
61
+ const fetchConversations = async () => {
62
+ try {
63
+ const res = await chatApi.listConversations();
64
+ const list = res.data.data || [];
65
+ setConversations(list);
66
+ if (list.length > 0 && !activeConvId) {
67
+ setActiveConvId(list[0].id);
68
+ setMessages(list[0].messages || []);
69
+ }
70
+ } catch (err) {
71
+ console.error("Error fetching conversations:", err);
72
+ }
73
+ };
74
+
75
+ useEffect(() => {
76
+ fetchConversations();
77
+ }, []);
78
+
79
+ useEffect(() => {
80
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
81
+ }, [messages]);
82
+
83
+ const handleSelectConversation = async (id: string) => {
84
+ setActiveConvId(id);
85
+ try {
86
+ const res = await chatApi.getConversation(id);
87
+ setMessages(res.data.data.messages || []);
88
+ } catch (err) {
89
+ console.error("Error loading conversation:", err);
90
+ }
91
+ };
92
+
93
+ const handleNewConversation = () => {
94
+ setActiveConvId(null);
95
+ setMessages([]);
96
+ };
97
+
98
+ const handleDeleteConversation = async (e: React.MouseEvent, id: string) => {
99
+ e.stopPropagation();
100
+ try {
101
+ await chatApi.deleteConversation(id);
102
+ setConversations((prev) => prev.filter((c) => c.id !== id));
103
+ if (activeConvId === id) {
104
+ handleNewConversation();
105
+ }
106
+ } catch (err) {
107
+ console.error("Error deleting conversation:", err);
108
+ }
109
+ };
110
+
111
+ const handleSend = async (customPrompt?: string) => {
112
+ const textToSend = customPrompt || inputText;
113
+ if (!textToSend.trim() || isSending) return;
114
+
115
+ const userMessage: Message = {
116
+ role: "user",
117
+ content: textToSend,
118
+ };
119
+
120
+ setMessages((prev) => [...prev, userMessage]);
121
+ setInputText("");
122
+ setIsSending(true);
123
+
124
+ try {
125
+ const res = await chatApi.sendMessage(textToSend, activeConvId || undefined);
126
+ const data = res.data.data;
127
+
128
+ if (!activeConvId) {
129
+ setActiveConvId(data.conversation_id);
130
+ }
131
+
132
+ setMessages((prev) => [...prev, data.message]);
133
+ fetchConversations();
134
+ } catch (err: any) {
135
+ const errMsg: Message = {
136
+ role: "assistant",
137
+ content: err.response?.data?.error?.message || "Failed to reach AI assistant service.",
138
+ };
139
+ setMessages((prev) => [...prev, errMsg]);
140
+ } finally {
141
+ setIsSending(false);
142
+ }
143
+ };
144
+
145
+ return (
146
+ <div className="flex-1 flex bg-slate-50">
147
+ <Sidebar />
148
+
149
+ <div className="flex-1 flex flex-col md:flex-row h-[calc(100vh-61px)]">
150
+ {/* Left Sub-Sidebar: Conversation History */}
151
+ <div className="w-full md:w-72 bg-white border-r border-slate-200/80 flex flex-col p-4 space-y-3">
152
+ <button
153
+ onClick={handleNewConversation}
154
+ className="w-full py-2 px-3 bg-teal-600 hover:bg-teal-700 text-white rounded-xl text-xs font-semibold shadow-xs transition-colors flex items-center justify-center gap-2"
155
+ >
156
+ <Plus className="w-4 h-4" />
157
+ <span>New Medical Consultation</span>
158
+ </button>
159
+
160
+ <div className="flex-1 overflow-y-auto space-y-1.5 pt-2">
161
+ <p className="text-[10px] font-bold uppercase tracking-wider text-slate-400 px-2 mb-1">
162
+ Previous Consultations
163
+ </p>
164
+ {conversations.length === 0 ? (
165
+ <p className="text-xs text-slate-400 px-2 py-4 italic">No previous chats.</p>
166
+ ) : (
167
+ conversations.map((c) => (
168
+ <div
169
+ key={c.id}
170
+ onClick={() => handleSelectConversation(c.id)}
171
+ className={`group flex items-center justify-between px-3 py-2.5 rounded-xl text-xs font-medium cursor-pointer transition-all ${
172
+ activeConvId === c.id
173
+ ? "bg-teal-50 text-teal-900 border border-teal-200 font-semibold"
174
+ : "text-slate-600 hover:bg-slate-50 hover:text-slate-900"
175
+ }`}
176
+ >
177
+ <span className="truncate flex-1">{c.title}</span>
178
+ <button
179
+ onClick={(e) => handleDeleteConversation(e, c.id)}
180
+ className="opacity-0 group-hover:opacity-100 p-1 hover:text-rose-600 transition-opacity"
181
+ title="Delete Chat"
182
+ >
183
+ <Trash2 className="w-3.5 h-3.5" />
184
+ </button>
185
+ </div>
186
+ ))
187
+ )}
188
+ </div>
189
+ </div>
190
+
191
+ {/* Main Chat Workspace */}
192
+ <div className="flex-1 flex flex-col bg-slate-50 min-w-0">
193
+ {/* Top Banner Notice */}
194
+ <div className="p-3 bg-white border-b border-slate-200/80">
195
+ <MedicalDisclaimer compact />
196
+ </div>
197
+
198
+ {/* Messages Thread */}
199
+ <div className="flex-1 overflow-y-auto p-4 sm:p-6 space-y-4">
200
+ {messages.length === 0 ? (
201
+ <div className="max-w-xl mx-auto my-auto py-12 text-center space-y-6">
202
+ <div className="w-14 h-14 rounded-2xl bg-teal-100 text-teal-700 flex items-center justify-center mx-auto shadow-xs">
203
+ <Bot className="w-8 h-8" />
204
+ </div>
205
+ <div className="space-y-1.5">
206
+ <h2 className="text-xl font-bold text-slate-900">Clinical AI Consultation Assistant</h2>
207
+ <p className="text-xs text-slate-500 max-w-md mx-auto">
208
+ Ask questions about medical conditions, pharmacological mechanisms, lab reports, or drug interactions.
209
+ </p>
210
+ </div>
211
+
212
+ {/* Prompt suggestions */}
213
+ <div className="space-y-2 text-left">
214
+ <p className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Suggested Queries:</p>
215
+ <div className="space-y-1.5">
216
+ {SAMPLE_PROMPTS.map((prompt, i) => (
217
+ <button
218
+ key={i}
219
+ onClick={() => handleSend(prompt)}
220
+ className={`w-full text-left p-3 rounded-xl border text-xs font-medium transition-all ${
221
+ i === 2
222
+ ? "bg-rose-50 hover:bg-rose-100 text-rose-900 border-rose-200"
223
+ : "bg-white hover:bg-slate-50 text-slate-700 border-slate-200 hover:border-teal-300"
224
+ }`}
225
+ >
226
+ {i === 2 && <span className="font-bold text-rose-700 mr-1.5">[Emergency Triage Test]</span>}
227
+ {prompt}
228
+ </button>
229
+ ))}
230
+ </div>
231
+ </div>
232
+ </div>
233
+ ) : (
234
+ messages.map((m, idx) => (
235
+ <div key={idx} className={`flex gap-3 ${m.role === "user" ? "justify-end" : "justify-start"}`}>
236
+ {m.role === "assistant" && (
237
+ <div className="w-8 h-8 rounded-lg bg-teal-600 text-white flex items-center justify-center flex-shrink-0 mt-1 shadow-xs">
238
+ <Bot className="w-4 h-4" />
239
+ </div>
240
+ )}
241
+
242
+ <div className={`max-w-2xl rounded-2xl p-4 text-xs sm:text-sm ${
243
+ m.role === "user"
244
+ ? "bg-teal-600 text-white shadow-xs ml-8"
245
+ : "bg-white border border-slate-200/80 text-slate-800 shadow-xs mr-8 space-y-3"
246
+ }`}>
247
+ {/* Plain Text or Summary */}
248
+ <p className="leading-relaxed whitespace-pre-wrap">{m.content}</p>
249
+
250
+ {/* Structured AI Output Rendering */}
251
+ {m.structured_data && (
252
+ <div className="space-y-3 pt-2 border-t border-slate-100 text-xs">
253
+ {/* Emergency Alert Box */}
254
+ {m.structured_data.is_emergency && (
255
+ <div className="p-3 bg-rose-50 border-2 border-rose-500 rounded-xl text-rose-950 space-y-1">
256
+ <div className="flex items-center gap-1.5 font-bold text-rose-800">
257
+ <ShieldAlert className="w-4 h-4 text-rose-600" />
258
+ <span>POTENTIAL MEDICAL EMERGENCY DETECTED</span>
259
+ </div>
260
+ <p className="leading-relaxed">{m.structured_data.emergency_instructions}</p>
261
+ </div>
262
+ )}
263
+
264
+ {/* Clinical Considerations */}
265
+ {(m.structured_data.possible_considerations || []).length > 0 && (
266
+ <div className="bg-slate-50 p-3 rounded-xl border border-slate-100 space-y-1.5">
267
+ <p className="font-bold text-slate-900 flex items-center gap-1.5">
268
+ <Info className="w-3.5 h-3.5 text-teal-600" />
269
+ <span>Clinical Considerations</span>
270
+ </p>
271
+ <ul className="space-y-1 text-slate-700 pl-2">
272
+ {m.structured_data.possible_considerations?.map((item, i) => (
273
+ <li key={i} className="list-disc list-inside">
274
+ {item}
275
+ </li>
276
+ ))}
277
+ </ul>
278
+ </div>
279
+ )}
280
+
281
+ {/* Questions for Doctor */}
282
+ {(m.structured_data.questions_for_doctor || []).length > 0 && (
283
+ <div className="bg-amber-50/60 p-3 rounded-xl border border-amber-200/80 space-y-1.5">
284
+ <p className="font-bold text-amber-950 flex items-center gap-1.5">
285
+ <HelpCircle className="w-3.5 h-3.5 text-amber-600" />
286
+ <span>Recommended Questions for your Physician</span>
287
+ </p>
288
+ <ul className="space-y-1 text-amber-900 pl-2">
289
+ {m.structured_data.questions_for_doctor?.map((q, i) => (
290
+ <li key={i} className="list-disc list-inside">
291
+ {q}
292
+ </li>
293
+ ))}
294
+ </ul>
295
+ </div>
296
+ )}
297
+
298
+ {/* Model Source Pill */}
299
+ <div className="flex items-center justify-between text-[10px] text-slate-400 font-mono pt-1">
300
+ <span>Provider: {m.model_provider || "Google Gemini / Local Engine"}</span>
301
+ <span>Non-Diagnostic Support</span>
302
+ </div>
303
+ </div>
304
+ )}
305
+ </div>
306
+
307
+ {m.role === "user" && (
308
+ <div className="w-8 h-8 rounded-lg bg-slate-200 text-slate-700 flex items-center justify-center flex-shrink-0 mt-1">
309
+ <UserIcon className="w-4 h-4" />
310
+ </div>
311
+ )}
312
+ </div>
313
+ ))
314
+ )}
315
+ {isSending && (
316
+ <div className="flex items-center gap-2 text-xs text-slate-500 pl-11">
317
+ <span className="w-4 h-4 border-2 border-teal-600 border-t-transparent rounded-full animate-spin"></span>
318
+ <span>Generating clinical decision support guidance...</span>
319
+ </div>
320
+ )}
321
+ <div ref={messagesEndRef} />
322
+ </div>
323
+
324
+ {/* Input Box */}
325
+ <div className="p-4 bg-white border-t border-slate-200/80">
326
+ <div className="max-w-4xl mx-auto flex items-center gap-2">
327
+ <input
328
+ type="text"
329
+ value={inputText}
330
+ onChange={(e) => setInputText(e.target.value)}
331
+ onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
332
+ placeholder="Ask about medications, symptoms, or clinical guidelines..."
333
+ className="flex-1 px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-xs sm:text-sm focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
334
+ />
335
+ <button
336
+ onClick={() => handleSend()}
337
+ disabled={isSending || !inputText.trim()}
338
+ className="p-3 bg-teal-600 hover:bg-teal-700 disabled:opacity-50 text-white rounded-xl shadow-xs transition-colors"
339
+ title="Send Message"
340
+ >
341
+ <Send className="w-4 h-4" />
342
+ </button>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ );
349
+ }
frontend/src/app/dashboard/page.tsx ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+ import Link from "next/link";
5
+ import { Sidebar } from "@/components/Sidebar";
6
+ import { MetricCard } from "@/components/MetricCard";
7
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
8
+ import { EntityBadge } from "@/components/EntityBadge";
9
+ import { useAuth } from "@/lib/auth-context";
10
+ import { documentApi, profileApi, historyApi } from "@/lib/api";
11
+ import {
12
+ FileText,
13
+ Activity,
14
+ Pill,
15
+ MessageSquareHeart,
16
+ Upload,
17
+ Cpu,
18
+ ArrowRight,
19
+ Clock,
20
+ CheckCircle,
21
+ } from "lucide-react";
22
+
23
+ export default function DashboardPage() {
24
+ const { user } = useAuth();
25
+ const [documents, setDocuments] = useState<any[]>([]);
26
+ const [profile, setProfile] = useState<any>(null);
27
+ const [history, setHistory] = useState<any[]>([]);
28
+ const [isLoading, setIsLoading] = useState(true);
29
+
30
+ useEffect(() => {
31
+ const fetchData = async () => {
32
+ try {
33
+ const [docsRes, profileRes, historyRes] = await Promise.allSettled([
34
+ documentApi.list(),
35
+ profileApi.getProfile(),
36
+ historyApi.getHistory(5),
37
+ ]);
38
+
39
+ if (docsRes.status === "fulfilled") setDocuments(docsRes.value.data.data || []);
40
+ if (profileRes.status === "fulfilled") setProfile(profileRes.value.data.data || null);
41
+ if (historyRes.status === "fulfilled") setHistory(historyRes.value.data.data || []);
42
+ } catch (err) {
43
+ console.error("Dashboard fetch error:", err);
44
+ } finally {
45
+ setIsLoading(false);
46
+ }
47
+ };
48
+ fetchData();
49
+ }, []);
50
+
51
+ // Aggregate conditions and medications from documents and profile
52
+ const conditions = Array.from(
53
+ new Set([
54
+ ...(profile?.chronic_conditions || []),
55
+ ...documents.flatMap((d) => d.analysis?.detected_conditions || []),
56
+ ])
57
+ );
58
+
59
+ const medications = Array.from(
60
+ new Set([
61
+ ...(profile?.current_medications || []),
62
+ ...documents.flatMap((d) => d.analysis?.detected_medications || []),
63
+ ])
64
+ );
65
+
66
+ return (
67
+ <div className="flex-1 flex bg-slate-50">
68
+ <Sidebar />
69
+
70
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-7xl mx-auto w-full">
71
+ {/* Welcome Header */}
72
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
73
+ <div>
74
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight">
75
+ Clinical Intelligence Dashboard
76
+ </h1>
77
+ <p className="text-xs text-slate-500 mt-0.5">
78
+ Welcome back, <span className="font-semibold text-slate-800">{user?.full_name || "Guest Clinician"}</span>.
79
+ Here is your active healthcare summary.
80
+ </p>
81
+ </div>
82
+
83
+ <div className="flex items-center gap-2">
84
+ <Link
85
+ href="/ner"
86
+ className="inline-flex items-center gap-1.5 px-3 py-2 bg-teal-50 hover:bg-teal-100 text-teal-800 border border-teal-200 rounded-xl text-xs font-semibold shadow-xs transition-colors"
87
+ >
88
+ <Cpu className="w-3.5 h-3.5 text-teal-600" />
89
+ <span>Biomedical NER</span>
90
+ </Link>
91
+ <Link
92
+ href="/reports"
93
+ className="inline-flex items-center gap-1.5 px-3.5 py-2 bg-teal-600 hover:bg-teal-700 text-white rounded-xl text-xs font-semibold shadow-xs transition-colors"
94
+ >
95
+ <Upload className="w-3.5 h-3.5" />
96
+ <span>Upload Report</span>
97
+ </Link>
98
+ </div>
99
+ </div>
100
+
101
+ <MedicalDisclaimer />
102
+
103
+ {/* Metric Cards */}
104
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
105
+ <MetricCard
106
+ title="Reports Analyzed"
107
+ value={documents.length}
108
+ subtitle="Clinical documents parsed"
109
+ icon={FileText}
110
+ iconBg="bg-blue-50"
111
+ iconColor="text-blue-600"
112
+ />
113
+ <MetricCard
114
+ title="Detected Conditions"
115
+ value={conditions.length}
116
+ subtitle="Clinical indications"
117
+ icon={Activity}
118
+ iconBg="bg-rose-50"
119
+ iconColor="text-rose-600"
120
+ />
121
+ <MetricCard
122
+ title="Active Medications"
123
+ value={medications.length}
124
+ subtitle="Pharmaceutical agents"
125
+ icon={Pill}
126
+ iconBg="bg-emerald-50"
127
+ iconColor="text-emerald-600"
128
+ />
129
+ <MetricCard
130
+ title="AI Model Status"
131
+ value="BC5CDR"
132
+ subtitle="RoBERTa-large Local GPU"
133
+ icon={Cpu}
134
+ iconBg="bg-teal-50"
135
+ iconColor="text-teal-600"
136
+ />
137
+ </div>
138
+
139
+ {/* 2-Column Content: Reports & Entities */}
140
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
141
+ {/* Left 2 Cols: Recent Documents */}
142
+ <div className="lg:col-span-2 bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs space-y-4">
143
+ <div className="flex items-center justify-between">
144
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
145
+ <FileText className="w-4 h-4 text-teal-600" />
146
+ <span>Recent Analyzed Medical Reports</span>
147
+ </h2>
148
+ <Link href="/reports" className="text-xs font-semibold text-teal-600 hover:underline flex items-center gap-1">
149
+ View All <ArrowRight className="w-3 h-3" />
150
+ </Link>
151
+ </div>
152
+
153
+ {documents.length === 0 ? (
154
+ <div className="text-center py-10 border border-dashed border-slate-200 rounded-xl bg-slate-50/50 space-y-3">
155
+ <FileText className="w-8 h-8 text-slate-300 mx-auto" />
156
+ <p className="text-xs text-slate-500">No medical reports uploaded yet.</p>
157
+ <Link
158
+ href="/reports"
159
+ className="inline-flex items-center gap-1 px-3 py-1.5 bg-teal-600 text-white rounded-lg text-xs font-semibold"
160
+ >
161
+ Upload First Report
162
+ </Link>
163
+ </div>
164
+ ) : (
165
+ <div className="space-y-3">
166
+ {documents.slice(0, 3).map((doc) => (
167
+ <div
168
+ key={doc.id}
169
+ className="p-3.5 rounded-xl border border-slate-100 bg-slate-50/50 hover:bg-slate-50 hover:border-slate-200 transition-all flex items-start justify-between gap-3"
170
+ >
171
+ <div className="space-y-1.5 flex-1 min-w-0">
172
+ <div className="flex items-center gap-2">
173
+ <span className="font-semibold text-xs text-slate-900 truncate">
174
+ {doc.original_filename}
175
+ </span>
176
+ <span className="text-[10px] uppercase font-bold bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded">
177
+ {doc.status}
178
+ </span>
179
+ </div>
180
+ {doc.analysis?.summary && (
181
+ <p className="text-[11px] text-slate-600 line-clamp-2 leading-relaxed">
182
+ {doc.analysis.summary}
183
+ </p>
184
+ )}
185
+ <div className="flex flex-wrap gap-1.5 pt-1">
186
+ {(doc.analysis?.detected_conditions || []).slice(0, 2).map((c: string) => (
187
+ <EntityBadge key={c} label="DISEASE" text={c} />
188
+ ))}
189
+ {(doc.analysis?.detected_medications || []).slice(0, 2).map((m: string) => (
190
+ <EntityBadge key={m} label="CHEMICAL" text={m} />
191
+ ))}
192
+ </div>
193
+ </div>
194
+ <Link
195
+ href={`/reports/${doc.id}`}
196
+ className="text-xs font-semibold text-teal-600 hover:text-teal-700 self-center whitespace-nowrap"
197
+ >
198
+ View Report →
199
+ </Link>
200
+ </div>
201
+ ))}
202
+ </div>
203
+ )}
204
+ </div>
205
+
206
+ {/* Right Col: Extracted Health Entities & Activity */}
207
+ <div className="space-y-6">
208
+ {/* Extracted Conditions & Meds */}
209
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs space-y-4">
210
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
211
+ <Activity className="w-4 h-4 text-rose-600" />
212
+ <span>Extracted Entities</span>
213
+ </h2>
214
+
215
+ <div className="space-y-3">
216
+ <div>
217
+ <p className="text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-2">Conditions</p>
218
+ {conditions.length === 0 ? (
219
+ <p className="text-xs text-slate-400">None detected yet</p>
220
+ ) : (
221
+ <div className="flex flex-wrap gap-1.5">
222
+ {conditions.map((c: any) => (
223
+ <EntityBadge key={c} label="DISEASE" text={c} />
224
+ ))}
225
+ </div>
226
+ )}
227
+ </div>
228
+
229
+ <div className="pt-2 border-t border-slate-100">
230
+ <p className="text-[11px] font-bold text-slate-500 uppercase tracking-wider mb-2">Medications</p>
231
+ {medications.length === 0 ? (
232
+ <p className="text-xs text-slate-400">None detected yet</p>
233
+ ) : (
234
+ <div className="flex flex-wrap gap-1.5">
235
+ {medications.map((m: any) => (
236
+ <EntityBadge key={m} label="CHEMICAL" text={m} />
237
+ ))}
238
+ </div>
239
+ )}
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ {/* Recent Timeline */}
245
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs space-y-3">
246
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
247
+ <Clock className="w-4 h-4 text-teal-600" />
248
+ <span>Recent Activity</span>
249
+ </h2>
250
+
251
+ {history.length === 0 ? (
252
+ <p className="text-xs text-slate-400">No recent activity recorded.</p>
253
+ ) : (
254
+ <div className="space-y-2.5">
255
+ {history.map((h) => (
256
+ <div key={h.id} className="flex items-start gap-2 text-xs">
257
+ <CheckCircle className="w-3.5 h-3.5 text-teal-600 mt-0.5 flex-shrink-0" />
258
+ <div>
259
+ <p className="text-slate-800 font-medium">{h.description}</p>
260
+ <p className="text-[10px] text-slate-400 font-mono">
261
+ {new Date(h.created_at).toLocaleDateString()} {new Date(h.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
262
+ </p>
263
+ </div>
264
+ </div>
265
+ ))}
266
+ </div>
267
+ )}
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ );
274
+ }
frontend/src/app/demo/page.tsx ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import Link from "next/link";
5
+ import { Sidebar } from "@/components/Sidebar";
6
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
7
+ import { useAuth } from "@/lib/auth-context";
8
+ import {
9
+ Sparkles,
10
+ Cpu,
11
+ FileText,
12
+ MessageSquareHeart,
13
+ User,
14
+ ShieldCheck,
15
+ ArrowRight,
16
+ CheckCircle2,
17
+ ExternalLink,
18
+ } from "lucide-react";
19
+
20
+ export default function DemoPage() {
21
+ const { quickLogin } = useAuth();
22
+ const [activeStep, setActiveStep] = useState(1);
23
+
24
+ const steps = [
25
+ {
26
+ id: 1,
27
+ title: "1. Verified Local ML Engine",
28
+ subtitle: "RoBERTa-large BC5CDR Token Classification",
29
+ desc: "Demonstrate that the model is loaded entirely locally from disk (models/bc5cdr-ner) with zero external cloud NER dependencies.",
30
+ link: "/ner",
31
+ actionText: "Open NER Inference Visualizer",
32
+ icon: Cpu,
33
+ color: "text-teal-600",
34
+ bg: "bg-teal-50",
35
+ },
36
+ {
37
+ id: 2,
38
+ title: "2. Medical Document Analysis",
39
+ subtitle: "Multi-Format PDF/Text Extraction & Summarization",
40
+ desc: "Upload or inspect clinical reports to see automated chemical and disease extraction, key findings, and non-diagnostic summaries.",
41
+ link: "/reports",
42
+ actionText: "Open Document Intelligence",
43
+ icon: FileText,
44
+ color: "text-blue-600",
45
+ bg: "bg-blue-50",
46
+ },
47
+ {
48
+ id: 3,
49
+ title: "3. Clinical AI Consultation & Emergency Triage",
50
+ subtitle: "Decision Support & Non-Diagnostic Guardrails",
51
+ desc: "Interact with the medical assistant. Test the heuristic red-flag detector with acute symptom queries (e.g. chest pain) to see emergency triage notices.",
52
+ link: "/assistant",
53
+ actionText: "Open AI Assistant",
54
+ icon: MessageSquareHeart,
55
+ color: "text-purple-600",
56
+ bg: "bg-purple-50",
57
+ },
58
+ {
59
+ id: 4,
60
+ title: "4. Longitudinal Health Profile & Timeline",
61
+ subtitle: "Patient Anthropometrics & Allergy Tracking",
62
+ desc: "Review patient physiological indicators, chronic conditions, and chronological activity records with full traceability.",
63
+ link: "/profile",
64
+ actionText: "Open Health Profile",
65
+ icon: User,
66
+ color: "text-emerald-600",
67
+ bg: "bg-emerald-50",
68
+ },
69
+ {
70
+ id: 5,
71
+ title: "5. Security, RBAC & Immutable Audit Logs",
72
+ subtitle: "Role-Based Access & Operation Traceability",
73
+ desc: "Switch to the Admin account to view platform statistics and tamper-evident audit logs of all authentication and document activities.",
74
+ link: "/admin",
75
+ actionText: "Open Admin & Audit Logs",
76
+ icon: ShieldCheck,
77
+ color: "text-rose-600",
78
+ bg: "bg-rose-50",
79
+ },
80
+ ];
81
+
82
+ return (
83
+ <div className="flex-1 flex bg-slate-50">
84
+ <Sidebar />
85
+
86
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-5xl mx-auto w-full">
87
+ {/* Header */}
88
+ <div className="bg-gradient-to-r from-teal-900 to-slate-900 rounded-2xl p-6 sm:p-8 text-white space-y-3 shadow-md">
89
+ <div className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-teal-500/20 text-teal-300 text-[10px] font-extrabold uppercase tracking-wider">
90
+ <Sparkles className="w-3.5 h-3.5" />
91
+ <span>Mentor Presentation Guide</span>
92
+ </div>
93
+ <h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight">
94
+ SanjeevaniAI Demonstration Walkthrough
95
+ </h1>
96
+ <p className="text-xs sm:text-sm text-slate-300 max-w-2xl leading-relaxed">
97
+ Follow this 5-step guided walkthrough to demonstrate every architectural component, local neural model inference,
98
+ clinical safety guardrails, and secure document processing to your technical mentor.
99
+ </p>
100
+
101
+ <div className="pt-2 flex flex-wrap gap-2">
102
+ <button
103
+ onClick={() => quickLogin("patient")}
104
+ className="px-3 py-1.5 bg-white text-slate-900 hover:bg-slate-100 rounded-lg text-xs font-bold transition-colors shadow-xs"
105
+ >
106
+ Demo as Patient (Alex Mercer)
107
+ </button>
108
+ <button
109
+ onClick={() => quickLogin("doctor")}
110
+ className="px-3 py-1.5 bg-teal-700 hover:bg-teal-600 text-white rounded-lg text-xs font-bold transition-colors"
111
+ >
112
+ Demo as Doctor (Dr. Jenkins)
113
+ </button>
114
+ <button
115
+ onClick={() => quickLogin("admin")}
116
+ className="px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded-lg text-xs font-bold transition-colors"
117
+ >
118
+ Demo as Admin
119
+ </button>
120
+ </div>
121
+ </div>
122
+
123
+ <MedicalDisclaimer />
124
+
125
+ {/* Guided Steps */}
126
+ <div className="space-y-4">
127
+ {steps.map((step) => {
128
+ const Icon = step.icon;
129
+ return (
130
+ <div
131
+ key={step.id}
132
+ className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs hover:border-teal-300 transition-all flex flex-col md:flex-row md:items-center md:justify-between gap-4"
133
+ >
134
+ <div className="flex items-start gap-4">
135
+ <div className={`p-3 rounded-xl ${step.bg} ${step.color} flex-shrink-0 mt-0.5`}>
136
+ <Icon className="w-6 h-6" />
137
+ </div>
138
+ <div className="space-y-1">
139
+ <div className="flex items-center gap-2">
140
+ <h3 className="text-sm font-bold text-slate-900">{step.title}</h3>
141
+ <span className="text-[10px] font-semibold text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
142
+ {step.subtitle}
143
+ </span>
144
+ </div>
145
+ <p className="text-xs text-slate-600 leading-relaxed max-w-xl">{step.desc}</p>
146
+ </div>
147
+ </div>
148
+
149
+ <Link
150
+ href={step.link}
151
+ className="inline-flex items-center gap-1.5 px-4 py-2.5 bg-slate-900 hover:bg-teal-600 text-white rounded-xl text-xs font-semibold shadow-xs transition-colors self-start md:self-center whitespace-nowrap"
152
+ >
153
+ <span>{step.actionText}</span>
154
+ <ArrowRight className="w-3.5 h-3.5" />
155
+ </Link>
156
+ </div>
157
+ );
158
+ })}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ );
163
+ }
frontend/src/app/globals.css ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 210 40% 98%;
8
+ --foreground: 222.2 84% 4.9%;
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 222.2 84% 4.9%;
11
+ --popover: 0 0% 100%;
12
+ --popover-foreground: 222.2 84% 4.9%;
13
+ --primary: 173 80% 36%;
14
+ --primary-foreground: 210 40% 98%;
15
+ --muted: 210 40% 96.1%;
16
+ --muted-foreground: 215.4 16.3% 46.9%;
17
+ --border: 214.3 31.8% 91.4%;
18
+ }
19
+
20
+ body {
21
+ @apply bg-slate-50 text-slate-900 antialiased selection:bg-teal-500 selection:text-white;
22
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
23
+ }
24
+ }
25
+
26
+ /* Custom scrollbars */
27
+ ::-webkit-scrollbar {
28
+ width: 6px;
29
+ height: 6px;
30
+ }
31
+
32
+ ::-webkit-scrollbar-track {
33
+ background: transparent;
34
+ }
35
+
36
+ ::-webkit-scrollbar-thumb {
37
+ background: #cbd5e1;
38
+ border-radius: 4px;
39
+ }
40
+
41
+ ::-webkit-scrollbar-thumb:hover {
42
+ background: #94a3b8;
43
+ }
44
+
45
+ /* NER Highlights */
46
+ .entity-chemical {
47
+ background-color: #ecfdf5;
48
+ border: 1px solid #10b981;
49
+ color: #065f46;
50
+ padding: 1px 6px;
51
+ border-radius: 4px;
52
+ font-weight: 500;
53
+ }
54
+
55
+ .entity-disease {
56
+ background-color: #fef2f2;
57
+ border: 1px solid #ef4444;
58
+ color: #991b1b;
59
+ padding: 1px 6px;
60
+ border-radius: 4px;
61
+ font-weight: 500;
62
+ }
frontend/src/app/history/page.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { Sidebar } from "@/components/Sidebar";
5
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
6
+ import { historyApi } from "@/lib/api";
7
+ import { History as HistoryIcon, Clock, FileText, MessageSquareHeart, UserCheck, Activity } from "lucide-react";
8
+
9
+ export default function HistoryPage() {
10
+ const [history, setHistory] = useState<any[]>([]);
11
+ const [filter, setFilter] = useState<string>("ALL");
12
+ const [isLoading, setIsLoading] = useState<boolean>(true);
13
+
14
+ useEffect(() => {
15
+ historyApi
16
+ .getHistory(100)
17
+ .then((res) => setHistory(res.data.data || []))
18
+ .catch((err) => console.error("Error loading history:", err))
19
+ .finally(() => setIsLoading(false));
20
+ }, []);
21
+
22
+ const filteredHistory = filter === "ALL" ? history : history.filter((h) => h.action_type === filter);
23
+
24
+ const getIcon = (type: string) => {
25
+ switch (type) {
26
+ case "REPORT_ANALYSIS":
27
+ return <FileText className="w-4 h-4 text-blue-600" />;
28
+ case "CHAT":
29
+ return <MessageSquareHeart className="w-4 h-4 text-teal-600" />;
30
+ case "PROFILE_UPDATE":
31
+ return <UserCheck className="w-4 h-4 text-emerald-600" />;
32
+ default:
33
+ return <Activity className="w-4 h-4 text-slate-600" />;
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="flex-1 flex bg-slate-50">
39
+ <Sidebar />
40
+
41
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-5xl mx-auto w-full">
42
+ {/* Header */}
43
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
44
+ <div>
45
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
46
+ <HistoryIcon className="w-6 h-6 text-teal-600" />
47
+ <span>Medical History & Activity Log</span>
48
+ </h1>
49
+ <p className="text-xs text-slate-500 mt-0.5">
50
+ Traceable chronological record of clinical report analyses, AI consultations, and profile updates.
51
+ </p>
52
+ </div>
53
+
54
+ {/* Filter Pills */}
55
+ <div className="flex items-center gap-1.5 bg-white p-1 rounded-xl border border-slate-200 shadow-xs text-xs">
56
+ {["ALL", "REPORT_ANALYSIS", "CHAT", "PROFILE_UPDATE"].map((t) => (
57
+ <button
58
+ key={t}
59
+ onClick={() => setFilter(t)}
60
+ className={`px-3 py-1.5 rounded-lg font-medium transition-all ${
61
+ filter === t
62
+ ? "bg-teal-600 text-white shadow-xs"
63
+ : "text-slate-600 hover:bg-slate-100"
64
+ }`}
65
+ >
66
+ {t.replace("_", " ")}
67
+ </button>
68
+ ))}
69
+ </div>
70
+ </div>
71
+
72
+ <MedicalDisclaimer />
73
+
74
+ {/* Timeline */}
75
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-6 shadow-xs">
76
+ {isLoading ? (
77
+ <div className="text-center py-8">
78
+ <span className="w-6 h-6 border-2 border-teal-600 border-t-transparent rounded-full animate-spin inline-block"></span>
79
+ <p className="text-xs text-slate-500 mt-2">Loading timeline...</p>
80
+ </div>
81
+ ) : filteredHistory.length === 0 ? (
82
+ <div className="text-center py-12 border border-dashed border-slate-200 rounded-xl space-y-2">
83
+ <Clock className="w-8 h-8 text-slate-300 mx-auto" />
84
+ <p className="text-xs text-slate-500">No activity records found.</p>
85
+ </div>
86
+ ) : (
87
+ <div className="relative pl-6 space-y-6 before:absolute before:left-2.5 before:top-2 before:bottom-2 before:w-0.5 before:bg-slate-200">
88
+ {filteredHistory.map((item) => (
89
+ <div key={item.id} className="relative flex items-start gap-4">
90
+ <div className="absolute -left-6 top-1 w-5 h-5 rounded-full bg-white border-2 border-teal-600 flex items-center justify-center">
91
+ <span className="w-1.5 h-1.5 rounded-full bg-teal-600"></span>
92
+ </div>
93
+
94
+ <div className="flex-1 bg-slate-50 border border-slate-200/70 p-3.5 rounded-xl flex items-start justify-between gap-3">
95
+ <div className="space-y-1">
96
+ <div className="flex items-center gap-2">
97
+ {getIcon(item.action_type)}
98
+ <span className="text-xs font-bold text-slate-900">{item.description}</span>
99
+ </div>
100
+ <p className="text-[10px] text-slate-400 font-mono">
101
+ {new Date(item.created_at).toLocaleString()}
102
+ </p>
103
+ </div>
104
+
105
+ {item.entity_count > 0 && (
106
+ <span className="text-[10px] font-bold bg-teal-100 text-teal-800 px-2 py-0.5 rounded">
107
+ {item.entity_count} Entities Extracted
108
+ </span>
109
+ )}
110
+ </div>
111
+ </div>
112
+ ))}
113
+ </div>
114
+ )}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ );
119
+ }
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+ import { AuthProvider } from "@/lib/auth-context";
4
+ import { Navbar } from "@/components/Navbar";
5
+
6
+ export const metadata: Metadata = {
7
+ title: "SanjeevaniAI — Healthcare Intelligence Platform",
8
+ description: "Industry-grade AI-powered healthcare intelligence and clinical decision-support system.",
9
+ };
10
+
11
+ export default function RootLayout({
12
+ children,
13
+ }: {
14
+ children: React.ReactNode;
15
+ }) {
16
+ return (
17
+ <html lang="en">
18
+ <body className="min-h-screen bg-slate-50 font-sans flex flex-col">
19
+ <AuthProvider>
20
+ <Navbar />
21
+ <main className="flex-1 flex flex-col">{children}</main>
22
+ </AuthProvider>
23
+ </body>
24
+ </html>
25
+ );
26
+ }
frontend/src/app/login/page.tsx ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { useAuth } from "@/lib/auth-context";
7
+ import { Activity, Lock, Mail, AlertCircle, Sparkles, ArrowRight } from "lucide-react";
8
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
9
+
10
+ export default function LoginPage() {
11
+ const router = useRouter();
12
+ const { login, quickLogin } = useAuth();
13
+
14
+ const [email, setEmail] = useState("");
15
+ const [password, setPassword] = useState("");
16
+ const [error, setError] = useState<string | null>(null);
17
+ const [isSubmitting, setIsSubmitting] = useState(false);
18
+
19
+ const handleSubmit = async (e: React.FormEvent) => {
20
+ e.preventDefault();
21
+ setError(null);
22
+ setIsSubmitting(true);
23
+ try {
24
+ await login(email, password);
25
+ router.push("/dashboard");
26
+ } catch (err: any) {
27
+ setError(err.response?.data?.error?.message || "Invalid email or password.");
28
+ } finally {
29
+ setIsSubmitting(false);
30
+ }
31
+ };
32
+
33
+ const handleQuickLogin = async (role: "patient" | "doctor" | "admin") => {
34
+ setError(null);
35
+ setIsSubmitting(true);
36
+ try {
37
+ await quickLogin(role);
38
+ router.push("/dashboard");
39
+ } catch (err: any) {
40
+ setError("Failed to log in with synthetic demo account.");
41
+ } finally {
42
+ setIsSubmitting(false);
43
+ }
44
+ };
45
+
46
+ return (
47
+ <div className="min-h-[calc(100vh-61px)] flex items-center justify-center p-4 bg-slate-50">
48
+ <div className="max-w-md w-full bg-white border border-slate-200/80 rounded-2xl p-6 sm:p-8 shadow-sm space-y-6">
49
+ <div className="text-center space-y-2">
50
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-tr from-teal-600 to-emerald-500 flex items-center justify-center text-white mx-auto shadow-xs">
51
+ <Activity className="w-6 h-6" />
52
+ </div>
53
+ <h1 className="text-2xl font-bold text-slate-900">Sign in to SanjeevaniAI</h1>
54
+ <p className="text-xs text-slate-500">Access your healthcare intelligence portal</p>
55
+ </div>
56
+
57
+ {error && (
58
+ <div className="p-3 bg-rose-50 border border-rose-200 rounded-xl text-rose-800 text-xs flex items-center gap-2">
59
+ <AlertCircle className="w-4 h-4 text-rose-600 flex-shrink-0" />
60
+ <span>{error}</span>
61
+ </div>
62
+ )}
63
+
64
+ <form onSubmit={handleSubmit} className="space-y-4">
65
+ <div>
66
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Email Address</label>
67
+ <div className="relative">
68
+ <Mail className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
69
+ <input
70
+ type="email"
71
+ required
72
+ value={email}
73
+ onChange={(e) => setEmail(e.target.value)}
74
+ placeholder="name@example.com"
75
+ className="w-full pl-9 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
76
+ />
77
+ </div>
78
+ </div>
79
+
80
+ <div>
81
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Password</label>
82
+ <div className="relative">
83
+ <Lock className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
84
+ <input
85
+ type="password"
86
+ required
87
+ value={password}
88
+ onChange={(e) => setPassword(e.target.value)}
89
+ placeholder="••••••••••••"
90
+ className="w-full pl-9 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ <button
96
+ type="submit"
97
+ disabled={isSubmitting}
98
+ className="w-full py-2.5 px-4 bg-teal-600 hover:bg-teal-700 disabled:opacity-50 text-white text-xs font-semibold rounded-xl shadow-xs transition-all flex items-center justify-center gap-2"
99
+ >
100
+ {isSubmitting ? "Signing in..." : "Sign In"}
101
+ </button>
102
+ </form>
103
+
104
+ {/* Demo Fast-Login Section */}
105
+ <div className="pt-4 border-t border-slate-100 space-y-3">
106
+ <div className="flex items-center gap-1.5 text-xs font-bold text-slate-700">
107
+ <Sparkles className="w-3.5 h-3.5 text-teal-600" />
108
+ <span>Mentor Presentation Quick Logins</span>
109
+ </div>
110
+
111
+ <div className="grid grid-cols-3 gap-2">
112
+ <button
113
+ onClick={() => handleQuickLogin("patient")}
114
+ disabled={isSubmitting}
115
+ className="px-2 py-1.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-800 border border-emerald-200 rounded-lg text-[11px] font-semibold transition-colors"
116
+ >
117
+ Patient
118
+ </button>
119
+ <button
120
+ onClick={() => handleQuickLogin("doctor")}
121
+ disabled={isSubmitting}
122
+ className="px-2 py-1.5 bg-blue-50 hover:bg-blue-100 text-blue-800 border border-blue-200 rounded-lg text-[11px] font-semibold transition-colors"
123
+ >
124
+ Doctor
125
+ </button>
126
+ <button
127
+ onClick={() => handleQuickLogin("admin")}
128
+ disabled={isSubmitting}
129
+ className="px-2 py-1.5 bg-purple-50 hover:bg-purple-100 text-purple-800 border border-purple-200 rounded-lg text-[11px] font-semibold transition-colors"
130
+ >
131
+ Admin
132
+ </button>
133
+ </div>
134
+ </div>
135
+
136
+ <div className="text-center text-xs text-slate-500">
137
+ Don&apos;t have an account?{" "}
138
+ <Link href="/register" className="text-teal-600 font-semibold hover:underline">
139
+ Register here
140
+ </Link>
141
+ </div>
142
+
143
+ <MedicalDisclaimer compact />
144
+ </div>
145
+ </div>
146
+ );
147
+ }
frontend/src/app/ner/page.tsx ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Sidebar } from "@/components/Sidebar";
5
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
6
+ import { EntityBadge } from "@/components/EntityBadge";
7
+ import { nerApi } from "@/lib/api";
8
+ import {
9
+ Cpu,
10
+ Sparkles,
11
+ Play,
12
+ Clock,
13
+ CheckCircle2,
14
+ AlertCircle,
15
+ Pill,
16
+ Activity,
17
+ Layers,
18
+ Copy,
19
+ Check,
20
+ } from "lucide-react";
21
+
22
+ interface NEREntity {
23
+ text: string;
24
+ label: string;
25
+ start: number;
26
+ end: number;
27
+ confidence?: number | null;
28
+ model: string;
29
+ }
30
+
31
+ interface NERResult {
32
+ request_id: string;
33
+ model: {
34
+ name: string;
35
+ version: string;
36
+ provider: string;
37
+ device: string;
38
+ status: string;
39
+ };
40
+ entities: NEREntity[];
41
+ entity_count: number;
42
+ processing_time_ms: number;
43
+ text_length: number;
44
+ }
45
+
46
+ const PRESETS = [
47
+ {
48
+ title: "Diabetes & Hypertension (Standard Case)",
49
+ text: "The patient was prescribed metformin 500mg and lisinopril 10mg for type 2 diabetes mellitus and secondary hypertension.",
50
+ },
51
+ {
52
+ title: "Cardiology Acute Coronary Syndrome",
53
+ text: "Following acute myocardial infarction, the patient received loading doses of aspirin, clopidogrel, and unfractionated heparin.",
54
+ },
55
+ {
56
+ title: "Rheumatology & Autoimmune Therapy",
57
+ text: "Patient diagnosed with active rheumatoid arthritis was initiated on methotrexate alongside daily folic acid supplementation.",
58
+ },
59
+ {
60
+ title: "Oncology & Chemotherapy Protocol",
61
+ text: "Patient with metastatic colorectal cancer undergoing treatment with oxaliplatin, fluorouracil, and leucovorin with peripheral neuropathy monitoring.",
62
+ },
63
+ ];
64
+
65
+ export default function NERPage() {
66
+ const [text, setText] = useState<string>(PRESETS[0].text);
67
+ const [result, setResult] = useState<NERResult | null>(null);
68
+ const [isLoading, setIsLoading] = useState<boolean>(false);
69
+ const [error, setError] = useState<string | null>(null);
70
+ const [copied, setCopied] = useState<boolean>(false);
71
+
72
+ const handleAnalyze = async (inputText?: string) => {
73
+ const textToAnalyze = inputText !== undefined ? inputText : text;
74
+ if (!textToAnalyze.trim()) return;
75
+
76
+ setIsLoading(true);
77
+ setError(null);
78
+ try {
79
+ const res = await nerApi.analyze(textToAnalyze);
80
+ setResult(res.data);
81
+ } catch (err: any) {
82
+ setError(err.response?.data?.error?.message || "Failed to execute NER model inference.");
83
+ } finally {
84
+ setIsLoading(false);
85
+ }
86
+ };
87
+
88
+ const handleCopy = () => {
89
+ if (!result) return;
90
+ navigator.clipboard.writeText(JSON.stringify(result, null, 2));
91
+ setCopied(true);
92
+ setTimeout(() => setCopied(false), 2000);
93
+ };
94
+
95
+ // Helper to render text with highlighted entities
96
+ const renderHighlightedText = () => {
97
+ if (!result || result.entities.length === 0) {
98
+ return <p className="text-sm text-slate-700 leading-relaxed font-mono">{text}</p>;
99
+ }
100
+
101
+ // Sort entities by start offset
102
+ const sorted = [...result.entities].sort((a, b) => a.start - b.start);
103
+ const elements: React.ReactNode[] = [];
104
+ let lastIndex = 0;
105
+
106
+ sorted.forEach((ent, i) => {
107
+ // Add plain text before entity
108
+ if (ent.start > lastIndex) {
109
+ elements.push(
110
+ <span key={`plain-${lastIndex}`}>{text.substring(lastIndex, ent.start)}</span>
111
+ );
112
+ }
113
+
114
+ // Add highlighted entity
115
+ const isChemical = ent.label === "CHEMICAL";
116
+ elements.push(
117
+ <mark
118
+ key={`ent-${i}`}
119
+ className={`px-1.5 py-0.5 rounded-md font-semibold text-xs transition-all cursor-help border inline-block my-0.5 ${
120
+ isChemical
121
+ ? "bg-emerald-100/90 text-emerald-950 border-emerald-400"
122
+ : "bg-rose-100/90 text-rose-950 border-rose-400"
123
+ }`}
124
+ title={`${ent.label} (Confidence: ${ent.confidence ? Math.round(ent.confidence * 100) + "%" : "N/A"})`}
125
+ >
126
+ {text.substring(ent.start, ent.end)}
127
+ <span
128
+ className={`ml-1 text-[9px] uppercase font-bold px-1 py-0.2 rounded ${
129
+ isChemical ? "bg-emerald-200 text-emerald-900" : "bg-rose-200 text-rose-900"
130
+ }`}
131
+ >
132
+ {ent.label}
133
+ </span>
134
+ </mark>
135
+ );
136
+
137
+ lastIndex = ent.end;
138
+ });
139
+
140
+ // Add remaining plain text
141
+ if (lastIndex < text.length) {
142
+ elements.push(
143
+ <span key={`plain-${lastIndex}`}>{text.substring(lastIndex)}</span>
144
+ );
145
+ }
146
+
147
+ return <div className="text-sm leading-loose font-mono bg-slate-50/70 p-4 rounded-xl border border-slate-200">{elements}</div>;
148
+ };
149
+
150
+ return (
151
+ <div className="flex-1 flex bg-slate-50">
152
+ <Sidebar />
153
+
154
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-7xl mx-auto w-full">
155
+ {/* Page Header with Model Spec Badge */}
156
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
157
+ <div>
158
+ <div className="flex items-center gap-2 mb-1">
159
+ <span className="px-2.5 py-0.5 bg-teal-100 text-teal-800 rounded text-[10px] font-extrabold uppercase tracking-wider">
160
+ Pretrained ML Engine
161
+ </span>
162
+ <span className="flex items-center gap-1 text-[11px] font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 px-2 py-0.5 rounded">
163
+ <CheckCircle2 className="w-3 h-3 text-emerald-600" />
164
+ Loaded Locally (D:\SanjeevaniAI\models\bc5cdr-ner)
165
+ </span>
166
+ </div>
167
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
168
+ <Cpu className="w-6 h-6 text-teal-600" />
169
+ <span>Biomedical Named Entity Recognition (NER)</span>
170
+ </h1>
171
+ <p className="text-xs text-slate-500 mt-0.5">
172
+ Live token classification using RoBERTa-large fine-tuned on BioCreative V CDR (BC5CDR).
173
+ </p>
174
+ </div>
175
+
176
+ <div className="flex items-center gap-2 text-xs font-mono text-slate-600 bg-white border border-slate-200 p-2 rounded-xl shadow-xs">
177
+ <span className="font-bold text-slate-800">Architecture:</span>
178
+ <span>RobertaForTokenClassification (1.4 GB)</span>
179
+ </div>
180
+ </div>
181
+
182
+ <MedicalDisclaimer />
183
+
184
+ {/* Presets Bar */}
185
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-4 shadow-xs space-y-2.5">
186
+ <p className="text-xs font-bold text-slate-700 flex items-center gap-1.5">
187
+ <Sparkles className="w-3.5 h-3.5 text-teal-600" />
188
+ <span>Mentor Demonstration Presets:</span>
189
+ </p>
190
+ <div className="flex flex-wrap gap-2">
191
+ {PRESETS.map((p, idx) => (
192
+ <button
193
+ key={idx}
194
+ onClick={() => {
195
+ setText(p.text);
196
+ handleAnalyze(p.text);
197
+ }}
198
+ className="px-3 py-1.5 bg-slate-50 hover:bg-teal-50 hover:text-teal-900 hover:border-teal-300 border border-slate-200 rounded-lg text-xs font-medium text-slate-700 transition-all text-left"
199
+ >
200
+ {p.title}
201
+ </button>
202
+ ))}
203
+ </div>
204
+ </div>
205
+
206
+ {/* Main 2-Column Inference Workspace */}
207
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
208
+ {/* Left Column: Input Text Area */}
209
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs flex flex-col space-y-4">
210
+ <div className="flex items-center justify-between">
211
+ <label className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-2">
212
+ <Layers className="w-4 h-4 text-teal-600" />
213
+ <span>Input Clinical Text</span>
214
+ </label>
215
+ <span className="text-[11px] text-slate-400 font-mono">{text.length} characters</span>
216
+ </div>
217
+
218
+ <textarea
219
+ rows={8}
220
+ value={text}
221
+ onChange={(e) => setText(e.target.value)}
222
+ placeholder="Enter patient notes, clinical summaries, or medical text containing diseases and medications..."
223
+ className="w-full p-3.5 bg-slate-50 border border-slate-200 rounded-xl text-xs sm:text-sm font-mono leading-relaxed focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
224
+ />
225
+
226
+ <div className="flex items-center justify-between pt-2">
227
+ <div className="flex items-center gap-3 text-xs text-slate-500">
228
+ <span className="flex items-center gap-1 text-emerald-700 font-semibold">
229
+ <span className="w-2.5 h-2.5 rounded-full bg-emerald-500"></span> CHEMICAL
230
+ </span>
231
+ <span className="flex items-center gap-1 text-rose-700 font-semibold">
232
+ <span className="w-2.5 h-2.5 rounded-full bg-rose-500"></span> DISEASE
233
+ </span>
234
+ </div>
235
+
236
+ <button
237
+ onClick={() => handleAnalyze()}
238
+ disabled={isLoading || !text.trim()}
239
+ className="px-5 py-2.5 bg-teal-600 hover:bg-teal-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold shadow-xs transition-all flex items-center gap-2"
240
+ >
241
+ {isLoading ? (
242
+ <>
243
+ <span className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
244
+ <span>Running Inference...</span>
245
+ </>
246
+ ) : (
247
+ <>
248
+ <Play className="w-3.5 h-3.5 fill-current" />
249
+ <span>Run BC5CDR Inference</span>
250
+ </>
251
+ )}
252
+ </button>
253
+ </div>
254
+ </div>
255
+
256
+ {/* Right Column: Visualizer & Entity Output */}
257
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs flex flex-col space-y-4">
258
+ <div className="flex items-center justify-between">
259
+ <h2 className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-2">
260
+ <Sparkles className="w-4 h-4 text-teal-600" />
261
+ <span>Extracted Entities & Visualizer</span>
262
+ </h2>
263
+
264
+ {result && (
265
+ <div className="flex items-center gap-3">
266
+ <span className="flex items-center gap-1 text-xs text-slate-600 font-mono">
267
+ <Clock className="w-3.5 h-3.5 text-teal-600" />
268
+ <strong>{result.processing_time_ms} ms</strong>
269
+ </span>
270
+ <button
271
+ onClick={handleCopy}
272
+ className="p-1 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded transition-colors"
273
+ title="Copy JSON response"
274
+ >
275
+ {copied ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />}
276
+ </button>
277
+ </div>
278
+ )}
279
+ </div>
280
+
281
+ {error && (
282
+ <div className="p-3 bg-rose-50 border border-rose-200 rounded-xl text-rose-800 text-xs flex items-center gap-2">
283
+ <AlertCircle className="w-4 h-4 text-rose-600 flex-shrink-0" />
284
+ <span>{error}</span>
285
+ </div>
286
+ )}
287
+
288
+ {!result && !isLoading && !error && (
289
+ <div className="flex-1 flex flex-col items-center justify-center p-8 text-center text-slate-400 border border-dashed border-slate-200 rounded-xl bg-slate-50/50 space-y-2">
290
+ <Cpu className="w-10 h-10 text-slate-300" />
291
+ <p className="text-xs font-medium">Click &quot;Run BC5CDR Inference&quot; or choose a preset.</p>
292
+ <p className="text-[11px] text-slate-400">Tokens are processed by the local neural model.</p>
293
+ </div>
294
+ )}
295
+
296
+ {result && (
297
+ <div className="space-y-4 flex-1">
298
+ {/* Visual Highlight Rendering */}
299
+ <div>
300
+ <p className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-1.5">
301
+ Token Span Highlighting
302
+ </p>
303
+ {renderHighlightedText()}
304
+ </div>
305
+
306
+ {/* Entity Table */}
307
+ <div>
308
+ <p className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mb-2">
309
+ Detected Biomedical Entities ({result.entity_count})
310
+ </p>
311
+ {result.entities.length === 0 ? (
312
+ <p className="text-xs text-slate-500 italic">No chemical or disease entities detected in text.</p>
313
+ ) : (
314
+ <div className="overflow-x-auto border border-slate-200 rounded-xl">
315
+ <table className="w-full text-left text-xs">
316
+ <thead className="bg-slate-50 text-slate-500 border-b border-slate-200">
317
+ <tr>
318
+ <th className="py-2 px-3 font-semibold">Entity Text</th>
319
+ <th className="py-2 px-3 font-semibold">Class</th>
320
+ <th className="py-2 px-3 font-semibold">Char Spans</th>
321
+ <th className="py-2 px-3 font-semibold">Confidence</th>
322
+ </tr>
323
+ </thead>
324
+ <tbody className="divide-y divide-slate-100 font-mono">
325
+ {result.entities.map((ent, idx) => (
326
+ <tr key={idx} className="hover:bg-slate-50/80 transition-colors">
327
+ <td className="py-2 px-3 font-bold text-slate-900">{ent.text}</td>
328
+ <td className="py-2 px-3">
329
+ {ent.label === "CHEMICAL" ? (
330
+ <span className="inline-flex items-center gap-1 text-[10px] uppercase font-bold text-emerald-800 bg-emerald-100 px-1.5 py-0.5 rounded">
331
+ <Pill className="w-3 h-3 text-emerald-600" />
332
+ Chemical
333
+ </span>
334
+ ) : (
335
+ <span className="inline-flex items-center gap-1 text-[10px] uppercase font-bold text-rose-800 bg-rose-100 px-1.5 py-0.5 rounded">
336
+ <Activity className="w-3 h-3 text-rose-600" />
337
+ Disease
338
+ </span>
339
+ )}
340
+ </td>
341
+ <td className="py-2 px-3 text-slate-500 text-[11px]">
342
+ [{ent.start}:{ent.end}]
343
+ </td>
344
+ <td className="py-2 px-3 font-bold text-slate-800">
345
+ {ent.confidence ? `${Math.round(ent.confidence * 100)}%` : "N/A"}
346
+ </td>
347
+ </tr>
348
+ ))}
349
+ </tbody>
350
+ </table>
351
+ </div>
352
+ )}
353
+ </div>
354
+ </div>
355
+ )}
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </div>
360
+ );
361
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import Link from "next/link";
3
+ import {
4
+ Activity,
5
+ Cpu,
6
+ FileText,
7
+ MessageSquareHeart,
8
+ ShieldCheck,
9
+ Zap,
10
+ ArrowRight,
11
+ Database,
12
+ Lock,
13
+ } from "lucide-react";
14
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
15
+
16
+ export default function HomePage() {
17
+ return (
18
+ <div className="min-h-screen bg-slate-50 flex flex-col">
19
+ {/* Top Banner */}
20
+ <div className="bg-slate-900 text-slate-200 text-xs py-2 px-4 text-center font-medium">
21
+ <span className="bg-teal-500/20 text-teal-300 px-2 py-0.5 rounded mr-2 font-mono text-[10px] uppercase tracking-wider">
22
+ Industry-Grade Clinical AI
23
+ </span>
24
+ Decision-support intelligence powered by verified local RoBERTa-large BC5CDR models.
25
+ </div>
26
+
27
+ {/* Hero Section */}
28
+ <section className="relative overflow-hidden pt-12 pb-20 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto w-full">
29
+ <div className="text-center max-w-3xl mx-auto space-y-6">
30
+ <div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-teal-50 border border-teal-200 text-teal-800 text-xs font-semibold">
31
+ <Activity className="w-3.5 h-3.5 text-teal-600" />
32
+ <span>SanjeevaniAI Platform v1.0</span>
33
+ </div>
34
+
35
+ <h1 className="text-4xl sm:text-5xl lg:text-6xl font-extrabold text-slate-900 tracking-tight leading-tight">
36
+ AI-Powered Healthcare <br />
37
+ <span className="bg-gradient-to-r from-teal-600 to-emerald-500 bg-clip-text text-transparent">
38
+ Intelligence & Decision Support
39
+ </span>
40
+ </h1>
41
+
42
+ <p className="text-base sm:text-lg text-slate-600 leading-relaxed max-w-2xl mx-auto">
43
+ A production-quality clinical AI system providing biomedical Named Entity Recognition (NER),
44
+ automated medical document analysis, structured clinical summarization, and interactive AI consultation.
45
+ </p>
46
+
47
+ {/* CTAs */}
48
+ <div className="flex flex-wrap items-center justify-center gap-4 pt-4">
49
+ <Link
50
+ href="/dashboard"
51
+ className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-teal-600 hover:bg-teal-700 text-white font-semibold shadow-md hover:shadow-lg transition-all text-sm"
52
+ >
53
+ <span>Explore Platform</span>
54
+ <ArrowRight className="w-4 h-4" />
55
+ </Link>
56
+ <Link
57
+ href="/ner"
58
+ className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-white hover:bg-slate-100 text-slate-800 border border-slate-300 font-semibold shadow-xs transition-all text-sm"
59
+ >
60
+ <Cpu className="w-4 h-4 text-teal-600" />
61
+ <span>Test Local NER Model</span>
62
+ </Link>
63
+ <Link
64
+ href="/demo"
65
+ className="inline-flex items-center gap-2 px-6 py-3 rounded-xl bg-amber-50 hover:bg-amber-100 text-amber-900 border border-amber-300 font-semibold text-sm transition-all"
66
+ >
67
+ <span>Mentor Demo Mode</span>
68
+ </Link>
69
+ </div>
70
+
71
+ {/* Prominent Medical Disclaimer */}
72
+ <div className="pt-6 max-w-2xl mx-auto text-left">
73
+ <MedicalDisclaimer />
74
+ </div>
75
+ </div>
76
+ </section>
77
+
78
+ {/* Feature Grid */}
79
+ <section className="bg-white border-y border-slate-200/80 py-16 px-4 sm:px-6 lg:px-8">
80
+ <div className="max-w-7xl mx-auto">
81
+ <div className="text-center max-w-2xl mx-auto mb-12">
82
+ <h2 className="text-2xl sm:text-3xl font-bold text-slate-900">Core Clinical AI Capabilities</h2>
83
+ <p className="text-slate-500 text-sm mt-2">
84
+ Engineered with medical privacy principles, reproducible ML inference, and explicit safety boundaries.
85
+ </p>
86
+ </div>
87
+
88
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
89
+ {/* Feature 1 */}
90
+ <div className="p-6 rounded-2xl border border-slate-200 bg-slate-50/50 hover:bg-white hover:shadow-md transition-all">
91
+ <div className="w-12 h-12 rounded-xl bg-teal-100 text-teal-700 flex items-center justify-center mb-4">
92
+ <Cpu className="w-6 h-6" />
93
+ </div>
94
+ <h3 className="text-lg font-bold text-slate-900 mb-2">Local Biomedical NER</h3>
95
+ <p className="text-xs text-slate-600 leading-relaxed">
96
+ Powered by a locally fine-tuned <strong>tner/roberta-large-bc5cdr</strong> token classification model.
97
+ Extracts <span className="text-emerald-700 font-semibold">CHEMICAL</span> and{" "}
98
+ <span className="text-rose-700 font-semibold">DISEASE</span> entities with exact offsets and confidence scores.
99
+ </p>
100
+ </div>
101
+
102
+ {/* Feature 2 */}
103
+ <div className="p-6 rounded-2xl border border-slate-200 bg-slate-50/50 hover:bg-white hover:shadow-md transition-all">
104
+ <div className="w-12 h-12 rounded-xl bg-blue-100 text-blue-700 flex items-center justify-center mb-4">
105
+ <FileText className="w-6 h-6" />
106
+ </div>
107
+ <h3 className="text-lg font-bold text-slate-900 mb-2">Medical Document Analysis</h3>
108
+ <p className="text-xs text-slate-600 leading-relaxed">
109
+ Secure multi-format PDF and text pipeline with SHA-256 integrity verification, automated clinical
110
+ findings extraction, and structured pharmacological summarization.
111
+ </p>
112
+ </div>
113
+
114
+ {/* Feature 3 */}
115
+ <div className="p-6 rounded-2xl border border-slate-200 bg-slate-50/50 hover:bg-white hover:shadow-md transition-all">
116
+ <div className="w-12 h-12 rounded-xl bg-purple-100 text-purple-700 flex items-center justify-center mb-4">
117
+ <MessageSquareHeart className="w-6 h-6" />
118
+ </div>
119
+ <h3 className="text-lg font-bold text-slate-900 mb-2">Clinical AI Assistant</h3>
120
+ <p className="text-xs text-slate-600 leading-relaxed">
121
+ Multi-provider LLM abstraction (Gemini + MockLLM) adhering to non-diagnostic safety guardrails,
122
+ automatic emergency red-flag triage, and patient profile context awareness.
123
+ </p>
124
+ </div>
125
+
126
+ {/* Feature 4 */}
127
+ <div className="p-6 rounded-2xl border border-slate-200 bg-slate-50/50 hover:bg-white hover:shadow-md transition-all">
128
+ <div className="w-12 h-12 rounded-xl bg-emerald-100 text-emerald-700 flex items-center justify-center mb-4">
129
+ <Database className="w-6 h-6" />
130
+ </div>
131
+ <h3 className="text-lg font-bold text-slate-900 mb-2">Health Profile & History</h3>
132
+ <p className="text-xs text-slate-600 leading-relaxed">
133
+ Patient longitudinal timeline tracking medications, allergies, chronic conditions, and chronological
134
+ analysis logs with audit traceability.
135
+ </p>
136
+ </div>
137
+
138
+ {/* Feature 5 */}
139
+ <div className="p-6 rounded-2xl border border-slate-200 bg-slate-50/50 hover:bg-white hover:shadow-md transition-all">
140
+ <div className="w-12 h-12 rounded-xl bg-amber-100 text-amber-700 flex items-center justify-center mb-4">
141
+ <Lock className="w-6 h-6" />
142
+ </div>
143
+ <h3 className="text-lg font-bold text-slate-900 mb-2">Security & RBAC</h3>
144
+ <p className="text-xs text-slate-600 leading-relaxed">
145
+ Role-based access control (PATIENT, DOCTOR, ADMIN), bcrypt password hashing, JWT session management,
146
+ and comprehensive security audit logging.
147
+ </p>
148
+ </div>
149
+
150
+ {/* Feature 6 */}
151
+ <div className="p-6 rounded-2xl border border-slate-200 bg-slate-50/50 hover:bg-white hover:shadow-md transition-all">
152
+ <div className="w-12 h-12 rounded-xl bg-rose-100 text-rose-700 flex items-center justify-center mb-4">
153
+ <ShieldCheck className="w-6 h-6" />
154
+ </div>
155
+ <h3 className="text-lg font-bold text-slate-900 mb-2">Emergency Triage Engine</h3>
156
+ <p className="text-xs text-slate-600 leading-relaxed">
157
+ Heuristic emergency symptom detection identifying critical cardiovascular and respiratory red flags
158
+ with instant emergency escalation notices.
159
+ </p>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </section>
164
+
165
+ {/* Footer */}
166
+ <footer className="mt-auto bg-slate-900 text-slate-400 text-xs py-8 px-4 sm:px-6 lg:px-8 border-t border-slate-800">
167
+ <div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
168
+ <div className="flex items-center gap-2 text-slate-200 font-bold">
169
+ <Activity className="w-4 h-4 text-teal-400" />
170
+ <span>SanjeevaniAI</span>
171
+ </div>
172
+ <p className="text-center sm:text-left">
173
+ Designed with healthcare-data privacy and clinical safety principles in mind.
174
+ </p>
175
+ <div className="flex items-center gap-4 text-slate-400">
176
+ <Link href="/demo" className="hover:text-teal-300">Mentor Demo</Link>
177
+ <Link href="/ner" className="hover:text-teal-300">NER Demo</Link>
178
+ <Link href="/dashboard" className="hover:text-teal-300">Dashboard</Link>
179
+ </div>
180
+ </div>
181
+ </footer>
182
+ </div>
183
+ );
184
+ }
frontend/src/app/profile/page.tsx ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import { Sidebar } from "@/components/Sidebar";
5
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
6
+ import { profileApi } from "@/lib/api";
7
+ import {
8
+ User,
9
+ Heart,
10
+ AlertTriangle,
11
+ Pill,
12
+ Activity,
13
+ Save,
14
+ CheckCircle2,
15
+ AlertCircle,
16
+ Phone,
17
+ } from "lucide-react";
18
+
19
+ export default function ProfilePage() {
20
+ const [profile, setProfile] = useState<any>({
21
+ age: "",
22
+ gender: "",
23
+ blood_group: "",
24
+ height_cm: "",
25
+ weight_kg: "",
26
+ known_allergies: [],
27
+ chronic_conditions: [],
28
+ current_medications: [],
29
+ emergency_contact: "",
30
+ });
31
+
32
+ const [allergyInput, setAllergyInput] = useState("");
33
+ const [conditionInput, setConditionInput] = useState("");
34
+ const [medicationInput, setMedicationInput] = useState("");
35
+
36
+ const [isLoading, setIsLoading] = useState(true);
37
+ const [isSaving, setIsSaving] = useState(false);
38
+ const [saveSuccess, setSaveSuccess] = useState(false);
39
+ const [error, setError] = useState<string | null>(null);
40
+
41
+ useEffect(() => {
42
+ profileApi
43
+ .getProfile()
44
+ .then((res) => {
45
+ if (res.data.data) {
46
+ setProfile({
47
+ age: res.data.data.age || "",
48
+ gender: res.data.data.gender || "",
49
+ blood_group: res.data.data.blood_group || "",
50
+ height_cm: res.data.data.height_cm || "",
51
+ weight_kg: res.data.data.weight_kg || "",
52
+ known_allergies: res.data.data.known_allergies || [],
53
+ chronic_conditions: res.data.data.chronic_conditions || [],
54
+ current_medications: res.data.data.current_medications || [],
55
+ emergency_contact: res.data.data.emergency_contact || "",
56
+ });
57
+ }
58
+ })
59
+ .catch((err) => console.error("Error fetching profile:", err))
60
+ .finally(() => setIsLoading(false));
61
+ }, []);
62
+
63
+ const calculateBMI = () => {
64
+ const h = parseFloat(profile.height_cm);
65
+ const w = parseFloat(profile.weight_kg);
66
+ if (!h || !w || h <= 0 || w <= 0) return null;
67
+ const bmi = w / Math.pow(h / 100, 2);
68
+ return bmi.toFixed(1);
69
+ };
70
+
71
+ const handleAddChip = (field: string, value: string, setter: (v: string) => void) => {
72
+ if (!value.trim()) return;
73
+ setProfile((prev: any) => ({
74
+ ...prev,
75
+ [field]: [...(prev[field] || []), value.trim()],
76
+ }));
77
+ setter("");
78
+ };
79
+
80
+ const handleRemoveChip = (field: string, index: number) => {
81
+ setProfile((prev: any) => ({
82
+ ...prev,
83
+ [field]: prev[field].filter((_: any, i: number) => i !== index),
84
+ }));
85
+ };
86
+
87
+ const handleSave = async (e: React.FormEvent) => {
88
+ e.preventDefault();
89
+ setIsSaving(true);
90
+ setError(null);
91
+ setSaveSuccess(false);
92
+
93
+ try {
94
+ const payload = {
95
+ age: profile.age ? parseInt(profile.age) : null,
96
+ gender: profile.gender || null,
97
+ blood_group: profile.blood_group || null,
98
+ height_cm: profile.height_cm ? parseFloat(profile.height_cm) : null,
99
+ weight_kg: profile.weight_kg ? parseFloat(profile.weight_kg) : null,
100
+ known_allergies: profile.known_allergies,
101
+ chronic_conditions: profile.chronic_conditions,
102
+ current_medications: profile.current_medications,
103
+ emergency_contact: profile.emergency_contact || null,
104
+ };
105
+ await profileApi.updateProfile(payload);
106
+ setSaveSuccess(true);
107
+ setTimeout(() => setSaveSuccess(false), 3000);
108
+ } catch (err: any) {
109
+ setError(err.response?.data?.error?.message || "Failed to update profile.");
110
+ } finally {
111
+ setIsSaving(false);
112
+ }
113
+ };
114
+
115
+ const bmi = calculateBMI();
116
+
117
+ return (
118
+ <div className="flex-1 flex bg-slate-50">
119
+ <Sidebar />
120
+
121
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-5xl mx-auto w-full">
122
+ {/* Header */}
123
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
124
+ <div>
125
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
126
+ <User className="w-6 h-6 text-teal-600" />
127
+ <span>Patient Health Profile</span>
128
+ </h1>
129
+ <p className="text-xs text-slate-500 mt-0.5">
130
+ Maintain physiological parameters, allergy profiles, and chronic conditions for clinical AI context.
131
+ </p>
132
+ </div>
133
+
134
+ <button
135
+ onClick={handleSave}
136
+ disabled={isSaving}
137
+ className="inline-flex items-center gap-2 px-5 py-2.5 bg-teal-600 hover:bg-teal-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold shadow-xs transition-all self-start sm:self-auto"
138
+ >
139
+ <Save className="w-4 h-4" />
140
+ <span>{isSaving ? "Saving..." : "Save Health Profile"}</span>
141
+ </button>
142
+ </div>
143
+
144
+ <MedicalDisclaimer />
145
+
146
+ {saveSuccess && (
147
+ <div className="p-3 bg-emerald-50 border border-emerald-200 rounded-xl text-emerald-900 text-xs flex items-center gap-2">
148
+ <CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0" />
149
+ <span>Health profile saved successfully! Context will be used in clinical AI consultations.</span>
150
+ </div>
151
+ )}
152
+
153
+ {error && (
154
+ <div className="p-3 bg-rose-50 border border-rose-200 rounded-xl text-rose-900 text-xs flex items-center gap-2">
155
+ <AlertCircle className="w-4 h-4 text-rose-600 flex-shrink-0" />
156
+ <span>{error}</span>
157
+ </div>
158
+ )}
159
+
160
+ {/* Profile Form */}
161
+ <form onSubmit={handleSave} className="space-y-6">
162
+ {/* Physiological Metrics */}
163
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-4">
164
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
165
+ <Heart className="w-4 h-4 text-rose-600" />
166
+ <span>Vitals & Anthropometrics</span>
167
+ </h2>
168
+
169
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
170
+ <div>
171
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Age (Years)</label>
172
+ <input
173
+ type="number"
174
+ value={profile.age}
175
+ onChange={(e) => setProfile({ ...profile, age: e.target.value })}
176
+ placeholder="e.g. 45"
177
+ className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500"
178
+ />
179
+ </div>
180
+
181
+ <div>
182
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Gender</label>
183
+ <select
184
+ value={profile.gender}
185
+ onChange={(e) => setProfile({ ...profile, gender: e.target.value })}
186
+ className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500"
187
+ >
188
+ <option value="">Select Gender</option>
189
+ <option value="Male">Male</option>
190
+ <option value="Female">Female</option>
191
+ <option value="Other">Other</option>
192
+ </select>
193
+ </div>
194
+
195
+ <div>
196
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Blood Group</label>
197
+ <select
198
+ value={profile.blood_group}
199
+ onChange={(e) => setProfile({ ...profile, blood_group: e.target.value })}
200
+ className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500"
201
+ >
202
+ <option value="">Select Blood Group</option>
203
+ <option value="A+">A+</option>
204
+ <option value="A-">A-</option>
205
+ <option value="B+">B+</option>
206
+ <option value="B-">B-</option>
207
+ <option value="AB+">AB+</option>
208
+ <option value="AB-">AB-</option>
209
+ <option value="O+">O+</option>
210
+ <option value="O-">O-</option>
211
+ </select>
212
+ </div>
213
+
214
+ <div>
215
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Calculated BMI</label>
216
+ <div className="px-3 py-2 bg-slate-100 border border-slate-200 rounded-xl text-xs font-bold text-slate-800">
217
+ {bmi ? `${bmi} kg/m²` : "--"}
218
+ </div>
219
+ </div>
220
+
221
+ <div>
222
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Height (cm)</label>
223
+ <input
224
+ type="number"
225
+ step="0.1"
226
+ value={profile.height_cm}
227
+ onChange={(e) => setProfile({ ...profile, height_cm: e.target.value })}
228
+ placeholder="e.g. 175"
229
+ className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500"
230
+ />
231
+ </div>
232
+
233
+ <div>
234
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Weight (kg)</label>
235
+ <input
236
+ type="number"
237
+ step="0.1"
238
+ value={profile.weight_kg}
239
+ onChange={(e) => setProfile({ ...profile, weight_kg: e.target.value })}
240
+ placeholder="e.g. 74.5"
241
+ className="w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500"
242
+ />
243
+ </div>
244
+
245
+ <div className="sm:col-span-2">
246
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Emergency Contact</label>
247
+ <div className="relative">
248
+ <Phone className="w-4 h-4 text-slate-400 absolute left-3 top-2.5" />
249
+ <input
250
+ type="text"
251
+ value={profile.emergency_contact}
252
+ onChange={(e) => setProfile({ ...profile, emergency_contact: e.target.value })}
253
+ placeholder="Name - +1 (555) 000-0000"
254
+ className="w-full pl-9 pr-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500"
255
+ />
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ {/* Allergies, Conditions, Medications Chips */}
262
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
263
+ {/* Allergies */}
264
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
265
+ <h3 className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-2">
266
+ <AlertTriangle className="w-4 h-4 text-amber-500" />
267
+ <span>Known Allergies</span>
268
+ </h3>
269
+
270
+ <div className="flex gap-2">
271
+ <input
272
+ type="text"
273
+ value={allergyInput}
274
+ onChange={(e) => setAllergyInput(e.target.value)}
275
+ onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), handleAddChip("known_allergies", allergyInput, setAllergyInput))}
276
+ placeholder="e.g. Penicillin"
277
+ className="flex-1 px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg text-xs"
278
+ />
279
+ <button
280
+ type="button"
281
+ onClick={() => handleAddChip("known_allergies", allergyInput, setAllergyInput)}
282
+ className="px-2.5 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-xs font-semibold"
283
+ >
284
+ Add
285
+ </button>
286
+ </div>
287
+
288
+ <div className="flex flex-wrap gap-1.5 min-h-[60px] p-2 bg-slate-50 rounded-xl border border-slate-100">
289
+ {profile.known_allergies.map((a: string, i: number) => (
290
+ <span key={i} className="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-100 text-amber-900 rounded-md text-xs font-medium">
291
+ <span>{a}</span>
292
+ <button type="button" onClick={() => handleRemoveChip("known_allergies", i)} className="hover:text-rose-600">×</button>
293
+ </span>
294
+ ))}
295
+ </div>
296
+ </div>
297
+
298
+ {/* Chronic Conditions */}
299
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
300
+ <h3 className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-2">
301
+ <Activity className="w-4 h-4 text-rose-500" />
302
+ <span>Chronic Conditions</span>
303
+ </h3>
304
+
305
+ <div className="flex gap-2">
306
+ <input
307
+ type="text"
308
+ value={conditionInput}
309
+ onChange={(e) => setConditionInput(e.target.value)}
310
+ onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), handleAddChip("chronic_conditions", conditionInput, setConditionInput))}
311
+ placeholder="e.g. Hypertension"
312
+ className="flex-1 px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg text-xs"
313
+ />
314
+ <button
315
+ type="button"
316
+ onClick={() => handleAddChip("chronic_conditions", conditionInput, setConditionInput)}
317
+ className="px-2.5 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-xs font-semibold"
318
+ >
319
+ Add
320
+ </button>
321
+ </div>
322
+
323
+ <div className="flex flex-wrap gap-1.5 min-h-[60px] p-2 bg-slate-50 rounded-xl border border-slate-100">
324
+ {profile.chronic_conditions.map((c: string, i: number) => (
325
+ <span key={i} className="inline-flex items-center gap-1 px-2 py-0.5 bg-rose-100 text-rose-900 rounded-md text-xs font-medium">
326
+ <span>{c}</span>
327
+ <button type="button" onClick={() => handleRemoveChip("chronic_conditions", i)} className="hover:text-rose-600">×</button>
328
+ </span>
329
+ ))}
330
+ </div>
331
+ </div>
332
+
333
+ {/* Medications */}
334
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
335
+ <h3 className="text-xs font-bold uppercase tracking-wider text-slate-700 flex items-center gap-2">
336
+ <Pill className="w-4 h-4 text-emerald-500" />
337
+ <span>Active Medications</span>
338
+ </h3>
339
+
340
+ <div className="flex gap-2">
341
+ <input
342
+ type="text"
343
+ value={medicationInput}
344
+ onChange={(e) => setMedicationInput(e.target.value)}
345
+ onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), handleAddChip("current_medications", medicationInput, setMedicationInput))}
346
+ placeholder="e.g. Metformin 500mg"
347
+ className="flex-1 px-3 py-1.5 bg-slate-50 border border-slate-200 rounded-lg text-xs"
348
+ />
349
+ <button
350
+ type="button"
351
+ onClick={() => handleAddChip("current_medications", medicationInput, setMedicationInput)}
352
+ className="px-2.5 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg text-xs font-semibold"
353
+ >
354
+ Add
355
+ </button>
356
+ </div>
357
+
358
+ <div className="flex flex-wrap gap-1.5 min-h-[60px] p-2 bg-slate-50 rounded-xl border border-slate-100">
359
+ {profile.current_medications.map((m: string, i: number) => (
360
+ <span key={i} className="inline-flex items-center gap-1 px-2 py-0.5 bg-emerald-100 text-emerald-900 rounded-md text-xs font-medium">
361
+ <span>{m}</span>
362
+ <button type="button" onClick={() => handleRemoveChip("current_medications", i)} className="hover:text-rose-600">×</button>
363
+ </span>
364
+ ))}
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </form>
369
+ </div>
370
+ </div>
371
+ );
372
+ }
frontend/src/app/register/page.tsx ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { useAuth } from "@/lib/auth-context";
7
+ import { Activity, Lock, Mail, User, AlertCircle } from "lucide-react";
8
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
9
+
10
+ export default function RegisterPage() {
11
+ const router = useRouter();
12
+ const { register } = useAuth();
13
+
14
+ const [email, setEmail] = useState("");
15
+ const [fullName, setFullName] = useState("");
16
+ const [password, setPassword] = useState("");
17
+ const [role, setRole] = useState("PATIENT");
18
+ const [error, setError] = useState<string | null>(null);
19
+ const [isSubmitting, setIsSubmitting] = useState(false);
20
+
21
+ const handleSubmit = async (e: React.FormEvent) => {
22
+ e.preventDefault();
23
+ setError(null);
24
+ setIsSubmitting(true);
25
+ try {
26
+ await register(email, password, fullName, role);
27
+ router.push("/dashboard");
28
+ } catch (err: any) {
29
+ setError(err.response?.data?.error?.message || "Registration failed. Please check your information.");
30
+ } finally {
31
+ setIsSubmitting(false);
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div className="min-h-[calc(100vh-61px)] flex items-center justify-center p-4 bg-slate-50">
37
+ <div className="max-w-md w-full bg-white border border-slate-200/80 rounded-2xl p-6 sm:p-8 shadow-sm space-y-6">
38
+ <div className="text-center space-y-2">
39
+ <div className="w-12 h-12 rounded-xl bg-gradient-to-tr from-teal-600 to-emerald-500 flex items-center justify-center text-white mx-auto shadow-xs">
40
+ <Activity className="w-6 h-6" />
41
+ </div>
42
+ <h1 className="text-2xl font-bold text-slate-900">Create your Account</h1>
43
+ <p className="text-xs text-slate-500">Join SanjeevaniAI Healthcare Intelligence</p>
44
+ </div>
45
+
46
+ {error && (
47
+ <div className="p-3 bg-rose-50 border border-rose-200 rounded-xl text-rose-800 text-xs flex items-center gap-2">
48
+ <AlertCircle className="w-4 h-4 text-rose-600 flex-shrink-0" />
49
+ <span>{error}</span>
50
+ </div>
51
+ )}
52
+
53
+ <form onSubmit={handleSubmit} className="space-y-4">
54
+ <div>
55
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Full Name</label>
56
+ <div className="relative">
57
+ <User className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
58
+ <input
59
+ type="text"
60
+ required
61
+ value={fullName}
62
+ onChange={(e) => setFullName(e.target.value)}
63
+ placeholder="Dr. / Mr. / Ms. Full Name"
64
+ className="w-full pl-9 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
65
+ />
66
+ </div>
67
+ </div>
68
+
69
+ <div>
70
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Email Address</label>
71
+ <div className="relative">
72
+ <Mail className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
73
+ <input
74
+ type="email"
75
+ required
76
+ value={email}
77
+ onChange={(e) => setEmail(e.target.value)}
78
+ placeholder="name@example.com"
79
+ className="w-full pl-9 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
80
+ />
81
+ </div>
82
+ </div>
83
+
84
+ <div>
85
+ <label className="block text-xs font-semibold text-slate-700 mb-1">Password</label>
86
+ <div className="relative">
87
+ <Lock className="w-4 h-4 text-slate-400 absolute left-3 top-3" />
88
+ <input
89
+ type="password"
90
+ required
91
+ minLength={8}
92
+ value={password}
93
+ onChange={(e) => setPassword(e.target.value)}
94
+ placeholder="Minimum 8 characters"
95
+ className="w-full pl-9 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
96
+ />
97
+ </div>
98
+ </div>
99
+
100
+ <div>
101
+ <label className="block text-xs font-semibold text-slate-700 mb-1">User Role</label>
102
+ <select
103
+ value={role}
104
+ onChange={(e) => setRole(e.target.value)}
105
+ className="w-full px-3 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-xs focus:bg-white focus:outline-none focus:ring-2 focus:ring-teal-500/20 focus:border-teal-500 transition-all"
106
+ >
107
+ <option value="PATIENT">Patient Account</option>
108
+ <option value="DOCTOR">Healthcare Professional / Doctor</option>
109
+ <option value="ADMIN">Platform Administrator</option>
110
+ </select>
111
+ </div>
112
+
113
+ <button
114
+ type="submit"
115
+ disabled={isSubmitting}
116
+ className="w-full py-2.5 px-4 bg-teal-600 hover:bg-teal-700 disabled:opacity-50 text-white text-xs font-semibold rounded-xl shadow-xs transition-all flex items-center justify-center gap-2"
117
+ >
118
+ {isSubmitting ? "Creating Account..." : "Create Account"}
119
+ </button>
120
+ </form>
121
+
122
+ <div className="text-center text-xs text-slate-500">
123
+ Already have an account?{" "}
124
+ <Link href="/login" className="text-teal-600 font-semibold hover:underline">
125
+ Sign In
126
+ </Link>
127
+ </div>
128
+
129
+ <MedicalDisclaimer compact />
130
+ </div>
131
+ </div>
132
+ );
133
+ }
frontend/src/app/reports/[id]/page.tsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import Link from "next/link";
5
+ import { useParams } from "next/navigation";
6
+ import { Sidebar } from "@/components/Sidebar";
7
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
8
+ import { EntityBadge } from "@/components/EntityBadge";
9
+ import { documentApi } from "@/lib/api";
10
+ import {
11
+ FileText,
12
+ ArrowLeft,
13
+ Activity,
14
+ Pill,
15
+ Clock,
16
+ ShieldCheck,
17
+ CheckCircle,
18
+ Copy,
19
+ Check,
20
+ Cpu,
21
+ Layers,
22
+ } from "lucide-react";
23
+
24
+ export default function ReportDetailPage() {
25
+ const params = useParams();
26
+ const docId = params.id as string;
27
+
28
+ const [doc, setDoc] = useState<any>(null);
29
+ const [isLoading, setIsLoading] = useState<boolean>(true);
30
+ const [copied, setCopied] = useState<boolean>(false);
31
+
32
+ useEffect(() => {
33
+ if (!docId) return;
34
+ documentApi
35
+ .getById(docId)
36
+ .then((res) => setDoc(res.data.data))
37
+ .catch((err) => console.error("Error fetching report details:", err))
38
+ .finally(() => setIsLoading(false));
39
+ }, [docId]);
40
+
41
+ const handleCopyText = () => {
42
+ if (!doc?.analysis?.raw_text) return;
43
+ navigator.clipboard.writeText(doc.analysis.raw_text);
44
+ setCopied(true);
45
+ setTimeout(() => setCopied(false), 2000);
46
+ };
47
+
48
+ if (isLoading) {
49
+ return (
50
+ <div className="flex-1 flex bg-slate-50">
51
+ <Sidebar />
52
+ <div className="flex-1 flex items-center justify-center p-8">
53
+ <div className="text-center space-y-3">
54
+ <span className="w-8 h-8 border-3 border-teal-600 border-t-transparent rounded-full animate-spin inline-block"></span>
55
+ <p className="text-xs text-slate-500 font-medium">Loading report analysis...</p>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
62
+ if (!doc) {
63
+ return (
64
+ <div className="flex-1 flex bg-slate-50">
65
+ <Sidebar />
66
+ <div className="flex-1 p-8 max-w-4xl mx-auto space-y-4">
67
+ <Link href="/reports" className="inline-flex items-center gap-1.5 text-xs text-teal-600 font-semibold hover:underline">
68
+ <ArrowLeft className="w-4 h-4" /> Back to Reports
69
+ </Link>
70
+ <div className="bg-white p-8 rounded-2xl border border-slate-200 text-center space-y-2">
71
+ <p className="text-sm font-bold text-slate-800">Report Not Found</p>
72
+ <p className="text-xs text-slate-500">The requested medical report does not exist or has been deleted.</p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ const analysis = doc.analysis;
80
+
81
+ return (
82
+ <div className="flex-1 flex bg-slate-50">
83
+ <Sidebar />
84
+
85
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-5xl mx-auto w-full">
86
+ {/* Navigation & Title */}
87
+ <div className="space-y-3">
88
+ <Link href="/reports" className="inline-flex items-center gap-1.5 text-xs text-teal-600 font-semibold hover:underline">
89
+ <ArrowLeft className="w-4 h-4" /> Back to Reports
90
+ </Link>
91
+
92
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs">
93
+ <div>
94
+ <div className="flex items-center gap-2">
95
+ <h1 className="text-xl font-bold text-slate-900">{doc.original_filename}</h1>
96
+ <span className="text-[10px] uppercase font-bold bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded">
97
+ {doc.status}
98
+ </span>
99
+ </div>
100
+ <div className="flex flex-wrap items-center gap-3 text-xs text-slate-400 font-mono mt-1">
101
+ <span>Format: {doc.file_type.toUpperCase()}</span>
102
+ <span>•</span>
103
+ <span>Size: {Math.round(doc.file_size / 1024)} KB</span>
104
+ <span>•</span>
105
+ <span>SHA-256: {doc.file_hash.substring(0, 16)}...</span>
106
+ </div>
107
+ </div>
108
+
109
+ <div className="flex items-center gap-2">
110
+ <button
111
+ onClick={handleCopyText}
112
+ className="px-3 py-1.5 bg-slate-100 hover:bg-slate-200/80 text-slate-700 text-xs font-semibold rounded-lg transition-colors flex items-center gap-1.5"
113
+ >
114
+ {copied ? <Check className="w-3.5 h-3.5 text-emerald-600" /> : <Copy className="w-3.5 h-3.5" />}
115
+ <span>{copied ? "Copied" : "Copy Extracted Text"}</span>
116
+ </button>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <MedicalDisclaimer />
122
+
123
+ {/* Clinical Summary & Findings */}
124
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
125
+ {/* Summary Card */}
126
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
127
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
128
+ <Activity className="w-4 h-4 text-teal-600" />
129
+ <span>Automated Clinical Summary</span>
130
+ </h2>
131
+ <p className="text-xs text-slate-700 leading-relaxed bg-slate-50 p-3.5 rounded-xl border border-slate-100">
132
+ {analysis?.summary || "Summary not generated."}
133
+ </p>
134
+ </div>
135
+
136
+ {/* Important Findings */}
137
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
138
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
139
+ <CheckCircle className="w-4 h-4 text-emerald-600" />
140
+ <span>Key Clinical Findings</span>
141
+ </h2>
142
+ <ul className="space-y-2 text-xs text-slate-700">
143
+ {(analysis?.important_findings || []).map((f: string, i: number) => (
144
+ <li key={i} className="flex items-start gap-2 bg-emerald-50/50 p-2.5 rounded-lg border border-emerald-100">
145
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 mt-1.5 flex-shrink-0"></span>
146
+ <span>{f}</span>
147
+ </li>
148
+ ))}
149
+ </ul>
150
+ </div>
151
+ </div>
152
+
153
+ {/* Extracted Conditions & Medications */}
154
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-4">
155
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
156
+ <Cpu className="w-4 h-4 text-teal-600" />
157
+ <span>Biomedical Entities Identified (RoBERTa-large BC5CDR)</span>
158
+ </h2>
159
+
160
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
161
+ <div className="p-4 bg-rose-50/40 border border-rose-100 rounded-xl space-y-2">
162
+ <p className="text-xs font-bold text-rose-900 uppercase tracking-wider">Identified Conditions</p>
163
+ <div className="flex flex-wrap gap-1.5">
164
+ {(analysis?.detected_conditions || []).map((c: string) => (
165
+ <EntityBadge key={c} label="DISEASE" text={c} />
166
+ ))}
167
+ </div>
168
+ </div>
169
+
170
+ <div className="p-4 bg-emerald-50/40 border border-emerald-100 rounded-xl space-y-2">
171
+ <p className="text-xs font-bold text-emerald-900 uppercase tracking-wider">Pharmaceutical Agents</p>
172
+ <div className="flex flex-wrap gap-1.5">
173
+ {(analysis?.detected_medications || []).map((m: string) => (
174
+ <EntityBadge key={m} label="CHEMICAL" text={m} />
175
+ ))}
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ {/* Raw Text Viewer */}
182
+ <div className="bg-white p-5 rounded-2xl border border-slate-200/80 shadow-xs space-y-3">
183
+ <h2 className="text-sm font-bold text-slate-900 flex items-center gap-2">
184
+ <Layers className="w-4 h-4 text-teal-600" />
185
+ <span>Extracted Document Text</span>
186
+ </h2>
187
+ <pre className="p-4 bg-slate-900 text-slate-100 rounded-xl text-xs font-mono whitespace-pre-wrap leading-relaxed max-h-80 overflow-y-auto">
188
+ {analysis?.raw_text || "No text available."}
189
+ </pre>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
frontend/src/app/reports/page.tsx ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+ import Link from "next/link";
5
+ import { Sidebar } from "@/components/Sidebar";
6
+ import { MedicalDisclaimer } from "@/components/MedicalDisclaimer";
7
+ import { EntityBadge } from "@/components/EntityBadge";
8
+ import { documentApi } from "@/lib/api";
9
+ import {
10
+ FileText,
11
+ Upload,
12
+ Trash2,
13
+ Eye,
14
+ AlertCircle,
15
+ CheckCircle2,
16
+ FileCheck,
17
+ Download,
18
+ Plus,
19
+ } from "lucide-react";
20
+
21
+ export default function ReportsPage() {
22
+ const [documents, setDocuments] = useState<any[]>([]);
23
+ const [isLoading, setIsLoading] = useState<boolean>(true);
24
+ const [isUploading, setIsUploading] = useState<boolean>(false);
25
+ const [uploadError, setUploadError] = useState<string | null>(null);
26
+ const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
27
+
28
+ const fetchDocuments = async () => {
29
+ try {
30
+ const res = await documentApi.list();
31
+ setDocuments(res.data.data || []);
32
+ } catch (err) {
33
+ console.error("Error fetching documents:", err);
34
+ } finally {
35
+ setIsLoading(false);
36
+ }
37
+ };
38
+
39
+ useEffect(() => {
40
+ fetchDocuments();
41
+ }, []);
42
+
43
+ const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
44
+ const file = e.target.files?.[0];
45
+ if (!file) return;
46
+
47
+ setUploadError(null);
48
+ setUploadSuccess(null);
49
+ setIsUploading(true);
50
+
51
+ const formData = new FormData();
52
+ formData.append("file", file);
53
+
54
+ try {
55
+ const res = await documentApi.upload(formData);
56
+ setUploadSuccess(`Report '${file.name}' analyzed successfully with local BC5CDR model!`);
57
+ fetchDocuments();
58
+ } catch (err: any) {
59
+ setUploadError(err.response?.data?.error?.message || "Failed to analyze medical report.");
60
+ } finally {
61
+ setIsUploading(false);
62
+ e.target.value = "";
63
+ }
64
+ };
65
+
66
+ const handleDelete = async (id: string) => {
67
+ if (!confirm("Are you sure you want to delete this report and its analysis?")) return;
68
+ try {
69
+ await documentApi.delete(id);
70
+ setDocuments((prev) => prev.filter((d) => d.id !== id));
71
+ } catch (err) {
72
+ alert("Failed to delete document.");
73
+ }
74
+ };
75
+
76
+ return (
77
+ <div className="flex-1 flex bg-slate-50">
78
+ <Sidebar />
79
+
80
+ <div className="flex-1 p-4 sm:p-6 lg:p-8 space-y-6 max-w-7xl mx-auto w-full">
81
+ {/* Header */}
82
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
83
+ <div>
84
+ <h1 className="text-2xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
85
+ <FileText className="w-6 h-6 text-teal-600" />
86
+ <span>Medical Reports & Document Intelligence</span>
87
+ </h1>
88
+ <p className="text-xs text-slate-500 mt-0.5">
89
+ Securely upload clinical PDFs, DOCX, and discharge summaries for automated entity extraction and summarization.
90
+ </p>
91
+ </div>
92
+
93
+ <div>
94
+ <label className="cursor-pointer inline-flex items-center gap-2 px-4 py-2.5 bg-teal-600 hover:bg-teal-700 text-white rounded-xl text-xs font-bold shadow-xs transition-all">
95
+ <Upload className="w-4 h-4" />
96
+ <span>{isUploading ? "Extracting & Analyzing..." : "Upload Medical Document"}</span>
97
+ <input
98
+ type="file"
99
+ accept=".pdf,.txt,.docx"
100
+ onChange={handleFileUpload}
101
+ disabled={isUploading}
102
+ className="hidden"
103
+ />
104
+ </label>
105
+ </div>
106
+ </div>
107
+
108
+ <MedicalDisclaimer />
109
+
110
+ {/* Upload Notifications */}
111
+ {uploadSuccess && (
112
+ <div className="p-3.5 bg-emerald-50 border border-emerald-200 rounded-xl text-emerald-900 text-xs flex items-center gap-2">
113
+ <CheckCircle2 className="w-4 h-4 text-emerald-600 flex-shrink-0" />
114
+ <span>{uploadSuccess}</span>
115
+ </div>
116
+ )}
117
+
118
+ {uploadError && (
119
+ <div className="p-3.5 bg-rose-50 border border-rose-200 rounded-xl text-rose-900 text-xs flex items-center gap-2">
120
+ <AlertCircle className="w-4 h-4 text-rose-600 flex-shrink-0" />
121
+ <span>{uploadError}</span>
122
+ </div>
123
+ )}
124
+
125
+ {/* Document List */}
126
+ <div className="bg-white rounded-2xl border border-slate-200/80 p-5 shadow-xs space-y-4">
127
+ <div className="flex items-center justify-between">
128
+ <h2 className="text-sm font-bold text-slate-900">Analyzed Reports ({documents.length})</h2>
129
+ </div>
130
+
131
+ {documents.length === 0 ? (
132
+ <div className="text-center py-12 border border-dashed border-slate-200 rounded-xl bg-slate-50/50 space-y-3">
133
+ <FileCheck className="w-10 h-10 text-slate-300 mx-auto" />
134
+ <p className="text-xs font-medium text-slate-600">No medical reports uploaded yet.</p>
135
+ <p className="text-[11px] text-slate-400">Supported formats: PDF, DOCX, TXT (up to 25MB).</p>
136
+ </div>
137
+ ) : (
138
+ <div className="divide-y divide-slate-100">
139
+ {documents.map((doc) => (
140
+ <div
141
+ key={doc.id}
142
+ className="py-4 first:pt-0 last:pb-0 flex flex-col md:flex-row md:items-center md:justify-between gap-4"
143
+ >
144
+ <div className="space-y-1.5 flex-1 min-w-0">
145
+ <div className="flex items-center gap-2">
146
+ <span className="font-bold text-sm text-slate-900 truncate">
147
+ {doc.original_filename}
148
+ </span>
149
+ <span className="text-[10px] uppercase font-mono font-bold bg-slate-100 text-slate-600 px-1.5 py-0.5 rounded">
150
+ {doc.file_type}
151
+ </span>
152
+ <span className="text-[10px] uppercase font-bold bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded">
153
+ {doc.status}
154
+ </span>
155
+ </div>
156
+
157
+ {doc.analysis?.summary && (
158
+ <p className="text-xs text-slate-600 line-clamp-2 leading-relaxed">
159
+ {doc.analysis.summary}
160
+ </p>
161
+ )}
162
+
163
+ <div className="flex flex-wrap items-center gap-1.5 pt-1">
164
+ {(doc.analysis?.detected_conditions || []).slice(0, 3).map((c: string) => (
165
+ <EntityBadge key={c} label="DISEASE" text={c} />
166
+ ))}
167
+ {(doc.analysis?.detected_medications || []).slice(0, 3).map((m: string) => (
168
+ <EntityBadge key={m} label="CHEMICAL" text={m} />
169
+ ))}
170
+ </div>
171
+ </div>
172
+
173
+ <div className="flex items-center gap-2 self-end md:self-center">
174
+ <Link
175
+ href={`/reports/${doc.id}`}
176
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-teal-50 hover:bg-teal-100 text-teal-800 rounded-lg text-xs font-semibold transition-colors"
177
+ >
178
+ <Eye className="w-3.5 h-3.5" />
179
+ <span>View Analysis</span>
180
+ </Link>
181
+ <button
182
+ onClick={() => handleDelete(doc.id)}
183
+ className="p-1.5 text-slate-400 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
184
+ title="Delete Report"
185
+ >
186
+ <Trash2 className="w-4 h-4" />
187
+ </button>
188
+ </div>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ )}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ );
197
+ }