@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,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
|
+
}
|