@omniradiology/omnirad 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +438 -0
  2. package/app/api/ai-config/route.ts +131 -0
  3. package/app/api/ai-config/test/route.ts +49 -0
  4. package/app/api/auth/auto-login/route.ts +66 -0
  5. package/app/api/auth/check/route.ts +17 -0
  6. package/app/api/auth/login/route.ts +72 -0
  7. package/app/api/auth/logout/route.ts +25 -0
  8. package/app/api/auth/me/route.ts +75 -0
  9. package/app/api/auth/password/route.ts +49 -0
  10. package/app/api/auth/setup/route.ts +63 -0
  11. package/app/api/auth/users/route.ts +100 -0
  12. package/app/api/auth/wipe/route.ts +27 -0
  13. package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
  14. package/app/api/compliance/audit/route.ts +110 -0
  15. package/app/api/compliance/export/patient/[id]/route.ts +108 -0
  16. package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
  17. package/app/api/compliance/settings/route.ts +93 -0
  18. package/app/api/copilot/annotate/route.ts +94 -0
  19. package/app/api/copilot/chat/route.ts +238 -0
  20. package/app/api/copilot/history/route.ts +95 -0
  21. package/app/api/copilot/reports/route.ts +81 -0
  22. package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
  23. package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
  24. package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
  25. package/app/api/fhir/Patient/[id]/route.ts +26 -0
  26. package/app/api/fhir/ServiceRequest/route.ts +85 -0
  27. package/app/api/fhir/config/route.ts +102 -0
  28. package/app/api/fhir/config/test-connection/route.ts +49 -0
  29. package/app/api/fhir/metadata/route.ts +51 -0
  30. package/app/api/pacs/metadata/route.ts +32 -0
  31. package/app/api/pacs/qido/instances/route.ts +39 -0
  32. package/app/api/pacs/qido/series/route.ts +38 -0
  33. package/app/api/pacs/qido/studies/route.ts +37 -0
  34. package/app/api/pacs/test/route.ts +30 -0
  35. package/app/api/pacs/wado/render/route.ts +51 -0
  36. package/app/api/patients/[id]/reports/route.ts +18 -0
  37. package/app/api/patients/[id]/route.ts +43 -0
  38. package/app/api/patients/merge/route.ts +57 -0
  39. package/app/api/patients/route.ts +67 -0
  40. package/app/api/patients/search/route.ts +25 -0
  41. package/app/api/reports/[id]/route.ts +84 -0
  42. package/app/api/reports/[id]/status/route.ts +87 -0
  43. package/app/api/reports/clear/route.ts +16 -0
  44. package/app/api/reports/route.ts +112 -0
  45. package/app/api/segmentation-config/route.ts +238 -0
  46. package/app/api/settings/route.ts +245 -0
  47. package/app/api/settings/test-supabase/route.ts +103 -0
  48. package/app/api/upload/route.ts +48 -0
  49. package/app/copilot/page.tsx +30 -0
  50. package/app/globals.css +141 -0
  51. package/app/history/page.tsx +242 -0
  52. package/app/icon.svg +3 -0
  53. package/app/layout.tsx +47 -0
  54. package/app/login/page.tsx +175 -0
  55. package/app/pacs/page.tsx +78 -0
  56. package/app/page.tsx +125 -0
  57. package/app/patients/[id]/page.tsx +315 -0
  58. package/app/patients/page.tsx +110 -0
  59. package/app/profile/page.tsx +208 -0
  60. package/app/reports/page.tsx +432 -0
  61. package/app/settings/page.tsx +454 -0
  62. package/app/setup/page.tsx +199 -0
  63. package/components/admin/AuditLogTable.tsx +293 -0
  64. package/components/copilot/ActivityIndicator.tsx +215 -0
  65. package/components/copilot/ChatHistoryPanel.tsx +140 -0
  66. package/components/copilot/ChatMessage.tsx +251 -0
  67. package/components/copilot/ClickableReference.tsx +40 -0
  68. package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
  69. package/components/copilot/CopilotPanel.tsx +311 -0
  70. package/components/copilot/FindingsList.tsx +75 -0
  71. package/components/copilot/ViewerPanel.tsx +460 -0
  72. package/components/copilot/WorkspaceLayout.tsx +398 -0
  73. package/components/dashboard/AIConfigPanel.tsx +339 -0
  74. package/components/dashboard/AppearancePanel.tsx +491 -0
  75. package/components/dashboard/ApprovalModal.tsx +163 -0
  76. package/components/dashboard/CollaborationPanel.tsx +134 -0
  77. package/components/dashboard/CopilotConfigPanel.tsx +337 -0
  78. package/components/dashboard/DicomViewer.tsx +645 -0
  79. package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
  80. package/components/dashboard/FullReportOverlay.tsx +269 -0
  81. package/components/dashboard/ImageViewer.tsx +541 -0
  82. package/components/dashboard/PatientForm.tsx +597 -0
  83. package/components/dashboard/RejectionModal.tsx +74 -0
  84. package/components/dashboard/ReportEditor.tsx +160 -0
  85. package/components/dashboard/ReportTemplates.tsx +729 -0
  86. package/components/dashboard/ReportView.tsx +539 -0
  87. package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
  88. package/components/dashboard/StudyPlaceholder.tsx +17 -0
  89. package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
  90. package/components/dashboard/UserManagementPanel.tsx +272 -0
  91. package/components/layout/ClientLayout.tsx +39 -0
  92. package/components/layout/Header.tsx +20 -0
  93. package/components/layout/Sidebar.tsx +119 -0
  94. package/components/pacs/PacsImageViewerModal.tsx +121 -0
  95. package/components/pacs/PacsSearchFilters.tsx +117 -0
  96. package/components/pacs/PacsSeriesViewer.tsx +190 -0
  97. package/components/pacs/PacsStudyTable.tsx +113 -0
  98. package/components/patients/patient-card.tsx +117 -0
  99. package/components/patients/patient-header.tsx +122 -0
  100. package/components/patients/patient-search.tsx +137 -0
  101. package/components/patients/patient-timeline.tsx +153 -0
  102. package/components/settings/ComplianceSettingsPanel.tsx +278 -0
  103. package/components/settings/SecurityPanel.tsx +418 -0
  104. package/components/ui/badge.tsx +19 -0
  105. package/components/ui/basic.tsx +156 -0
  106. package/db/index.ts +350 -0
  107. package/db/migrations/0000_odd_quasimodo.sql +117 -0
  108. package/db/migrations/meta/0000_snapshot.json +778 -0
  109. package/db/migrations/meta/_journal.json +13 -0
  110. package/db/schema.ts +239 -0
  111. package/drizzle.config.ts +10 -0
  112. package/lib/api.ts +689 -0
  113. package/lib/auth.ts +22 -0
  114. package/lib/copilot/action-executor.ts +94 -0
  115. package/lib/copilot/action-types.ts +72 -0
  116. package/lib/copilot/coordinate-mapper.ts +84 -0
  117. package/lib/dicomImageExtractor.ts +103 -0
  118. package/lib/dicomMetadataParser.ts +111 -0
  119. package/lib/fhir/client.ts +25 -0
  120. package/lib/fhir/constants.ts +21 -0
  121. package/lib/fhir/diagnostic-report.ts +88 -0
  122. package/lib/fhir/helpers.ts +73 -0
  123. package/lib/fhir/imaging-study.ts +49 -0
  124. package/lib/fhir/patient.ts +55 -0
  125. package/lib/fhir/service-request.ts +85 -0
  126. package/lib/fhir.ts +6 -0
  127. package/lib/pacs/dicom-utils.ts +72 -0
  128. package/lib/pacs/dicomweb.ts +72 -0
  129. package/lib/pacs/server-utils.ts +37 -0
  130. package/lib/patients.ts +25 -0
  131. package/lib/pdfHelper.ts +119 -0
  132. package/lib/reportHtmlGenerator.ts +581 -0
  133. package/lib/security/audit.ts +180 -0
  134. package/lib/security/authz.ts +246 -0
  135. package/lib/security/phi-redaction.ts +156 -0
  136. package/lib/security/rate-limit.ts +106 -0
  137. package/lib/security/secrets.ts +179 -0
  138. package/lib/supabase.ts +72 -0
  139. package/lib/utils.ts +6 -0
  140. package/next.config.ts +35 -0
  141. package/package.json +76 -0
  142. package/public/file.svg +1 -0
  143. package/public/globe.svg +1 -0
  144. package/public/logo.svg +8 -0
  145. package/public/next.svg +1 -0
  146. package/public/omnirad-favicon.svg +8 -0
  147. package/public/vercel.svg +1 -0
  148. package/public/window.svg +1 -0
  149. package/tsconfig.json +34 -0
  150. package/types/copilot-viewer.ts +155 -0
  151. package/types/copilot.ts +105 -0
  152. package/types/fhir.ts +21 -0
  153. package/types/html2pdf.d.ts +20 -0
  154. package/types/index.ts +139 -0
  155. package/types/pacs.ts +41 -0
