@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,339 @@
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, BrainCircuit, Activity, CheckCircle, AlertCircle, RefreshCw, ChevronDown, ChevronUp } from "lucide-react"
6
+
7
+ export function AIConfigPanel() {
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 [backendStatus, setBackendStatus] = React.useState<"checking" | "connected" | "disconnected">("checking")
12
+ const [isLocked, setIsLocked] = React.useState(false)
13
+
14
+ // Connection specific state
15
+ const [connStatus, setConnStatus] = React.useState<"idle" | "checking" | "verified" | "invalid">("idle")
16
+ const [connError, setConnError] = React.useState("")
17
+ const [availableModels, setAvailableModels] = React.useState<string[]>([])
18
+ const [showDropdown, setShowDropdown] = React.useState(false)
19
+
20
+ const [formData, setFormData] = React.useState({
21
+ providerType: "custom_api",
22
+ apiEndpointUrl: "https://openrouter.ai/api/v1",
23
+ apiSecretKey: "",
24
+ modelName: "",
25
+ maxTokens: 4096,
26
+ temperature: 0.3,
27
+ isActive: true,
28
+ })
29
+
30
+ React.useEffect(() => {
31
+ fetch("/api/ai-config")
32
+ .then(res => res.json())
33
+ .then(data => {
34
+ if (Array.isArray(data) && data.length > 0) {
35
+ const active = data.find((d: any) => d.isActive) || data[0]
36
+ setFormData({
37
+ providerType: active.providerType || "custom_api",
38
+ apiEndpointUrl: active.apiEndpointUrl || "",
39
+ apiSecretKey: active.apiSecretKey || "",
40
+ modelName: active.modelName || "",
41
+ maxTokens: active.maxTokens || 4096,
42
+ temperature: active.temperature || 0.3,
43
+ isActive: true,
44
+ })
45
+ if (active.apiEndpointUrl && active.apiSecretKey) {
46
+ checkConnection(active.providerType, active.apiEndpointUrl, active.apiSecretKey)
47
+ setIsLocked(true)
48
+ }
49
+ }
50
+ })
51
+ .catch(console.error)
52
+ .finally(() => setLoading(false))
53
+
54
+ // Check backend health
55
+ fetch("http://localhost:8000/health")
56
+ .then(res => {
57
+ if (res.ok) setBackendStatus("connected")
58
+ else setBackendStatus("disconnected")
59
+ })
60
+ .catch(() => setBackendStatus("disconnected"))
61
+ }, [])
62
+
63
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
64
+ const { id, value } = e.target
65
+ setFormData(prev => ({ ...prev, [id]: value }))
66
+
67
+ // If they change endpoint or key, reset the connection state to force a re-check
68
+ if (id === "apiEndpointUrl" || id === "apiSecretKey" || id === "providerType") {
69
+ setConnStatus("idle")
70
+ setAvailableModels([])
71
+ if (id === "providerType") {
72
+ if (value === "ollama") {
73
+ setFormData(prev => ({ ...prev, providerType: value, apiEndpointUrl: "http://localhost:11434", modelName: "" }))
74
+ } else if (value === "custom_api") {
75
+ setFormData(prev => ({ ...prev, providerType: value, apiEndpointUrl: "https://openrouter.ai/api/v1", modelName: "" }))
76
+ }
77
+ }
78
+ }
79
+ setSaveStatus("idle")
80
+ }
81
+
82
+ const checkConnection = async (type = formData.providerType, url = formData.apiEndpointUrl, key = formData.apiSecretKey) => {
83
+ setConnStatus("checking")
84
+ setConnError("")
85
+ try {
86
+ const payload = {
87
+ ai_config: { providerType: type, apiEndpointUrl: url, apiSecretKey: key }
88
+ }
89
+ const res = await fetch("http://localhost:8001/test_ai_connection", {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify(payload)
93
+ })
94
+ const data = await res.json()
95
+ if (res.ok && data.success) {
96
+ setAvailableModels(data.models || [])
97
+ setConnStatus("verified")
98
+ } else {
99
+ setConnStatus("invalid")
100
+ setConnError(data.error || "Failed to fetch models.")
101
+ }
102
+ } catch(e) {
103
+ setConnStatus("invalid")
104
+ setConnError("Ensure the Python Global Backend is running.")
105
+ }
106
+ }
107
+
108
+ const handleSave = async () => {
109
+ if (!formData.apiEndpointUrl) {
110
+ setErrorMsg("Endpoint URL is required.")
111
+ setSaveStatus("error")
112
+ return
113
+ }
114
+ if (formData.providerType !== "ollama" && !formData.apiSecretKey) {
115
+ setErrorMsg("API Secret Key is required.")
116
+ setSaveStatus("error")
117
+ return
118
+ }
119
+ if (!formData.modelName) {
120
+ setErrorMsg("Please check connection and select a Model Name.")
121
+ setSaveStatus("error")
122
+ return
123
+ }
124
+
125
+ setSaveStatus("saving")
126
+ setErrorMsg("")
127
+ try {
128
+ const res = await fetch("/api/ai-config", {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" },
131
+ body: JSON.stringify(formData),
132
+ })
133
+ const result = await res.json()
134
+ if (res.ok && result.success) {
135
+ setSaveStatus("saved")
136
+ setIsLocked(true)
137
+ setTimeout(() => setSaveStatus("idle"), 3000)
138
+ } else {
139
+ setErrorMsg(result.error || "Failed to save configuration")
140
+ setSaveStatus("error")
141
+ }
142
+ } catch (e) {
143
+ setErrorMsg(String(e))
144
+ setSaveStatus("error")
145
+ }
146
+ }
147
+
148
+ if (loading) return null
149
+
150
+ return (
151
+ <Card className="bg-bg-surface border-border-primary border-t-4 border-t-indigo-500">
152
+ <CardHeader>
153
+ <CardTitle className="text-text-heading flex items-center gap-2">
154
+ <BrainCircuit size={20} className="text-indigo-500" />
155
+ AI Service Configuration
156
+ </CardTitle>
157
+ <p className="text-sm text-text-secondary">Configure your AI Provider and select a model for report generation</p>
158
+ </CardHeader>
159
+ <CardContent className="space-y-5">
160
+
161
+ {/* 1. Provider Type */}
162
+ <div>
163
+ <Label htmlFor="providerType" className="text-text-primary">Provider Type</Label>
164
+ <select
165
+ id="providerType"
166
+ value={formData.providerType}
167
+ onChange={handleChange}
168
+ disabled={isLocked}
169
+ 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-primary"
170
+ >
171
+ <option value="custom_api">Custom API</option>
172
+ <option value="ollama">Ollama (Local Models)</option>
173
+ </select>
174
+ </div>
175
+
176
+ {/* 2. Endpoint URL */}
177
+ <div>
178
+ <Label htmlFor="apiEndpointUrl" className="text-text-primary">API Endpoint URL</Label>
179
+ <Input
180
+ id="apiEndpointUrl"
181
+ value={formData.apiEndpointUrl}
182
+ onChange={handleChange}
183
+ disabled={isLocked}
184
+ placeholder={formData.providerType === "ollama" ? "http://localhost:11434" : "https://openrouter.ai/api/v1"}
185
+ className="mt-1 bg-bg-panel border-border-primary text-text-primary disabled:opacity-50"
186
+ />
187
+ </div>
188
+
189
+ {/* 3. API Secret Key */}
190
+ {formData.providerType !== "ollama" && (
191
+ <div>
192
+ <Label htmlFor="apiSecretKey" className="text-text-primary">API Secret Key</Label>
193
+ <div className="mt-1">
194
+ <Input
195
+ id="apiSecretKey"
196
+ type="password"
197
+ value={formData.apiSecretKey}
198
+ onChange={handleChange}
199
+ disabled={isLocked}
200
+ placeholder="Enter your API key"
201
+ className="bg-bg-panel border-border-primary text-text-primary disabled:opacity-50"
202
+ />
203
+ </div>
204
+ </div>
205
+ )}
206
+
207
+ {/* 4. Check Connection Button */}
208
+ <Button
209
+ variant="outline"
210
+ onClick={() => checkConnection()}
211
+ disabled={connStatus === "checking" || isLocked}
212
+ className="w-full border-indigo-500 text-indigo-500 hover:bg-indigo-50 gap-2 disabled:opacity-50"
213
+ >
214
+ <RefreshCw size={16} className={connStatus === "checking" ? "animate-spin" : ""} />
215
+ {connStatus === "checking" ? "Connecting to Provider..." : "Check Connection & Fetch Models"}
216
+ </Button>
217
+
218
+ {connStatus === "invalid" && (
219
+ <div className="text-xs text-red-500 bg-red-500/10 p-2 rounded-md border border-red-500/20">
220
+ <strong>Connection Failed:</strong> {connError}
221
+ </div>
222
+ )}
223
+
224
+ {connStatus === "verified" && (
225
+ <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">
226
+ <CheckCircle size={14} /> <strong>Connection Verified:</strong> Successfully fetched {availableModels.length} models.
227
+ </div>
228
+ )}
229
+
230
+ <div className="pt-2 border-t border-border-primary"></div>
231
+
232
+ {/* 5. Model Name (Select or type manually) */}
233
+ {/* 5. Model Name (Select or type manually) */}
234
+ <div className="relative">
235
+ <Label htmlFor="modelName" className="text-text-primary">Model Name</Label>
236
+ <div className="relative mt-1">
237
+ <Input
238
+ id="modelName"
239
+ value={formData.modelName}
240
+ onChange={(e) => {
241
+ handleChange(e)
242
+ setShowDropdown(true)
243
+ }}
244
+ onFocus={() => setShowDropdown(true)}
245
+ onBlur={() => setTimeout(() => setShowDropdown(false), 150)}
246
+ disabled={connStatus !== "verified" || isLocked}
247
+ placeholder={connStatus === "verified" ? "Type or select a model..." : "Waiting for connection check..."}
248
+ className="bg-bg-panel border-border-primary text-text-primary pr-10 disabled:opacity-50"
249
+ autoComplete="off"
250
+ />
251
+ {connStatus === "verified" && !isLocked && (
252
+ <button
253
+ type="button"
254
+ onMouseDown={(e) => {
255
+ e.preventDefault() // prevent input from losing focus
256
+ setShowDropdown(!showDropdown)
257
+ }}
258
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary"
259
+ >
260
+ {showDropdown ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
261
+ </button>
262
+ )}
263
+ </div>
264
+
265
+ {/* Custom Dropdown Menu */}
266
+ {showDropdown && connStatus === "verified" && availableModels.length > 0 && (
267
+ <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">
268
+ {(availableModels.includes(formData.modelName)
269
+ ? availableModels
270
+ : availableModels.filter(m => m.toLowerCase().includes(formData.modelName.toLowerCase()))
271
+ ).map(name => (
272
+ <li
273
+ key={name}
274
+ onMouseDown={(e) => {
275
+ // onMouseDown fires before onBlur
276
+ e.preventDefault()
277
+ setFormData(prev => ({ ...prev, modelName: name }))
278
+ setShowDropdown(false)
279
+ }}
280
+ className="cursor-pointer px-3 py-2 text-text-primary hover:bg-slate-200 dark:hover:bg-indigo-600/40"
281
+ >
282
+ {name}
283
+ </li>
284
+ ))}
285
+ {!availableModels.includes(formData.modelName) && availableModels.filter(m => m.toLowerCase().includes(formData.modelName.toLowerCase())).length === 0 && (
286
+ <li className="px-3 py-2 text-text-muted">No matching models found</li>
287
+ )}
288
+ </ul>
289
+ )}
290
+ </div>
291
+
292
+ {saveStatus === "error" && errorMsg && (
293
+ <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">
294
+ <AlertCircle size={16} /> {errorMsg}
295
+ </div>
296
+ )}
297
+
298
+ {saveStatus === "saved" && (
299
+ <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">
300
+ <CheckCircle size={16} /> Configuration saved and activated successfully!
301
+ </div>
302
+ )}
303
+
304
+ {/* Save / Change button */}
305
+ {isLocked ? (
306
+ <Button
307
+ onClick={() => setIsLocked(false)}
308
+ className="w-full mt-4 bg-green-600 hover:bg-green-700 text-white gap-2"
309
+ >
310
+ <CheckCircle size={16} />
311
+ Change Configuration
312
+ </Button>
313
+ ) : (
314
+ <Button
315
+ onClick={handleSave}
316
+ disabled={saveStatus === "saving" || connStatus !== "verified" || !formData.modelName}
317
+ className="w-full mt-4 bg-indigo-600 hover:bg-indigo-700 text-white gap-2 disabled:opacity-50"
318
+ >
319
+ <Save size={16} />
320
+ {saveStatus === "saving" ? "Saving..." : saveStatus === "saved" ? "Saved Active Configuration!" : "Save & Activate Provider"}
321
+ </Button>
322
+ )}
323
+
324
+ {/* Python Dependency Badge */}
325
+ <div className="mt-4 p-4 bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-100 dark:border-indigo-500/30 rounded-lg flex items-center justify-between text-indigo-800 dark:text-indigo-300">
326
+ <div className="flex flex-col gap-2">
327
+ <div className="flex items-center gap-3">
328
+ <span className="flex items-center gap-2 text-sm font-medium"><Activity size={16} /> Python Local DeepAgents Backend</span>
329
+ {backendStatus === "checking" && <span className="text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full animate-pulse">Checking...</span>}
330
+ {backendStatus === "connected" && <span className="text-xs bg-green-500/20 text-green-700 dark:text-green-400 border border-green-500/30 px-2 py-0.5 rounded-full flex items-center gap-1.5"><span className="w-1.5 h-1.5 rounded-full bg-green-500"></span>Connected</span>}
331
+ {backendStatus === "disconnected" && <span className="text-xs bg-red-500/20 text-red-700 dark:text-red-400 border border-red-500/30 px-2 py-0.5 rounded-full flex items-center gap-1.5"><span className="w-1.5 h-1.5 rounded-full bg-red-500"></span>Offline</span>}
332
+ </div>
333
+ </div>
334
+ <span className="text-xs font-semibold px-2 py-1 bg-indigo-200 dark:bg-indigo-800 text-indigo-900 dark:text-indigo-200 rounded-md">http://localhost:8000</span>
335
+ </div>
336
+ </CardContent>
337
+ </Card>
338
+ )
339
+ }