@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,1220 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { createAgent, updateAgent, deleteAgent } from '@/lib/agents'
|
|
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
|
+
import type { ProviderType, ClaudeSkill } from '@/types'
|
|
11
|
+
|
|
12
|
+
const AVAILABLE_TOOLS: { id: string; label: string; description: string }[] = [
|
|
13
|
+
{ id: 'shell', label: 'Shell', description: 'Execute commands in the working directory' },
|
|
14
|
+
{ id: 'files', label: 'Files', description: 'Read, write, list, move, copy, and send files' },
|
|
15
|
+
{ id: 'copy_file', label: 'Copy File', description: 'Copy files within the working directory' },
|
|
16
|
+
{ id: 'move_file', label: 'Move File', description: 'Move/rename files within the working directory' },
|
|
17
|
+
{ id: 'delete_file', label: 'Delete File', description: 'Delete files/directories (disabled by default)' },
|
|
18
|
+
{ id: 'edit_file', label: 'Edit File', description: 'Search-and-replace editing within files' },
|
|
19
|
+
{ id: 'process', label: 'Process', description: 'Monitor and control long-running shell commands' },
|
|
20
|
+
{ id: 'web_search', label: 'Web Search', description: 'Search the web via DuckDuckGo' },
|
|
21
|
+
{ id: 'web_fetch', label: 'Web Fetch', description: 'Fetch and extract text from URLs' },
|
|
22
|
+
{ id: 'claude_code', label: 'Claude Code', description: 'Delegate complex tasks to Claude Code CLI' },
|
|
23
|
+
{ id: 'codex_cli', label: 'Codex CLI', description: 'Delegate complex tasks to OpenAI Codex CLI' },
|
|
24
|
+
{ id: 'opencode_cli', label: 'OpenCode CLI', description: 'Delegate complex tasks to OpenCode CLI' },
|
|
25
|
+
{ id: 'browser', label: 'Browser', description: 'Playwright — browse, scrape, interact with web pages' },
|
|
26
|
+
{ id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across sessions' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const PLATFORM_TOOLS: { id: string; label: string; description: string }[] = [
|
|
30
|
+
{ id: 'manage_agents', label: 'Agents', description: 'Create, edit, and delete agents' },
|
|
31
|
+
{ id: 'manage_tasks', label: 'Tasks', description: 'Create, edit, and delete tasks' },
|
|
32
|
+
{ id: 'manage_schedules', label: 'Schedules', description: 'Create, edit, and delete schedules' },
|
|
33
|
+
{ id: 'manage_skills', label: 'Skills', description: 'Create, edit, and delete skills' },
|
|
34
|
+
{ id: 'manage_documents', label: 'Documents', description: 'Upload, search, and delete indexed documents' },
|
|
35
|
+
{ id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent sessions' },
|
|
36
|
+
{ id: 'manage_connectors', label: 'Connectors', description: 'Create, edit, and delete connectors' },
|
|
37
|
+
{ id: 'manage_sessions', label: 'Sessions', description: 'List sessions, send messages, and spawn session work' },
|
|
38
|
+
{ id: 'manage_secrets', label: 'Secrets', description: 'Store and retrieve encrypted service secrets' },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
const NATIVE_CAPABILITY_PROVIDER_IDS = new Set<ProviderType>(['claude-cli', 'codex-cli', 'opencode-cli', 'openclaw'])
|
|
42
|
+
|
|
43
|
+
export function AgentSheet() {
|
|
44
|
+
const open = useAppStore((s) => s.agentSheetOpen)
|
|
45
|
+
const setOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
46
|
+
const editingId = useAppStore((s) => s.editingAgentId)
|
|
47
|
+
const setEditingId = useAppStore((s) => s.setEditingAgentId)
|
|
48
|
+
const agents = useAppStore((s) => s.agents)
|
|
49
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
50
|
+
const providers = useAppStore((s) => s.providers)
|
|
51
|
+
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
52
|
+
const credentials = useAppStore((s) => s.credentials)
|
|
53
|
+
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
54
|
+
const dynamicSkills = useAppStore((s) => s.skills)
|
|
55
|
+
const mcpServers = useAppStore((s) => s.mcpServers)
|
|
56
|
+
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
57
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
58
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
59
|
+
|
|
60
|
+
const [claudeSkills, setClaudeSkills] = useState<ClaudeSkill[]>([])
|
|
61
|
+
const [claudeSkillsLoading, setClaudeSkillsLoading] = useState(false)
|
|
62
|
+
const loadClaudeSkills = async () => {
|
|
63
|
+
setClaudeSkillsLoading(true)
|
|
64
|
+
try {
|
|
65
|
+
const skills = await api<ClaudeSkill[]>('GET', '/claude-skills')
|
|
66
|
+
setClaudeSkills(skills)
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
finally { setClaudeSkillsLoading(false) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [name, setName] = useState('')
|
|
72
|
+
const [description, setDescription] = useState('')
|
|
73
|
+
const [soul, setSoul] = useState('')
|
|
74
|
+
const [systemPrompt, setSystemPrompt] = useState('')
|
|
75
|
+
const [provider, setProvider] = useState<ProviderType>('claude-cli')
|
|
76
|
+
const [model, setModel] = useState('')
|
|
77
|
+
const [credentialId, setCredentialId] = useState<string | null>(null)
|
|
78
|
+
const [apiEndpoint, setApiEndpoint] = useState<string | null>(null)
|
|
79
|
+
const [isOrchestrator, setIsOrchestrator] = useState(false)
|
|
80
|
+
const [subAgentIds, setAgentAgentIds] = useState<string[]>([])
|
|
81
|
+
const [tools, setTools] = useState<string[]>([])
|
|
82
|
+
const [skills, setSkills] = useState<string[]>([])
|
|
83
|
+
const [skillIds, setSkillIds] = useState<string[]>([])
|
|
84
|
+
const [mcpServerIds, setMcpServerIds] = useState<string[]>([])
|
|
85
|
+
const [mcpDisabledTools, setMcpDisabledTools] = useState<string[]>([])
|
|
86
|
+
const [mcpTools, setMcpTools] = useState<Record<string, { name: string; description: string }[]>>({})
|
|
87
|
+
const [mcpToolsLoading, setMcpToolsLoading] = useState(false)
|
|
88
|
+
const [fallbackCredentialIds, setFallbackCredentialIds] = useState<string[]>([])
|
|
89
|
+
const [platformAssignScope, setPlatformAssignScope] = useState<'self' | 'all'>('self')
|
|
90
|
+
const [capabilities, setCapabilities] = useState<string[]>([])
|
|
91
|
+
const [capInput, setCapInput] = useState('')
|
|
92
|
+
const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
|
|
93
|
+
const [openclawEnabled, setOpenclawEnabled] = useState(false)
|
|
94
|
+
const [addingKey, setAddingKey] = useState(false)
|
|
95
|
+
const [newKeyName, setNewKeyName] = useState('')
|
|
96
|
+
const [newKeyValue, setNewKeyValue] = useState('')
|
|
97
|
+
const [savingKey, setSavingKey] = useState(false)
|
|
98
|
+
|
|
99
|
+
// Test connection state
|
|
100
|
+
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'pass' | 'fail'>('idle')
|
|
101
|
+
const [testMessage, setTestMessage] = useState('')
|
|
102
|
+
const [testErrorCode, setTestErrorCode] = useState<string | null>(null)
|
|
103
|
+
const [testDeviceId, setTestDeviceId] = useState<string | null>(null)
|
|
104
|
+
|
|
105
|
+
const soulFileRef = useRef<HTMLInputElement>(null)
|
|
106
|
+
const promptFileRef = useRef<HTMLInputElement>(null)
|
|
107
|
+
const importFileRef = useRef<HTMLInputElement>(null)
|
|
108
|
+
|
|
109
|
+
const handleFileUpload = (setter: (v: string) => void) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
110
|
+
const file = e.target.files?.[0]
|
|
111
|
+
if (!file) return
|
|
112
|
+
const reader = new FileReader()
|
|
113
|
+
reader.onload = (ev) => setter(ev.target?.result as string)
|
|
114
|
+
reader.readAsText(file)
|
|
115
|
+
e.target.value = ''
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// AI generation state
|
|
119
|
+
const [aiPrompt, setAiPrompt] = useState('')
|
|
120
|
+
const [generating, setGenerating] = useState(false)
|
|
121
|
+
const [generated, setGenerated] = useState(false)
|
|
122
|
+
const [genError, setGenError] = useState('')
|
|
123
|
+
|
|
124
|
+
const currentProvider = providers.find((p) => p.id === provider)
|
|
125
|
+
const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
|
|
126
|
+
const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
|
|
127
|
+
const editing = editingId ? agents[editingId] : null
|
|
128
|
+
const hasNativeCapabilities = NATIVE_CAPABILITY_PROVIDER_IDS.has(provider)
|
|
129
|
+
|
|
130
|
+
const providerNeedsKey = !editing && (
|
|
131
|
+
(currentProvider?.requiresApiKey && providerCredentials.length === 0 && !addingKey) ||
|
|
132
|
+
(provider === 'ollama' && ollamaMode === 'cloud' && providerCredentials.length === 0 && !addingKey)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (open) {
|
|
137
|
+
loadProviders()
|
|
138
|
+
loadCredentials()
|
|
139
|
+
loadSkills()
|
|
140
|
+
loadClaudeSkills()
|
|
141
|
+
loadSettings()
|
|
142
|
+
setAiPrompt('')
|
|
143
|
+
setGenerating(false)
|
|
144
|
+
setGenerated(false)
|
|
145
|
+
setGenError('')
|
|
146
|
+
setTestStatus('idle')
|
|
147
|
+
setTestMessage('')
|
|
148
|
+
if (editing) {
|
|
149
|
+
setName(editing.name)
|
|
150
|
+
setDescription(editing.description)
|
|
151
|
+
setSoul(editing.soul || '')
|
|
152
|
+
setSystemPrompt(editing.systemPrompt)
|
|
153
|
+
setProvider(editing.provider)
|
|
154
|
+
setModel(editing.model)
|
|
155
|
+
setCredentialId(editing.credentialId || null)
|
|
156
|
+
setApiEndpoint(editing.apiEndpoint || null)
|
|
157
|
+
setIsOrchestrator(editing.isOrchestrator || false)
|
|
158
|
+
setAgentAgentIds(editing.subAgentIds || [])
|
|
159
|
+
setTools(editing.tools || [])
|
|
160
|
+
setSkills(editing.skills || [])
|
|
161
|
+
setSkillIds(editing.skillIds || [])
|
|
162
|
+
setMcpServerIds(editing.mcpServerIds || [])
|
|
163
|
+
setMcpDisabledTools(editing.mcpDisabledTools || [])
|
|
164
|
+
setFallbackCredentialIds(editing.fallbackCredentialIds || [])
|
|
165
|
+
setPlatformAssignScope(editing.platformAssignScope || 'self')
|
|
166
|
+
setCapabilities(editing.capabilities || [])
|
|
167
|
+
setCapInput('')
|
|
168
|
+
setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
|
|
169
|
+
setOpenclawEnabled(editing.provider === 'openclaw')
|
|
170
|
+
} else {
|
|
171
|
+
setName('')
|
|
172
|
+
setDescription('')
|
|
173
|
+
setSoul('')
|
|
174
|
+
setSystemPrompt('')
|
|
175
|
+
setProvider('claude-cli')
|
|
176
|
+
setModel('')
|
|
177
|
+
setCredentialId(null)
|
|
178
|
+
setApiEndpoint(null)
|
|
179
|
+
setIsOrchestrator(false)
|
|
180
|
+
setAgentAgentIds([])
|
|
181
|
+
setTools([])
|
|
182
|
+
setSkills([])
|
|
183
|
+
setSkillIds([])
|
|
184
|
+
setMcpDisabledTools([])
|
|
185
|
+
setFallbackCredentialIds([])
|
|
186
|
+
setPlatformAssignScope('self')
|
|
187
|
+
setCapabilities([])
|
|
188
|
+
setCapInput('')
|
|
189
|
+
setOllamaMode('local')
|
|
190
|
+
setOpenclawEnabled(false)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
194
|
+
}, [open, editingId])
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (currentProvider?.models.length && !editing) {
|
|
198
|
+
setModel(currentProvider.models[0])
|
|
199
|
+
}
|
|
200
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
201
|
+
}, [provider, providers])
|
|
202
|
+
|
|
203
|
+
// Reset test status when connection params change
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
setTestStatus('idle')
|
|
206
|
+
setTestMessage('')
|
|
207
|
+
}, [provider, credentialId, apiEndpoint])
|
|
208
|
+
|
|
209
|
+
// Fetch MCP tools when selected servers change
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!mcpServerIds.length) {
|
|
212
|
+
setMcpTools({})
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
let cancelled = false
|
|
216
|
+
setMcpToolsLoading(true)
|
|
217
|
+
Promise.all(
|
|
218
|
+
mcpServerIds.map(async (id) => {
|
|
219
|
+
try {
|
|
220
|
+
const tools = await api<{ name: string; description: string }[]>('GET', `/mcp-servers/${id}/tools`)
|
|
221
|
+
return { id, tools: Array.isArray(tools) ? tools : [] }
|
|
222
|
+
} catch {
|
|
223
|
+
return { id, tools: [] }
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
).then((results) => {
|
|
227
|
+
if (cancelled) return
|
|
228
|
+
const map: Record<string, { name: string; description: string }[]> = {}
|
|
229
|
+
for (const r of results) map[r.id] = r.tools
|
|
230
|
+
setMcpTools(map)
|
|
231
|
+
setMcpToolsLoading(false)
|
|
232
|
+
})
|
|
233
|
+
return () => { cancelled = true }
|
|
234
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
+
}, [mcpServerIds.join(',')])
|
|
236
|
+
|
|
237
|
+
const handleGenerate = async () => {
|
|
238
|
+
if (!aiPrompt.trim()) return
|
|
239
|
+
setGenerating(true)
|
|
240
|
+
setGenError('')
|
|
241
|
+
try {
|
|
242
|
+
const result = await api<{ name?: string; description?: string; systemPrompt?: string; isOrchestrator?: boolean; error?: string }>('POST', '/agents/generate', { prompt: aiPrompt })
|
|
243
|
+
if (result.error) {
|
|
244
|
+
setGenError(result.error)
|
|
245
|
+
} else if (result.name || result.systemPrompt) {
|
|
246
|
+
if (result.name) setName(result.name)
|
|
247
|
+
if (result.description) setDescription(result.description)
|
|
248
|
+
if (result.systemPrompt) setSystemPrompt(result.systemPrompt)
|
|
249
|
+
if (result.isOrchestrator !== undefined) setIsOrchestrator(result.isOrchestrator)
|
|
250
|
+
setGenerated(true)
|
|
251
|
+
} else {
|
|
252
|
+
setGenError('AI returned empty response — try again')
|
|
253
|
+
}
|
|
254
|
+
} catch (err: unknown) {
|
|
255
|
+
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
256
|
+
}
|
|
257
|
+
setGenerating(false)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const onClose = () => {
|
|
261
|
+
setOpen(false)
|
|
262
|
+
setEditingId(null)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const handleSave = async () => {
|
|
266
|
+
// For any endpoint, just ensure bare host:port gets a protocol prepended
|
|
267
|
+
let normalizedEndpoint = apiEndpoint
|
|
268
|
+
if (normalizedEndpoint) {
|
|
269
|
+
const url = normalizedEndpoint.trim().replace(/\/+$/, '')
|
|
270
|
+
normalizedEndpoint = /^(https?|wss?):\/\//i.test(url) ? url : `http://${url}`
|
|
271
|
+
}
|
|
272
|
+
const data = {
|
|
273
|
+
name: name.trim() || 'Unnamed Agent',
|
|
274
|
+
description,
|
|
275
|
+
soul,
|
|
276
|
+
systemPrompt,
|
|
277
|
+
provider,
|
|
278
|
+
model,
|
|
279
|
+
credentialId,
|
|
280
|
+
apiEndpoint: normalizedEndpoint,
|
|
281
|
+
isOrchestrator,
|
|
282
|
+
subAgentIds: isOrchestrator ? subAgentIds : [],
|
|
283
|
+
tools,
|
|
284
|
+
skills,
|
|
285
|
+
skillIds,
|
|
286
|
+
mcpServerIds,
|
|
287
|
+
mcpDisabledTools: mcpDisabledTools.length ? mcpDisabledTools : undefined,
|
|
288
|
+
fallbackCredentialIds,
|
|
289
|
+
platformAssignScope,
|
|
290
|
+
capabilities,
|
|
291
|
+
}
|
|
292
|
+
if (editing) {
|
|
293
|
+
await updateAgent(editing.id, data)
|
|
294
|
+
} else {
|
|
295
|
+
await createAgent(data)
|
|
296
|
+
}
|
|
297
|
+
await loadAgents()
|
|
298
|
+
onClose()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const handleDelete = async () => {
|
|
302
|
+
if (editing) {
|
|
303
|
+
await deleteAgent(editing.id)
|
|
304
|
+
await loadAgents()
|
|
305
|
+
onClose()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const handleExport = () => {
|
|
310
|
+
if (!editing) return
|
|
311
|
+
const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...exportData } = editing
|
|
312
|
+
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
|
313
|
+
const url = URL.createObjectURL(blob)
|
|
314
|
+
const a = document.createElement('a')
|
|
315
|
+
a.href = url
|
|
316
|
+
a.download = `${editing.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.agent.json`
|
|
317
|
+
a.click()
|
|
318
|
+
URL.revokeObjectURL(url)
|
|
319
|
+
toast.success('Agent exported')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
323
|
+
const file = e.target.files?.[0]
|
|
324
|
+
if (!file) return
|
|
325
|
+
const reader = new FileReader()
|
|
326
|
+
reader.onload = async (ev) => {
|
|
327
|
+
try {
|
|
328
|
+
const data = JSON.parse(ev.target?.result as string)
|
|
329
|
+
// Strip IDs and timestamps
|
|
330
|
+
const { id: _id, createdAt: _ca, updatedAt: _ua, threadSessionId: _ts, ...agentData } = data
|
|
331
|
+
await createAgent({ ...agentData, name: agentData.name || 'Imported Agent' })
|
|
332
|
+
await loadAgents()
|
|
333
|
+
toast.success('Agent imported')
|
|
334
|
+
onClose()
|
|
335
|
+
} catch (err) {
|
|
336
|
+
toast.error('Invalid agent JSON file')
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
reader.readAsText(file)
|
|
340
|
+
e.target.value = ''
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const handleTestConnection = async (): Promise<boolean> => {
|
|
344
|
+
setTestStatus('testing')
|
|
345
|
+
setTestMessage('')
|
|
346
|
+
setTestErrorCode(null)
|
|
347
|
+
try {
|
|
348
|
+
const result = await api<{ ok: boolean; message: string; errorCode?: string; deviceId?: string }>('POST', '/setup/check-provider', {
|
|
349
|
+
provider,
|
|
350
|
+
credentialId,
|
|
351
|
+
endpoint: apiEndpoint,
|
|
352
|
+
model,
|
|
353
|
+
})
|
|
354
|
+
if (result.deviceId) setTestDeviceId(result.deviceId)
|
|
355
|
+
if (result.ok) {
|
|
356
|
+
setTestStatus('pass')
|
|
357
|
+
setTestMessage(result.message)
|
|
358
|
+
return true
|
|
359
|
+
} else {
|
|
360
|
+
setTestStatus('fail')
|
|
361
|
+
setTestMessage(result.message)
|
|
362
|
+
setTestErrorCode(result.errorCode || null)
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
} catch (err: unknown) {
|
|
366
|
+
setTestStatus('fail')
|
|
367
|
+
setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Whether this provider needs a connection test before saving.
|
|
373
|
+
// Only CLI providers (no remote connection) skip the test.
|
|
374
|
+
const CLI_ONLY_PROVIDERS: Set<ProviderType> = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
375
|
+
const needsTest = !providerNeedsKey && !CLI_ONLY_PROVIDERS.has(provider)
|
|
376
|
+
|
|
377
|
+
const [saving, setSaving] = useState(false)
|
|
378
|
+
|
|
379
|
+
const handleTestAndSave = async () => {
|
|
380
|
+
if (needsTest) {
|
|
381
|
+
const passed = await handleTestConnection()
|
|
382
|
+
if (!passed) return
|
|
383
|
+
// Brief pause so the user can see the success state on the button
|
|
384
|
+
await new Promise((r) => setTimeout(r, 1500))
|
|
385
|
+
}
|
|
386
|
+
setSaving(true)
|
|
387
|
+
await handleSave()
|
|
388
|
+
setSaving(false)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const agentOptions = Object.values(agents).filter((p) => !p.isOrchestrator && p.id !== editingId)
|
|
392
|
+
|
|
393
|
+
const toggleAgent = (id: string) => {
|
|
394
|
+
setAgentAgentIds((prev) =>
|
|
395
|
+
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
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"
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<BottomSheet open={open} onClose={onClose} wide>
|
|
403
|
+
<div className="mb-10">
|
|
404
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
405
|
+
{editing ? 'Edit Agent' : 'New Agent'}
|
|
406
|
+
</h2>
|
|
407
|
+
<p className="text-[14px] text-text-3">Define an AI agent or orchestrator</p>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{/* AI Generation */}
|
|
411
|
+
{!editing && <AiGenBlock
|
|
412
|
+
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
413
|
+
generating={generating} generated={generated} genError={genError}
|
|
414
|
+
onGenerate={handleGenerate} appSettings={appSettings}
|
|
415
|
+
placeholder='Describe the agent you want, e.g. "An SEO keyword researcher that finds low-competition long-tail keywords"'
|
|
416
|
+
/>}
|
|
417
|
+
|
|
418
|
+
<div className="mb-8">
|
|
419
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
|
|
420
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<div className="mb-8">
|
|
424
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Description</label>
|
|
425
|
+
<input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
{/* Capabilities */}
|
|
429
|
+
<div className="mb-8">
|
|
430
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
431
|
+
Capabilities <span className="normal-case tracking-normal font-normal text-text-3">(for agent delegation)</span>
|
|
432
|
+
</label>
|
|
433
|
+
<div className="flex flex-wrap gap-1.5 mb-2">
|
|
434
|
+
{capabilities.map((cap) => (
|
|
435
|
+
<span
|
|
436
|
+
key={cap}
|
|
437
|
+
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-accent-soft text-accent-bright text-[12px] font-600"
|
|
438
|
+
>
|
|
439
|
+
{cap}
|
|
440
|
+
<button
|
|
441
|
+
onClick={() => setCapabilities((prev) => prev.filter((c) => c !== cap))}
|
|
442
|
+
className="bg-transparent border-none text-accent-bright/60 hover:text-accent-bright cursor-pointer text-[14px] leading-none p-0"
|
|
443
|
+
>
|
|
444
|
+
x
|
|
445
|
+
</button>
|
|
446
|
+
</span>
|
|
447
|
+
))}
|
|
448
|
+
</div>
|
|
449
|
+
<div className="flex gap-2">
|
|
450
|
+
<input
|
|
451
|
+
type="text"
|
|
452
|
+
value={capInput}
|
|
453
|
+
onChange={(e) => setCapInput(e.target.value)}
|
|
454
|
+
onKeyDown={(e) => {
|
|
455
|
+
if ((e.key === 'Enter' || e.key === ',') && capInput.trim()) {
|
|
456
|
+
e.preventDefault()
|
|
457
|
+
const val = capInput.trim().toLowerCase().replace(/,/g, '')
|
|
458
|
+
if (val && !capabilities.includes(val)) {
|
|
459
|
+
setCapabilities((prev) => [...prev, val])
|
|
460
|
+
}
|
|
461
|
+
setCapInput('')
|
|
462
|
+
}
|
|
463
|
+
}}
|
|
464
|
+
placeholder="e.g. frontend, research, devops"
|
|
465
|
+
className={inputClass}
|
|
466
|
+
style={{ fontFamily: 'inherit' }}
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
<p className="text-[11px] text-text-3/70 mt-1.5">Press Enter or comma to add. Other agents see these when deciding delegation.</p>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
{provider !== 'openclaw' && (
|
|
473
|
+
<div className="mb-8">
|
|
474
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
475
|
+
Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
476
|
+
</label>
|
|
477
|
+
<div className="flex items-center gap-2 mb-3">
|
|
478
|
+
<p className="text-[12px] text-text-3/60">Define the agent's voice, tone, and personality. Injected before the system prompt.</p>
|
|
479
|
+
<button onClick={() => soulFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
|
|
480
|
+
<input ref={soulFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSoul)} className="hidden" />
|
|
481
|
+
</div>
|
|
482
|
+
<textarea
|
|
483
|
+
value={soul}
|
|
484
|
+
onChange={(e) => setSoul(e.target.value)}
|
|
485
|
+
placeholder="e.g. You speak concisely and directly. You have a dry sense of humor. You always back claims with data."
|
|
486
|
+
rows={3}
|
|
487
|
+
className={`${inputClass} resize-y min-h-[80px]`}
|
|
488
|
+
style={{ fontFamily: 'inherit' }}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{provider !== 'openclaw' && (
|
|
494
|
+
<div className="mb-8">
|
|
495
|
+
<div className="flex items-center gap-2 mb-3">
|
|
496
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">System Prompt</label>
|
|
497
|
+
<button onClick={() => promptFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
|
|
498
|
+
<input ref={promptFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSystemPrompt)} className="hidden" />
|
|
499
|
+
</div>
|
|
500
|
+
<textarea
|
|
501
|
+
value={systemPrompt}
|
|
502
|
+
onChange={(e) => setSystemPrompt(e.target.value)}
|
|
503
|
+
placeholder="You are an expert..."
|
|
504
|
+
rows={5}
|
|
505
|
+
className={`${inputClass} resize-y min-h-[120px]`}
|
|
506
|
+
style={{ fontFamily: 'inherit' }}
|
|
507
|
+
/>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
{/* OpenClaw Gateway Toggle */}
|
|
512
|
+
<div className="mb-8">
|
|
513
|
+
<div className="flex items-center justify-between">
|
|
514
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">OpenClaw Gateway</label>
|
|
515
|
+
<button
|
|
516
|
+
type="button"
|
|
517
|
+
onClick={() => {
|
|
518
|
+
if (!openclawEnabled) {
|
|
519
|
+
setOpenclawEnabled(true)
|
|
520
|
+
setProvider('openclaw')
|
|
521
|
+
setModel('default')
|
|
522
|
+
if (!apiEndpoint) setApiEndpoint('http://localhost:18789')
|
|
523
|
+
} else {
|
|
524
|
+
setOpenclawEnabled(false)
|
|
525
|
+
setProvider('claude-cli')
|
|
526
|
+
setModel('')
|
|
527
|
+
setApiEndpoint(null)
|
|
528
|
+
setCredentialId(null)
|
|
529
|
+
}
|
|
530
|
+
}}
|
|
531
|
+
className={`relative w-11 h-6 rounded-full transition-colors duration-200 cursor-pointer border-none ${openclawEnabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
532
|
+
>
|
|
533
|
+
<span className={`absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform duration-200 ${openclawEnabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
534
|
+
</button>
|
|
535
|
+
</div>
|
|
536
|
+
{openclawEnabled && (
|
|
537
|
+
<div className="mt-4 space-y-4">
|
|
538
|
+
<div>
|
|
539
|
+
<label className="block text-[12px] text-text-3 mb-2">Gateway URL</label>
|
|
540
|
+
<input
|
|
541
|
+
type="text"
|
|
542
|
+
value={apiEndpoint || ''}
|
|
543
|
+
onChange={(e) => setApiEndpoint(e.target.value || null)}
|
|
544
|
+
placeholder="http://localhost:18789"
|
|
545
|
+
className={inputClass}
|
|
546
|
+
style={{ fontFamily: 'inherit' }}
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
<div>
|
|
550
|
+
<label className="block text-[12px] text-text-3 mb-2">Gateway Token</label>
|
|
551
|
+
{openclawCredentials.length > 0 && !addingKey ? (
|
|
552
|
+
<div className="flex gap-2">
|
|
553
|
+
<select value={credentialId || ''} onChange={(e) => {
|
|
554
|
+
if (e.target.value === '__add__') {
|
|
555
|
+
setAddingKey(true)
|
|
556
|
+
setNewKeyName('')
|
|
557
|
+
setNewKeyValue('')
|
|
558
|
+
} else {
|
|
559
|
+
setCredentialId(e.target.value || null)
|
|
560
|
+
}
|
|
561
|
+
}} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
|
|
562
|
+
<option value="">Select a token...</option>
|
|
563
|
+
{openclawCredentials.map((c) => (
|
|
564
|
+
<option key={c.id} value={c.id}>{c.name}</option>
|
|
565
|
+
))}
|
|
566
|
+
<option value="__add__">+ Add new token...</option>
|
|
567
|
+
</select>
|
|
568
|
+
<button
|
|
569
|
+
type="button"
|
|
570
|
+
onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
|
|
571
|
+
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"
|
|
572
|
+
>
|
|
573
|
+
+ New
|
|
574
|
+
</button>
|
|
575
|
+
</div>
|
|
576
|
+
) : (
|
|
577
|
+
<div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
|
|
578
|
+
<input
|
|
579
|
+
type="text"
|
|
580
|
+
value={newKeyName}
|
|
581
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
582
|
+
placeholder="Token name (optional)"
|
|
583
|
+
className={inputClass}
|
|
584
|
+
style={{ fontFamily: 'inherit' }}
|
|
585
|
+
/>
|
|
586
|
+
<input
|
|
587
|
+
type="password"
|
|
588
|
+
value={newKeyValue}
|
|
589
|
+
onChange={(e) => setNewKeyValue(e.target.value)}
|
|
590
|
+
placeholder="Paste gateway token..."
|
|
591
|
+
className={inputClass}
|
|
592
|
+
style={{ fontFamily: 'inherit' }}
|
|
593
|
+
/>
|
|
594
|
+
<div className="flex gap-2 justify-end">
|
|
595
|
+
{openclawCredentials.length > 0 && (
|
|
596
|
+
<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>
|
|
597
|
+
)}
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
disabled={savingKey || !newKeyValue.trim()}
|
|
601
|
+
onClick={async () => {
|
|
602
|
+
setSavingKey(true)
|
|
603
|
+
try {
|
|
604
|
+
const cred = await api<any>('POST', '/credentials', { provider: 'openclaw', name: newKeyName.trim() || 'OpenClaw token', apiKey: newKeyValue.trim() })
|
|
605
|
+
await loadCredentials()
|
|
606
|
+
setCredentialId(cred.id)
|
|
607
|
+
setAddingKey(false)
|
|
608
|
+
setNewKeyName('')
|
|
609
|
+
setNewKeyValue('')
|
|
610
|
+
} catch (err: any) { toast.error(`Failed to save: ${err.message}`) }
|
|
611
|
+
finally { setSavingKey(false) }
|
|
612
|
+
}}
|
|
613
|
+
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"
|
|
614
|
+
style={{ fontFamily: 'inherit' }}
|
|
615
|
+
>
|
|
616
|
+
{savingKey ? 'Saving...' : 'Save Token'}
|
|
617
|
+
</button>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
<p className="text-[11px] text-text-3/70">Enter the URL and token for your local or remote OpenClaw gateway.</p>
|
|
623
|
+
{/* Insecure connection warning */}
|
|
624
|
+
{(() => {
|
|
625
|
+
const url = (apiEndpoint || '').trim().toLowerCase()
|
|
626
|
+
const isRemote = url && !/localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(url)
|
|
627
|
+
const isSecure = /^(https|wss):\/\//i.test(url)
|
|
628
|
+
if (isRemote && !isSecure) return (
|
|
629
|
+
<div className="mt-3 p-3 rounded-[10px] bg-[#fbbf24]/10 border border-[#fbbf24]/30">
|
|
630
|
+
<p className="text-[11px] text-[#fbbf24] font-500 leading-[1.5]">
|
|
631
|
+
This connection is not encrypted. Credentials and chat data could be intercepted on the network.
|
|
632
|
+
For production use, put your gateway behind HTTPS (e.g. Caddy, nginx) or use an SSH tunnel.
|
|
633
|
+
</p>
|
|
634
|
+
</div>
|
|
635
|
+
)
|
|
636
|
+
return null
|
|
637
|
+
})()}
|
|
638
|
+
{/* OpenClaw troubleshooting — shown on any test failure */}
|
|
639
|
+
{testStatus === 'fail' && openclawEnabled && (
|
|
640
|
+
<div className="mt-3 p-4 rounded-[12px] bg-accent-soft/30 border border-accent-bright/20">
|
|
641
|
+
{testErrorCode === 'PAIRING_REQUIRED' ? (
|
|
642
|
+
<div className="space-y-2">
|
|
643
|
+
<p className="text-[12px] text-accent-bright font-600 mb-1">Device Pairing Required</p>
|
|
644
|
+
<p className="text-[11px] text-text-3 leading-[1.6]">
|
|
645
|
+
Your gateway needs to approve this SwarmClaw instance before it can connect. Follow these steps:
|
|
646
|
+
</p>
|
|
647
|
+
<ol className="text-[11px] text-text-3 leading-[1.8] list-decimal list-inside space-y-1">
|
|
648
|
+
<li>Open your OpenClaw control UI at <span className="text-accent-bright font-500">{apiEndpoint || 'http://localhost:18789'}</span></li>
|
|
649
|
+
<li>Go to <span className="text-text-2 font-500">Devices</span></li>
|
|
650
|
+
<li>Find and approve the pending device {testDeviceId ? <span className="text-text-2 font-mono text-[10px]">({testDeviceId.slice(0, 12)}...)</span> : null}</li>
|
|
651
|
+
<li>Come back here and click <span className="text-text-2 font-500">Test & Save</span> again</li>
|
|
652
|
+
</ol>
|
|
653
|
+
</div>
|
|
654
|
+
) : testErrorCode === 'DEVICE_AUTH_INVALID' ? (
|
|
655
|
+
<div className="space-y-2">
|
|
656
|
+
<p className="text-[12px] text-accent-bright font-600 mb-1">Device Authentication Failed</p>
|
|
657
|
+
<p className="text-[11px] text-text-3 leading-[1.6]">
|
|
658
|
+
The gateway rejected this device's signature. This usually means it needs to be paired first, or there's a protocol mismatch.
|
|
659
|
+
</p>
|
|
660
|
+
<p className="text-[11px] text-text-3 font-500 mt-2 mb-1">Try these steps:</p>
|
|
661
|
+
<ol className="text-[11px] text-text-3 leading-[1.8] list-decimal list-inside space-y-1">
|
|
662
|
+
<li>Open your OpenClaw control UI at <span className="text-accent-bright font-500">{apiEndpoint || 'http://localhost:18789'}</span></li>
|
|
663
|
+
<li>Go to <span className="text-text-2 font-500">Devices</span> and look for a pending device request</li>
|
|
664
|
+
<li>If the device is listed, approve it and click <span className="text-text-2 font-500">Test & Save</span> again</li>
|
|
665
|
+
<li>If not listed, update your gateway to the latest version and restart it</li>
|
|
666
|
+
</ol>
|
|
667
|
+
</div>
|
|
668
|
+
) : (
|
|
669
|
+
<div className="space-y-2">
|
|
670
|
+
<p className="text-[12px] text-accent-bright font-600 mb-1">Connection Failed</p>
|
|
671
|
+
<p className="text-[11px] text-text-3 leading-[1.6]">
|
|
672
|
+
Could not connect to the OpenClaw gateway. Check the following:
|
|
673
|
+
</p>
|
|
674
|
+
<ul className="text-[11px] text-text-3 leading-[1.8] list-disc list-inside space-y-1">
|
|
675
|
+
<li>The gateway is running and reachable at the URL above</li>
|
|
676
|
+
<li>The gateway token matches exactly (if required)</li>
|
|
677
|
+
<li>No firewall is blocking the connection</li>
|
|
678
|
+
</ul>
|
|
679
|
+
</div>
|
|
680
|
+
)}
|
|
681
|
+
{testDeviceId && (
|
|
682
|
+
<div className="mt-3 pt-3 border-t border-white/[0.06]">
|
|
683
|
+
<p className="text-[10px] text-text-3/60">
|
|
684
|
+
SwarmClaw Device ID: <span className="font-mono text-text-3/80 select-all">{testDeviceId}</span>
|
|
685
|
+
</p>
|
|
686
|
+
</div>
|
|
687
|
+
)}
|
|
688
|
+
</div>
|
|
689
|
+
)}
|
|
690
|
+
{/* Device ID info — shown after any successful OpenClaw test */}
|
|
691
|
+
{testStatus === 'pass' && openclawEnabled && testDeviceId && (
|
|
692
|
+
<div className="mt-3 px-3 py-2 rounded-[10px] bg-emerald-500/[0.06] border border-emerald-500/15">
|
|
693
|
+
<p className="text-[10px] text-text-3/60">
|
|
694
|
+
Device ID: <span className="font-mono text-emerald-400/70 select-all">{testDeviceId.slice(0, 16)}...</span>
|
|
695
|
+
</p>
|
|
696
|
+
</div>
|
|
697
|
+
)}
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
{!openclawEnabled && <div className="mb-8">
|
|
703
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Provider</label>
|
|
704
|
+
<div className="grid grid-cols-3 gap-3">
|
|
705
|
+
{providers.filter((p) => !isOrchestrator || p.id !== 'claude-cli').map((p) => {
|
|
706
|
+
const isConnected = !p.requiresApiKey || Object.values(credentials).some((c) => c.provider === p.id)
|
|
707
|
+
return (
|
|
708
|
+
<button
|
|
709
|
+
key={p.id}
|
|
710
|
+
onClick={() => {
|
|
711
|
+
setProvider(p.id)
|
|
712
|
+
}}
|
|
713
|
+
className={`relative py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
|
|
714
|
+
active:scale-[0.97] text-[14px] font-600 border
|
|
715
|
+
${provider === p.id
|
|
716
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
717
|
+
: 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
|
|
718
|
+
style={{ fontFamily: 'inherit' }}
|
|
719
|
+
>
|
|
720
|
+
{isConnected && (
|
|
721
|
+
<span className="absolute top-2 right-2 w-2 h-2 rounded-full bg-emerald-400" />
|
|
722
|
+
)}
|
|
723
|
+
{p.name}
|
|
724
|
+
</button>
|
|
725
|
+
)
|
|
726
|
+
})}
|
|
727
|
+
</div>
|
|
728
|
+
</div>}
|
|
729
|
+
|
|
730
|
+
{!openclawEnabled && currentProvider && currentProvider.models.length > 0 && (
|
|
731
|
+
<div className="mb-8">
|
|
732
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Model</label>
|
|
733
|
+
<select value={model} onChange={(e) => setModel(e.target.value)} className={`${inputClass} appearance-none cursor-pointer`} style={{ fontFamily: 'inherit' }}>
|
|
734
|
+
{currentProvider.models.map((m) => (
|
|
735
|
+
<option key={m} value={m}>{m}</option>
|
|
736
|
+
))}
|
|
737
|
+
</select>
|
|
738
|
+
</div>
|
|
739
|
+
)}
|
|
740
|
+
|
|
741
|
+
{/* Ollama Mode Toggle */}
|
|
742
|
+
{!openclawEnabled && provider === 'ollama' && (
|
|
743
|
+
<div className="mb-8">
|
|
744
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Mode</label>
|
|
745
|
+
<div className="flex p-1 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
746
|
+
{(['local', 'cloud'] as const).map((mode) => (
|
|
747
|
+
<button
|
|
748
|
+
key={mode}
|
|
749
|
+
onClick={() => {
|
|
750
|
+
setOllamaMode(mode)
|
|
751
|
+
if (mode === 'local') {
|
|
752
|
+
setApiEndpoint('http://localhost:11434')
|
|
753
|
+
setCredentialId(null)
|
|
754
|
+
} else {
|
|
755
|
+
setApiEndpoint(null)
|
|
756
|
+
if (providerCredentials.length > 0) setCredentialId(providerCredentials[0].id)
|
|
757
|
+
}
|
|
758
|
+
}}
|
|
759
|
+
className={`flex-1 py-3 rounded-[12px] text-center cursor-pointer transition-all duration-200
|
|
760
|
+
text-[14px] font-600 capitalize
|
|
761
|
+
${ollamaMode === mode
|
|
762
|
+
? 'bg-accent-soft text-accent-bright shadow-[0_0_20px_rgba(99,102,241,0.1)]'
|
|
763
|
+
: 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
764
|
+
style={{ fontFamily: 'inherit' }}
|
|
765
|
+
>
|
|
766
|
+
{mode}
|
|
767
|
+
</button>
|
|
768
|
+
))}
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
)}
|
|
772
|
+
|
|
773
|
+
{!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && (
|
|
774
|
+
<div className="mb-8">
|
|
775
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
776
|
+
API Key{currentProvider?.optionalApiKey && !currentProvider?.requiresApiKey && <span className="normal-case tracking-normal font-normal text-text-3"> (optional)</span>}
|
|
777
|
+
</label>
|
|
778
|
+
{providerCredentials.length > 0 && !addingKey ? (
|
|
779
|
+
<div className="flex gap-2">
|
|
780
|
+
<select value={credentialId || ''} onChange={(e) => {
|
|
781
|
+
if (e.target.value === '__add__') {
|
|
782
|
+
setAddingKey(true)
|
|
783
|
+
setNewKeyName('')
|
|
784
|
+
setNewKeyValue('')
|
|
785
|
+
} else {
|
|
786
|
+
setCredentialId(e.target.value || null)
|
|
787
|
+
}
|
|
788
|
+
}} className={`${inputClass} appearance-none cursor-pointer flex-1`} style={{ fontFamily: 'inherit' }}>
|
|
789
|
+
<option value="">Select a key...</option>
|
|
790
|
+
{providerCredentials.map((c) => (
|
|
791
|
+
<option key={c.id} value={c.id}>{c.name}</option>
|
|
792
|
+
))}
|
|
793
|
+
<option value="__add__">+ Add new key...</option>
|
|
794
|
+
</select>
|
|
795
|
+
<button
|
|
796
|
+
type="button"
|
|
797
|
+
onClick={() => { setAddingKey(true); setNewKeyName(''); setNewKeyValue('') }}
|
|
798
|
+
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"
|
|
799
|
+
>
|
|
800
|
+
+ New
|
|
801
|
+
</button>
|
|
802
|
+
</div>
|
|
803
|
+
) : (
|
|
804
|
+
<div className="space-y-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20">
|
|
805
|
+
<input
|
|
806
|
+
type="text"
|
|
807
|
+
value={newKeyName}
|
|
808
|
+
onChange={(e) => setNewKeyName(e.target.value)}
|
|
809
|
+
placeholder="Key name (optional)"
|
|
810
|
+
className={inputClass}
|
|
811
|
+
style={{ fontFamily: 'inherit' }}
|
|
812
|
+
/>
|
|
813
|
+
<input
|
|
814
|
+
type="password"
|
|
815
|
+
value={newKeyValue}
|
|
816
|
+
onChange={(e) => setNewKeyValue(e.target.value)}
|
|
817
|
+
placeholder="Paste API key..."
|
|
818
|
+
className={inputClass}
|
|
819
|
+
style={{ fontFamily: 'inherit' }}
|
|
820
|
+
/>
|
|
821
|
+
<div className="flex gap-2 justify-end">
|
|
822
|
+
{providerCredentials.length > 0 && (
|
|
823
|
+
<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>
|
|
824
|
+
)}
|
|
825
|
+
<button
|
|
826
|
+
type="button"
|
|
827
|
+
disabled={savingKey || !newKeyValue.trim()}
|
|
828
|
+
onClick={async () => {
|
|
829
|
+
setSavingKey(true)
|
|
830
|
+
try {
|
|
831
|
+
const cred = await api<any>('POST', '/credentials', { provider, name: newKeyName.trim() || `${provider} key`, apiKey: newKeyValue.trim() })
|
|
832
|
+
await loadCredentials()
|
|
833
|
+
setCredentialId(cred.id)
|
|
834
|
+
setAddingKey(false)
|
|
835
|
+
setNewKeyName('')
|
|
836
|
+
setNewKeyValue('')
|
|
837
|
+
} catch (err: any) { toast.error(`Failed to save: ${err.message}`) }
|
|
838
|
+
finally { setSavingKey(false) }
|
|
839
|
+
}}
|
|
840
|
+
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"
|
|
841
|
+
style={{ fontFamily: 'inherit' }}
|
|
842
|
+
>
|
|
843
|
+
{savingKey ? 'Saving...' : 'Save Key'}
|
|
844
|
+
</button>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
848
|
+
</div>
|
|
849
|
+
)}
|
|
850
|
+
|
|
851
|
+
{/* Fallback Credentials */}
|
|
852
|
+
{!openclawEnabled && (currentProvider?.requiresApiKey || currentProvider?.optionalApiKey || (provider === 'ollama' && ollamaMode === 'cloud')) && providerCredentials.length > 1 && (
|
|
853
|
+
<div className="mb-8">
|
|
854
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
855
|
+
Fallback Keys <span className="normal-case tracking-normal font-normal text-text-3">(for auto-failover)</span>
|
|
856
|
+
</label>
|
|
857
|
+
<p className="text-[12px] text-text-3/60 mb-3">If the primary key fails (rate limit, auth error), these keys will be tried in order.</p>
|
|
858
|
+
<div className="flex flex-wrap gap-2">
|
|
859
|
+
{providerCredentials.filter((c) => c.id !== credentialId).map((c) => {
|
|
860
|
+
const active = fallbackCredentialIds.includes(c.id)
|
|
861
|
+
return (
|
|
862
|
+
<button
|
|
863
|
+
key={c.id}
|
|
864
|
+
onClick={() => setFallbackCredentialIds((prev) => active ? prev.filter((x) => x !== c.id) : [...prev, c.id])}
|
|
865
|
+
className={`px-3 py-2 rounded-[10px] text-[12px] font-600 cursor-pointer transition-all border
|
|
866
|
+
${active
|
|
867
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
868
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
869
|
+
style={{ fontFamily: 'inherit' }}
|
|
870
|
+
>
|
|
871
|
+
{c.name}
|
|
872
|
+
</button>
|
|
873
|
+
)
|
|
874
|
+
})}
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
|
|
879
|
+
{currentProvider?.requiresEndpoint && (provider === 'openclaw' || (provider === 'ollama' && ollamaMode === 'local')) && (
|
|
880
|
+
<div className="mb-8">
|
|
881
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
882
|
+
{provider === 'openclaw' ? 'OpenClaw Endpoint' : 'Endpoint'}
|
|
883
|
+
</label>
|
|
884
|
+
<input type="text" value={apiEndpoint || ''} onChange={(e) => setApiEndpoint(e.target.value || null)} placeholder={currentProvider.defaultEndpoint || 'http://localhost:11434'} className={`${inputClass} font-mono text-[14px]`} />
|
|
885
|
+
{provider === 'openclaw' && (
|
|
886
|
+
<p className="text-[11px] text-text-3/60 mt-2">The /v1 endpoint of your remote OpenClaw instance</p>
|
|
887
|
+
)}
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
890
|
+
|
|
891
|
+
{/* Tools — hidden for providers that manage capabilities outside LangGraph */}
|
|
892
|
+
{!hasNativeCapabilities && (
|
|
893
|
+
<div className="mb-8">
|
|
894
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Tools</label>
|
|
895
|
+
<p className="text-[12px] text-text-3/60 mb-3">Enable tools for LangGraph agent sessions.</p>
|
|
896
|
+
<div className="space-y-3">
|
|
897
|
+
{AVAILABLE_TOOLS.map((t) => (
|
|
898
|
+
<label key={t.id} className="flex items-center gap-3 cursor-pointer">
|
|
899
|
+
<div
|
|
900
|
+
onClick={() => setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
|
|
901
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
902
|
+
${tools.includes(t.id) ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
903
|
+
>
|
|
904
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
905
|
+
${tools.includes(t.id) ? 'left-[22px]' : 'left-0.5'}`} />
|
|
906
|
+
</div>
|
|
907
|
+
<span className="font-display text-[14px] font-600 text-text-2">{t.label}</span>
|
|
908
|
+
<span className="text-[12px] text-text-3">{t.description}</span>
|
|
909
|
+
</label>
|
|
910
|
+
))}
|
|
911
|
+
</div>
|
|
912
|
+
</div>
|
|
913
|
+
)}
|
|
914
|
+
|
|
915
|
+
{/* Platform — hidden for providers that manage capabilities outside LangGraph */}
|
|
916
|
+
{!hasNativeCapabilities && (
|
|
917
|
+
<div className="mb-8">
|
|
918
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Platform</label>
|
|
919
|
+
<p className="text-[12px] text-text-3/60 mb-3">Allow this agent to manage platform resources directly.</p>
|
|
920
|
+
<div className="space-y-3">
|
|
921
|
+
{PLATFORM_TOOLS.map((t) => (
|
|
922
|
+
<label key={t.id} className="flex items-center gap-3 cursor-pointer">
|
|
923
|
+
<div
|
|
924
|
+
onClick={() => setTools((prev) => prev.includes(t.id) ? prev.filter((x) => x !== t.id) : [...prev, t.id])}
|
|
925
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
926
|
+
${tools.includes(t.id) ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
927
|
+
>
|
|
928
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
929
|
+
${tools.includes(t.id) ? 'left-[22px]' : 'left-0.5'}`} />
|
|
930
|
+
</div>
|
|
931
|
+
<span className="font-display text-[14px] font-600 text-text-2">{t.label}</span>
|
|
932
|
+
<span className="text-[12px] text-text-3">{t.description}</span>
|
|
933
|
+
</label>
|
|
934
|
+
))}
|
|
935
|
+
</div>
|
|
936
|
+
{(tools.includes('manage_tasks') || tools.includes('manage_schedules')) && (
|
|
937
|
+
<div className="mt-4 ml-1 pt-3 border-t border-white/[0.04]">
|
|
938
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
939
|
+
<div
|
|
940
|
+
onClick={() => setPlatformAssignScope((prev) => prev === 'all' ? 'self' : 'all')}
|
|
941
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
942
|
+
${platformAssignScope === 'all' ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
943
|
+
>
|
|
944
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
945
|
+
${platformAssignScope === 'all' ? 'left-[22px]' : 'left-0.5'}`} />
|
|
946
|
+
</div>
|
|
947
|
+
<span className="font-display text-[14px] font-600 text-text-2">Assign to Other Agents</span>
|
|
948
|
+
<span className="text-[12px] text-text-3">Allow this agent to assign tasks and schedules to other agents</span>
|
|
949
|
+
</label>
|
|
950
|
+
</div>
|
|
951
|
+
)}
|
|
952
|
+
</div>
|
|
953
|
+
)}
|
|
954
|
+
|
|
955
|
+
{/* Native capability provider note */}
|
|
956
|
+
{hasNativeCapabilities && (
|
|
957
|
+
<div className="mb-8 p-4 rounded-[14px] bg-white/[0.02] border border-white/[0.06]">
|
|
958
|
+
<p className="text-[13px] text-text-3">
|
|
959
|
+
{provider === 'openclaw'
|
|
960
|
+
? 'OpenClaw manages tools/platform capabilities in the remote OpenClaw instance — no local tool toggles are applied here.'
|
|
961
|
+
: provider === 'claude-cli'
|
|
962
|
+
? 'Claude CLI uses its own built-in capabilities — no additional local tool/platform configuration is needed.'
|
|
963
|
+
: provider === 'codex-cli'
|
|
964
|
+
? 'OpenAI Codex CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'
|
|
965
|
+
: 'OpenCode CLI uses its own built-in tools (shell, files, etc.) — no additional local tool configuration needed.'}
|
|
966
|
+
</p>
|
|
967
|
+
</div>
|
|
968
|
+
)}
|
|
969
|
+
|
|
970
|
+
{/* Skills — discovered from ~/.claude/skills/ */}
|
|
971
|
+
{provider === 'claude-cli' && (
|
|
972
|
+
<div className="mb-8">
|
|
973
|
+
<div className="flex items-center justify-between mb-2">
|
|
974
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
975
|
+
Skills <span className="normal-case tracking-normal font-normal text-text-3">(from ~/.claude/skills/)</span>
|
|
976
|
+
</label>
|
|
977
|
+
<button
|
|
978
|
+
onClick={loadClaudeSkills}
|
|
979
|
+
disabled={claudeSkillsLoading}
|
|
980
|
+
className="text-[11px] text-text-3 hover:text-accent-bright transition-colors cursor-pointer bg-transparent border-none flex items-center gap-1"
|
|
981
|
+
style={{ fontFamily: 'inherit' }}
|
|
982
|
+
title="Refresh skills from ~/.claude/skills/"
|
|
983
|
+
>
|
|
984
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
985
|
+
className={claudeSkillsLoading ? 'animate-spin' : ''}>
|
|
986
|
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" />
|
|
987
|
+
</svg>
|
|
988
|
+
Refresh
|
|
989
|
+
</button>
|
|
990
|
+
</div>
|
|
991
|
+
<p className="text-[12px] text-text-3/60 mb-3">When delegated to, this agent will be instructed to use these skills.</p>
|
|
992
|
+
{claudeSkills.length > 0 ? (
|
|
993
|
+
<div className="flex flex-wrap gap-2">
|
|
994
|
+
{claudeSkills.map((s) => {
|
|
995
|
+
const active = skills.includes(s.id)
|
|
996
|
+
return (
|
|
997
|
+
<button
|
|
998
|
+
key={s.id}
|
|
999
|
+
onClick={() => setSkills((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
|
|
1000
|
+
className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
1001
|
+
${active
|
|
1002
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
1003
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
1004
|
+
style={{ fontFamily: 'inherit' }}
|
|
1005
|
+
title={s.description}
|
|
1006
|
+
>
|
|
1007
|
+
{s.name}
|
|
1008
|
+
</button>
|
|
1009
|
+
)
|
|
1010
|
+
})}
|
|
1011
|
+
</div>
|
|
1012
|
+
) : (
|
|
1013
|
+
<p className="text-[12px] text-text-3/70">No skills found in ~/.claude/skills/</p>
|
|
1014
|
+
)}
|
|
1015
|
+
</div>
|
|
1016
|
+
)}
|
|
1017
|
+
|
|
1018
|
+
{/* Dynamic Skills from Skills Manager */}
|
|
1019
|
+
{Object.keys(dynamicSkills).length > 0 && (
|
|
1020
|
+
<div className="mb-8">
|
|
1021
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
1022
|
+
Custom Skills <span className="normal-case tracking-normal font-normal text-text-3">(from Skills manager)</span>
|
|
1023
|
+
</label>
|
|
1024
|
+
<p className="text-[12px] text-text-3/60 mb-3">Skill content is injected into the system prompt when this agent runs.</p>
|
|
1025
|
+
<div className="flex flex-wrap gap-2">
|
|
1026
|
+
{Object.values(dynamicSkills).map((s) => {
|
|
1027
|
+
const active = skillIds.includes(s.id)
|
|
1028
|
+
return (
|
|
1029
|
+
<button
|
|
1030
|
+
key={s.id}
|
|
1031
|
+
onClick={() => setSkillIds((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
|
|
1032
|
+
className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
1033
|
+
${active
|
|
1034
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
1035
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
1036
|
+
style={{ fontFamily: 'inherit' }}
|
|
1037
|
+
title={s.description || s.filename}
|
|
1038
|
+
>
|
|
1039
|
+
{s.name}
|
|
1040
|
+
</button>
|
|
1041
|
+
)
|
|
1042
|
+
})}
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
)}
|
|
1046
|
+
|
|
1047
|
+
{/* MCP Servers */}
|
|
1048
|
+
{Object.keys(mcpServers).length > 0 && (
|
|
1049
|
+
<div className="mb-8">
|
|
1050
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
1051
|
+
MCP Servers
|
|
1052
|
+
</label>
|
|
1053
|
+
<p className="text-[12px] text-text-3/60 mb-3">Connect external tool servers to this agent via MCP.</p>
|
|
1054
|
+
<div className="flex flex-wrap gap-2">
|
|
1055
|
+
{Object.values(mcpServers).map((s: any) => {
|
|
1056
|
+
const active = mcpServerIds.includes(s.id)
|
|
1057
|
+
return (
|
|
1058
|
+
<button
|
|
1059
|
+
key={s.id}
|
|
1060
|
+
onClick={() => setMcpServerIds((prev) => active ? prev.filter((x) => x !== s.id) : [...prev, s.id])}
|
|
1061
|
+
className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
1062
|
+
${active
|
|
1063
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
1064
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
1065
|
+
style={{ fontFamily: 'inherit' }}
|
|
1066
|
+
title={`${s.transport} — ${s.command || s.url || ''}`}
|
|
1067
|
+
>
|
|
1068
|
+
{s.name}
|
|
1069
|
+
</button>
|
|
1070
|
+
)
|
|
1071
|
+
})}
|
|
1072
|
+
</div>
|
|
1073
|
+
</div>
|
|
1074
|
+
)}
|
|
1075
|
+
|
|
1076
|
+
{/* MCP Tools — per-tool enable/disable toggles */}
|
|
1077
|
+
{mcpServerIds.length > 0 && Object.keys(mcpTools).length > 0 && (
|
|
1078
|
+
<div className="mb-8">
|
|
1079
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
1080
|
+
MCP Tools
|
|
1081
|
+
</label>
|
|
1082
|
+
<p className="text-[12px] text-text-3/60 mb-3">
|
|
1083
|
+
Toggle individual tools from connected MCP servers.{mcpToolsLoading ? ' Loading…' : ''}
|
|
1084
|
+
</p>
|
|
1085
|
+
<div className="space-y-4">
|
|
1086
|
+
{mcpServerIds.map((serverId) => {
|
|
1087
|
+
const server = (mcpServers as Record<string, any>)[serverId]
|
|
1088
|
+
const serverTools = mcpTools[serverId]
|
|
1089
|
+
if (!server || !serverTools?.length) return null
|
|
1090
|
+
const safeName = server.name.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
1091
|
+
return (
|
|
1092
|
+
<div key={serverId}>
|
|
1093
|
+
<p className="text-[12px] font-600 text-text-3 mb-2">{server.name}</p>
|
|
1094
|
+
<div className="space-y-3">
|
|
1095
|
+
{serverTools.map((t) => {
|
|
1096
|
+
const fullName = `mcp_${safeName}_${t.name}`
|
|
1097
|
+
const enabled = !mcpDisabledTools.includes(fullName)
|
|
1098
|
+
return (
|
|
1099
|
+
<label key={fullName} className="flex items-center gap-3 cursor-pointer">
|
|
1100
|
+
<div
|
|
1101
|
+
onClick={() => setMcpDisabledTools((prev) =>
|
|
1102
|
+
enabled ? [...prev, fullName] : prev.filter((x) => x !== fullName)
|
|
1103
|
+
)}
|
|
1104
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
1105
|
+
${enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
1106
|
+
>
|
|
1107
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
1108
|
+
${enabled ? 'left-[22px]' : 'left-0.5'}`} />
|
|
1109
|
+
</div>
|
|
1110
|
+
<span className="font-display text-[14px] font-600 text-text-2">{t.name}</span>
|
|
1111
|
+
<span className="text-[12px] text-text-3 truncate">{t.description}</span>
|
|
1112
|
+
</label>
|
|
1113
|
+
)
|
|
1114
|
+
})}
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
)
|
|
1118
|
+
})}
|
|
1119
|
+
</div>
|
|
1120
|
+
</div>
|
|
1121
|
+
)}
|
|
1122
|
+
|
|
1123
|
+
{provider !== 'openclaw' && (
|
|
1124
|
+
<div className="mb-8">
|
|
1125
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
1126
|
+
<div
|
|
1127
|
+
onClick={() => {
|
|
1128
|
+
const next = !isOrchestrator
|
|
1129
|
+
setIsOrchestrator(next)
|
|
1130
|
+
if (next && provider === 'claude-cli') setProvider('anthropic')
|
|
1131
|
+
}}
|
|
1132
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
|
|
1133
|
+
${isOrchestrator ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
1134
|
+
>
|
|
1135
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
1136
|
+
${isOrchestrator ? 'left-[22px]' : 'left-0.5'}`} />
|
|
1137
|
+
</div>
|
|
1138
|
+
<span className="font-display text-[14px] font-600 text-text-2">Orchestrator</span>
|
|
1139
|
+
<span className="text-[12px] text-text-3">Can delegate tasks to other agents</span>
|
|
1140
|
+
</label>
|
|
1141
|
+
</div>
|
|
1142
|
+
)}
|
|
1143
|
+
|
|
1144
|
+
{provider !== 'openclaw' && isOrchestrator && agentOptions.length > 0 && (
|
|
1145
|
+
<div className="mb-8">
|
|
1146
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Available Agents</label>
|
|
1147
|
+
<div className="flex flex-wrap gap-2">
|
|
1148
|
+
{agentOptions.map((a) => (
|
|
1149
|
+
<button
|
|
1150
|
+
key={a.id}
|
|
1151
|
+
onClick={() => toggleAgent(a.id)}
|
|
1152
|
+
className={`px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
1153
|
+
${subAgentIds.includes(a.id)
|
|
1154
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
1155
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
1156
|
+
style={{ fontFamily: 'inherit' }}
|
|
1157
|
+
>
|
|
1158
|
+
{a.name}
|
|
1159
|
+
</button>
|
|
1160
|
+
))}
|
|
1161
|
+
</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
)}
|
|
1164
|
+
|
|
1165
|
+
{/* Provider key warning */}
|
|
1166
|
+
{providerNeedsKey && (
|
|
1167
|
+
<div className="mb-4 p-3 rounded-[12px] bg-amber-500/[0.08] border border-amber-500/20">
|
|
1168
|
+
<p className="text-[13px] text-amber-400">
|
|
1169
|
+
Add an API key for {currentProvider?.name || provider} above before creating this agent.
|
|
1170
|
+
</p>
|
|
1171
|
+
</div>
|
|
1172
|
+
)}
|
|
1173
|
+
|
|
1174
|
+
{/* Test connection result */}
|
|
1175
|
+
{testStatus === 'fail' && (
|
|
1176
|
+
<div className="mb-4 p-3 rounded-[12px] bg-red-500/[0.08] border border-red-500/20">
|
|
1177
|
+
<p className="text-[13px] text-red-400">{testMessage || 'Connection test failed'}</p>
|
|
1178
|
+
</div>
|
|
1179
|
+
)}
|
|
1180
|
+
{testStatus === 'pass' && (
|
|
1181
|
+
<div className="mb-4 p-3 rounded-[12px] bg-emerald-500/[0.08] border border-emerald-500/20">
|
|
1182
|
+
<p className="text-[13px] text-emerald-400">{testMessage || 'Connected successfully'}</p>
|
|
1183
|
+
</div>
|
|
1184
|
+
)}
|
|
1185
|
+
|
|
1186
|
+
{/* Import file input (hidden) */}
|
|
1187
|
+
<input ref={importFileRef} type="file" accept=".json" onChange={handleImport} className="hidden" />
|
|
1188
|
+
|
|
1189
|
+
<div className="flex gap-3 pt-2 border-t border-white/[0.04]">
|
|
1190
|
+
{editing && (
|
|
1191
|
+
<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' }}>
|
|
1192
|
+
Delete
|
|
1193
|
+
</button>
|
|
1194
|
+
)}
|
|
1195
|
+
{editing && (
|
|
1196
|
+
<button onClick={handleExport} className="py-3.5 px-4 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[13px] font-600 cursor-pointer hover:bg-surface-2 hover:text-text-2 transition-all" style={{ fontFamily: 'inherit' }} title="Export agent as JSON">
|
|
1197
|
+
Export
|
|
1198
|
+
</button>
|
|
1199
|
+
)}
|
|
1200
|
+
{!editing && (
|
|
1201
|
+
<button onClick={() => importFileRef.current?.click()} className="py-3.5 px-4 rounded-[14px] border border-white/[0.08] bg-transparent text-text-3 text-[13px] font-600 cursor-pointer hover:bg-surface-2 hover:text-text-2 transition-all" style={{ fontFamily: 'inherit' }} title="Import agent from JSON">
|
|
1202
|
+
Import
|
|
1203
|
+
</button>
|
|
1204
|
+
)}
|
|
1205
|
+
<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' }}>
|
|
1206
|
+
Cancel
|
|
1207
|
+
</button>
|
|
1208
|
+
<button
|
|
1209
|
+
onClick={handleTestAndSave}
|
|
1210
|
+
disabled={!name.trim() || providerNeedsKey || testStatus === 'testing' || testStatus === 'pass' || saving}
|
|
1211
|
+
className={`flex-1 py-3.5 rounded-[14px] border-none text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-60 transition-all hover:brightness-110
|
|
1212
|
+
${testStatus === 'pass' ? 'bg-emerald-600 shadow-[0_4px_20px_rgba(16,185,129,0.25)]' : 'bg-[#6366F1] shadow-[0_4px_20px_rgba(99,102,241,0.25)]'}`}
|
|
1213
|
+
style={{ fontFamily: 'inherit' }}
|
|
1214
|
+
>
|
|
1215
|
+
{testStatus === 'testing' ? 'Testing...' : testStatus === 'pass' ? (saving ? 'Saving...' : 'Connected!') : needsTest ? 'Test & Save' : editing ? 'Save' : 'Create'}
|
|
1216
|
+
</button>
|
|
1217
|
+
</div>
|
|
1218
|
+
</BottomSheet>
|
|
1219
|
+
)
|
|
1220
|
+
}
|