@@ -0,0 +1,460 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { ViewerTab } from "@/types/copilot";
5
+ import { MonitorDot, FileText, ClipboardList, FolderOpen, User, Calendar, Stethoscope, Activity, ExternalLink } from "lucide-react";
6
+ import CopilotCornerstoneViewer from "./CopilotCornerstoneViewer";
7
+ import type { CopilotViewerRef } from "@/types/copilot-viewer";
8
+
9
+ interface ViewerPanelProps {
10
+ activeTab: ViewerTab;
11
+ onTabChange: (tab: ViewerTab) => void;
12
+ currentReportId: string | null;
13
+ currentStudyId: string | null;
14
+ currentSlice: number;
15
+ currentPatientId: string | null;
16
+ onReportSelect: (reportId: string) => void;
17
+ onPatientContext: (patientId: string, patientName?: string) => void;
18
+ viewerRef?: React.RefObject<CopilotViewerRef | null>;
19
+ }
20
+
21
+ interface ReportData {
22
+ id: string;
23
+ reportData: any;
24
+ imageData?: string | null;
25
+ patientId?: string;
26
+ patientName?: string;
27
+ }
28
+
29
+ export default function ViewerPanel({
30
+ activeTab,
31
+ onTabChange,
32
+ currentReportId,
33
+ currentStudyId,
34
+ currentSlice,
35
+ currentPatientId,
36
+ onReportSelect,
37
+ onPatientContext,
38
+ viewerRef,
39
+ }: ViewerPanelProps) {
40
+ const [reportData, setReportData] = useState<ReportData | null>(null);
41
+ const [patientReports, setPatientReports] = useState<any[]>([]);
42
+ const [patientInfo, setPatientInfo] = useState<any>(null);
43
+ const [isLoading, setIsLoading] = useState(false);
44
+
45
+ // Fetch report data when currentReportId changes
46
+ useEffect(() => {
47
+ if (!currentReportId) return;
48
+ setIsLoading(true);
49
+ fetch(`/api/copilot/reports?id=${encodeURIComponent(currentReportId)}`)
50
+ .then(r => r.json())
51
+ .then(data => {
52
+ if (!data.error) {
53
+ setReportData(data);
54
+ // Update patient context if we got patient info
55
+ if (data.patientId) {
56
+ onPatientContext(data.patientId, data.patientName);
57
+ }
58
+ }
59
+ })
60
+ .catch(e => console.error("[Viewer] Error fetching report:", e))
61
+ .finally(() => setIsLoading(false));
62
+ }, [currentReportId, onPatientContext]);
63
+
64
+ // Fetch patient reports list when currentPatientId changes
65
+ useEffect(() => {
66
+ if (!currentPatientId) return;
67
+ fetch(`/api/copilot/reports?patientId=${encodeURIComponent(currentPatientId)}`)
68
+ .then(r => r.json())
69
+ .then(data => {
70
+ if (Array.isArray(data)) setPatientReports(data);
71
+ })
72
+ .catch(e => console.error("[Viewer] Error fetching patient reports:", e));
73
+
74
+ // Also fetch patient info
75
+ fetch(`/api/patients/${encodeURIComponent(currentPatientId)}`)
76
+ .then(r => r.json())
77
+ .then(data => {
78
+ if (!data.error) setPatientInfo(data);
79
+ })
80
+ .catch(e => console.error("[Viewer] Error fetching patient:", e));
81
+ }, [currentPatientId]);
82
+
83
+ const tabs: { id: ViewerTab; label: string; icon: React.ReactNode }[] = [
84
+ { id: "dicom", label: "DICOM", icon: <MonitorDot size={16} /> },
85
+ { id: "report", label: "REPORT", icon: <FileText size={16} /> },
86
+ { id: "metadata", label: "METADATA", icon: <ClipboardList size={16} /> },
87
+ ];
88
+
89
+ return (
90
+ <div className="flex flex-col h-full">
91
+ {/* Tab Bar */}
92
+ <div className="flex border-b border-border-primary bg-bg-surface shrink-0">
93
+ {tabs.map(tab => (
94
+ <button
95
+ key={tab.id}
96
+ onClick={() => onTabChange(tab.id)}
97
+ className={`
98
+ flex items-center gap-2 px-5 py-3 text-sm font-semibold uppercase tracking-wider
99
+ transition-all duration-200 relative
100
+ ${activeTab === tab.id
101
+ ? "text-primary"
102
+ : "text-text-muted hover:text-text-primary hover:bg-bg-panel/50"
103
+ }
104
+ `}
105
+ >
106
+ {tab.icon}
107
+ {tab.label}
108
+ {activeTab === tab.id && (
109
+ <span className="absolute bottom-0 left-0 right-0 h-[2px] bg-primary rounded-t-full" />
110
+ )}
111
+ </button>
112
+ ))}
113
+ </div>
114
+
115
+ {/* Tab Content */}
116
+ <div className="flex-1 overflow-auto">
117
+ {isLoading && (
118
+ <div className="flex items-center justify-center h-full">
119
+ <div className="flex items-center gap-3 text-text-muted">
120
+ <div className="w-5 h-5 border-2 border-primary/30 border-t-primary rounded-full animate-spin" />
121
+ Loading...
122
+ </div>
123
+ </div>
124
+ )}
125
+
126
+ {!isLoading && activeTab === "dicom" && (
127
+ <DicomTab reportData={reportData} currentSlice={currentSlice} viewerRef={viewerRef} />
128
+ )}
129
+
130
+ {!isLoading && activeTab === "report" && (
131
+ <ReportTab reportData={reportData} />
132
+ )}
133
+
134
+ {!isLoading && activeTab === "metadata" && (
135
+ <MetadataTab patientInfo={patientInfo} patientReports={patientReports} />
136
+ )}
137
+ </div>
138
+
139
+ {/* Associated Files (bottom) */}
140
+ <div className="border-t border-border-primary bg-bg-surface shrink-0 max-h-[180px] overflow-auto">
141
+ <div className="px-4 py-2 flex items-center gap-2 text-xs font-semibold text-text-muted uppercase tracking-wider">
142
+ <FolderOpen size={14} />
143
+ Associated Files
144
+ </div>
145
+ {patientReports.length > 0 ? (
146
+ <div className="px-2 pb-2 space-y-0.5">
147
+ {patientReports.map((r: any) => (
148
+ <div
149
+ key={r.id}
150
+ className={`
151
+ w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-left
152
+ transition-all duration-150 group
153
+ ${currentReportId === r.id
154
+ ? "bg-primary/10 text-primary border border-primary/20"
155
+ : "text-text-primary hover:bg-bg-panel"
156
+ }
157
+ `}
158
+ >
159
+ {/* Clickable area for viewer panel */}
160
+ <div
161
+ className="flex-1 min-w-0 flex items-center gap-3 cursor-pointer"
162
+ onClick={() => onReportSelect(r.id)}
163
+ >
164
+ <FileText size={14} className="shrink-0 text-text-muted" />
165
+ <div className="flex-1 min-w-0">
166
+ <div className="truncate font-medium">{r.reportId || r.id}</div>
167
+ <div className="text-xs text-text-muted truncate">
168
+ {r.modality} • {r.date ? new Date(r.date).toLocaleDateString() : "N/A"}
169
+ </div>
170
+ </div>
171
+ {r.hasImages && (
172
+ <MonitorDot size={12} className="shrink-0 text-blue-500" />
173
+ )}
174
+ </div>
175
+
176
+ {/* Status Tag */}
177
+ <span className={`text-[10px] px-2 py-0.5 rounded-full font-bold uppercase tracking-wide shrink-0 ${
178
+ r.status === 'Approved' ? 'bg-green-500/15 text-green-500' :
179
+ r.status === 'Rejected' ? 'bg-red-500/15 text-red-500' :
180
+ 'bg-yellow-500/15 text-yellow-600'
181
+ }`}>
182
+ {r.status || "Pending"}
183
+ </span>
184
+
185
+ {/* Open Original Button */}
186
+ <a
187
+ href={`/reports?id=${r.id}`}
188
+ target="_blank"
189
+ rel="noopener noreferrer"
190
+ className="p-1.5 rounded-md text-text-muted hover:text-primary hover:bg-primary/10 transition-colors opacity-0 group-hover:opacity-100"
191
+ title="Open Full Report view securely"
192
+ >
193
+ <ExternalLink size={14} />
194
+ </a>
195
+ </div>
196
+ ))}
197
+ </div>
198
+ ) : (
199
+ <div className="px-4 pb-3 text-xs text-text-muted italic">
200
+ No files loaded. Ask the AI copilot about a patient to see their files here.
201
+ </div>
202
+ )}
203
+ </div>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ // ─── DICOM Tab ───────────────────────────────────────────────────────────────
209
+ function DicomTab({ reportData, currentSlice, viewerRef }: { reportData: ReportData | null; currentSlice: number; viewerRef?: React.RefObject<CopilotViewerRef | null> }) {
210
+ const imageData = reportData?.reportData?.image_data || reportData?.imageData;
211
+ const imagesData = reportData?.reportData?.images_data;
212
+
213
+ const images: string[] = imagesData && imagesData.length > 0
214
+ ? imagesData
215
+ : imageData ? [imageData] : [];
216
+
217
+ return (
218
+ <div className="flex flex-col h-full bg-black/90 w-full">
219
+ <CopilotCornerstoneViewer
220
+ ref={viewerRef}
221
+ images={images}
222
+ currentSlice={currentSlice > 0 ? currentSlice - 1 : 0}
223
+ />
224
+ </div>
225
+ );
226
+ }
227
+
228
+ // ─── Report Tab ──────────────────────────────────────────────────────────────
229
+ function ReportTab({ reportData }: { reportData: ReportData | null }) {
230
+ if (!reportData?.reportData) {
231
+ return (
232
+ <div className="flex flex-col items-center justify-center h-full text-text-muted gap-4 p-8">
233
+ <FileText size={48} className="opacity-30" />
234
+ <div className="text-center">
235
+ <p className="font-medium text-text-secondary">No Report Loaded</p>
236
+ <p className="text-sm mt-1">Ask the copilot to show a patient&apos;s report, or click a file below.</p>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ const rd = reportData.reportData;
243
+ const findings = rd.findings || [];
244
+ const impression = rd.impression || [];
245
+ const recommendations = rd.recommendations || [];
246
+
247
+ return (
248
+ <div className="p-6 space-y-6 max-w-3xl mx-auto">
249
+ {/* Report Header */}
250
+ <div className="border-b border-border-primary pb-4">
251
+ <div className="flex items-center justify-between">
252
+ <div>
253
+ <h2 className="text-lg font-bold text-text-heading">
254
+ {rd.report_header?.report_title || "Radiology Report"}
255
+ </h2>
256
+ <p className="text-sm text-text-muted mt-1">
257
+ {rd.report_header?.hospital_name} • {rd.report_header?.department}
258
+ </p>
259
+ </div>
260
+ <div className="text-right">
261
+ <p className="text-sm font-mono text-text-secondary">{rd.report_header?.report_id}</p>
262
+ <p className="text-xs text-text-muted">
263
+ {rd.report_header?.report_date ? new Date(rd.report_header.report_date).toLocaleString() : ""}
264
+ </p>
265
+ </div>
266
+ </div>
267
+ </div>
268
+
269
+ {/* Patient Info */}
270
+ <div className="grid grid-cols-2 gap-4 bg-bg-panel/50 rounded-xl p-4 border border-border-card">
271
+ <InfoRow label="Patient" value={rd.patient?.name} icon={<User size={14} />} />
272
+ <InfoRow label="Age / Gender" value={`${rd.patient?.age || "?"} / ${rd.patient?.gender || "?"}`} icon={<Calendar size={14} />} />
273
+ <InfoRow label="Modality" value={rd.study?.modality} icon={<MonitorDot size={14} />} />
274
+ <InfoRow label="Urgency" value={rd.urgency} icon={<Activity size={14} />} />
275
+ </div>
276
+
277
+ {/* Clinical Information */}
278
+ {rd.clinical_information && (
279
+ <Section title="Clinical Information" icon={<Stethoscope size={16} />}>
280
+ <div className="space-y-2 text-sm text-text-primary">
281
+ {rd.clinical_information.symptoms && (
282
+ <p><span className="font-medium text-text-secondary">Symptoms:</span> {rd.clinical_information.symptoms}</p>
283
+ )}
284
+ {rd.clinical_information.history && (
285
+ <p><span className="font-medium text-text-secondary">History:</span> {rd.clinical_information.history}</p>
286
+ )}
287
+ {rd.clinical_information.indication && (
288
+ <p><span className="font-medium text-text-secondary">Indication:</span> {rd.clinical_information.indication}</p>
289
+ )}
290
+ </div>
291
+ </Section>
292
+ )}
293
+
294
+ {/* Findings */}
295
+ {findings.length > 0 && (
296
+ <Section title="Findings" icon={<ClipboardList size={16} />}>
297
+ <div className="space-y-3">
298
+ {findings.map((f: any, i: number) => (
299
+ <div key={i} className="flex gap-3 items-start">
300
+ <span className={`mt-0.5 w-2.5 h-2.5 rounded-full shrink-0 ${
301
+ f.status === 'abnormal' ? 'bg-red-500' : 'bg-green-500'
302
+ }`} />
303
+ <div>
304
+ <p className="text-sm font-medium text-text-heading">{f.anatomical_region}</p>
305
+ <p className="text-sm text-text-primary">{f.observation}</p>
306
+ </div>
307
+ </div>
308
+ ))}
309
+ </div>
310
+ </Section>
311
+ )}
312
+
313
+ {/* Impression */}
314
+ {impression.length > 0 && (
315
+ <Section title="Impression" icon={<Activity size={16} />}>
316
+ <ul className="space-y-1.5">
317
+ {impression.map((item: string, i: number) => (
318
+ <li key={i} className="text-sm text-text-primary flex gap-2">
319
+ <span className="text-primary font-bold shrink-0">•</span>
320
+ {item}
321
+ </li>
322
+ ))}
323
+ </ul>
324
+ </Section>
325
+ )}
326
+
327
+ {/* Recommendations */}
328
+ {recommendations.length > 0 && (
329
+ <Section title="Recommendations" icon={<FileText size={16} />}>
330
+ <ul className="space-y-1.5">
331
+ {recommendations.map((item: string, i: number) => (
332
+ <li key={i} className="text-sm text-text-primary flex gap-2">
333
+ <span className="text-warning font-bold shrink-0">→</span>
334
+ {item}
335
+ </li>
336
+ ))}
337
+ </ul>
338
+ </Section>
339
+ )}
340
+
341
+ {/* Footer */}
342
+ {rd.report_footer && (
343
+ <div className="border-t border-border-primary pt-4 text-xs text-text-muted space-y-1">
344
+ <p>Prepared by: {rd.report_footer.prepared_by}</p>
345
+ {rd.report_footer.approved_by && <p>Approved by: {rd.report_footer.approved_by}</p>}
346
+ <p className="italic">{rd.disclaimer}</p>
347
+ </div>
348
+ )}
349
+ </div>
350
+ );
351
+ }
352
+
353
+ // ─── Metadata Tab ────────────────────────────────────────────────────────────
354
+ function MetadataTab({ patientInfo, patientReports }: { patientInfo: any; patientReports: any[] }) {
355
+ if (!patientInfo) {
356
+ return (
357
+ <div className="flex flex-col items-center justify-center h-full text-text-muted gap-4 p-8">
358
+ <ClipboardList size={48} className="opacity-30" />
359
+ <div className="text-center">
360
+ <p className="font-medium text-text-secondary">No Patient Selected</p>
361
+ <p className="text-sm mt-1">Ask the copilot about a patient to see their metadata here.</p>
362
+ </div>
363
+ </div>
364
+ );
365
+ }
366
+
367
+ return (
368
+ <div className="p-6 space-y-6 max-w-3xl mx-auto">
369
+ {/* Patient Demographics */}
370
+ <div>
371
+ <h3 className="text-lg font-bold text-text-heading mb-4 flex items-center gap-2">
372
+ <User size={18} className="text-primary" />
373
+ Patient Information
374
+ </h3>
375
+ <div className="grid grid-cols-2 gap-4 bg-bg-panel/50 rounded-xl p-4 border border-border-card">
376
+ <InfoRow label="Full Name" value={patientInfo.patientName} icon={<User size={14} />} />
377
+ <InfoRow label="Patient ID" value={patientInfo.patientIdNumber || patientInfo.id} icon={<ClipboardList size={14} />} />
378
+ <InfoRow label="Age" value={patientInfo.age?.toString()} icon={<Calendar size={14} />} />
379
+ <InfoRow label="Gender" value={patientInfo.gender} icon={<User size={14} />} />
380
+ <InfoRow label="Date of Birth" value={patientInfo.dob} icon={<Calendar size={14} />} />
381
+ <InfoRow label="Mobile" value={patientInfo.mobile} icon={<User size={14} />} />
382
+ </div>
383
+ </div>
384
+
385
+ {/* Study Timeline */}
386
+ <div>
387
+ <h3 className="text-lg font-bold text-text-heading mb-4 flex items-center gap-2">
388
+ <Activity size={18} className="text-primary" />
389
+ Study Timeline ({patientReports.length} reports)
390
+ </h3>
391
+ {patientReports.length > 0 ? (
392
+ <div className="space-y-3">
393
+ {patientReports.map((r: any, i: number) => (
394
+ <div key={r.id} className="flex items-start gap-4">
395
+ <div className="flex flex-col items-center">
396
+ <div className={`w-3 h-3 rounded-full ${i === 0 ? 'bg-primary' : 'bg-border-card'}`} />
397
+ {i < patientReports.length - 1 && (
398
+ <div className="w-0.5 h-8 bg-border-card" />
399
+ )}
400
+ </div>
401
+ <div className="flex-1 pb-3">
402
+ <p className="text-sm font-medium text-text-heading">
403
+ {r.modality || "Study"} — {r.reportId || r.id}
404
+ </p>
405
+ <p className="text-xs text-text-muted">
406
+ {r.date ? new Date(r.date).toLocaleString() : "Date unknown"}
407
+ </p>
408
+ <span className={`inline-block mt-1 text-xs px-2 py-0.5 rounded-full font-medium ${
409
+ r.status === 'Approved' ? 'bg-green-500/15 text-green-500' :
410
+ r.status === 'Rejected' ? 'bg-red-500/15 text-red-500' :
411
+ 'bg-yellow-500/15 text-yellow-600'
412
+ }`}>
413
+ {r.status || "Pending"}
414
+ </span>
415
+ </div>
416
+ </div>
417
+ ))}
418
+ </div>
419
+ ) : (
420
+ <p className="text-sm text-text-muted italic">No studies found for this patient.</p>
421
+ )}
422
+ </div>
423
+
424
+ {/* Notes */}
425
+ {patientInfo.notes && (
426
+ <div>
427
+ <h3 className="text-lg font-bold text-text-heading mb-2">Notes</h3>
428
+ <p className="text-sm text-text-primary bg-bg-panel/50 rounded-lg p-4 border border-border-card">
429
+ {patientInfo.notes}
430
+ </p>
431
+ </div>
432
+ )}
433
+ </div>
434
+ );
435
+ }
436
+
437
+ // ─── Helper Components ───────────────────────────────────────────────────────
438
+ function InfoRow({ label, value, icon }: { label: string; value?: string; icon?: React.ReactNode }) {
439
+ return (
440
+ <div className="flex items-center gap-2">
441
+ {icon && <span className="text-text-muted">{icon}</span>}
442
+ <div>
443
+ <p className="text-xs text-text-muted">{label}</p>
444
+ <p className="text-sm font-medium text-text-primary">{value || "—"}</p>
445
+ </div>
446
+ </div>
447
+ );
448
+ }
449
+
450
+ function Section({ title, icon, children }: { title: string; icon?: React.ReactNode; children: React.ReactNode }) {
451
+ return (
452
+ <div>
453
+ <h3 className="text-base font-bold text-text-heading mb-3 flex items-center gap-2">
454
+ {icon && <span className="text-primary">{icon}</span>}
455
+ {title}
456
+ </h3>
457
+ {children}
458
+ </div>
459
+ );
460
+ }