@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,311 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
|
+
import { ChatMessage as ChatMessageType, Reference, CopilotPatientContext, QuickAction, ActivityState } from "@/types/copilot";
|
|
5
|
+
import ChatMessage from "./ChatMessage";
|
|
6
|
+
import ChatHistoryPanel from "./ChatHistoryPanel";
|
|
7
|
+
import ActivityIndicator from "./ActivityIndicator";
|
|
8
|
+
import { Bot, Send, Plus, Trash2, Sparkles, FileText, GitCompare, Clock, Stethoscope } from "lucide-react";
|
|
9
|
+
|
|
10
|
+
interface CopilotPanelProps {
|
|
11
|
+
messages: ChatMessageType[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
activityState: ActivityState;
|
|
14
|
+
onSendMessage: (message: string) => void;
|
|
15
|
+
onReferenceClick: (ref: Reference) => void;
|
|
16
|
+
onExecuteActions?: (actions: any[]) => void;
|
|
17
|
+
onNewChat: () => void;
|
|
18
|
+
onLoadSession: (sessionId: string) => void;
|
|
19
|
+
patientContext: CopilotPatientContext;
|
|
20
|
+
findingsCount?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const QUICK_ACTIONS: QuickAction[] = [
|
|
24
|
+
{ label: "Summarize history", prompt: "Summarize this patient's report history", icon: "📋" },
|
|
25
|
+
{ label: "Compare with previous", prompt: "Compare the current report with the previous one", icon: "🔄" },
|
|
26
|
+
{ label: "Highlight findings", prompt: "Highlight the findings from the report on the image", icon: "🎯" },
|
|
27
|
+
{ label: "Where is the lesion?", prompt: "Where is the lesion in this image?", icon: "🔍" },
|
|
28
|
+
{ label: "Point to abnormality", prompt: "Point to the abnormal area", icon: "👆" },
|
|
29
|
+
{ label: "Show suspicious region", prompt: "Highlight the suspicious region", icon: "⚠️" },
|
|
30
|
+
{ label: "Overlay segmentation", prompt: "Make an overlay segmentation for it.", icon: "🎨" },
|
|
31
|
+
{ label: "Clear AI findings", prompt: "Clear all AI findings", icon: "🧹" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export default function CopilotPanel({
|
|
35
|
+
messages,
|
|
36
|
+
isLoading,
|
|
37
|
+
activityState,
|
|
38
|
+
onSendMessage,
|
|
39
|
+
onReferenceClick,
|
|
40
|
+
onExecuteActions,
|
|
41
|
+
onNewChat,
|
|
42
|
+
onLoadSession,
|
|
43
|
+
patientContext,
|
|
44
|
+
findingsCount = 0,
|
|
45
|
+
}: CopilotPanelProps) {
|
|
46
|
+
const [inputValue, setInputValue] = useState("");
|
|
47
|
+
const [showHistory, setShowHistory] = useState(false);
|
|
48
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
49
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
51
|
+
|
|
52
|
+
// Prevent hydration mismatch
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setIsMounted(true);
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// Auto-scroll to bottom when new messages arrive
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
60
|
+
}, [messages, isLoading]);
|
|
61
|
+
|
|
62
|
+
// Auto-focus input
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
inputRef.current?.focus();
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
const handleSend = useCallback(() => {
|
|
68
|
+
if (!inputValue.trim() || isLoading) return;
|
|
69
|
+
onSendMessage(inputValue.trim());
|
|
70
|
+
setInputValue("");
|
|
71
|
+
// Reset textarea height
|
|
72
|
+
if (inputRef.current) {
|
|
73
|
+
inputRef.current.style.height = "auto";
|
|
74
|
+
}
|
|
75
|
+
}, [inputValue, isLoading, onSendMessage]);
|
|
76
|
+
|
|
77
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
78
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
handleSend();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Auto-resize textarea
|
|
85
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
86
|
+
setInputValue(e.target.value);
|
|
87
|
+
e.target.style.height = "auto";
|
|
88
|
+
e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className="flex flex-col h-full relative overflow-hidden">
|
|
93
|
+
{/* Header */}
|
|
94
|
+
<div className="shrink-0 px-5 py-4 border-b border-border-primary bg-bg-surface">
|
|
95
|
+
<div className="flex items-center justify-between">
|
|
96
|
+
<div className="flex items-center gap-3">
|
|
97
|
+
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center shadow-lg shadow-emerald-500/20">
|
|
98
|
+
<Bot size={18} className="text-white" />
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<h3 className="text-sm font-bold text-text-heading">AI Copilot</h3>
|
|
103
|
+
{findingsCount > 0 && (
|
|
104
|
+
<span className="flex items-center gap-1 text-[10px] font-bold text-red-400 bg-red-500/15 px-2 py-0.5 rounded-full">
|
|
105
|
+
<span className="w-1.5 h-1.5 rounded-full bg-red-400 animate-pulse" />
|
|
106
|
+
{findingsCount}
|
|
107
|
+
</span>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
<p className="text-[11px] text-text-muted">
|
|
111
|
+
{patientContext.patientName
|
|
112
|
+
? `Patient: ${patientContext.patientName}`
|
|
113
|
+
: "Ready to assist"}
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-center gap-1.5">
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => setShowHistory(true)}
|
|
120
|
+
className="p-2 rounded-lg text-text-muted hover:text-primary hover:bg-bg-panel transition-all"
|
|
121
|
+
title="Chat History"
|
|
122
|
+
>
|
|
123
|
+
<Clock size={16} />
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
onClick={onNewChat}
|
|
127
|
+
className="p-2 rounded-lg text-text-muted hover:text-primary hover:bg-bg-panel transition-all"
|
|
128
|
+
title="New chat"
|
|
129
|
+
>
|
|
130
|
+
<Plus size={16} />
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* History Panel Overlay */}
|
|
137
|
+
{showHistory && (
|
|
138
|
+
<ChatHistoryPanel
|
|
139
|
+
onClose={() => setShowHistory(false)}
|
|
140
|
+
onSelectSession={(id) => {
|
|
141
|
+
onLoadSession(id);
|
|
142
|
+
setShowHistory(false);
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Messages Area */}
|
|
148
|
+
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
|
|
149
|
+
{!isMounted ? (
|
|
150
|
+
<div className="flex-1 opacity-0" />
|
|
151
|
+
) : messages.length === 0 ? (
|
|
152
|
+
<WelcomeMessage
|
|
153
|
+
onQuickAction={onSendMessage}
|
|
154
|
+
patientContext={patientContext}
|
|
155
|
+
/>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
{messages.map((msg) => (
|
|
159
|
+
<ChatMessage
|
|
160
|
+
key={msg.id}
|
|
161
|
+
message={msg}
|
|
162
|
+
onReferenceClick={onReferenceClick}
|
|
163
|
+
onExecuteActions={onExecuteActions}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
|
|
167
|
+
{/* Streaming Activity Indicator */}
|
|
168
|
+
{(isLoading || activityState.isActive) && (
|
|
169
|
+
<ActivityIndicator activityState={activityState} />
|
|
170
|
+
)}
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
<div ref={messagesEndRef} />
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Quick Actions (shown when there's context) */}
|
|
177
|
+
{messages.length > 0 && patientContext.patientId && !isLoading && (
|
|
178
|
+
<div className="shrink-0 px-4 pb-2 flex gap-1.5 overflow-x-auto">
|
|
179
|
+
{QUICK_ACTIONS.map((action) => (
|
|
180
|
+
<button
|
|
181
|
+
key={action.label}
|
|
182
|
+
onClick={() => onSendMessage(action.prompt)}
|
|
183
|
+
className="shrink-0 text-xs px-3 py-1.5 rounded-full border border-border-card
|
|
184
|
+
text-text-secondary hover:text-primary hover:border-primary/30 hover:bg-primary/5
|
|
185
|
+
transition-all duration-150 whitespace-nowrap"
|
|
186
|
+
>
|
|
187
|
+
{action.icon} {action.label}
|
|
188
|
+
</button>
|
|
189
|
+
))}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{/* Input Area */}
|
|
194
|
+
<div className="shrink-0 px-4 py-3 border-t border-border-primary bg-bg-surface">
|
|
195
|
+
<div className="flex items-end gap-2 bg-bg-panel rounded-xl border border-border-card p-2 focus-within:border-primary/50 focus-within:ring-1 focus-within:ring-primary/20 transition-all">
|
|
196
|
+
<textarea
|
|
197
|
+
ref={inputRef}
|
|
198
|
+
value={inputValue}
|
|
199
|
+
onChange={handleInputChange}
|
|
200
|
+
onKeyDown={handleKeyDown}
|
|
201
|
+
placeholder="Ask about a patient, report, or scan..."
|
|
202
|
+
rows={1}
|
|
203
|
+
className="flex-1 bg-transparent text-sm text-text-primary placeholder-text-muted resize-none outline-none px-2 py-1.5 max-h-[120px]"
|
|
204
|
+
disabled={isLoading}
|
|
205
|
+
/>
|
|
206
|
+
<button
|
|
207
|
+
onClick={handleSend}
|
|
208
|
+
disabled={!inputValue.trim() || isLoading}
|
|
209
|
+
className={`
|
|
210
|
+
shrink-0 p-2 rounded-lg transition-all duration-200
|
|
211
|
+
${inputValue.trim() && !isLoading
|
|
212
|
+
? "bg-primary text-white hover:bg-primary-hover shadow-md shadow-primary/25"
|
|
213
|
+
: "bg-border-card text-text-muted cursor-not-allowed"
|
|
214
|
+
}
|
|
215
|
+
`}
|
|
216
|
+
>
|
|
217
|
+
<Send size={16} />
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
<p className="text-[10px] text-text-muted mt-1.5 text-center opacity-60">
|
|
221
|
+
Press Enter to send • Shift+Enter for new line
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Welcome Message ─────────────────────────────────────────────────────────
|
|
229
|
+
function WelcomeMessage({
|
|
230
|
+
onQuickAction,
|
|
231
|
+
patientContext,
|
|
232
|
+
}: {
|
|
233
|
+
onQuickAction: (prompt: string) => void;
|
|
234
|
+
patientContext: CopilotPatientContext;
|
|
235
|
+
}) {
|
|
236
|
+
return (
|
|
237
|
+
<div className="flex flex-col items-center justify-center h-full text-center px-6 py-8">
|
|
238
|
+
{/* Logo */}
|
|
239
|
+
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center mb-6 shadow-2xl shadow-emerald-500/20">
|
|
240
|
+
<Sparkles size={28} className="text-white" />
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<h3 className="text-xl font-bold text-text-heading mb-2">
|
|
244
|
+
OmniRad AI Copilot
|
|
245
|
+
</h3>
|
|
246
|
+
<p className="text-sm text-text-muted mb-8 max-w-sm">
|
|
247
|
+
I can help you navigate patient reports, compare studies, summarize findings, and more.
|
|
248
|
+
</p>
|
|
249
|
+
|
|
250
|
+
{/* Suggestion Cards */}
|
|
251
|
+
<div className="grid grid-cols-2 gap-3 w-full max-w-sm">
|
|
252
|
+
<SuggestionCard
|
|
253
|
+
icon={<FileText size={18} />}
|
|
254
|
+
title="View Report"
|
|
255
|
+
description="Show me the report for patient..."
|
|
256
|
+
onClick={() => onQuickAction("Show me the latest report")}
|
|
257
|
+
/>
|
|
258
|
+
<SuggestionCard
|
|
259
|
+
icon={<GitCompare size={18} />}
|
|
260
|
+
title="Compare Studies"
|
|
261
|
+
description="Compare current with previous"
|
|
262
|
+
onClick={() => onQuickAction("Compare current report with previous")}
|
|
263
|
+
/>
|
|
264
|
+
<SuggestionCard
|
|
265
|
+
icon={<Clock size={18} />}
|
|
266
|
+
title="Patient History"
|
|
267
|
+
description="Summarize patient timeline"
|
|
268
|
+
onClick={() => onQuickAction("Summarize this patient's history")}
|
|
269
|
+
/>
|
|
270
|
+
<SuggestionCard
|
|
271
|
+
icon={<Stethoscope size={18} />}
|
|
272
|
+
title="Clinical Query"
|
|
273
|
+
description="What changed from the last study?"
|
|
274
|
+
onClick={() => onQuickAction("What changed from the previous report?")}
|
|
275
|
+
/>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function SuggestionCard({
|
|
282
|
+
icon,
|
|
283
|
+
title,
|
|
284
|
+
description,
|
|
285
|
+
onClick,
|
|
286
|
+
}: {
|
|
287
|
+
icon: React.ReactNode;
|
|
288
|
+
title: string;
|
|
289
|
+
description: string;
|
|
290
|
+
onClick: () => void;
|
|
291
|
+
}) {
|
|
292
|
+
return (
|
|
293
|
+
<button
|
|
294
|
+
onClick={onClick}
|
|
295
|
+
className="
|
|
296
|
+
group flex flex-col items-start gap-2 p-4 rounded-xl
|
|
297
|
+
bg-bg-panel border border-border-card
|
|
298
|
+
hover:border-primary/30 hover:bg-primary/5
|
|
299
|
+
transition-all duration-200 text-left
|
|
300
|
+
"
|
|
301
|
+
>
|
|
302
|
+
<span className="text-primary group-hover:scale-110 transition-transform duration-200">
|
|
303
|
+
{icon}
|
|
304
|
+
</span>
|
|
305
|
+
<div>
|
|
306
|
+
<p className="text-sm font-semibold text-text-heading">{title}</p>
|
|
307
|
+
<p className="text-xs text-text-muted mt-0.5">{description}</p>
|
|
308
|
+
</div>
|
|
309
|
+
</button>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FindingSummary, CopilotViewerRef } from "@/types/copilot-viewer";
|
|
4
|
+
import { Target, Trash2, Crosshair, X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface FindingsListProps {
|
|
7
|
+
findings: FindingSummary[];
|
|
8
|
+
viewerRef: CopilotViewerRef | null;
|
|
9
|
+
onClearAll: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function FindingsList({ findings, viewerRef, onClearAll }: FindingsListProps) {
|
|
13
|
+
if (findings.length === 0) return null;
|
|
14
|
+
|
|
15
|
+
const handleFocusFinding = (finding: FindingSummary) => {
|
|
16
|
+
if (!viewerRef) return;
|
|
17
|
+
if (finding.slice !== undefined) {
|
|
18
|
+
viewerRef.jumpToSlice(finding.slice);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="border-t border-border-primary bg-bg-surface shrink-0">
|
|
24
|
+
{/* Header */}
|
|
25
|
+
<div className="px-4 py-2 flex items-center justify-between">
|
|
26
|
+
<div className="flex items-center gap-2 text-xs font-semibold text-text-muted uppercase tracking-wider">
|
|
27
|
+
<Target size={14} className="text-red-400" />
|
|
28
|
+
AI Findings
|
|
29
|
+
<span className="bg-red-500/20 text-red-400 px-1.5 py-0.5 rounded-full text-[10px] font-bold">
|
|
30
|
+
{findings.length}
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<button
|
|
34
|
+
onClick={onClearAll}
|
|
35
|
+
className="flex items-center gap-1 text-[10px] font-medium text-text-muted hover:text-red-400 transition-colors px-2 py-1 rounded-md hover:bg-red-500/10"
|
|
36
|
+
title="Clear all AI findings"
|
|
37
|
+
>
|
|
38
|
+
<Trash2 size={11} />
|
|
39
|
+
CLEAR
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Findings List */}
|
|
44
|
+
<div className="px-2 pb-2 space-y-0.5 max-h-[140px] overflow-auto">
|
|
45
|
+
{findings.map((finding, idx) => (
|
|
46
|
+
<button
|
|
47
|
+
key={finding.annotation_id || idx}
|
|
48
|
+
onClick={() => handleFocusFinding(finding)}
|
|
49
|
+
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-left
|
|
50
|
+
text-text-primary hover:bg-red-500/5 hover:border-red-500/20
|
|
51
|
+
transition-all duration-150 group border border-transparent"
|
|
52
|
+
>
|
|
53
|
+
<Crosshair size={14} className="shrink-0 text-red-400 group-hover:scale-110 transition-transform" />
|
|
54
|
+
<div className="flex-1 min-w-0">
|
|
55
|
+
<div className="truncate font-medium text-text-heading text-[13px]">
|
|
56
|
+
{finding.name}
|
|
57
|
+
</div>
|
|
58
|
+
{finding.confidence !== undefined && finding.confidence > 0 && (
|
|
59
|
+
<div className="text-[11px] text-text-muted">
|
|
60
|
+
{Math.round(finding.confidence * 100)}% confidence
|
|
61
|
+
{finding.slice !== undefined && ` · Slice ${finding.slice + 1}`}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex items-center gap-1">
|
|
66
|
+
<span className="text-[9px] font-bold text-red-400 bg-red-500/10 px-1.5 py-0.5 rounded">
|
|
67
|
+
AI
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</button>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|