@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,490 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle, Input, Label, Button } from "@/components/ui/basic"
|
|
5
|
+
import { Save, Cpu, CheckCircle, AlertCircle, RefreshCw, Eye, EyeOff, ChevronDown, ChevronUp } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
export function SegmentationConfigPanel() {
|
|
8
|
+
const [loading, setLoading] = React.useState(true)
|
|
9
|
+
const [saveStatus, setSaveStatus] = React.useState<"idle" | "saving" | "saved" | "error">("idle")
|
|
10
|
+
const [errorMsg, setErrorMsg] = React.useState("")
|
|
11
|
+
const [isLocked, setIsLocked] = React.useState(false)
|
|
12
|
+
|
|
13
|
+
// Connection state
|
|
14
|
+
const [connStatus, setConnStatus] = React.useState<"idle" | "checking" | "verified" | "invalid">("idle")
|
|
15
|
+
const [connError, setConnError] = React.useState("")
|
|
16
|
+
const [showApiKey, setShowApiKey] = React.useState(false)
|
|
17
|
+
const [showDropdown, setShowDropdown] = React.useState(false)
|
|
18
|
+
const [availableModels, setAvailableModels] = React.useState<string[]>([])
|
|
19
|
+
|
|
20
|
+
const [formData, setFormData] = React.useState({
|
|
21
|
+
deploymentMode: "localhost" as "localhost" | "custom_api",
|
|
22
|
+
modelType: "medsam3" as "medsam2" | "medsam3",
|
|
23
|
+
providerName: "MedSAM3",
|
|
24
|
+
modelName: "",
|
|
25
|
+
baseUrl: "http://localhost:5000",
|
|
26
|
+
healthEndpoint: "/healthz",
|
|
27
|
+
predictEndpoint: "/v1/segmentations",
|
|
28
|
+
apiSecretKey: "",
|
|
29
|
+
timeoutSeconds: 120,
|
|
30
|
+
supportsContours: true,
|
|
31
|
+
supports3D: false,
|
|
32
|
+
returnsMask: true,
|
|
33
|
+
returnsBox: true,
|
|
34
|
+
isActive: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
fetch("/api/segmentation-config")
|
|
39
|
+
.then(res => res.json())
|
|
40
|
+
.then(data => {
|
|
41
|
+
if (!data.error) {
|
|
42
|
+
setFormData({
|
|
43
|
+
deploymentMode: data.deploymentMode || "localhost",
|
|
44
|
+
modelType: data.modelType || "medsam3",
|
|
45
|
+
providerName: data.providerName || "MedSAM3",
|
|
46
|
+
modelName: data.modelName || "medsam3",
|
|
47
|
+
baseUrl: data.baseUrl || "http://localhost:5000",
|
|
48
|
+
healthEndpoint: data.healthEndpoint || "/health",
|
|
49
|
+
predictEndpoint: data.predictEndpoint || "/predict",
|
|
50
|
+
apiSecretKey: data.apiSecretKey || "",
|
|
51
|
+
timeoutSeconds: data.timeoutSeconds || 120,
|
|
52
|
+
supportsContours: data.supportsContours ?? true,
|
|
53
|
+
supports3D: data.supports3D ?? false,
|
|
54
|
+
returnsMask: data.returnsMask ?? true,
|
|
55
|
+
returnsBox: data.returnsBox ?? true,
|
|
56
|
+
isActive: data.isActive ?? false,
|
|
57
|
+
})
|
|
58
|
+
if (data.isActive) {
|
|
59
|
+
setIsLocked(true)
|
|
60
|
+
setConnStatus("verified")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
.catch(console.error)
|
|
65
|
+
.finally(() => setLoading(false))
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
|
69
|
+
const { id, value, type } = e.target as HTMLInputElement
|
|
70
|
+
const fieldName = id.replace("seg_", "")
|
|
71
|
+
|
|
72
|
+
if (type === "checkbox") {
|
|
73
|
+
setFormData(prev => ({ ...prev, [fieldName]: (e.target as HTMLInputElement).checked }))
|
|
74
|
+
} else if (type === "number") {
|
|
75
|
+
setFormData(prev => ({ ...prev, [fieldName]: parseInt(value) || 0 }))
|
|
76
|
+
} else {
|
|
77
|
+
setFormData(prev => ({ ...prev, [fieldName]: value }))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (fieldName === "modelType") {
|
|
81
|
+
setConnStatus("idle")
|
|
82
|
+
if (value === "medsam2") {
|
|
83
|
+
setFormData(prev => ({
|
|
84
|
+
...prev,
|
|
85
|
+
modelType: "medsam2" as const,
|
|
86
|
+
providerName: "MedSAM2",
|
|
87
|
+
modelName: "",
|
|
88
|
+
}))
|
|
89
|
+
} else {
|
|
90
|
+
setFormData(prev => ({
|
|
91
|
+
...prev,
|
|
92
|
+
modelType: "medsam3" as const,
|
|
93
|
+
providerName: "MedSAM3",
|
|
94
|
+
modelName: "",
|
|
95
|
+
}))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (fieldName === "deploymentMode") {
|
|
100
|
+
setConnStatus("idle")
|
|
101
|
+
if (value === "localhost") {
|
|
102
|
+
setFormData(prev => ({
|
|
103
|
+
...prev,
|
|
104
|
+
deploymentMode: "localhost" as const,
|
|
105
|
+
baseUrl: "http://localhost:5000",
|
|
106
|
+
apiSecretKey: "",
|
|
107
|
+
}))
|
|
108
|
+
} else {
|
|
109
|
+
setFormData(prev => ({
|
|
110
|
+
...prev,
|
|
111
|
+
deploymentMode: "custom_api" as const,
|
|
112
|
+
baseUrl: "",
|
|
113
|
+
}))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setSaveStatus("idle")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const testConnection = async () => {
|
|
121
|
+
setConnStatus("checking")
|
|
122
|
+
setConnError("")
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch("/api/segmentation-config", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
baseUrl: formData.baseUrl,
|
|
129
|
+
healthEndpoint: formData.healthEndpoint,
|
|
130
|
+
apiSecretKey: formData.apiSecretKey,
|
|
131
|
+
timeoutSeconds: 15,
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
const data = await res.json()
|
|
135
|
+
if (data.success) {
|
|
136
|
+
setConnStatus("verified")
|
|
137
|
+
|
|
138
|
+
// Read available models fetched securely via Next.js backend
|
|
139
|
+
if (data.availableModels && data.availableModels.length > 0) {
|
|
140
|
+
setAvailableModels(data.availableModels)
|
|
141
|
+
|
|
142
|
+
// If current modelName isn't in fetched list, auto-select the first one
|
|
143
|
+
if (!data.availableModels.includes(formData.modelName)) {
|
|
144
|
+
setFormData(prev => ({ ...prev, modelName: data.availableModels[0] }))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
setConnStatus("invalid")
|
|
149
|
+
setConnError(data.error || "Connection failed.")
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
setConnStatus("invalid")
|
|
153
|
+
setConnError("Could not reach the segmentation backend.")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleSave = async () => {
|
|
158
|
+
if (!formData.baseUrl) { setErrorMsg("Base URL is required."); setSaveStatus("error"); return }
|
|
159
|
+
if (!formData.predictEndpoint) { setErrorMsg("Predict Endpoint is required."); setSaveStatus("error"); return }
|
|
160
|
+
|
|
161
|
+
setSaveStatus("saving")
|
|
162
|
+
setErrorMsg("")
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch("/api/segmentation-config", {
|
|
165
|
+
method: "PUT",
|
|
166
|
+
headers: { "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify({ ...formData, isActive: true }),
|
|
168
|
+
})
|
|
169
|
+
const result = await res.json()
|
|
170
|
+
if (res.ok && result.success) {
|
|
171
|
+
setSaveStatus("saved")
|
|
172
|
+
setIsLocked(true)
|
|
173
|
+
setFormData(prev => ({ ...prev, isActive: true }))
|
|
174
|
+
setTimeout(() => setSaveStatus("idle"), 3000)
|
|
175
|
+
} else {
|
|
176
|
+
setErrorMsg(result.error || "Failed to save")
|
|
177
|
+
setSaveStatus("error")
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
setErrorMsg(String(e))
|
|
181
|
+
setSaveStatus("error")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (loading) return null
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<Card className="bg-bg-surface border-border-primary border-t-4 border-t-violet-500">
|
|
189
|
+
<CardHeader>
|
|
190
|
+
<CardTitle className="text-text-heading flex items-center gap-2">
|
|
191
|
+
<Cpu size={20} className="text-violet-500" />
|
|
192
|
+
Segmentation Model Integration
|
|
193
|
+
</CardTitle>
|
|
194
|
+
<p className="text-sm text-text-secondary">
|
|
195
|
+
Configure a MedSAM2 or MedSAM3 segmentation backend for AI-powered annotations in the Copilot workspace.
|
|
196
|
+
</p>
|
|
197
|
+
</CardHeader>
|
|
198
|
+
<CardContent className="space-y-5">
|
|
199
|
+
{/* Model Type Selector */}
|
|
200
|
+
<div>
|
|
201
|
+
<Label htmlFor="seg_modelType" className="text-text-primary">Model Type</Label>
|
|
202
|
+
<select
|
|
203
|
+
id="seg_modelType"
|
|
204
|
+
value={formData.modelType}
|
|
205
|
+
onChange={handleChange}
|
|
206
|
+
disabled={isLocked}
|
|
207
|
+
className="flex h-10 w-full rounded-md border border-border-primary bg-bg-panel disabled:opacity-50 px-3 py-2 text-sm text-text-primary mt-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500"
|
|
208
|
+
>
|
|
209
|
+
<option value="medsam3">MedSAM3 — Text-Guided Segmentation</option>
|
|
210
|
+
<option value="medsam2">MedSAM2 — Box/Point-Guided Segmentation</option>
|
|
211
|
+
</select>
|
|
212
|
+
{formData.modelType === "medsam2" ? (
|
|
213
|
+
<p className="mt-1.5 text-xs text-amber-400/80 bg-amber-500/10 border border-amber-500/20 rounded-md px-2.5 py-1.5 flex items-center gap-1.5">
|
|
214
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 3v18"/></svg>
|
|
215
|
+
Spatial prompts — uses bounding box or point coordinates. Best for precise, localization-guided segmentation.
|
|
216
|
+
</p>
|
|
217
|
+
) : (
|
|
218
|
+
<p className="mt-1.5 text-xs text-sky-400/80 bg-sky-500/10 border border-sky-500/20 rounded-md px-2.5 py-1.5 flex items-center gap-1.5">
|
|
219
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
220
|
+
Text prompts — uses natural language like "brain lesion". Best for concept-driven segmentation.
|
|
221
|
+
</p>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
{/* Deployment Mode */}
|
|
225
|
+
<div>
|
|
226
|
+
<Label htmlFor="seg_deploymentMode" className="text-text-primary">Deployment Mode</Label>
|
|
227
|
+
<select
|
|
228
|
+
id="seg_deploymentMode"
|
|
229
|
+
value={formData.deploymentMode}
|
|
230
|
+
onChange={handleChange}
|
|
231
|
+
disabled={isLocked}
|
|
232
|
+
className="flex h-10 w-full rounded-md border border-border-primary bg-bg-panel disabled:opacity-50 px-3 py-2 text-sm text-text-primary mt-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500"
|
|
233
|
+
>
|
|
234
|
+
<option value="localhost">Localhost</option>
|
|
235
|
+
<option value="custom_api">Custom API (Remote)</option>
|
|
236
|
+
</select>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{/* Base URL */}
|
|
240
|
+
<div>
|
|
241
|
+
<Label htmlFor="seg_baseUrl" className="text-text-primary">Base URL *</Label>
|
|
242
|
+
<Input
|
|
243
|
+
id="seg_baseUrl"
|
|
244
|
+
value={formData.baseUrl}
|
|
245
|
+
onChange={handleChange}
|
|
246
|
+
disabled={isLocked}
|
|
247
|
+
placeholder={formData.deploymentMode === "localhost" ? "http://localhost:5000" : `https://your-${formData.modelType}-api.com`}
|
|
248
|
+
className="mt-1 bg-bg-panel border-border-primary text-text-primary disabled:opacity-50"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* API Key (custom_api only) */}
|
|
253
|
+
{formData.deploymentMode === "custom_api" && (
|
|
254
|
+
<div>
|
|
255
|
+
<Label htmlFor="seg_apiSecretKey" className="text-text-primary">API Secret Key</Label>
|
|
256
|
+
<div className="relative mt-1">
|
|
257
|
+
<Input
|
|
258
|
+
id="seg_apiSecretKey"
|
|
259
|
+
type={showApiKey ? "text" : "password"}
|
|
260
|
+
value={formData.apiSecretKey}
|
|
261
|
+
onChange={handleChange}
|
|
262
|
+
disabled={isLocked}
|
|
263
|
+
placeholder="Enter API key (if required)"
|
|
264
|
+
className="bg-bg-panel border-border-primary text-text-primary disabled:opacity-50 pr-10"
|
|
265
|
+
/>
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
269
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary"
|
|
270
|
+
>
|
|
271
|
+
{showApiKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
|
272
|
+
</button>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{/* Advanced Connection Settings */}
|
|
278
|
+
<details className="group">
|
|
279
|
+
<summary className="cursor-pointer text-sm font-medium text-text-secondary hover:text-text-primary flex items-center gap-2">
|
|
280
|
+
<span className="group-open:rotate-90 transition-transform">▸</span>
|
|
281
|
+
Advanced Connection Settings
|
|
282
|
+
</summary>
|
|
283
|
+
<div className="mt-4 pl-4 border-l-2 border-violet-500/20 grid grid-cols-3 gap-4">
|
|
284
|
+
<div>
|
|
285
|
+
<Label htmlFor="seg_healthEndpoint" className="text-text-primary">Health Endpoint</Label>
|
|
286
|
+
<Input
|
|
287
|
+
id="seg_healthEndpoint"
|
|
288
|
+
value={formData.healthEndpoint}
|
|
289
|
+
onChange={handleChange}
|
|
290
|
+
disabled={isLocked}
|
|
291
|
+
placeholder="/health"
|
|
292
|
+
className="mt-1 bg-bg-panel border-border-primary text-text-primary disabled:opacity-50"
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
<div>
|
|
296
|
+
<Label htmlFor="seg_predictEndpoint" className="text-text-primary">Predict Endpoint</Label>
|
|
297
|
+
<Input
|
|
298
|
+
id="seg_predictEndpoint"
|
|
299
|
+
value={formData.predictEndpoint}
|
|
300
|
+
onChange={handleChange}
|
|
301
|
+
disabled={isLocked}
|
|
302
|
+
placeholder="/predict"
|
|
303
|
+
className="mt-1 bg-bg-panel border-border-primary text-text-primary disabled:opacity-50"
|
|
304
|
+
/>
|
|
305
|
+
</div>
|
|
306
|
+
<div>
|
|
307
|
+
<Label htmlFor="seg_timeoutSeconds" className="text-text-primary">Timeout (sec)</Label>
|
|
308
|
+
<Input
|
|
309
|
+
id="seg_timeoutSeconds"
|
|
310
|
+
type="number"
|
|
311
|
+
value={formData.timeoutSeconds.toString()}
|
|
312
|
+
onChange={handleChange}
|
|
313
|
+
disabled={isLocked}
|
|
314
|
+
className="mt-1 bg-bg-panel border-border-primary text-text-primary disabled:opacity-50 w-full"
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
</details>
|
|
319
|
+
|
|
320
|
+
{/* Test Connection Button */}
|
|
321
|
+
<Button
|
|
322
|
+
variant="outline"
|
|
323
|
+
onClick={testConnection}
|
|
324
|
+
disabled={connStatus === "checking" || isLocked || !formData.baseUrl}
|
|
325
|
+
className="w-full border-violet-500 text-violet-500 hover:bg-violet-50 gap-2 disabled:opacity-50"
|
|
326
|
+
>
|
|
327
|
+
<RefreshCw size={16} className={connStatus === "checking" ? "animate-spin" : ""} />
|
|
328
|
+
{connStatus === "checking" ? "Testing Connection..." : "Test Connection"}
|
|
329
|
+
</Button>
|
|
330
|
+
|
|
331
|
+
{connStatus === "invalid" && (
|
|
332
|
+
<div className="text-xs text-red-500 bg-red-500/10 p-2 rounded-md border border-red-500/20">
|
|
333
|
+
<strong>Connection Failed:</strong> {connError}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{connStatus === "verified" && (
|
|
337
|
+
<div className="text-xs text-green-600 dark:text-green-400 bg-green-500/10 p-2 rounded-md border border-green-500/20 flex items-center gap-1.5">
|
|
338
|
+
<CheckCircle size={14} /> <strong>Connection Verified:</strong> Segmentation backend is reachable.
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
<div className="pt-2 border-t border-border-primary"></div>
|
|
343
|
+
|
|
344
|
+
{/* Model Name (Provider silently tracked) */}
|
|
345
|
+
<div className="relative">
|
|
346
|
+
<Label htmlFor="seg_modelName" className="text-text-primary">Model Name</Label>
|
|
347
|
+
<div className="relative mt-1">
|
|
348
|
+
<Input
|
|
349
|
+
id="seg_modelName"
|
|
350
|
+
value={connStatus === "verified" ? formData.modelName : ""}
|
|
351
|
+
onChange={(e) => {
|
|
352
|
+
handleChange(e)
|
|
353
|
+
setShowDropdown(true)
|
|
354
|
+
}}
|
|
355
|
+
onFocus={() => setShowDropdown(true)}
|
|
356
|
+
onBlur={() => setTimeout(() => setShowDropdown(false), 150)}
|
|
357
|
+
disabled={connStatus !== "verified" || isLocked}
|
|
358
|
+
placeholder="Models"
|
|
359
|
+
className="bg-bg-panel border-border-primary text-text-primary pr-10 disabled:opacity-50 w-full"
|
|
360
|
+
autoComplete="off"
|
|
361
|
+
/>
|
|
362
|
+
{connStatus === "verified" && !isLocked && (
|
|
363
|
+
<button
|
|
364
|
+
type="button"
|
|
365
|
+
onMouseDown={(e) => {
|
|
366
|
+
e.preventDefault()
|
|
367
|
+
setShowDropdown(!showDropdown)
|
|
368
|
+
}}
|
|
369
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary"
|
|
370
|
+
>
|
|
371
|
+
{showDropdown ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
372
|
+
</button>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
{/* Dropdown Menu */}
|
|
376
|
+
{showDropdown && connStatus === "verified" && availableModels.length > 0 && (
|
|
377
|
+
<ul className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border border-border-primary bg-bg-panel py-1 text-sm shadow-xl hide-scrollbar">
|
|
378
|
+
{(availableModels.includes(formData.modelName)
|
|
379
|
+
? availableModels
|
|
380
|
+
: availableModels.filter(m => m.toLowerCase().includes(formData.modelName.toLowerCase()))
|
|
381
|
+
).map(name => (
|
|
382
|
+
<li
|
|
383
|
+
key={name}
|
|
384
|
+
onMouseDown={(e) => {
|
|
385
|
+
e.preventDefault()
|
|
386
|
+
setFormData(prev => ({ ...prev, modelName: name, providerName: formData.modelType === "medsam2" ? "MedSAM2" : "MedSAM3" }))
|
|
387
|
+
setShowDropdown(false)
|
|
388
|
+
}}
|
|
389
|
+
className="cursor-pointer px-3 py-2 text-text-primary hover:bg-slate-200 dark:hover:bg-violet-600/40"
|
|
390
|
+
>
|
|
391
|
+
{name}
|
|
392
|
+
</li>
|
|
393
|
+
))}
|
|
394
|
+
{!availableModels.includes(formData.modelName) && availableModels.filter(m => m.toLowerCase().includes(formData.modelName.toLowerCase())).length === 0 && (
|
|
395
|
+
<li className="px-3 py-2 text-text-muted cursor-pointer" onMouseDown={(e) => {
|
|
396
|
+
e.preventDefault()
|
|
397
|
+
setShowDropdown(false)
|
|
398
|
+
}}>
|
|
399
|
+
Use custom model: '{formData.modelName}'
|
|
400
|
+
</li>
|
|
401
|
+
)}
|
|
402
|
+
</ul>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
{/* Capability Toggles */}
|
|
407
|
+
<details className="group">
|
|
408
|
+
<summary className="cursor-pointer text-sm font-medium text-text-secondary hover:text-text-primary flex items-center gap-2">
|
|
409
|
+
<span className="group-open:rotate-90 transition-transform">▸</span>
|
|
410
|
+
Model Capabilities
|
|
411
|
+
</summary>
|
|
412
|
+
<div className="mt-3 space-y-3 pl-4 border-l-2 border-violet-500/20">
|
|
413
|
+
<label className="flex items-center gap-3 text-sm text-text-primary cursor-pointer w-fit">
|
|
414
|
+
<input
|
|
415
|
+
type="checkbox"
|
|
416
|
+
id="seg_returnsMask"
|
|
417
|
+
checked={formData.returnsMask}
|
|
418
|
+
onChange={handleChange}
|
|
419
|
+
disabled={connStatus !== "verified" || isLocked}
|
|
420
|
+
className="rounded border-border-primary accent-violet-500"
|
|
421
|
+
/>
|
|
422
|
+
Returns Mask
|
|
423
|
+
</label>
|
|
424
|
+
<label className="flex items-center gap-3 text-sm text-text-primary cursor-pointer w-fit">
|
|
425
|
+
<input
|
|
426
|
+
type="checkbox"
|
|
427
|
+
id="seg_returnsBox"
|
|
428
|
+
checked={formData.returnsBox}
|
|
429
|
+
onChange={handleChange}
|
|
430
|
+
disabled={connStatus !== "verified" || isLocked}
|
|
431
|
+
className="rounded border-border-primary accent-violet-500"
|
|
432
|
+
/>
|
|
433
|
+
Returns Bounding Box
|
|
434
|
+
</label>
|
|
435
|
+
<label className="flex items-center gap-3 text-sm text-text-primary cursor-pointer w-fit">
|
|
436
|
+
<input
|
|
437
|
+
type="checkbox"
|
|
438
|
+
id="seg_supportsContours"
|
|
439
|
+
checked={formData.supportsContours}
|
|
440
|
+
onChange={handleChange}
|
|
441
|
+
disabled={connStatus !== "verified" || isLocked}
|
|
442
|
+
className="rounded border-border-primary accent-violet-500"
|
|
443
|
+
/>
|
|
444
|
+
Supports Contour Extraction
|
|
445
|
+
</label>
|
|
446
|
+
<label className="flex items-center gap-3 text-sm text-text-primary cursor-pointer w-fit">
|
|
447
|
+
<input
|
|
448
|
+
type="checkbox"
|
|
449
|
+
id="seg_supports3D"
|
|
450
|
+
checked={formData.supports3D}
|
|
451
|
+
onChange={handleChange}
|
|
452
|
+
disabled={connStatus !== "verified" || isLocked}
|
|
453
|
+
className="rounded border-border-primary accent-violet-500"
|
|
454
|
+
/>
|
|
455
|
+
Supports 3D Volumes
|
|
456
|
+
</label>
|
|
457
|
+
</div>
|
|
458
|
+
</details>
|
|
459
|
+
|
|
460
|
+
{/* Status messages */}
|
|
461
|
+
{saveStatus === "error" && errorMsg && (
|
|
462
|
+
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2 text-red-400 text-sm">
|
|
463
|
+
<AlertCircle size={16} /> {errorMsg}
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
{saveStatus === "saved" && (
|
|
467
|
+
<div className="p-3 bg-green-500/10 border border-green-500/30 rounded-lg flex items-center gap-2 text-green-400 text-sm">
|
|
468
|
+
<CheckCircle size={16} /> Configuration saved and activated!
|
|
469
|
+
</div>
|
|
470
|
+
)}
|
|
471
|
+
|
|
472
|
+
{/* Save / Change button */}
|
|
473
|
+
{isLocked ? (
|
|
474
|
+
<Button onClick={() => setIsLocked(false)} className="w-full mt-4 bg-green-600 hover:bg-green-700 text-white gap-2">
|
|
475
|
+
<CheckCircle size={16} /> Change Configuration
|
|
476
|
+
</Button>
|
|
477
|
+
) : (
|
|
478
|
+
<Button
|
|
479
|
+
onClick={handleSave}
|
|
480
|
+
disabled={saveStatus === "saving" || connStatus !== "verified" || !formData.modelName}
|
|
481
|
+
className="w-full mt-4 bg-violet-600 hover:bg-violet-700 text-white gap-2 disabled:opacity-50"
|
|
482
|
+
>
|
|
483
|
+
<Save size={16} />
|
|
484
|
+
{saveStatus === "saving" ? "Saving..." : "Save & Activate Segmentation"}
|
|
485
|
+
</Button>
|
|
486
|
+
)}
|
|
487
|
+
</CardContent>
|
|
488
|
+
</Card>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FileText } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
export function StudyPlaceholder() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="h-full flex flex-col items-center justify-center p-6 bg-bg-panel/50 rounded-xl border-2 border-dashed border-border-card m-6">
|
|
6
|
+
<div className="flex flex-col items-center gap-4 text-center max-w-sm">
|
|
7
|
+
<div className="w-16 h-16 rounded-2xl bg-border-card flex items-center justify-center">
|
|
8
|
+
<FileText className="w-8 h-8 text-text-muted" />
|
|
9
|
+
</div>
|
|
10
|
+
<div>
|
|
11
|
+
<h3 className="text-xl font-medium text-text-heading mb-2">Ready to Assist</h3>
|
|
12
|
+
<p className="text-text-secondary">Fill in the patient details and upload an image to generate a comprehensive AI-assisted radiology report.</p>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|