@swarmclawai/swarmclaw 0.2.0
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 +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { createProviderConfig, updateProviderConfig, deleteProviderConfig } from '@/lib/provider-config'
|
|
6
|
+
import { api } from '@/lib/api-client'
|
|
7
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
|
+
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
9
|
+
import { toast } from 'sonner'
|
|
10
|
+
|
|
11
|
+
export function ProviderSheet() {
|
|
12
|
+
const open = useAppStore((s) => s.providerSheetOpen)
|
|
13
|
+
const setOpen = useAppStore((s) => s.setProviderSheetOpen)
|
|
14
|
+
const editingId = useAppStore((s) => s.editingProviderId)
|
|
15
|
+
const setEditingId = useAppStore((s) => s.setEditingProviderId)
|
|
16
|
+
const providerConfigs = useAppStore((s) => s.providerConfigs)
|
|
17
|
+
const loadProviderConfigs = useAppStore((s) => s.loadProviderConfigs)
|
|
18
|
+
const providers = useAppStore((s) => s.providers)
|
|
19
|
+
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
20
|
+
const credentials = useAppStore((s) => s.credentials)
|
|
21
|
+
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
22
|
+
|
|
23
|
+
const [name, setName] = useState('')
|
|
24
|
+
const [baseUrl, setBaseUrl] = useState('')
|
|
25
|
+
const [models, setModels] = useState('')
|
|
26
|
+
const [requiresApiKey, setRequiresApiKey] = useState(true)
|
|
27
|
+
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
28
|
+
const [isEnabled, setIsEnabled] = useState(true)
|
|
29
|
+
const [addingKey, setAddingKey] = useState(false)
|
|
30
|
+
const [newKeyName, setNewKeyName] = useState('')
|
|
31
|
+
const [newKeyValue, setNewKeyValue] = useState('')
|
|
32
|
+
const [savingKey, setSavingKey] = useState(false)
|
|
33
|
+
const [newModel, setNewModel] = useState('')
|
|
34
|
+
|
|
35
|
+
// Test connection state
|
|
36
|
+
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
|
|
37
|
+
const [testMessage, setTestMessage] = useState('')
|
|
38
|
+
|
|
39
|
+
// Ollama local models
|
|
40
|
+
const [localModels, setLocalModels] = useState<string[]>([])
|
|
41
|
+
const [localLoading, setLocalLoading] = useState(false)
|
|
42
|
+
const [localError, setLocalError] = useState('')
|
|
43
|
+
|
|
44
|
+
// AI generation state
|
|
45
|
+
const [aiPrompt, setAiPrompt] = useState('')
|
|
46
|
+
const [generating, setGenerating] = useState(false)
|
|
47
|
+
const [generated, setGenerated] = useState(false)
|
|
48
|
+
const [genError, setGenError] = useState('')
|
|
49
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
50
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
51
|
+
|
|
52
|
+
// Find editing provider in custom configs OR built-in list
|
|
53
|
+
const editingCustom = editingId ? providerConfigs.find((c) => c.id === editingId) : null
|
|
54
|
+
const editingBuiltin = editingId ? providers.find((p) => p.id === editingId) : null
|
|
55
|
+
const isBuiltin = !!editingBuiltin && !editingCustom
|
|
56
|
+
const editing = editingCustom || editingBuiltin
|
|
57
|
+
|
|
58
|
+
const handleGenerate = async () => {
|
|
59
|
+
if (!aiPrompt.trim()) return
|
|
60
|
+
setGenerating(true)
|
|
61
|
+
setGenError('')
|
|
62
|
+
try {
|
|
63
|
+
const result = await api<{ name?: string; baseUrl?: string; models?: string; requiresApiKey?: boolean; error?: string }>('POST', '/generate', { type: 'provider', prompt: aiPrompt })
|
|
64
|
+
if (result.error) {
|
|
65
|
+
setGenError(result.error)
|
|
66
|
+
} else if (result.name || result.baseUrl) {
|
|
67
|
+
if (result.name) setName(result.name)
|
|
68
|
+
if (result.baseUrl) setBaseUrl(result.baseUrl)
|
|
69
|
+
if (result.models) setModels(result.models)
|
|
70
|
+
if (result.requiresApiKey !== undefined) setRequiresApiKey(result.requiresApiKey)
|
|
71
|
+
setGenerated(true)
|
|
72
|
+
} else {
|
|
73
|
+
setGenError('AI returned empty response — try again')
|
|
74
|
+
}
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
77
|
+
}
|
|
78
|
+
setGenerating(false)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (open) {
|
|
83
|
+
loadCredentials()
|
|
84
|
+
loadSettings()
|
|
85
|
+
setAiPrompt('')
|
|
86
|
+
setGenerating(false)
|
|
87
|
+
setGenerated(false)
|
|
88
|
+
setGenError('')
|
|
89
|
+
setNewModel('')
|
|
90
|
+
setLocalModels([])
|
|
91
|
+
setLocalError('')
|
|
92
|
+
setTestStatus('idle')
|
|
93
|
+
setTestMessage('')
|
|
94
|
+
if (editingCustom) {
|
|
95
|
+
setName(editingCustom.name)
|
|
96
|
+
setBaseUrl(editingCustom.baseUrl || '')
|
|
97
|
+
setModels(editingCustom.models.join(', '))
|
|
98
|
+
setRequiresApiKey(editingCustom.requiresApiKey)
|
|
99
|
+
setCredentialId(editingCustom.credentialId || null)
|
|
100
|
+
setIsEnabled(editingCustom.isEnabled)
|
|
101
|
+
} else if (editingBuiltin) {
|
|
102
|
+
setName(editingBuiltin.name)
|
|
103
|
+
setBaseUrl(editingBuiltin.defaultEndpoint || '')
|
|
104
|
+
setModels(editingBuiltin.models.join(', '))
|
|
105
|
+
setRequiresApiKey(editingBuiltin.requiresApiKey)
|
|
106
|
+
// Default to existing credential for this provider
|
|
107
|
+
const existingCred = Object.values(credentials).find((c) => c.provider === editingBuiltin.id)
|
|
108
|
+
setCredentialId(existingCred?.id || null)
|
|
109
|
+
setIsEnabled(true)
|
|
110
|
+
} else {
|
|
111
|
+
setName('')
|
|
112
|
+
setBaseUrl('')
|
|
113
|
+
setModels('')
|
|
114
|
+
setRequiresApiKey(true)
|
|
115
|
+
setCredentialId(null)
|
|
116
|
+
setIsEnabled(true)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, [open, editingId])
|
|
120
|
+
|
|
121
|
+
// Fetch local Ollama models when editing Ollama provider
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!open || editingId !== 'ollama') return
|
|
124
|
+
setLocalLoading(true)
|
|
125
|
+
const endpoint = baseUrl || 'http://localhost:11434'
|
|
126
|
+
api<{ models: { name: string }[]; error?: string }>('GET', `/providers/ollama?endpoint=${encodeURIComponent(endpoint)}`)
|
|
127
|
+
.then((res) => {
|
|
128
|
+
if (res.error) {
|
|
129
|
+
setLocalError(res.error)
|
|
130
|
+
setLocalModels([])
|
|
131
|
+
} else {
|
|
132
|
+
setLocalModels(res.models.map((m) => m.name))
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.catch(() => {
|
|
136
|
+
setLocalError('Failed to connect')
|
|
137
|
+
setLocalModels([])
|
|
138
|
+
})
|
|
139
|
+
.finally(() => setLocalLoading(false))
|
|
140
|
+
}, [open, editingId, baseUrl])
|
|
141
|
+
|
|
142
|
+
// Reset test status when connection params change
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
setTestStatus('idle')
|
|
145
|
+
setTestMessage('')
|
|
146
|
+
}, [credentialId, baseUrl])
|
|
147
|
+
|
|
148
|
+
const handleTestConnection = async () => {
|
|
149
|
+
setTestStatus('testing')
|
|
150
|
+
setTestMessage('')
|
|
151
|
+
try {
|
|
152
|
+
const result = await api<{ ok: boolean; message: string }>('POST', '/setup/check-provider', {
|
|
153
|
+
provider: editingId || 'custom',
|
|
154
|
+
credentialId,
|
|
155
|
+
endpoint: baseUrl,
|
|
156
|
+
})
|
|
157
|
+
if (result.ok) {
|
|
158
|
+
setTestStatus('pass')
|
|
159
|
+
setTestMessage(result.message)
|
|
160
|
+
} else {
|
|
161
|
+
setTestStatus('fail')
|
|
162
|
+
setTestMessage(result.message)
|
|
163
|
+
}
|
|
164
|
+
} catch (err: unknown) {
|
|
165
|
+
setTestStatus('fail')
|
|
166
|
+
setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const onClose = () => {
|
|
171
|
+
setOpen(false)
|
|
172
|
+
setEditingId(null)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const handleSave = async () => {
|
|
176
|
+
if (isBuiltin) {
|
|
177
|
+
// Save model overrides for built-in providers
|
|
178
|
+
const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
|
|
179
|
+
await api('PUT', `/providers/${editingId}/models`, { models: modelList })
|
|
180
|
+
await loadProviders()
|
|
181
|
+
onClose()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
|
|
185
|
+
const data = {
|
|
186
|
+
name: name.trim() || 'Custom Provider',
|
|
187
|
+
baseUrl: baseUrl.trim(),
|
|
188
|
+
models: modelList,
|
|
189
|
+
requiresApiKey,
|
|
190
|
+
credentialId,
|
|
191
|
+
isEnabled,
|
|
192
|
+
}
|
|
193
|
+
if (editingCustom) {
|
|
194
|
+
await updateProviderConfig(editingCustom.id, data)
|
|
195
|
+
} else {
|
|
196
|
+
await createProviderConfig(data)
|
|
197
|
+
}
|
|
198
|
+
await loadProviderConfigs()
|
|
199
|
+
onClose()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const handleDelete = async () => {
|
|
203
|
+
if (editingCustom) {
|
|
204
|
+
await deleteProviderConfig(editingCustom.id)
|
|
205
|
+
await loadProviderConfigs()
|
|
206
|
+
onClose()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const handleResetModels = async () => {
|
|
211
|
+
if (isBuiltin && editingId) {
|
|
212
|
+
await api('DELETE', `/providers/${editingId}/models`)
|
|
213
|
+
await loadProviders()
|
|
214
|
+
// Re-read the reset models
|
|
215
|
+
const updated = providers.find((p) => p.id === editingId)
|
|
216
|
+
if (updated) setModels(updated.models.join(', '))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const handleAddModel = () => {
|
|
221
|
+
if (!newModel.trim()) return
|
|
222
|
+
const current = models ? models + ', ' + newModel.trim() : newModel.trim()
|
|
223
|
+
setModels(current)
|
|
224
|
+
setNewModel('')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const handleRemoveModel = (index: number) => {
|
|
228
|
+
const list = models.split(',').map((m) => m.trim()).filter(Boolean)
|
|
229
|
+
list.splice(index, 1)
|
|
230
|
+
setModels(list.join(', '))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const credList = Object.values(credentials)
|
|
234
|
+
const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
|
|
235
|
+
const isNew = !editing
|
|
236
|
+
const showApiKey = isBuiltin ? editingBuiltin?.requiresApiKey || editingBuiltin?.optionalApiKey : requiresApiKey
|
|
237
|
+
|
|
238
|
+
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<BottomSheet open={open} onClose={onClose} wide>
|
|
242
|
+
<div className="mb-10">
|
|
243
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
244
|
+
{isBuiltin ? editing?.name : editing ? 'Edit Provider' : 'New Provider'}
|
|
245
|
+
</h2>
|
|
246
|
+
<p className="text-[14px] text-text-3">
|
|
247
|
+
{isBuiltin ? 'Manage models and API key for this built-in provider' : 'Add an OpenAI-compatible provider (OpenRouter, Together, Groq, etc.)'}
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* AI Generation — only for new custom providers */}
|
|
252
|
+
{isNew && <AiGenBlock
|
|
253
|
+
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
254
|
+
generating={generating} generated={generated} genError={genError}
|
|
255
|
+
onGenerate={handleGenerate} appSettings={appSettings}
|
|
256
|
+
placeholder='Name a provider, e.g. "Groq", "Together AI", "z.ai", "DeepSeek"'
|
|
257
|
+
/>}
|
|
258
|
+
|
|
259
|
+
{/* Name */}
|
|
260
|
+
<div className="mb-8">
|
|
261
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
|
|
262
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. OpenRouter"
|
|
263
|
+
disabled={isBuiltin} className={`${inputClass} ${isBuiltin ? 'opacity-50' : ''}`} style={{ fontFamily: 'inherit' }} />
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{/* Base URL — for custom providers and built-ins with endpoints (Ollama, OpenClaw) */}
|
|
267
|
+
{(!isBuiltin || editingBuiltin?.requiresEndpoint) && (
|
|
268
|
+
<div className="mb-8">
|
|
269
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
270
|
+
{isBuiltin ? 'Endpoint' : 'Base URL'}
|
|
271
|
+
</label>
|
|
272
|
+
<input type="text" value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)}
|
|
273
|
+
placeholder={editingBuiltin?.defaultEndpoint || 'https://openrouter.ai/api/v1'}
|
|
274
|
+
className={`${inputClass} font-mono text-[14px]`} />
|
|
275
|
+
<p className="text-[11px] text-text-3/70 mt-2">
|
|
276
|
+
{isBuiltin ? `Default: ${editingBuiltin?.defaultEndpoint || 'none'}` : 'OpenAI-compatible API endpoint (without /chat/completions)'}
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{/* Models — chip editor for built-in, textarea for custom */}
|
|
282
|
+
<div className="mb-8">
|
|
283
|
+
<div className="flex items-center justify-between mb-3">
|
|
284
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Models</label>
|
|
285
|
+
{isBuiltin && (
|
|
286
|
+
<button onClick={handleResetModels}
|
|
287
|
+
className="text-[11px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none"
|
|
288
|
+
style={{ fontFamily: 'inherit' }}>
|
|
289
|
+
Reset to defaults
|
|
290
|
+
</button>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{isBuiltin ? (
|
|
295
|
+
<>
|
|
296
|
+
<div className="flex flex-wrap gap-1.5 mb-3">
|
|
297
|
+
{modelList.map((model, i) => {
|
|
298
|
+
const isLocal = editingId === 'ollama' && localModels.includes(model)
|
|
299
|
+
return (
|
|
300
|
+
<div key={`${model}-${i}`} className={`group/model flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] border
|
|
301
|
+
${isLocal ? 'bg-emerald-500/[0.08] border-emerald-500/20' : 'bg-white/[0.04] border-white/[0.06]'}`}>
|
|
302
|
+
<span className="text-[12px] text-text-2 font-mono">{model}</span>
|
|
303
|
+
{isLocal && (
|
|
304
|
+
<span className="text-[9px] font-600 px-1.5 py-0.5 rounded-[4px] bg-emerald-500/15 text-emerald-400 uppercase tracking-wider">local</span>
|
|
305
|
+
)}
|
|
306
|
+
<button
|
|
307
|
+
onClick={() => handleRemoveModel(i)}
|
|
308
|
+
className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] text-text-3
|
|
309
|
+
opacity-0 group-hover/model:opacity-100 hover:bg-red-500/20 hover:text-red-400
|
|
310
|
+
transition-all cursor-pointer bg-transparent border-none"
|
|
311
|
+
>
|
|
312
|
+
×
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
})}
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Ollama: show available local models not yet in the list */}
|
|
320
|
+
{editingId === 'ollama' && !localLoading && localModels.length > 0 && (() => {
|
|
321
|
+
const missing = localModels.filter((m) => !modelList.includes(m))
|
|
322
|
+
if (missing.length === 0) return null
|
|
323
|
+
return (
|
|
324
|
+
<div className="mb-3">
|
|
325
|
+
<p className="text-[11px] text-text-3/60 mb-2">Available locally — click to add:</p>
|
|
326
|
+
<div className="flex flex-wrap gap-1.5">
|
|
327
|
+
{missing.map((m) => (
|
|
328
|
+
<button key={m} onClick={() => { setModels(models ? models + ', ' + m : m) }}
|
|
329
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] bg-emerald-500/[0.05] border border-emerald-500/15
|
|
330
|
+
hover:bg-emerald-500/10 transition-all cursor-pointer text-[12px] text-emerald-300/80 font-mono"
|
|
331
|
+
style={{ fontFamily: 'inherit' }}>
|
|
332
|
+
<span>+</span> {m}
|
|
333
|
+
</button>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)
|
|
338
|
+
})()}
|
|
339
|
+
|
|
340
|
+
{editingId === 'ollama' && localLoading && (
|
|
341
|
+
<p className="text-[11px] text-text-3/70 mb-3">Checking local Ollama instance...</p>
|
|
342
|
+
)}
|
|
343
|
+
{editingId === 'ollama' && localError && (
|
|
344
|
+
<p className="text-[11px] text-amber-400/60 mb-3">{localError}</p>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
<div className="flex gap-2">
|
|
348
|
+
<input
|
|
349
|
+
type="text"
|
|
350
|
+
value={newModel}
|
|
351
|
+
onChange={(e) => setNewModel(e.target.value)}
|
|
352
|
+
placeholder="Add model ID..."
|
|
353
|
+
className={`${inputClass} flex-1 font-mono text-[14px]`}
|
|
354
|
+
style={{ fontFamily: 'inherit' }}
|
|
355
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddModel() } }}
|
|
356
|
+
/>
|
|
357
|
+
<button
|
|
358
|
+
onClick={handleAddModel}
|
|
359
|
+
disabled={!newModel.trim()}
|
|
360
|
+
className="px-4 py-3 rounded-[14px] border-none bg-accent-soft text-accent-bright text-[13px] font-600
|
|
361
|
+
cursor-pointer disabled:opacity-30 hover:brightness-110 transition-all shrink-0"
|
|
362
|
+
style={{ fontFamily: 'inherit' }}
|
|
363
|
+
>
|
|
364
|
+
Add
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
) : (
|
|
369
|
+
<>
|
|
370
|
+
<textarea
|
|
371
|
+
value={models}
|
|
372
|
+
onChange={(e) => setModels(e.target.value)}
|
|
373
|
+
placeholder="model-1, model-2, model-3"
|
|
374
|
+
rows={3}
|
|
375
|
+
className={`${inputClass} resize-y min-h-[80px] font-mono text-[14px]`}
|
|
376
|
+
style={{ fontFamily: 'inherit' }}
|
|
377
|
+
/>
|
|
378
|
+
<p className="text-[11px] text-text-3/70 mt-2">Comma-separated model IDs</p>
|
|
379
|
+
</>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Requires API Key toggle — only for custom */}
|
|
384
|
+
{!isBuiltin && (
|
|
385
|
+
<div className="mb-8">
|
|
386
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
387
|
+
<div
|
|
388
|
+
onClick={() => setRequiresApiKey(!requiresApiKey)}
|
|
389
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
|
|
390
|
+
${requiresApiKey ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
391
|
+
>
|
|
392
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
393
|
+
${requiresApiKey ? 'left-[22px]' : 'left-0.5'}`} />
|
|
394
|
+
</div>
|
|
395
|
+
<span className="font-display text-[14px] font-600 text-text-2">Requires API Key</span>
|
|
396
|
+
</label>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{/* API Key section */}
|
|
401
|
+
{showApiKey && (
|
|
402
|
+
<div className="mb-8">
|
|
403
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
404
|
+
{isBuiltin ? 'API Key' : 'Linked API Key'}
|
|
405
|
+
{isBuiltin && editingBuiltin?.optionalApiKey && !editingBuiltin?.requiresApiKey && (
|
|
406
|
+
<span className="normal-case tracking-normal font-normal text-text-3 ml-1">(optional)</span>
|
|
407
|
+
)}
|
|
408
|
+
</label>
|
|
409
|
+
{credList.length > 0 && !addingKey ? (
|
|
410
|
+
<div className="flex gap-2">
|
|
411
|
+
<select value={credentialId || ''} onChange={(e) => {
|
|
412
|
+
if (e.target.value === '__add__') {
|
|
413
|
+
setAddingKey(true)
|
|
414
|
+
setNewKeyName('')
|
|
415
|
+
setNewKeyValue('')
|
|
416
|
+
} else {
|
|
417
|
+
setCredentialId(e.target.value || null)
|
|
418
|
+
}
|
|
419
|
+
}} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
|
|
420
|
+
<option value="">Select a key...</option>
|
|
421
|
+
{credList.map((c) => (
|
|
422
|
+
<option key={c.id} value={c.id}>{c.name} ({c.provider})</option>
|
|
423
|
+
))}
|
|
424
|
+
<option value="__add__">+ Add new key...</option>
|
|
425
|
+
</select>
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
|
|
429
|
+
className="shrink-0 px-3 py-2.5 rounded-[10px] bg-accent-soft/50 text-accent-bright text-[12px] font-600 hover:bg-accent-soft transition-colors cursor-pointer border border-accent-bright/20"
|
|
430
|
+
>
|
|
431
|
+
+ New
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
) : (
|
|
435
|
+
<div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
|
|
436
|
+
<input
|
|
437
|
+
type="text"
|
|
438
|
+
value={newKeyName}
|
|
439
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
440
|
+
placeholder="Key name (optional)"
|
|
441
|
+
className={inputClass}
|
|
442
|
+
style={{ fontFamily: 'inherit' }}
|
|
443
|
+
/>
|
|
444
|
+
<input
|
|
445
|
+
type="password"
|
|
446
|
+
value={newKeyValue}
|
|
447
|
+
onChange={(e) => setNewKeyValue(e.target.value)}
|
|
448
|
+
placeholder="Paste API key..."
|
|
449
|
+
className={inputClass}
|
|
450
|
+
style={{ fontFamily: 'inherit' }}
|
|
451
|
+
/>
|
|
452
|
+
<div className="flex gap-2 justify-end">
|
|
453
|
+
{credList.length > 0 && (
|
|
454
|
+
<button type="button" onClick={() => setAddingKey(false)} className="px-3 py-1.5 text-[12px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none" style={{ fontFamily: 'inherit' }}>Cancel</button>
|
|
455
|
+
)}
|
|
456
|
+
<button
|
|
457
|
+
type="button"
|
|
458
|
+
disabled={savingKey || !newKeyValue.trim()}
|
|
459
|
+
onClick={async () => {
|
|
460
|
+
setSavingKey(true)
|
|
461
|
+
try {
|
|
462
|
+
const cred = await api<any>('POST', '/credentials', { provider: editingId || name || 'custom', name: newKeyName.trim() || `${name || editingId || 'Custom'} key`, apiKey: newKeyValue.trim() })
|
|
463
|
+
await loadCredentials()
|
|
464
|
+
setCredentialId(cred.id)
|
|
465
|
+
setAddingKey(false)
|
|
466
|
+
setNewKeyName('')
|
|
467
|
+
setNewKeyValue('')
|
|
468
|
+
} catch (err: any) { toast.error(`Failed to save: ${err.message}`) }
|
|
469
|
+
finally { setSavingKey(false) }
|
|
470
|
+
}}
|
|
471
|
+
className="px-4 py-1.5 rounded-[8px] bg-accent-bright text-white text-[12px] font-600 cursor-pointer border-none hover:brightness-110 transition-all disabled:opacity-40"
|
|
472
|
+
style={{ fontFamily: 'inherit' }}
|
|
473
|
+
>
|
|
474
|
+
{savingKey ? 'Saving...' : 'Save Key'}
|
|
475
|
+
</button>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
|
|
482
|
+
{/* Enabled toggle — only for custom */}
|
|
483
|
+
{!isBuiltin && editingCustom && (
|
|
484
|
+
<div className="mb-8">
|
|
485
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
486
|
+
<div
|
|
487
|
+
onClick={() => setIsEnabled(!isEnabled)}
|
|
488
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
|
|
489
|
+
${isEnabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
490
|
+
>
|
|
491
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
492
|
+
${isEnabled ? 'left-[22px]' : 'left-0.5'}`} />
|
|
493
|
+
</div>
|
|
494
|
+
<span className="font-display text-[14px] font-600 text-text-2">Enabled</span>
|
|
495
|
+
</label>
|
|
496
|
+
</div>
|
|
497
|
+
)}
|
|
498
|
+
|
|
499
|
+
{/* Test connection result */}
|
|
500
|
+
{testStatus === 'fail' && (
|
|
501
|
+
<div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
|
|
502
|
+
<p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
{testStatus === 'pass' && (
|
|
506
|
+
<div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
|
|
507
|
+
<p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
<div className="flex gap-3 pt-2 border-t border-white/[0.04]">
|
|
512
|
+
{editingCustom && (
|
|
513
|
+
<button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
514
|
+
Delete
|
|
515
|
+
</button>
|
|
516
|
+
)}
|
|
517
|
+
<button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
518
|
+
Cancel
|
|
519
|
+
</button>
|
|
520
|
+
{showApiKey && credentialId && testStatus !== 'pass' ? (
|
|
521
|
+
<button
|
|
522
|
+
onClick={handleTestConnection}
|
|
523
|
+
disabled={testStatus === 'testing'}
|
|
524
|
+
className="flex-1 py-3.5 rounded-[14px] border-none bg-emerald-600 text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(16,185,129,0.2)] hover:brightness-110"
|
|
525
|
+
style={{ fontFamily: 'inherit' }}
|
|
526
|
+
>
|
|
527
|
+
{testStatus === 'testing' ? 'Testing...' : testStatus === 'fail' ? 'Retry Connection' : 'Test Connection'}
|
|
528
|
+
</button>
|
|
529
|
+
) : (
|
|
530
|
+
<button
|
|
531
|
+
onClick={handleSave}
|
|
532
|
+
disabled={isBuiltin ? false : (!name.trim() || !baseUrl.trim())}
|
|
533
|
+
className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
|
|
534
|
+
style={{ fontFamily: 'inherit' }}
|
|
535
|
+
>
|
|
536
|
+
{editing ? 'Save' : 'Create'}
|
|
537
|
+
</button>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
</BottomSheet>
|
|
541
|
+
)
|
|
542
|
+
}
|