@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,251 @@
1
+ "use client";
2
+
3
+ import { ChatMessage as ChatMessageType, Reference } from "@/types/copilot";
4
+ import ClickableReference from "./ClickableReference";
5
+ import { Bot, User, Eye, Scan, FileText, ClipboardList, ZoomIn, Sparkles, Layers } from "lucide-react";
6
+
7
+ import React from "react";
8
+
9
+ // A simple local markdown renderer
10
+ function SimpleMarkdown({ content }: { content: string }) {
11
+ if (!content) return null;
12
+
13
+ // Split by lines to handle list item blocks and paragraphs
14
+ const lines = content.split('\n');
15
+ const elements: React.ReactNode[] = [];
16
+
17
+ let inList = false;
18
+ let currentList: React.ReactNode[] = [];
19
+
20
+ const parseInline = (text: string, key: string) => {
21
+ // Handle bold **text**
22
+ let parts = text.split(/(\*\*.*?\*\*)/g);
23
+ return parts.map((part, i) => {
24
+ if (part.startsWith('**') && part.endsWith('**')) {
25
+ return <strong key={`${key}-b-${i}`}>{part.slice(2, -2)}</strong>;
26
+ }
27
+ // Handle italic *text* or _text_ inside
28
+ let subParts = part.split(/(\*.*?\*|_.*?_)/g);
29
+ return subParts.map((sp, j) => {
30
+ if ((sp.startsWith('*') && sp.endsWith('*')) || (sp.startsWith('_') && sp.endsWith('_'))) {
31
+ return <em key={`${key}-i-${i}-${j}`}>{sp.slice(1, -1)}</em>;
32
+ }
33
+ // Handle inline code `code`
34
+ let codeParts = sp.split(/(`.*?`)/g);
35
+ return codeParts.map((cp, k) => {
36
+ if (cp.startsWith('`') && cp.endsWith('`')) {
37
+ return <code key={`${key}-c-${i}-${j}-${k}`} className="bg-black/10 dark:bg-white/10 px-1 py-0.5 rounded text-xs mx-0.5 font-mono">{cp.slice(1, -1)}</code>;
38
+ }
39
+ return cp;
40
+ });
41
+ });
42
+ });
43
+ };
44
+
45
+ lines.forEach((line, index) => {
46
+ const trimmed = line.trim();
47
+ const isBulletList = trimmed.startsWith('* ') || trimmed.startsWith('- ');
48
+ const isNumList = /^\d+\.\s/.test(trimmed);
49
+
50
+ if (isBulletList || isNumList) {
51
+ if (!inList) inList = true;
52
+ // remove the marker
53
+ const cleanLine = trimmed.replace(/^(\* |- |\d+\.\s)/, '');
54
+ currentList.push(
55
+ <li key={`li-${index}`} className="ml-4 mt-1 list-inside">
56
+ {isBulletList ? <span className="mr-2 opacity-70">•</span> : null}
57
+ {parseInline(cleanLine, `li-inline-${index}`)}
58
+ </li>
59
+ );
60
+ } else {
61
+ // End list if we were in one
62
+ if (inList) {
63
+ elements.push(<ul key={`ul-${index}`} className="my-2">{currentList}</ul>);
64
+ inList = false;
65
+ currentList = [];
66
+ }
67
+
68
+ if (trimmed === '') {
69
+ // empty line = spacer
70
+ elements.push(<div key={`br-${index}`} className="h-2" />);
71
+ } else {
72
+ elements.push(
73
+ <p key={`p-${index}`} className="mb-2 last:mb-0">
74
+ {parseInline(line, `p-inline-${index}`)}
75
+ </p>
76
+ );
77
+ }
78
+ }
79
+ });
80
+
81
+ if (inList) {
82
+ elements.push(<ul key="ul-last" className="my-2">{currentList}</ul>);
83
+ }
84
+
85
+ return <div className="markdown-body">{elements}</div>;
86
+ }
87
+
88
+ // ── Determine label + style for a single replay button ───────────────────────
89
+ function getActionButtonStyle(actions: any[]): {
90
+ label: string;
91
+ icon: React.ReactNode;
92
+ bgClass: string;
93
+ textClass: string;
94
+ borderClass: string;
95
+ hoverClass: string;
96
+ } | null {
97
+ if (!actions || actions.length === 0) return null;
98
+
99
+ const hasFindings = actions.some(
100
+ (a) => a?.type === "annotation" || a?.type === "segmentation"
101
+ );
102
+ const hasReport = actions.some((a) => a?.type === "OPEN_REPORT");
103
+ const hasMetadata = actions.some((a) => a?.type === "OPEN_METADATA");
104
+ const hasViewport = actions.some((a) => a?.type === "viewport");
105
+ const hasClear = actions.some((a) => a?.type === "clear");
106
+
107
+ if (hasFindings) {
108
+ return {
109
+ label: "View AI Findings",
110
+ icon: <Sparkles size={13} />,
111
+ bgClass: "bg-emerald-500/10",
112
+ textClass: "text-emerald-400",
113
+ borderClass: "border-emerald-500/25",
114
+ hoverClass: "hover:bg-emerald-500/20 hover:border-emerald-500/40",
115
+ };
116
+ }
117
+ if (hasViewport) {
118
+ return {
119
+ label: "Zoom to Region",
120
+ icon: <ZoomIn size={13} />,
121
+ bgClass: "bg-rose-500/10",
122
+ textClass: "text-rose-400",
123
+ borderClass: "border-rose-500/25",
124
+ hoverClass: "hover:bg-rose-500/20 hover:border-rose-500/40",
125
+ };
126
+ }
127
+ if (hasReport) {
128
+ return {
129
+ label: "View Report",
130
+ icon: <FileText size={13} />,
131
+ bgClass: "bg-violet-500/10",
132
+ textClass: "text-violet-400",
133
+ borderClass: "border-violet-500/25",
134
+ hoverClass: "hover:bg-violet-500/20 hover:border-violet-500/40",
135
+ };
136
+ }
137
+ if (hasMetadata) {
138
+ return {
139
+ label: "View Patient Info",
140
+ icon: <ClipboardList size={13} />,
141
+ bgClass: "bg-amber-500/10",
142
+ textClass: "text-amber-400",
143
+ borderClass: "border-amber-500/25",
144
+ hoverClass: "hover:bg-amber-500/20 hover:border-amber-500/40",
145
+ };
146
+ }
147
+ if (hasClear) {
148
+ return {
149
+ label: "Clear Findings",
150
+ icon: <Layers size={13} />,
151
+ bgClass: "bg-slate-500/10",
152
+ textClass: "text-slate-400",
153
+ borderClass: "border-slate-500/25",
154
+ hoverClass: "hover:bg-slate-500/20 hover:border-slate-500/40",
155
+ };
156
+ }
157
+
158
+ // Default for OPEN_DICOM / navigate / other
159
+ return {
160
+ label: "Open in Viewer",
161
+ icon: <Scan size={13} />,
162
+ bgClass: "bg-sky-500/10",
163
+ textClass: "text-sky-400",
164
+ borderClass: "border-sky-500/25",
165
+ hoverClass: "hover:bg-sky-500/20 hover:border-sky-500/40",
166
+ };
167
+ }
168
+
169
+ interface ChatMessageProps {
170
+ message: ChatMessageType;
171
+ onReferenceClick: (ref: Reference) => void;
172
+ onExecuteActions?: (actions: any[]) => void;
173
+ }
174
+
175
+ export default function ChatMessage({ message, onReferenceClick, onExecuteActions }: ChatMessageProps) {
176
+ const isUser = message.role === "user";
177
+ const hasViewerActions = message.viewerActions && message.viewerActions.length > 0;
178
+ const hasReferences = message.references && message.references.length > 0;
179
+
180
+ const actionStyle = hasViewerActions ? getActionButtonStyle(message.viewerActions!) : null;
181
+
182
+ return (
183
+ <div className={`flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"}`}>
184
+ {/* Avatar */}
185
+ <div className={`
186
+ shrink-0 w-8 h-8 rounded-full flex items-center justify-center
187
+ ${isUser
188
+ ? "bg-primary/20 text-primary"
189
+ : "bg-gradient-to-br from-emerald-500/20 to-cyan-500/20 text-emerald-400"
190
+ }
191
+ `}>
192
+ {isUser ? <User size={16} /> : <Bot size={16} />}
193
+ </div>
194
+
195
+ {/* Message Content */}
196
+ <div className={`flex flex-col max-w-[85%] ${isUser ? "items-end" : "items-start"}`}>
197
+ {/* Bubble */}
198
+ <div className={`
199
+ rounded-2xl px-4 py-3 text-sm leading-relaxed
200
+ ${isUser
201
+ ? "bg-primary text-white rounded-br-md"
202
+ : "bg-bg-panel border border-border-card text-text-primary rounded-bl-md"
203
+ }
204
+ `}>
205
+ {/* Render message with simple markdown */}
206
+ <SimpleMarkdown content={
207
+ typeof message.content === "string"
208
+ ? message.content
209
+ : Array.isArray(message.content)
210
+ ? message.content.map((c: any) => c.text || JSON.stringify(c)).join("\n")
211
+ : JSON.stringify(message.content)
212
+ } />
213
+ </div>
214
+
215
+ {/* References */}
216
+ {hasReferences && (
217
+ <div className="flex flex-wrap gap-1.5 mt-2">
218
+ {message.references!.map((ref) => (
219
+ <ClickableReference
220
+ key={ref.id}
221
+ reference={ref}
222
+ onClick={onReferenceClick}
223
+ />
224
+ ))}
225
+ </div>
226
+ )}
227
+
228
+ {/* Single Viewer Action Button */}
229
+ {actionStyle && (
230
+ <button
231
+ onClick={() => onExecuteActions?.(message.viewerActions!)}
232
+ className={`
233
+ flex items-center gap-1.5 px-3 py-1.5 mt-2 rounded-lg text-xs font-medium
234
+ border transition-all duration-200 cursor-pointer
235
+ ${actionStyle.bgClass} ${actionStyle.textClass} ${actionStyle.borderClass} ${actionStyle.hoverClass}
236
+ `}
237
+ >
238
+ {actionStyle.icon}
239
+ {actionStyle.label}
240
+ </button>
241
+ )}
242
+
243
+ {/* Timestamp */}
244
+ <p className="text-[10px] text-text-muted mt-1 opacity-60">
245
+ {new Date(message.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
246
+ </p>
247
+ </div>
248
+ </div>
249
+ );
250
+ }
251
+
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { Reference, ViewerAction } from "@/types/copilot";
4
+ import { FileText, MonitorDot, User } from "lucide-react";
5
+
6
+ interface ClickableReferenceProps {
7
+ reference: Reference;
8
+ onClick: (ref: Reference) => void;
9
+ }
10
+
11
+ export default function ClickableReference({ reference, onClick }: ClickableReferenceProps) {
12
+ const iconMap: Record<string, React.ReactNode> = {
13
+ report: <FileText size={12} />,
14
+ study: <MonitorDot size={12} />,
15
+ scan: <MonitorDot size={12} />,
16
+ patient: <User size={12} />,
17
+ };
18
+
19
+ const colorMap: Record<string, string> = {
20
+ report: "bg-blue-500/15 text-blue-400 hover:bg-blue-500/25 border-blue-500/20",
21
+ study: "bg-purple-500/15 text-purple-400 hover:bg-purple-500/25 border-purple-500/20",
22
+ scan: "bg-purple-500/15 text-purple-400 hover:bg-purple-500/25 border-purple-500/20",
23
+ patient: "bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/25 border-emerald-500/20",
24
+ };
25
+
26
+ return (
27
+ <button
28
+ onClick={() => onClick(reference)}
29
+ className={`
30
+ inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium
31
+ border cursor-pointer transition-all duration-150
32
+ ${colorMap[reference.type] || colorMap.report}
33
+ `}
34
+ title={`Click to open: ${reference.label}`}
35
+ >
36
+ {iconMap[reference.type] || iconMap.report}
37
+ <span>{reference.label}</span>
38
+ </button>
39
+ );
40
+ }