@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.
- package/README.md +438 -0
- package/app/api/ai-config/route.ts +131 -0
- package/app/api/ai-config/test/route.ts +49 -0
- package/app/api/auth/auto-login/route.ts +66 -0
- package/app/api/auth/check/route.ts +17 -0
- package/app/api/auth/login/route.ts +72 -0
- package/app/api/auth/logout/route.ts +25 -0
- package/app/api/auth/me/route.ts +75 -0
- package/app/api/auth/password/route.ts +49 -0
- package/app/api/auth/setup/route.ts +63 -0
- package/app/api/auth/users/route.ts +100 -0
- package/app/api/auth/wipe/route.ts +27 -0
- package/app/api/compliance/anonymize/patient/[id]/route.ts +104 -0
- package/app/api/compliance/audit/route.ts +110 -0
- package/app/api/compliance/export/patient/[id]/route.ts +108 -0
- package/app/api/compliance/restrict/patient/[id]/route.ts +59 -0
- package/app/api/compliance/settings/route.ts +93 -0
- package/app/api/copilot/annotate/route.ts +94 -0
- package/app/api/copilot/chat/route.ts +238 -0
- package/app/api/copilot/history/route.ts +95 -0
- package/app/api/copilot/reports/route.ts +81 -0
- package/app/api/fhir/Bundle/report/[id]/route.ts +85 -0
- package/app/api/fhir/DiagnosticReport/[id]/route.ts +45 -0
- package/app/api/fhir/ImagingStudy/[id]/route.ts +57 -0
- package/app/api/fhir/Patient/[id]/route.ts +26 -0
- package/app/api/fhir/ServiceRequest/route.ts +85 -0
- package/app/api/fhir/config/route.ts +102 -0
- package/app/api/fhir/config/test-connection/route.ts +49 -0
- package/app/api/fhir/metadata/route.ts +51 -0
- package/app/api/pacs/metadata/route.ts +32 -0
- package/app/api/pacs/qido/instances/route.ts +39 -0
- package/app/api/pacs/qido/series/route.ts +38 -0
- package/app/api/pacs/qido/studies/route.ts +37 -0
- package/app/api/pacs/test/route.ts +30 -0
- package/app/api/pacs/wado/render/route.ts +51 -0
- package/app/api/patients/[id]/reports/route.ts +18 -0
- package/app/api/patients/[id]/route.ts +43 -0
- package/app/api/patients/merge/route.ts +57 -0
- package/app/api/patients/route.ts +67 -0
- package/app/api/patients/search/route.ts +25 -0
- package/app/api/reports/[id]/route.ts +84 -0
- package/app/api/reports/[id]/status/route.ts +87 -0
- package/app/api/reports/clear/route.ts +16 -0
- package/app/api/reports/route.ts +112 -0
- package/app/api/segmentation-config/route.ts +238 -0
- package/app/api/settings/route.ts +245 -0
- package/app/api/settings/test-supabase/route.ts +103 -0
- package/app/api/upload/route.ts +48 -0
- package/app/copilot/page.tsx +30 -0
- package/app/globals.css +141 -0
- package/app/history/page.tsx +242 -0
- package/app/icon.svg +3 -0
- package/app/layout.tsx +47 -0
- package/app/login/page.tsx +175 -0
- package/app/pacs/page.tsx +78 -0
- package/app/page.tsx +125 -0
- package/app/patients/[id]/page.tsx +315 -0
- package/app/patients/page.tsx +110 -0
- package/app/profile/page.tsx +208 -0
- package/app/reports/page.tsx +432 -0
- package/app/settings/page.tsx +454 -0
- package/app/setup/page.tsx +199 -0
- package/components/admin/AuditLogTable.tsx +293 -0
- package/components/copilot/ActivityIndicator.tsx +215 -0
- package/components/copilot/ChatHistoryPanel.tsx +140 -0
- package/components/copilot/ChatMessage.tsx +251 -0
- package/components/copilot/ClickableReference.tsx +40 -0
- package/components/copilot/CopilotCornerstoneViewer.tsx +562 -0
- package/components/copilot/CopilotPanel.tsx +311 -0
- package/components/copilot/FindingsList.tsx +75 -0
- package/components/copilot/ViewerPanel.tsx +460 -0
- package/components/copilot/WorkspaceLayout.tsx +398 -0
- package/components/dashboard/AIConfigPanel.tsx +339 -0
- package/components/dashboard/AppearancePanel.tsx +491 -0
- package/components/dashboard/ApprovalModal.tsx +163 -0
- package/components/dashboard/CollaborationPanel.tsx +134 -0
- package/components/dashboard/CopilotConfigPanel.tsx +337 -0
- package/components/dashboard/DicomViewer.tsx +645 -0
- package/components/dashboard/FhirIntegrationPanel.tsx +331 -0
- package/components/dashboard/FullReportOverlay.tsx +269 -0
- package/components/dashboard/ImageViewer.tsx +541 -0
- package/components/dashboard/PatientForm.tsx +597 -0
- package/components/dashboard/RejectionModal.tsx +74 -0
- package/components/dashboard/ReportEditor.tsx +160 -0
- package/components/dashboard/ReportTemplates.tsx +729 -0
- package/components/dashboard/ReportView.tsx +539 -0
- package/components/dashboard/SegmentationConfigPanel.tsx +490 -0
- package/components/dashboard/StudyPlaceholder.tsx +17 -0
- package/components/dashboard/SupabaseIntegrationPanel.tsx +345 -0
- package/components/dashboard/UserManagementPanel.tsx +272 -0
- package/components/layout/ClientLayout.tsx +39 -0
- package/components/layout/Header.tsx +20 -0
- package/components/layout/Sidebar.tsx +119 -0
- package/components/pacs/PacsImageViewerModal.tsx +121 -0
- package/components/pacs/PacsSearchFilters.tsx +117 -0
- package/components/pacs/PacsSeriesViewer.tsx +190 -0
- package/components/pacs/PacsStudyTable.tsx +113 -0
- package/components/patients/patient-card.tsx +117 -0
- package/components/patients/patient-header.tsx +122 -0
- package/components/patients/patient-search.tsx +137 -0
- package/components/patients/patient-timeline.tsx +153 -0
- package/components/settings/ComplianceSettingsPanel.tsx +278 -0
- package/components/settings/SecurityPanel.tsx +418 -0
- package/components/ui/badge.tsx +19 -0
- package/components/ui/basic.tsx +156 -0
- package/db/index.ts +350 -0
- package/db/migrations/0000_odd_quasimodo.sql +117 -0
- package/db/migrations/meta/0000_snapshot.json +778 -0
- package/db/migrations/meta/_journal.json +13 -0
- package/db/schema.ts +239 -0
- package/drizzle.config.ts +10 -0
- package/lib/api.ts +689 -0
- package/lib/auth.ts +22 -0
- package/lib/copilot/action-executor.ts +94 -0
- package/lib/copilot/action-types.ts +72 -0
- package/lib/copilot/coordinate-mapper.ts +84 -0
- package/lib/dicomImageExtractor.ts +103 -0
- package/lib/dicomMetadataParser.ts +111 -0
- package/lib/fhir/client.ts +25 -0
- package/lib/fhir/constants.ts +21 -0
- package/lib/fhir/diagnostic-report.ts +88 -0
- package/lib/fhir/helpers.ts +73 -0
- package/lib/fhir/imaging-study.ts +49 -0
- package/lib/fhir/patient.ts +55 -0
- package/lib/fhir/service-request.ts +85 -0
- package/lib/fhir.ts +6 -0
- package/lib/pacs/dicom-utils.ts +72 -0
- package/lib/pacs/dicomweb.ts +72 -0
- package/lib/pacs/server-utils.ts +37 -0
- package/lib/patients.ts +25 -0
- package/lib/pdfHelper.ts +119 -0
- package/lib/reportHtmlGenerator.ts +581 -0
- package/lib/security/audit.ts +180 -0
- package/lib/security/authz.ts +246 -0
- package/lib/security/phi-redaction.ts +156 -0
- package/lib/security/rate-limit.ts +106 -0
- package/lib/security/secrets.ts +179 -0
- package/lib/supabase.ts +72 -0
- package/lib/utils.ts +6 -0
- package/next.config.ts +35 -0
- package/package.json +76 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +8 -0
- package/public/next.svg +1 -0
- package/public/omnirad-favicon.svg +8 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
- package/types/copilot-viewer.ts +155 -0
- package/types/copilot.ts +105 -0
- package/types/fhir.ts +21 -0
- package/types/html2pdf.d.ts +20 -0
- package/types/index.ts +139 -0
- 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
|
+
}
|