@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,166 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
|
|
6
|
+
function webhookUrl(id: string): string {
|
|
7
|
+
if (typeof window === 'undefined') return `/api/webhooks/${id}`
|
|
8
|
+
return `${window.location.origin}/api/webhooks/${id}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatEvents(events: string[] | undefined): string {
|
|
12
|
+
const list = Array.isArray(events) ? events.filter(Boolean) : []
|
|
13
|
+
if (list.length === 0) return 'all events'
|
|
14
|
+
if (list.length <= 2) return list.join(', ')
|
|
15
|
+
return `${list.slice(0, 2).join(', ')}, +${list.length - 2}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function WebhookList({ inSidebar }: { inSidebar?: boolean }) {
|
|
19
|
+
const webhooks = useAppStore((s) => s.webhooks)
|
|
20
|
+
const loadWebhooks = useAppStore((s) => s.loadWebhooks)
|
|
21
|
+
const setWebhookSheetOpen = useAppStore((s) => s.setWebhookSheetOpen)
|
|
22
|
+
const setEditingWebhookId = useAppStore((s) => s.setEditingWebhookId)
|
|
23
|
+
const agents = useAppStore((s) => s.agents)
|
|
24
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
25
|
+
const [copied, setCopied] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
loadWebhooks()
|
|
29
|
+
loadAgents()
|
|
30
|
+
}, [loadWebhooks, loadAgents])
|
|
31
|
+
|
|
32
|
+
const list = useMemo(
|
|
33
|
+
() => Object.values(webhooks).sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)),
|
|
34
|
+
[webhooks]
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const copyText = async (key: string, value: string) => {
|
|
38
|
+
try {
|
|
39
|
+
await navigator.clipboard.writeText(value)
|
|
40
|
+
setCopied(key)
|
|
41
|
+
setTimeout(() => setCopied((prev) => (prev === key ? null : prev)), 1400)
|
|
42
|
+
} catch {
|
|
43
|
+
// ignore clipboard failures (e.g. unsupported environment)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!list.length) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex-1 flex flex-col items-center justify-center px-6 py-12 text-center">
|
|
50
|
+
<div className="w-12 h-12 rounded-[14px] bg-white/[0.03] border border-white/[0.06] flex items-center justify-center mb-4">
|
|
51
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3">
|
|
52
|
+
<path d="M22 12h-4l-3 7L9 5l-3 7H2" />
|
|
53
|
+
</svg>
|
|
54
|
+
</div>
|
|
55
|
+
<p className="text-[13px] text-text-3 mb-1 font-600">No webhooks yet</p>
|
|
56
|
+
<p className="text-[12px] text-text-3/60">Create inbound endpoints to trigger agent runs</p>
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => {
|
|
59
|
+
setEditingWebhookId(null)
|
|
60
|
+
setWebhookSheetOpen(true)
|
|
61
|
+
}}
|
|
62
|
+
className="mt-3 text-[13px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
|
|
63
|
+
>
|
|
64
|
+
+ Add Webhook
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'pb-10' : 'pb-20'}`}>
|
|
72
|
+
{list.map((hook) => {
|
|
73
|
+
const agentName = hook.agentId ? agents[hook.agentId]?.name : null
|
|
74
|
+
const endpoint = webhookUrl(hook.id)
|
|
75
|
+
const copiedEndpoint = copied === `endpoint:${hook.id}`
|
|
76
|
+
const copiedSecret = copied === `secret:${hook.id}`
|
|
77
|
+
const hasSecret = typeof hook.secret === 'string' && hook.secret.trim().length > 0
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
key={hook.id}
|
|
82
|
+
className="w-full flex items-center gap-2.5 px-5 py-3 hover:bg-white/[0.02] transition-colors group"
|
|
83
|
+
>
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => {
|
|
86
|
+
setEditingWebhookId(hook.id)
|
|
87
|
+
setWebhookSheetOpen(true)
|
|
88
|
+
}}
|
|
89
|
+
className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer bg-transparent border-none text-left p-0"
|
|
90
|
+
>
|
|
91
|
+
<div className={`shrink-0 w-9 h-9 rounded-[10px] border flex items-center justify-center ${
|
|
92
|
+
hook.isEnabled
|
|
93
|
+
? 'bg-emerald-500/12 border-emerald-500/20 text-emerald-300'
|
|
94
|
+
: 'bg-white/[0.03] border-white/[0.08] text-text-3'
|
|
95
|
+
}`}>
|
|
96
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
97
|
+
<path d="M22 12h-4l-3 7L9 5l-3 7H2" />
|
|
98
|
+
</svg>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="flex-1 min-w-0">
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<span className="text-[13px] font-600 text-text truncate">{hook.name || 'Unnamed Webhook'}</span>
|
|
104
|
+
<span className={`shrink-0 w-2 h-2 rounded-full ${hook.isEnabled ? 'bg-emerald-400' : 'bg-white/20'}`} />
|
|
105
|
+
</div>
|
|
106
|
+
<div className="text-[11px] text-text-3 truncate">
|
|
107
|
+
{hook.source || 'custom'} · {formatEvents(hook.events)}{agentName ? ` · ${agentName}` : ''}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
<button
|
|
113
|
+
onClick={(e) => {
|
|
114
|
+
e.stopPropagation()
|
|
115
|
+
copyText(`endpoint:${hook.id}`, endpoint)
|
|
116
|
+
}}
|
|
117
|
+
title={copiedEndpoint ? 'Copied endpoint' : 'Copy endpoint URL'}
|
|
118
|
+
className={`shrink-0 w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer border-none ${
|
|
119
|
+
copiedEndpoint
|
|
120
|
+
? 'opacity-100 bg-emerald-500/15 text-emerald-300'
|
|
121
|
+
: 'opacity-0 group-hover:opacity-100 focus:opacity-100 bg-accent-soft/40 text-accent-bright hover:bg-accent-soft'
|
|
122
|
+
}`}
|
|
123
|
+
>
|
|
124
|
+
{copiedEndpoint ? (
|
|
125
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
126
|
+
<polyline points="20 6 9 17 4 12" />
|
|
127
|
+
</svg>
|
|
128
|
+
) : (
|
|
129
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
130
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
131
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
132
|
+
</svg>
|
|
133
|
+
)}
|
|
134
|
+
</button>
|
|
135
|
+
|
|
136
|
+
{hasSecret && (
|
|
137
|
+
<button
|
|
138
|
+
onClick={(e) => {
|
|
139
|
+
e.stopPropagation()
|
|
140
|
+
copyText(`secret:${hook.id}`, hook.secret!.trim())
|
|
141
|
+
}}
|
|
142
|
+
title={copiedSecret ? 'Copied secret' : 'Copy secret'}
|
|
143
|
+
className={`shrink-0 w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer border-none ${
|
|
144
|
+
copiedSecret
|
|
145
|
+
? 'opacity-100 bg-emerald-500/15 text-emerald-300'
|
|
146
|
+
: 'opacity-0 group-hover:opacity-100 focus:opacity-100 bg-white/[0.04] text-text-2 hover:bg-white/[0.08]'
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{copiedSecret ? (
|
|
150
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
|
151
|
+
<polyline points="20 6 9 17 4 12" />
|
|
152
|
+
</svg>
|
|
153
|
+
) : (
|
|
154
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
155
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
156
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
157
|
+
</svg>
|
|
158
|
+
)}
|
|
159
|
+
</button>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
})}
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } 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 type { Webhook, WebhookLogEntry } from '@/types'
|
|
8
|
+
|
|
9
|
+
type WebhookApiResponse = Webhook | { error: string }
|
|
10
|
+
type DeleteWebhookResponse = { ok: boolean } | { error: string }
|
|
11
|
+
|
|
12
|
+
const inputClass = 'w-full px-4 py-3 rounded-[14px] bg-bg border border-white/[0.06] text-text text-[14px] outline-none focus:border-accent-bright/40 transition-colors placeholder:text-text-3/70'
|
|
13
|
+
|
|
14
|
+
function webhookUrl(id: string): string {
|
|
15
|
+
if (typeof window === 'undefined') return `/api/webhooks/${id}`
|
|
16
|
+
return `${window.location.origin}/api/webhooks/${id}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseEvents(input: string): string[] {
|
|
20
|
+
const values = input
|
|
21
|
+
.split(/[\n,]+/)
|
|
22
|
+
.map((v) => v.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
return Array.from(new Set(values))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeSecret(length = 28): string {
|
|
28
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
|
|
29
|
+
const arr = new Uint8Array(length)
|
|
30
|
+
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
|
|
31
|
+
crypto.getRandomValues(arr)
|
|
32
|
+
} else {
|
|
33
|
+
for (let i = 0; i < length; i++) arr[i] = Math.floor(Math.random() * 256)
|
|
34
|
+
}
|
|
35
|
+
let out = ''
|
|
36
|
+
for (let i = 0; i < length; i++) out += chars[arr[i] % chars.length]
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function WebhookSheet() {
|
|
41
|
+
const open = useAppStore((s) => s.webhookSheetOpen)
|
|
42
|
+
const setOpen = useAppStore((s) => s.setWebhookSheetOpen)
|
|
43
|
+
const editingId = useAppStore((s) => s.editingWebhookId)
|
|
44
|
+
const setEditingId = useAppStore((s) => s.setEditingWebhookId)
|
|
45
|
+
const webhooks = useAppStore((s) => s.webhooks)
|
|
46
|
+
const loadWebhooks = useAppStore((s) => s.loadWebhooks)
|
|
47
|
+
const agents = useAppStore((s) => s.agents)
|
|
48
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
49
|
+
|
|
50
|
+
const [name, setName] = useState('')
|
|
51
|
+
const [source, setSource] = useState('custom')
|
|
52
|
+
const [eventsText, setEventsText] = useState('')
|
|
53
|
+
const [agentId, setAgentId] = useState('')
|
|
54
|
+
const [secret, setSecret] = useState('')
|
|
55
|
+
const [isEnabled, setIsEnabled] = useState(true)
|
|
56
|
+
const [saving, setSaving] = useState(false)
|
|
57
|
+
const [copied, setCopied] = useState<'endpoint' | 'secret' | null>(null)
|
|
58
|
+
const [error, setError] = useState<string | null>(null)
|
|
59
|
+
const [tab, setTab] = useState<'config' | 'history'>('config')
|
|
60
|
+
const [history, setHistory] = useState<WebhookLogEntry[]>([])
|
|
61
|
+
const [historyLoading, setHistoryLoading] = useState(false)
|
|
62
|
+
|
|
63
|
+
const editing = editingId ? (webhooks[editingId] as Webhook | undefined) : null
|
|
64
|
+
const endpoint = editing ? webhookUrl(editing.id) : ''
|
|
65
|
+
const orchestrators = useMemo(
|
|
66
|
+
() => Object.values(agents).filter((a) => a.isOrchestrator),
|
|
67
|
+
[agents]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (open) {
|
|
72
|
+
loadWebhooks()
|
|
73
|
+
loadAgents()
|
|
74
|
+
setCopied(null)
|
|
75
|
+
setError(null)
|
|
76
|
+
setTab('config')
|
|
77
|
+
setHistory([])
|
|
78
|
+
}
|
|
79
|
+
}, [open, loadWebhooks, loadAgents])
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (tab === 'history' && editing) {
|
|
83
|
+
setHistoryLoading(true)
|
|
84
|
+
api<WebhookLogEntry[]>('GET', `/webhooks/${editing.id}/history`)
|
|
85
|
+
.then((res) => setHistory(Array.isArray(res) ? res : []))
|
|
86
|
+
.catch(() => setHistory([]))
|
|
87
|
+
.finally(() => setHistoryLoading(false))
|
|
88
|
+
}
|
|
89
|
+
}, [tab, editing])
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (editing) {
|
|
93
|
+
setName(editing.name || '')
|
|
94
|
+
setSource(editing.source || 'custom')
|
|
95
|
+
setEventsText((editing.events || []).join(', '))
|
|
96
|
+
setAgentId(editing.agentId || '')
|
|
97
|
+
setSecret(editing.secret || '')
|
|
98
|
+
setIsEnabled(editing.isEnabled !== false)
|
|
99
|
+
} else {
|
|
100
|
+
setName('')
|
|
101
|
+
setSource('custom')
|
|
102
|
+
setEventsText('')
|
|
103
|
+
setAgentId('')
|
|
104
|
+
setSecret(makeSecret())
|
|
105
|
+
setIsEnabled(true)
|
|
106
|
+
}
|
|
107
|
+
}, [editing, open])
|
|
108
|
+
|
|
109
|
+
const handleClose = () => {
|
|
110
|
+
setOpen(false)
|
|
111
|
+
setEditingId(null)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const copyText = async (type: 'endpoint' | 'secret', value: string) => {
|
|
115
|
+
if (!value) return
|
|
116
|
+
try {
|
|
117
|
+
await navigator.clipboard.writeText(value)
|
|
118
|
+
setCopied(type)
|
|
119
|
+
setTimeout(() => setCopied((prev) => (prev === type ? null : prev)), 1500)
|
|
120
|
+
} catch {
|
|
121
|
+
// ignore clipboard errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const handleSave = async () => {
|
|
126
|
+
if (!agentId) {
|
|
127
|
+
setError('An orchestrator agent is required.')
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const payload = {
|
|
132
|
+
name: name.trim() || 'Unnamed Webhook',
|
|
133
|
+
source: source.trim() || 'custom',
|
|
134
|
+
events: parseEvents(eventsText),
|
|
135
|
+
agentId: agentId || null,
|
|
136
|
+
secret: secret.trim(),
|
|
137
|
+
isEnabled,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setSaving(true)
|
|
141
|
+
setError(null)
|
|
142
|
+
try {
|
|
143
|
+
if (editing) {
|
|
144
|
+
const updated = await api<WebhookApiResponse>('PUT', `/webhooks/${editing.id}`, payload)
|
|
145
|
+
if ('error' in updated && updated.error) throw new Error(updated.error)
|
|
146
|
+
} else {
|
|
147
|
+
const created = await api<WebhookApiResponse>('POST', '/webhooks', payload)
|
|
148
|
+
if ('error' in created && created.error) throw new Error(created.error)
|
|
149
|
+
}
|
|
150
|
+
await loadWebhooks()
|
|
151
|
+
handleClose()
|
|
152
|
+
} catch (err: unknown) {
|
|
153
|
+
setError(err instanceof Error ? err.message : 'Failed to save webhook')
|
|
154
|
+
} finally {
|
|
155
|
+
setSaving(false)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const handleDelete = async () => {
|
|
160
|
+
if (!editing || !confirm('Delete this webhook?')) return
|
|
161
|
+
try {
|
|
162
|
+
const res = await api<DeleteWebhookResponse>('DELETE', `/webhooks/${editing.id}`)
|
|
163
|
+
if ('error' in res && res.error) throw new Error(res.error)
|
|
164
|
+
await loadWebhooks()
|
|
165
|
+
handleClose()
|
|
166
|
+
} catch (err: unknown) {
|
|
167
|
+
setError(err instanceof Error ? err.message : 'Failed to delete webhook')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<BottomSheet open={open} onClose={handleClose} wide>
|
|
173
|
+
<div className="space-y-6">
|
|
174
|
+
<div>
|
|
175
|
+
<h2 className="font-display text-[24px] font-700 tracking-[-0.02em] mb-1">
|
|
176
|
+
{editing ? 'Edit Webhook' : 'New Webhook'}
|
|
177
|
+
</h2>
|
|
178
|
+
<p className="text-[13px] text-text-3">Create an inbound endpoint that triggers an orchestrator</p>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{editing && (
|
|
182
|
+
<div className="flex gap-1 p-1 rounded-[12px] bg-bg border border-white/[0.06]">
|
|
183
|
+
{(['config', 'history'] as const).map((t) => (
|
|
184
|
+
<button
|
|
185
|
+
key={t}
|
|
186
|
+
onClick={() => setTab(t)}
|
|
187
|
+
className={`flex-1 py-2 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none capitalize ${
|
|
188
|
+
tab === t ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
|
|
189
|
+
}`}
|
|
190
|
+
style={{ fontFamily: 'inherit' }}
|
|
191
|
+
>
|
|
192
|
+
{t}
|
|
193
|
+
</button>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{tab === 'history' && editing ? (
|
|
199
|
+
<div>
|
|
200
|
+
{historyLoading ? (
|
|
201
|
+
<div className="text-center py-8 text-[13px] text-text-3">Loading history...</div>
|
|
202
|
+
) : history.length === 0 ? (
|
|
203
|
+
<div className="text-center py-8 text-[13px] text-text-3/60">No webhook invocations yet</div>
|
|
204
|
+
) : (
|
|
205
|
+
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
|
206
|
+
{history.map((entry) => (
|
|
207
|
+
<div key={entry.id} className="p-3 rounded-[10px] border border-white/[0.06] bg-white/[0.02]">
|
|
208
|
+
<div className="flex items-center gap-2 mb-1">
|
|
209
|
+
<span className={`text-[10px] font-700 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px] ${
|
|
210
|
+
entry.status === 'success' ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'
|
|
211
|
+
}`}>
|
|
212
|
+
{entry.status}
|
|
213
|
+
</span>
|
|
214
|
+
<span className="text-[11px] text-text-3/60 font-mono">{entry.event}</span>
|
|
215
|
+
<span className="text-[10px] text-text-3/40 ml-auto">
|
|
216
|
+
{new Date(entry.timestamp).toLocaleString()}
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
219
|
+
{entry.error && (
|
|
220
|
+
<div className="text-[11px] text-red-300/80 mt-1">{entry.error}</div>
|
|
221
|
+
)}
|
|
222
|
+
{entry.sessionId && (
|
|
223
|
+
<div className="text-[10px] text-text-3/50 mt-1 font-mono">Session: {entry.sessionId}</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
))}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
) : null}
|
|
231
|
+
|
|
232
|
+
{tab === 'config' && error && (
|
|
233
|
+
<div className="px-3.5 py-2.5 rounded-[12px] bg-red-500/10 border border-red-500/20 text-[12px] text-red-300">
|
|
234
|
+
{error}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{tab === 'config' && editing && (
|
|
239
|
+
<div className="p-4 rounded-[14px] bg-white/[0.02] border border-white/[0.06]">
|
|
240
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Endpoint URL</label>
|
|
241
|
+
<div className="flex gap-2">
|
|
242
|
+
<input
|
|
243
|
+
readOnly
|
|
244
|
+
value={endpoint}
|
|
245
|
+
className={`${inputClass} font-mono text-[12px]`}
|
|
246
|
+
/>
|
|
247
|
+
<button
|
|
248
|
+
onClick={() => copyText('endpoint', endpoint)}
|
|
249
|
+
className="px-3.5 py-2 rounded-[10px] border border-accent-bright/20 bg-accent-soft/40 text-accent-bright text-[12px] font-600 cursor-pointer hover:bg-accent-soft transition-colors"
|
|
250
|
+
style={{ fontFamily: 'inherit' }}
|
|
251
|
+
>
|
|
252
|
+
{copied === 'endpoint' ? 'Copied' : 'Copy'}
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
<p className="mt-2 text-[11px] text-text-3/70">
|
|
256
|
+
POST JSON payloads to this URL. Include <code className="font-mono">x-webhook-secret</code> if a secret is set.
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{tab === 'config' && <>
|
|
262
|
+
<div>
|
|
263
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Name</label>
|
|
264
|
+
<input
|
|
265
|
+
type="text"
|
|
266
|
+
value={name}
|
|
267
|
+
onChange={(e) => setName(e.target.value)}
|
|
268
|
+
placeholder="e.g. GitHub Push"
|
|
269
|
+
className={inputClass}
|
|
270
|
+
style={{ fontFamily: 'inherit' }}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div>
|
|
275
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Source</label>
|
|
276
|
+
<input
|
|
277
|
+
type="text"
|
|
278
|
+
value={source}
|
|
279
|
+
onChange={(e) => setSource(e.target.value)}
|
|
280
|
+
placeholder="custom, github, slack..."
|
|
281
|
+
className={inputClass}
|
|
282
|
+
style={{ fontFamily: 'inherit' }}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div>
|
|
287
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Route to Orchestrator</label>
|
|
288
|
+
<select
|
|
289
|
+
value={agentId}
|
|
290
|
+
onChange={(e) => setAgentId(e.target.value)}
|
|
291
|
+
className={`${inputClass} appearance-none cursor-pointer`}
|
|
292
|
+
style={{ fontFamily: 'inherit' }}
|
|
293
|
+
>
|
|
294
|
+
<option value="">Select orchestrator...</option>
|
|
295
|
+
{orchestrators.map((agent) => (
|
|
296
|
+
<option key={agent.id} value={agent.id}>{agent.name}</option>
|
|
297
|
+
))}
|
|
298
|
+
</select>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div>
|
|
302
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
|
|
303
|
+
Events <span className="normal-case tracking-normal font-normal text-text-3/70">(optional)</span>
|
|
304
|
+
</label>
|
|
305
|
+
<textarea
|
|
306
|
+
value={eventsText}
|
|
307
|
+
onChange={(e) => setEventsText(e.target.value)}
|
|
308
|
+
placeholder="push, release or *"
|
|
309
|
+
rows={3}
|
|
310
|
+
className={`${inputClass} resize-y min-h-[86px] font-mono text-[12px]`}
|
|
311
|
+
style={{ fontFamily: 'inherit' }}
|
|
312
|
+
/>
|
|
313
|
+
<p className="mt-1.5 text-[11px] text-text-3/70">Leave blank for all events. Use commas or new lines. Use <code>*</code> to match all.</p>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div>
|
|
317
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
|
|
318
|
+
Secret <span className="normal-case tracking-normal font-normal text-text-3/70">(optional but recommended)</span>
|
|
319
|
+
</label>
|
|
320
|
+
<div className="flex gap-2">
|
|
321
|
+
<input
|
|
322
|
+
type="text"
|
|
323
|
+
value={secret}
|
|
324
|
+
onChange={(e) => setSecret(e.target.value)}
|
|
325
|
+
placeholder="x-webhook-secret value"
|
|
326
|
+
className={`${inputClass} font-mono text-[12px]`}
|
|
327
|
+
style={{ fontFamily: 'inherit' }}
|
|
328
|
+
/>
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => copyText('secret', secret)}
|
|
331
|
+
disabled={!secret.trim()}
|
|
332
|
+
className="px-3.5 py-2 rounded-[10px] border border-white/[0.1] bg-white/[0.04] text-text-2 text-[12px] font-600 cursor-pointer hover:bg-white/[0.08] transition-colors disabled:opacity-40"
|
|
333
|
+
style={{ fontFamily: 'inherit' }}
|
|
334
|
+
>
|
|
335
|
+
{copied === 'secret' ? 'Copied' : 'Copy'}
|
|
336
|
+
</button>
|
|
337
|
+
<button
|
|
338
|
+
onClick={() => setSecret(makeSecret())}
|
|
339
|
+
className="px-3.5 py-2 rounded-[10px] border border-accent-bright/20 bg-accent-soft/40 text-accent-bright text-[12px] font-600 cursor-pointer hover:bg-accent-soft transition-colors"
|
|
340
|
+
style={{ fontFamily: 'inherit' }}
|
|
341
|
+
>
|
|
342
|
+
Regenerate
|
|
343
|
+
</button>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div>
|
|
348
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Status</label>
|
|
349
|
+
<div className="flex p-1 rounded-[12px] bg-bg border border-white/[0.06]">
|
|
350
|
+
<button
|
|
351
|
+
onClick={() => setIsEnabled(true)}
|
|
352
|
+
className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
|
|
353
|
+
isEnabled ? 'bg-emerald-500/15 text-emerald-300' : 'bg-transparent text-text-3 hover:text-text-2'
|
|
354
|
+
}`}
|
|
355
|
+
style={{ fontFamily: 'inherit' }}
|
|
356
|
+
>
|
|
357
|
+
Enabled
|
|
358
|
+
</button>
|
|
359
|
+
<button
|
|
360
|
+
onClick={() => setIsEnabled(false)}
|
|
361
|
+
className={`flex-1 py-2.5 rounded-[10px] text-center cursor-pointer transition-all text-[13px] font-600 border-none ${
|
|
362
|
+
!isEnabled ? 'bg-white/[0.08] text-text-2' : 'bg-transparent text-text-3 hover:text-text-2'
|
|
363
|
+
}`}
|
|
364
|
+
style={{ fontFamily: 'inherit' }}
|
|
365
|
+
>
|
|
366
|
+
Disabled
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
371
|
+
<div className="flex gap-3 pt-2">
|
|
372
|
+
{editing && (
|
|
373
|
+
<button
|
|
374
|
+
onClick={handleDelete}
|
|
375
|
+
className="px-5 py-3 rounded-[14px] border border-danger/30 bg-transparent text-danger text-[14px] font-600 cursor-pointer hover:bg-danger/10 transition-colors"
|
|
376
|
+
style={{ fontFamily: 'inherit' }}
|
|
377
|
+
>
|
|
378
|
+
Delete
|
|
379
|
+
</button>
|
|
380
|
+
)}
|
|
381
|
+
<div className="flex-1" />
|
|
382
|
+
<button
|
|
383
|
+
onClick={handleClose}
|
|
384
|
+
className="px-5 py-3 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-600 cursor-pointer hover:bg-surface-2 transition-colors"
|
|
385
|
+
style={{ fontFamily: 'inherit' }}
|
|
386
|
+
>
|
|
387
|
+
Cancel
|
|
388
|
+
</button>
|
|
389
|
+
<button
|
|
390
|
+
onClick={handleSave}
|
|
391
|
+
disabled={saving}
|
|
392
|
+
className="px-8 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
393
|
+
style={{ fontFamily: 'inherit' }}
|
|
394
|
+
>
|
|
395
|
+
{saving ? 'Saving...' : editing ? 'Update' : 'Create'}
|
|
396
|
+
</button>
|
|
397
|
+
</div>
|
|
398
|
+
</>}
|
|
399
|
+
</div>
|
|
400
|
+
</BottomSheet>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
export function useAutoResize(maxHeight = 120) {
|
|
6
|
+
const ref = useRef<HTMLTextAreaElement>(null)
|
|
7
|
+
|
|
8
|
+
const resize = useCallback(() => {
|
|
9
|
+
const el = ref.current
|
|
10
|
+
if (!el) return
|
|
11
|
+
el.style.height = 'auto'
|
|
12
|
+
el.style.height = Math.min(el.scrollHeight, maxHeight) + 'px'
|
|
13
|
+
}, [maxHeight])
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
resize()
|
|
17
|
+
}, [resize])
|
|
18
|
+
|
|
19
|
+
return { ref, resize }
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useSyncExternalStore } from 'react'
|
|
4
|
+
|
|
5
|
+
export function useMediaQuery(query: string): boolean {
|
|
6
|
+
const subscribe = useCallback(
|
|
7
|
+
(callback: () => void) => {
|
|
8
|
+
const mql = window.matchMedia(query)
|
|
9
|
+
mql.addEventListener('change', callback)
|
|
10
|
+
return () => mql.removeEventListener('change', callback)
|
|
11
|
+
},
|
|
12
|
+
[query],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const getSnapshot = () => window.matchMedia(query).matches
|
|
16
|
+
|
|
17
|
+
// Return false during SSR — matches initial client render before hydration
|
|
18
|
+
const getServerSnapshot = () => false
|
|
19
|
+
|
|
20
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
21
|
+
}
|