@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,804 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { api } from '@/lib/api-client'
|
|
7
|
+
import { toast } from 'sonner'
|
|
8
|
+
import type { Connector, ConnectorPlatform } from '@/types'
|
|
9
|
+
import { ConnectorPlatformBadge } from '@/components/shared/connector-platform-icon'
|
|
10
|
+
|
|
11
|
+
/** Auto-detect URLs in text and make them clickable links that open in a new tab */
|
|
12
|
+
function linkify(text: string) {
|
|
13
|
+
const urlRegex = /(https?:\/\/[^\s,)]+)/gi
|
|
14
|
+
const parts = text.split(urlRegex)
|
|
15
|
+
if (parts.length === 1) return text
|
|
16
|
+
return parts.map((part, i) => {
|
|
17
|
+
if (urlRegex.test(part)) {
|
|
18
|
+
urlRegex.lastIndex = 0
|
|
19
|
+
return <a key={i} href={part} target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">{part}</a>
|
|
20
|
+
}
|
|
21
|
+
return part
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PLATFORMS: {
|
|
26
|
+
id: ConnectorPlatform
|
|
27
|
+
label: string
|
|
28
|
+
color: string
|
|
29
|
+
setupSteps: string[]
|
|
30
|
+
tokenLabel: string
|
|
31
|
+
tokenHelp: string
|
|
32
|
+
configFields: { key: string; label: string; placeholder: string; help?: string }[]
|
|
33
|
+
}[] = [
|
|
34
|
+
{
|
|
35
|
+
id: 'discord',
|
|
36
|
+
label: 'Discord',
|
|
37
|
+
color: '#5865F2',
|
|
38
|
+
setupSteps: [
|
|
39
|
+
'Go to https://discord.com/developers/applications and create a new app',
|
|
40
|
+
'Under "Bot", click "Reset Token" and copy it',
|
|
41
|
+
'Enable MESSAGE CONTENT intent under "Privileged Gateway Intents"',
|
|
42
|
+
'Under "OAuth2 > URL Generator", check the "bot" scope — a Bot Permissions panel will appear below',
|
|
43
|
+
'In Bot Permissions, check "Send Messages" and "Read Message History"',
|
|
44
|
+
'Copy the generated URL at the bottom and open it to invite the bot to your server',
|
|
45
|
+
],
|
|
46
|
+
tokenLabel: 'Bot Token',
|
|
47
|
+
tokenHelp: 'From Discord Developer Portal > Your App > Bot > Token',
|
|
48
|
+
configFields: [
|
|
49
|
+
{ key: 'channelIds', label: 'Channel IDs', placeholder: '123456789,987654321', help: 'Leave empty to listen in all channels the bot can see' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'telegram',
|
|
54
|
+
label: 'Telegram',
|
|
55
|
+
color: '#229ED9',
|
|
56
|
+
setupSteps: [
|
|
57
|
+
'Message @BotFather on Telegram',
|
|
58
|
+
'Send /newbot and follow the prompts to create a bot',
|
|
59
|
+
'Copy the bot token BotFather gives you',
|
|
60
|
+
],
|
|
61
|
+
tokenLabel: 'Bot Token',
|
|
62
|
+
tokenHelp: 'From @BotFather after creating your bot',
|
|
63
|
+
configFields: [
|
|
64
|
+
{ key: 'chatIds', label: 'Chat IDs', placeholder: '-100123456789', help: 'Leave empty to respond in all chats. Use negative IDs for groups.' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'slack',
|
|
69
|
+
label: 'Slack',
|
|
70
|
+
color: '#4A154B',
|
|
71
|
+
setupSteps: [
|
|
72
|
+
'Go to https://api.slack.com/apps and create a new app "From scratch"',
|
|
73
|
+
'Under "Socket Mode", enable it. Then go to "Basic Information > App-Level Tokens", generate a token with connections:write scope, and copy the xapp-... token',
|
|
74
|
+
'Under "OAuth & Permissions", add bot scopes: chat:write, channels:history, channels:read, im:history, im:read, users:read, app_mentions:read',
|
|
75
|
+
'Under "Event Subscriptions", enable events and subscribe to: message.channels, message.im, app_mention',
|
|
76
|
+
'Under "App Home", enable the Messages Tab and check "Allow users to send Slash commands and messages from the messages tab"',
|
|
77
|
+
'Install the app to your workspace and copy the Bot Token (xoxb-...) from OAuth & Permissions',
|
|
78
|
+
],
|
|
79
|
+
tokenLabel: 'Bot Token (xoxb-...)',
|
|
80
|
+
tokenHelp: 'From Slack App > OAuth & Permissions > Bot User OAuth Token',
|
|
81
|
+
configFields: [
|
|
82
|
+
{ key: 'appToken', label: 'App-Level Token (xapp-...)', placeholder: 'xapp-1-...', help: 'Required for Socket Mode. From Slack App > Basic Information > App-Level Tokens' },
|
|
83
|
+
{ key: 'channelIds', label: 'Channel IDs', placeholder: 'C0123456789', help: 'Leave empty to listen in all channels the bot is in' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'whatsapp',
|
|
88
|
+
label: 'WhatsApp',
|
|
89
|
+
color: '#25D366',
|
|
90
|
+
setupSteps: [
|
|
91
|
+
'No token needed — WhatsApp uses QR code pairing',
|
|
92
|
+
'When you start this connector, a QR code will appear in the server terminal',
|
|
93
|
+
'Open WhatsApp > Settings > Linked Devices > Link a Device',
|
|
94
|
+
'Scan the QR code to connect',
|
|
95
|
+
],
|
|
96
|
+
tokenLabel: '',
|
|
97
|
+
tokenHelp: '',
|
|
98
|
+
configFields: [
|
|
99
|
+
{ key: 'allowedJids', label: 'Allowed Numbers/Groups', placeholder: '1234567890,MyGroup', help: 'Leave empty to respond to all messages' },
|
|
100
|
+
{ key: 'outboundJid', label: 'Default Outbound Recipient', placeholder: '15551234567 or 15551234567@s.whatsapp.net', help: 'Used by connector_message_tool when the agent sends proactive WhatsApp updates without an explicit "to" value' },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'openclaw',
|
|
105
|
+
label: 'OpenClaw',
|
|
106
|
+
color: '#F97316',
|
|
107
|
+
setupSteps: [
|
|
108
|
+
'Ensure your OpenClaw instance is running (default: localhost:18789)',
|
|
109
|
+
'If authentication is enabled, create a credential with your OpenClaw gateway token',
|
|
110
|
+
'The connector will connect via OpenClaw Gateway RPC and handle chat events',
|
|
111
|
+
],
|
|
112
|
+
tokenLabel: 'Gateway Token',
|
|
113
|
+
tokenHelp: 'Required when your OpenClaw gateway is auth-protected',
|
|
114
|
+
configFields: [
|
|
115
|
+
{ key: 'wsUrl', label: 'WebSocket URL', placeholder: 'ws://localhost:18789', help: 'OpenClaw gateway WebSocket endpoint (root URL, not /ws)' },
|
|
116
|
+
{ key: 'sessionKey', label: 'Session Key Filter', placeholder: 'main', help: 'Optional. If set, only inbound events for this OpenClaw session are processed.' },
|
|
117
|
+
{ key: 'nodeId', label: 'Client Label', placeholder: 'swarmclaw', help: 'Optional display label shown in OpenClaw presence metadata.' },
|
|
118
|
+
{ key: 'role', label: 'Gateway Role', placeholder: 'operator', help: 'Optional role claim for connect handshake. Default is operator.' },
|
|
119
|
+
{ key: 'scopes', label: 'Scopes (CSV)', placeholder: 'operator.read,operator.write', help: 'Optional comma-separated scopes for OpenClaw connect.' },
|
|
120
|
+
{ key: 'tickWatchdog', label: 'Tick Watchdog', placeholder: 'true', help: 'Set false to disable stale-tick reconnect watchdog.' },
|
|
121
|
+
{ key: 'tickIntervalMs', label: 'Tick Interval Override (ms)', placeholder: '30000', help: 'Optional watchdog interval override when policy tick is unavailable.' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'matrix',
|
|
126
|
+
label: 'Matrix',
|
|
127
|
+
color: '#0DBD8B',
|
|
128
|
+
setupSteps: [
|
|
129
|
+
'Create a bot user on your Matrix homeserver',
|
|
130
|
+
'Generate an access token for the bot user',
|
|
131
|
+
'Set the homeserver URL (e.g. https://matrix.org)',
|
|
132
|
+
'Optionally restrict to specific room IDs',
|
|
133
|
+
],
|
|
134
|
+
tokenLabel: 'Access Token',
|
|
135
|
+
tokenHelp: 'Matrix access token for the bot user',
|
|
136
|
+
configFields: [
|
|
137
|
+
{ key: 'homeserverUrl', label: 'Homeserver URL', placeholder: 'https://matrix.org', help: 'The Matrix homeserver URL' },
|
|
138
|
+
{ key: 'roomIds', label: 'Room IDs', placeholder: '!abc123:matrix.org', help: 'Comma-separated room IDs. Leave empty for all rooms.' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'googlechat',
|
|
143
|
+
label: 'Google Chat',
|
|
144
|
+
color: '#00AC47',
|
|
145
|
+
setupSteps: [
|
|
146
|
+
'Create a Google Cloud project and enable the Google Chat API',
|
|
147
|
+
'Create a service account and download the JSON key file',
|
|
148
|
+
'In Google Chat Admin, configure the bot with your app URL',
|
|
149
|
+
'Paste the full service account JSON as the bot token',
|
|
150
|
+
],
|
|
151
|
+
tokenLabel: 'Service Account JSON',
|
|
152
|
+
tokenHelp: 'Paste the full service account JSON key file contents',
|
|
153
|
+
configFields: [
|
|
154
|
+
{ key: 'spaceIds', label: 'Space IDs', placeholder: 'spaces/AAAA123', help: 'Comma-separated Google Chat space IDs' },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'teams',
|
|
159
|
+
label: 'Microsoft Teams',
|
|
160
|
+
color: '#6264A7',
|
|
161
|
+
setupSteps: [
|
|
162
|
+
'Register a bot in the Azure Bot Framework portal',
|
|
163
|
+
'Note the Microsoft App ID and generate an App Secret',
|
|
164
|
+
'Set up a public HTTPS endpoint for webhook delivery',
|
|
165
|
+
'Configure the messaging endpoint in Azure to your notify URL',
|
|
166
|
+
],
|
|
167
|
+
tokenLabel: 'App Secret',
|
|
168
|
+
tokenHelp: 'Microsoft App Secret from Azure Bot registration',
|
|
169
|
+
configFields: [
|
|
170
|
+
{ key: 'appId', label: 'Microsoft App ID', placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', help: 'Azure Bot Framework App ID' },
|
|
171
|
+
{ key: 'notifyUrl', label: 'Notify URL', placeholder: 'https://your-server.com/api/teams/webhook', help: 'Public HTTPS endpoint for receiving messages' },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'signal',
|
|
176
|
+
label: 'Signal',
|
|
177
|
+
color: '#3A76F0',
|
|
178
|
+
setupSteps: [
|
|
179
|
+
'Install signal-cli (https://github.com/AsamK/signal-cli)',
|
|
180
|
+
'Register a phone number: signal-cli -u +1234567890 register',
|
|
181
|
+
'Verify the number: signal-cli -u +1234567890 verify CODE',
|
|
182
|
+
'The connector spawns signal-cli daemon to listen for messages',
|
|
183
|
+
],
|
|
184
|
+
tokenLabel: '',
|
|
185
|
+
tokenHelp: '',
|
|
186
|
+
configFields: [
|
|
187
|
+
{ key: 'phoneNumber', label: 'Phone Number', placeholder: '+1234567890', help: 'Pre-registered Signal phone number' },
|
|
188
|
+
{ key: 'signalCliPath', label: 'signal-cli Path', placeholder: 'signal-cli', help: 'Path to signal-cli binary (defaults to signal-cli)' },
|
|
189
|
+
{ key: 'signalCliMode', label: 'Mode', placeholder: 'stdio', help: 'stdio (default) or http' },
|
|
190
|
+
{ key: 'signalCliHttpUrl', label: 'HTTP API URL', placeholder: 'http://localhost:8080', help: 'Only needed for http mode' },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
export function ConnectorSheet() {
|
|
196
|
+
const open = useAppStore((s) => s.connectorSheetOpen)
|
|
197
|
+
const setOpen = useAppStore((s) => s.setConnectorSheetOpen)
|
|
198
|
+
const editingId = useAppStore((s) => s.editingConnectorId)
|
|
199
|
+
const setEditingId = useAppStore((s) => s.setEditingConnectorId)
|
|
200
|
+
const connectors = useAppStore((s) => s.connectors)
|
|
201
|
+
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
202
|
+
const agents = useAppStore((s) => s.agents)
|
|
203
|
+
const credentials = useAppStore((s) => s.credentials)
|
|
204
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
205
|
+
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
206
|
+
|
|
207
|
+
const [name, setName] = useState('')
|
|
208
|
+
const [platform, setPlatform] = useState<ConnectorPlatform>('discord')
|
|
209
|
+
const [agentId, setAgentId] = useState('')
|
|
210
|
+
const [credentialId, setCredentialId] = useState('')
|
|
211
|
+
const [config, setConfig] = useState<Record<string, string>>({})
|
|
212
|
+
const [saving, setSaving] = useState(false)
|
|
213
|
+
const [actionLoading, setActionLoading] = useState(false)
|
|
214
|
+
const [showSetup, setShowSetup] = useState(false)
|
|
215
|
+
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null)
|
|
216
|
+
const [waAuthenticated, setWaAuthenticated] = useState(false)
|
|
217
|
+
const [waHasCreds, setWaHasCreds] = useState(false)
|
|
218
|
+
const [waConnecting, setWaConnecting] = useState(false)
|
|
219
|
+
const [showNewCred, setShowNewCred] = useState(false)
|
|
220
|
+
const [newCredName, setNewCredName] = useState('')
|
|
221
|
+
const [newCredValue, setNewCredValue] = useState('')
|
|
222
|
+
const [savingCred, setSavingCred] = useState(false)
|
|
223
|
+
|
|
224
|
+
const editing = editingId ? connectors[editingId] as Connector | undefined : null
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (open) {
|
|
228
|
+
loadAgents()
|
|
229
|
+
loadCredentials()
|
|
230
|
+
setShowSetup(false)
|
|
231
|
+
}
|
|
232
|
+
}, [open])
|
|
233
|
+
|
|
234
|
+
// Sync form fields when editing connector changes (by ID, not reference)
|
|
235
|
+
const editingIdRef = editing?.id ?? null
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (editing) {
|
|
238
|
+
setName(editing.name)
|
|
239
|
+
setPlatform(editing.platform)
|
|
240
|
+
setAgentId(editing.agentId)
|
|
241
|
+
setCredentialId(editing.credentialId || '')
|
|
242
|
+
setConfig(editing.config || {})
|
|
243
|
+
} else {
|
|
244
|
+
setName('')
|
|
245
|
+
setPlatform('discord')
|
|
246
|
+
setAgentId('')
|
|
247
|
+
setCredentialId('')
|
|
248
|
+
setConfig({})
|
|
249
|
+
}
|
|
250
|
+
setQrDataUrl(null)
|
|
251
|
+
setWaAuthenticated(false)
|
|
252
|
+
setWaHasCreds(false)
|
|
253
|
+
setWaConnecting(false)
|
|
254
|
+
}, [editingIdRef, open])
|
|
255
|
+
|
|
256
|
+
// Poll for QR code when WhatsApp connector is running or connecting
|
|
257
|
+
const isWaRunning = editing?.platform === 'whatsapp' && (editing?.status === 'running' || waConnecting)
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (!editing || !isWaRunning) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
let cancelled = false
|
|
263
|
+
const poll = async () => {
|
|
264
|
+
try {
|
|
265
|
+
const data = await api<any>('GET', `/connectors/${editing.id}`)
|
|
266
|
+
if (!cancelled) {
|
|
267
|
+
setQrDataUrl(data.qrDataUrl || null)
|
|
268
|
+
setWaAuthenticated(data.authenticated ?? false)
|
|
269
|
+
setWaHasCreds(data.hasCredentials ?? false)
|
|
270
|
+
// Sync store with the individual endpoint's runtime status
|
|
271
|
+
if (data.status === 'running' && editing.status !== 'running') {
|
|
272
|
+
// Store is stale — update it directly
|
|
273
|
+
const store = useAppStore.getState()
|
|
274
|
+
const updated = { ...store.connectors }
|
|
275
|
+
if (updated[editing.id]) {
|
|
276
|
+
updated[editing.id] = { ...updated[editing.id], status: 'running' as const }
|
|
277
|
+
useAppStore.setState({ connectors: updated })
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch { /* ignore */ }
|
|
282
|
+
}
|
|
283
|
+
poll()
|
|
284
|
+
const interval = setInterval(poll, 2000)
|
|
285
|
+
return () => { cancelled = true; clearInterval(interval) }
|
|
286
|
+
}, [editing?.id, isWaRunning])
|
|
287
|
+
|
|
288
|
+
const handleSave = async () => {
|
|
289
|
+
if (!agentId) return
|
|
290
|
+
setSaving(true)
|
|
291
|
+
try {
|
|
292
|
+
if (editing) {
|
|
293
|
+
await api('PUT', `/connectors/${editing.id}`, { name, agentId, credentialId: credentialId || null, config })
|
|
294
|
+
} else {
|
|
295
|
+
await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform, agentId, credentialId: credentialId || null, config })
|
|
296
|
+
}
|
|
297
|
+
await loadConnectors()
|
|
298
|
+
setOpen(false)
|
|
299
|
+
setEditingId(null)
|
|
300
|
+
} catch (err: any) {
|
|
301
|
+
toast.error(err.message)
|
|
302
|
+
} finally {
|
|
303
|
+
setSaving(false)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const handleStartStop = async (action: 'start' | 'stop') => {
|
|
308
|
+
if (!editing) return
|
|
309
|
+
setActionLoading(true)
|
|
310
|
+
try {
|
|
311
|
+
await api('PUT', `/connectors/${editing.id}`, { action })
|
|
312
|
+
if (action === 'start' && editing.platform === 'whatsapp') {
|
|
313
|
+
setWaConnecting(true)
|
|
314
|
+
setWaAuthenticated(false)
|
|
315
|
+
setQrDataUrl(null)
|
|
316
|
+
// Don't reset waHasCreds — it will be updated by poll
|
|
317
|
+
} else if (action === 'stop') {
|
|
318
|
+
setWaConnecting(false)
|
|
319
|
+
setWaAuthenticated(false)
|
|
320
|
+
setWaHasCreds(false)
|
|
321
|
+
setQrDataUrl(null)
|
|
322
|
+
}
|
|
323
|
+
await loadConnectors()
|
|
324
|
+
} catch (err: any) {
|
|
325
|
+
setWaConnecting(false)
|
|
326
|
+
toast.error(`Failed to ${action}: ${err.message}`)
|
|
327
|
+
} finally {
|
|
328
|
+
setActionLoading(false)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const handleDelete = async () => {
|
|
333
|
+
if (!editing || !confirm('Delete this connector?')) return
|
|
334
|
+
await api('DELETE', `/connectors/${editing.id}`)
|
|
335
|
+
await loadConnectors()
|
|
336
|
+
setOpen(false)
|
|
337
|
+
setEditingId(null)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const platformConfig = PLATFORMS.find((p) => p.id === platform)!
|
|
341
|
+
const agentList = Object.values(agents)
|
|
342
|
+
const credList = Object.values(credentials)
|
|
343
|
+
|
|
344
|
+
const inputClass = "w-full px-4 py-3 rounded-[12px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus:border-white/[0.15]"
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<BottomSheet open={open} onClose={() => { setOpen(false); setEditingId(null) }} wide>
|
|
348
|
+
<div className="mb-8">
|
|
349
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
350
|
+
{editing ? 'Edit Connector' : 'New Connector'}
|
|
351
|
+
</h2>
|
|
352
|
+
<p className="text-[14px] text-text-3">Bridge a chat platform to an AI agent</p>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
{/* Platform selector (only for new) */}
|
|
356
|
+
{!editing && (
|
|
357
|
+
<div className="mb-8">
|
|
358
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Platform</label>
|
|
359
|
+
<div className="grid grid-cols-2 gap-3">
|
|
360
|
+
{PLATFORMS.map((p) => (
|
|
361
|
+
<button
|
|
362
|
+
key={p.id}
|
|
363
|
+
onClick={() => { setPlatform(p.id); setShowSetup(false) }}
|
|
364
|
+
className={`flex items-center gap-3 p-4 rounded-[14px] cursor-pointer transition-all duration-200 border text-left
|
|
365
|
+
${platform === p.id
|
|
366
|
+
? 'bg-white/[0.04] border-white/[0.15] shadow-[0_0_20px_rgba(255,255,255,0.02)]'
|
|
367
|
+
: 'bg-transparent border-white/[0.04] hover:border-white/[0.08] hover:bg-white/[0.01]'}`}
|
|
368
|
+
style={{ fontFamily: 'inherit' }}
|
|
369
|
+
>
|
|
370
|
+
<ConnectorPlatformBadge platform={p.id} size={40} iconSize={18} />
|
|
371
|
+
<div>
|
|
372
|
+
<div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
|
|
373
|
+
<div className="text-[11px] text-text-3 mt-0.5">
|
|
374
|
+
{p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : 'Bot token'}
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</button>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* Editing: show platform badge */}
|
|
384
|
+
{editing && (
|
|
385
|
+
<div className="mb-6 flex items-center gap-3">
|
|
386
|
+
<ConnectorPlatformBadge platform={platformConfig.id} size={40} iconSize={18} />
|
|
387
|
+
<div>
|
|
388
|
+
<div className="text-[14px] font-600 text-text">{platformConfig.label}</div>
|
|
389
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
390
|
+
<span className={`w-2 h-2 rounded-full ${
|
|
391
|
+
editing.status === 'running' ? 'bg-green-400 shadow-[0_0_6px_rgba(74,222,128,0.5)]' :
|
|
392
|
+
editing.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
393
|
+
}`} />
|
|
394
|
+
<span className="text-[12px] text-text-3 capitalize">{editing.status}</span>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
|
|
400
|
+
{/* Setup guide (collapsible) */}
|
|
401
|
+
<div className="mb-6">
|
|
402
|
+
<button
|
|
403
|
+
onClick={() => setShowSetup(!showSetup)}
|
|
404
|
+
className="flex items-center gap-2 text-[13px] font-600 text-accent-bright hover:text-accent-bright/80 transition-colors cursor-pointer bg-transparent border-none"
|
|
405
|
+
style={{ fontFamily: 'inherit' }}
|
|
406
|
+
>
|
|
407
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
|
|
408
|
+
className={`transition-transform ${showSetup ? 'rotate-90' : ''}`}>
|
|
409
|
+
<polyline points="9 18 15 12 9 6" />
|
|
410
|
+
</svg>
|
|
411
|
+
{platformConfig.label} Setup Guide
|
|
412
|
+
</button>
|
|
413
|
+
{showSetup && (
|
|
414
|
+
<div className="mt-3 p-4 rounded-[12px] border border-white/[0.06] bg-white/[0.01] space-y-2.5"
|
|
415
|
+
style={{ animation: 'fade-in 0.2s ease-out' }}>
|
|
416
|
+
{platformConfig.setupSteps.map((step, i) => (
|
|
417
|
+
<div key={i} className="flex items-start gap-3">
|
|
418
|
+
<span className="w-5 h-5 rounded-full bg-white/[0.06] flex items-center justify-center text-[10px] font-700 text-text-3 shrink-0 mt-0.5">
|
|
419
|
+
{i + 1}
|
|
420
|
+
</span>
|
|
421
|
+
<span className="text-[13px] text-text-2/80 leading-[1.5]">{linkify(step)}</span>
|
|
422
|
+
</div>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
{/* Name */}
|
|
429
|
+
<div className="mb-6">
|
|
430
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Name</label>
|
|
431
|
+
<input
|
|
432
|
+
value={name}
|
|
433
|
+
onChange={(e) => setName(e.target.value)}
|
|
434
|
+
placeholder={`My ${platformConfig.label} Bot`}
|
|
435
|
+
className={inputClass}
|
|
436
|
+
style={{ fontFamily: 'inherit' }}
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{/* Agent selector */}
|
|
441
|
+
<div className="mb-6">
|
|
442
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Route to Agent</label>
|
|
443
|
+
<p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be handled by this agent</p>
|
|
444
|
+
<select
|
|
445
|
+
value={agentId || ''}
|
|
446
|
+
onChange={(e) => setAgentId(e.target.value)}
|
|
447
|
+
className={`${inputClass} appearance-none cursor-pointer`}
|
|
448
|
+
style={{ fontFamily: 'inherit' }}
|
|
449
|
+
>
|
|
450
|
+
<option value="">Select a agent...</option>
|
|
451
|
+
{agentList.map((p: any) => (
|
|
452
|
+
<option key={p.id} value={p.id}>{p.name}{p.isOrchestrator ? ' (Orchestrator)' : ''}</option>
|
|
453
|
+
))}
|
|
454
|
+
</select>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{/* Bot token credential */}
|
|
458
|
+
{platform !== 'whatsapp' && (
|
|
459
|
+
<div className="mb-6">
|
|
460
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">{platformConfig.tokenLabel}</label>
|
|
461
|
+
<p className="text-[12px] text-text-3/60 mb-2">{platformConfig.tokenHelp}</p>
|
|
462
|
+
<div className="flex gap-2">
|
|
463
|
+
<select
|
|
464
|
+
value={credentialId}
|
|
465
|
+
onChange={(e) => {
|
|
466
|
+
if (e.target.value === '__new__') {
|
|
467
|
+
setShowNewCred(true)
|
|
468
|
+
setNewCredName(`${platformConfig.label} Bot Token`)
|
|
469
|
+
setNewCredValue('')
|
|
470
|
+
} else {
|
|
471
|
+
setCredentialId(e.target.value)
|
|
472
|
+
setShowNewCred(false)
|
|
473
|
+
}
|
|
474
|
+
}}
|
|
475
|
+
className={`${inputClass} appearance-none cursor-pointer flex-1`}
|
|
476
|
+
style={{ fontFamily: 'inherit' }}
|
|
477
|
+
>
|
|
478
|
+
<option value="">Select credential...</option>
|
|
479
|
+
{credList.map((c: any) => (
|
|
480
|
+
<option key={c.id} value={c.id}>{c.name} ({c.provider})</option>
|
|
481
|
+
))}
|
|
482
|
+
<option value="__new__">+ Add new key...</option>
|
|
483
|
+
</select>
|
|
484
|
+
{!showNewCred && (
|
|
485
|
+
<button
|
|
486
|
+
type="button"
|
|
487
|
+
onClick={() => {
|
|
488
|
+
setShowNewCred(true)
|
|
489
|
+
setNewCredName(`${platformConfig.label} Bot Token`)
|
|
490
|
+
setNewCredValue('')
|
|
491
|
+
}}
|
|
492
|
+
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"
|
|
493
|
+
>
|
|
494
|
+
+ New
|
|
495
|
+
</button>
|
|
496
|
+
)}
|
|
497
|
+
</div>
|
|
498
|
+
{showNewCred && (
|
|
499
|
+
<div className="mt-3 p-4 rounded-[12px] border border-accent-bright/15 bg-accent-soft/20 space-y-3"
|
|
500
|
+
style={{ animation: 'fade-in 0.2s ease-out' }}>
|
|
501
|
+
<input
|
|
502
|
+
value={newCredName}
|
|
503
|
+
onChange={(e) => setNewCredName(e.target.value)}
|
|
504
|
+
placeholder="Key name (e.g. My Discord Bot)"
|
|
505
|
+
className={`${inputClass} !bg-surface text-[13px]`}
|
|
506
|
+
style={{ fontFamily: 'inherit' }}
|
|
507
|
+
/>
|
|
508
|
+
<input
|
|
509
|
+
type="password"
|
|
510
|
+
value={newCredValue}
|
|
511
|
+
onChange={(e) => setNewCredValue(e.target.value)}
|
|
512
|
+
placeholder="Paste your token here..."
|
|
513
|
+
className={`${inputClass} !bg-surface font-mono text-[13px]`}
|
|
514
|
+
style={{ fontFamily: undefined }}
|
|
515
|
+
/>
|
|
516
|
+
<div className="flex gap-2 justify-end">
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
519
|
+
onClick={() => setShowNewCred(false)}
|
|
520
|
+
className="px-3 py-1.5 text-[12px] text-text-3 hover:text-text-2 transition-colors cursor-pointer bg-transparent border-none"
|
|
521
|
+
style={{ fontFamily: 'inherit' }}
|
|
522
|
+
>
|
|
523
|
+
Cancel
|
|
524
|
+
</button>
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
527
|
+
disabled={savingCred || !newCredValue.trim()}
|
|
528
|
+
onClick={async () => {
|
|
529
|
+
setSavingCred(true)
|
|
530
|
+
try {
|
|
531
|
+
const cred = await api<any>('POST', '/credentials', {
|
|
532
|
+
provider: platform,
|
|
533
|
+
name: newCredName.trim() || `${platformConfig.label} Bot Token`,
|
|
534
|
+
apiKey: newCredValue.trim(),
|
|
535
|
+
})
|
|
536
|
+
await loadCredentials()
|
|
537
|
+
setCredentialId(cred.id)
|
|
538
|
+
setShowNewCred(false)
|
|
539
|
+
setNewCredName('')
|
|
540
|
+
setNewCredValue('')
|
|
541
|
+
} catch (err: any) {
|
|
542
|
+
toast.error(`Failed to save: ${err.message}`)
|
|
543
|
+
} finally {
|
|
544
|
+
setSavingCred(false)
|
|
545
|
+
}
|
|
546
|
+
}}
|
|
547
|
+
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"
|
|
548
|
+
style={{ fontFamily: 'inherit' }}
|
|
549
|
+
>
|
|
550
|
+
{savingCred ? 'Saving...' : 'Save Key'}
|
|
551
|
+
</button>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
)}
|
|
557
|
+
|
|
558
|
+
{/* Platform-specific config */}
|
|
559
|
+
{platformConfig.configFields.map((field) => {
|
|
560
|
+
const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds'
|
|
561
|
+
if (isTagField) {
|
|
562
|
+
const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
563
|
+
return (
|
|
564
|
+
<div key={field.key} className="mb-6">
|
|
565
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
566
|
+
{field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
567
|
+
</label>
|
|
568
|
+
{field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
|
|
569
|
+
<div className="flex flex-wrap gap-2 mb-2">
|
|
570
|
+
{tags.map((tag, i) => (
|
|
571
|
+
<span key={i} className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-accent-soft/50 border border-accent-bright/20 text-[12px] font-mono text-accent-bright">
|
|
572
|
+
{tag}
|
|
573
|
+
<button
|
|
574
|
+
onClick={() => {
|
|
575
|
+
const next = tags.filter((_, j) => j !== i).join(',')
|
|
576
|
+
setConfig({ ...config, [field.key]: next })
|
|
577
|
+
}}
|
|
578
|
+
className="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-white/10 transition-colors cursor-pointer text-accent-bright/50 hover:text-accent-bright"
|
|
579
|
+
>
|
|
580
|
+
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round">
|
|
581
|
+
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
582
|
+
</svg>
|
|
583
|
+
</button>
|
|
584
|
+
</span>
|
|
585
|
+
))}
|
|
586
|
+
</div>
|
|
587
|
+
<div className="flex gap-2">
|
|
588
|
+
<input
|
|
589
|
+
id={`tag-input-${field.key}`}
|
|
590
|
+
placeholder={field.placeholder}
|
|
591
|
+
className={`${inputClass} font-mono text-[13px] flex-1`}
|
|
592
|
+
style={{ fontFamily: undefined }}
|
|
593
|
+
onKeyDown={(e) => {
|
|
594
|
+
if (e.key === 'Enter' || e.key === ',') {
|
|
595
|
+
e.preventDefault()
|
|
596
|
+
const input = e.currentTarget
|
|
597
|
+
const val = input.value.trim().replace(/,/g, '')
|
|
598
|
+
if (val) {
|
|
599
|
+
const next = tags.length > 0 ? `${tags.join(',')},${val}` : val
|
|
600
|
+
setConfig({ ...config, [field.key]: next })
|
|
601
|
+
input.value = ''
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}}
|
|
605
|
+
/>
|
|
606
|
+
<button
|
|
607
|
+
type="button"
|
|
608
|
+
onClick={() => {
|
|
609
|
+
const input = document.getElementById(`tag-input-${field.key}`) as HTMLInputElement
|
|
610
|
+
const val = input?.value.trim().replace(/,/g, '')
|
|
611
|
+
if (val) {
|
|
612
|
+
const next = tags.length > 0 ? `${tags.join(',')},${val}` : val
|
|
613
|
+
setConfig({ ...config, [field.key]: next })
|
|
614
|
+
input.value = ''
|
|
615
|
+
}
|
|
616
|
+
}}
|
|
617
|
+
className="px-4 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"
|
|
618
|
+
>
|
|
619
|
+
Add
|
|
620
|
+
</button>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
return (
|
|
626
|
+
<div key={field.key} className="mb-6">
|
|
627
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
628
|
+
{field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
629
|
+
</label>
|
|
630
|
+
{field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
|
|
631
|
+
<input
|
|
632
|
+
value={config[field.key] || ''}
|
|
633
|
+
onChange={(e) => setConfig({ ...config, [field.key]: e.target.value })}
|
|
634
|
+
placeholder={field.placeholder}
|
|
635
|
+
className={`${inputClass} font-mono text-[13px]`}
|
|
636
|
+
style={{ fontFamily: undefined }}
|
|
637
|
+
/>
|
|
638
|
+
</div>
|
|
639
|
+
)
|
|
640
|
+
})}
|
|
641
|
+
|
|
642
|
+
{/* Start/Stop controls for editing */}
|
|
643
|
+
{editing && (() => {
|
|
644
|
+
const effectiveRunning = editing.status === 'running' || waConnecting
|
|
645
|
+
return (
|
|
646
|
+
<div className="mb-6 p-4 rounded-[14px] border border-white/[0.06] bg-white/[0.01]">
|
|
647
|
+
<div className="flex items-center justify-between">
|
|
648
|
+
<div>
|
|
649
|
+
<div className="text-[13px] font-600 text-text-2">Connection</div>
|
|
650
|
+
<div className="text-[12px] text-text-3 mt-0.5 flex items-center gap-1.5">
|
|
651
|
+
<span className={`w-2 h-2 rounded-full inline-block ${
|
|
652
|
+
effectiveRunning ? 'bg-green-400 shadow-[0_0_6px_rgba(74,222,128,0.5)]' :
|
|
653
|
+
editing.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
654
|
+
}`} />
|
|
655
|
+
{effectiveRunning ? (waAuthenticated ? 'Connected and listening' : 'Connecting...') :
|
|
656
|
+
editing.status === 'error' ? 'Error — see below' : 'Not connected'}
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
{effectiveRunning ? (
|
|
660
|
+
<button
|
|
661
|
+
onClick={() => handleStartStop('stop')}
|
|
662
|
+
disabled={actionLoading}
|
|
663
|
+
className="px-5 py-2 rounded-[10px] bg-red-500/15 text-red-400 text-[13px] font-600 cursor-pointer border border-red-500/20 hover:bg-red-500/25 transition-all disabled:opacity-50"
|
|
664
|
+
style={{ fontFamily: 'inherit' }}
|
|
665
|
+
>
|
|
666
|
+
{actionLoading ? 'Stopping...' : 'Disconnect'}
|
|
667
|
+
</button>
|
|
668
|
+
) : (
|
|
669
|
+
<button
|
|
670
|
+
onClick={() => handleStartStop('start')}
|
|
671
|
+
disabled={actionLoading}
|
|
672
|
+
className="px-5 py-2 rounded-[10px] bg-green-500/15 text-green-400 text-[13px] font-600 cursor-pointer border border-green-500/20 hover:bg-green-500/25 transition-all disabled:opacity-50"
|
|
673
|
+
style={{ fontFamily: 'inherit' }}
|
|
674
|
+
>
|
|
675
|
+
{actionLoading ? 'Connecting...' : 'Connect'}
|
|
676
|
+
</button>
|
|
677
|
+
)}
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
)
|
|
681
|
+
})()}
|
|
682
|
+
|
|
683
|
+
{/* WhatsApp QR code */}
|
|
684
|
+
{editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && qrDataUrl && (
|
|
685
|
+
<div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center"
|
|
686
|
+
style={{ animation: 'fade-in 0.3s ease-out' }}>
|
|
687
|
+
<div className="text-[13px] font-600 text-text-2 mb-1">Scan with WhatsApp</div>
|
|
688
|
+
<p className="text-[11px] text-text-3 mb-4">
|
|
689
|
+
Open WhatsApp > Settings > Linked Devices > Link a Device
|
|
690
|
+
</p>
|
|
691
|
+
<div className="inline-block p-2 bg-white rounded-[12px]">
|
|
692
|
+
<img src={qrDataUrl} alt="WhatsApp QR Code" className="w-[240px] h-[240px]" />
|
|
693
|
+
</div>
|
|
694
|
+
<p className="text-[11px] text-text-3 mt-3">QR code refreshes automatically</p>
|
|
695
|
+
</div>
|
|
696
|
+
)}
|
|
697
|
+
|
|
698
|
+
{/* WhatsApp connected (authenticated, no QR) */}
|
|
699
|
+
{editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && !qrDataUrl && waAuthenticated && (
|
|
700
|
+
<div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center">
|
|
701
|
+
<div className="text-[13px] font-600 text-green-400 mb-1">Connected</div>
|
|
702
|
+
<p className="text-[11px] text-text-3 mb-3">WhatsApp is paired and listening for messages</p>
|
|
703
|
+
<button
|
|
704
|
+
onClick={async () => {
|
|
705
|
+
if (!confirm('Unlink this device? You will need to scan a new QR code.')) return
|
|
706
|
+
setActionLoading(true)
|
|
707
|
+
try {
|
|
708
|
+
await api('PUT', `/connectors/${editing.id}`, { action: 'repair' })
|
|
709
|
+
setWaAuthenticated(false)
|
|
710
|
+
setWaHasCreds(false)
|
|
711
|
+
setQrDataUrl(null)
|
|
712
|
+
setWaConnecting(true)
|
|
713
|
+
await loadConnectors()
|
|
714
|
+
} catch (err: any) {
|
|
715
|
+
toast.error(`Failed to unlink: ${err.message}`)
|
|
716
|
+
} finally {
|
|
717
|
+
setActionLoading(false)
|
|
718
|
+
}
|
|
719
|
+
}}
|
|
720
|
+
disabled={actionLoading}
|
|
721
|
+
className="text-[12px] text-text-3 hover:text-red-400 transition-colors cursor-pointer bg-transparent border-none underline underline-offset-2"
|
|
722
|
+
style={{ fontFamily: 'inherit' }}
|
|
723
|
+
>
|
|
724
|
+
Unlink device
|
|
725
|
+
</button>
|
|
726
|
+
</div>
|
|
727
|
+
)}
|
|
728
|
+
|
|
729
|
+
{/* WhatsApp waiting for QR / reconnecting (not yet authenticated, no QR yet) */}
|
|
730
|
+
{editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && !qrDataUrl && !waAuthenticated && (
|
|
731
|
+
<div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center">
|
|
732
|
+
<div className="flex items-center justify-center gap-2 mb-1">
|
|
733
|
+
<span className="w-3 h-3 rounded-full border-2 border-[#3B82F6] border-t-transparent animate-spin" />
|
|
734
|
+
<span className="text-[13px] font-600 text-[#3B82F6]">
|
|
735
|
+
{waHasCreds ? 'Reconnecting...' : 'Waiting for QR code...'}
|
|
736
|
+
</span>
|
|
737
|
+
</div>
|
|
738
|
+
<p className="text-[11px] text-text-3">
|
|
739
|
+
{waHasCreds
|
|
740
|
+
? 'Reconnecting with saved session, this should only take a moment'
|
|
741
|
+
: 'Connecting to WhatsApp, QR code will appear shortly'}
|
|
742
|
+
</p>
|
|
743
|
+
{waHasCreds && (
|
|
744
|
+
<button
|
|
745
|
+
onClick={async () => {
|
|
746
|
+
if (!confirm('Force re-pair? This will clear saved credentials and show a new QR code.')) return
|
|
747
|
+
setActionLoading(true)
|
|
748
|
+
try {
|
|
749
|
+
await api('PUT', `/connectors/${editing.id}`, { action: 'repair' })
|
|
750
|
+
setWaAuthenticated(false)
|
|
751
|
+
setWaHasCreds(false)
|
|
752
|
+
setQrDataUrl(null)
|
|
753
|
+
setWaConnecting(true)
|
|
754
|
+
await loadConnectors()
|
|
755
|
+
} catch (err: any) {
|
|
756
|
+
toast.error(`Failed to re-pair: ${err.message}`)
|
|
757
|
+
} finally {
|
|
758
|
+
setActionLoading(false)
|
|
759
|
+
}
|
|
760
|
+
}}
|
|
761
|
+
disabled={actionLoading}
|
|
762
|
+
className="mt-3 text-[12px] text-text-3 hover:text-amber-400 transition-colors cursor-pointer bg-transparent border-none underline underline-offset-2"
|
|
763
|
+
style={{ fontFamily: 'inherit' }}
|
|
764
|
+
>
|
|
765
|
+
Force re-pair with new QR code
|
|
766
|
+
</button>
|
|
767
|
+
)}
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
|
|
771
|
+
{/* Error display */}
|
|
772
|
+
{editing?.lastError && (
|
|
773
|
+
<div className="mb-6 p-4 rounded-[14px] bg-red-500/[0.06] border border-red-500/15">
|
|
774
|
+
<div className="text-[12px] font-600 text-red-400 mb-1">Error</div>
|
|
775
|
+
<div className="text-[12px] text-red-400/70 leading-[1.5] font-mono">{editing.lastError}</div>
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
|
|
779
|
+
{/* Actions */}
|
|
780
|
+
<div className="flex gap-3 pt-4 border-t border-white/[0.04]">
|
|
781
|
+
{editing && (
|
|
782
|
+
<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' }}>
|
|
783
|
+
Delete
|
|
784
|
+
</button>
|
|
785
|
+
)}
|
|
786
|
+
<button
|
|
787
|
+
onClick={() => { setOpen(false); setEditingId(null) }}
|
|
788
|
+
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"
|
|
789
|
+
style={{ fontFamily: 'inherit' }}
|
|
790
|
+
>
|
|
791
|
+
Cancel
|
|
792
|
+
</button>
|
|
793
|
+
<button
|
|
794
|
+
onClick={handleSave}
|
|
795
|
+
disabled={saving || !agentId}
|
|
796
|
+
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"
|
|
797
|
+
style={{ fontFamily: 'inherit' }}
|
|
798
|
+
>
|
|
799
|
+
{saving ? 'Saving...' : editing ? 'Save' : 'Create Connector'}
|
|
800
|
+
</button>
|
|
801
|
+
</div>
|
|
802
|
+
</BottomSheet>
|
|
803
|
+
)
|
|
804
|
+
}
|