Spaces:
Running on Zero
Running on Zero
Commit ·
49f45e4
1
Parent(s): 76dd92e
update
Browse files- .gitignore +0 -1
- frontend/src/app/admin/page.tsx +169 -0
- frontend/src/app/assistant/page.tsx +349 -0
- frontend/src/app/dashboard/page.tsx +274 -0
- frontend/src/app/demo/page.tsx +163 -0
- frontend/src/app/globals.css +62 -0
- frontend/src/app/history/page.tsx +119 -0
- frontend/src/app/layout.tsx +26 -0
- frontend/src/app/login/page.tsx +147 -0
- frontend/src/app/ner/page.tsx +361 -0
- frontend/src/app/page.tsx +184 -0
- frontend/src/app/profile/page.tsx +372 -0
- frontend/src/app/register/page.tsx +133 -0
- frontend/src/app/reports/[id]/page.tsx +194 -0
- frontend/src/app/reports/page.tsx +197 -0
.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'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 "Run BC5CDR Inference" 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 |
+
}
|