@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,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 &quot;brain lesion&quot;. 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
+ }