@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,56 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
|
|
6
|
+
interface DaemonStatus {
|
|
7
|
+
running: boolean
|
|
8
|
+
schedulerActive: boolean
|
|
9
|
+
queueLength: number
|
|
10
|
+
lastProcessed: number | null
|
|
11
|
+
nextScheduled: number | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DaemonIndicator() {
|
|
15
|
+
const [status, setStatus] = useState<DaemonStatus | null>(null)
|
|
16
|
+
|
|
17
|
+
const fetchStatus = async () => {
|
|
18
|
+
try {
|
|
19
|
+
const data = await api<DaemonStatus>('GET', '/daemon')
|
|
20
|
+
setStatus(data)
|
|
21
|
+
} catch { /* ignore */ }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
fetchStatus()
|
|
26
|
+
const interval = setInterval(fetchStatus, 30_000)
|
|
27
|
+
return () => clearInterval(interval)
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const toggle = async () => {
|
|
31
|
+
try {
|
|
32
|
+
await api('POST', '/daemon', { action: status?.running ? 'stop' : 'start' })
|
|
33
|
+
fetchStatus()
|
|
34
|
+
} catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!status) return null
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
onClick={toggle}
|
|
42
|
+
className="flex items-center gap-2 px-3 py-2 rounded-[10px] bg-surface border border-white/[0.06] hover:bg-surface-2 transition-colors cursor-pointer w-full"
|
|
43
|
+
title={status.running ? 'Daemon running — click to pause' : 'Daemon paused — click to start'}
|
|
44
|
+
>
|
|
45
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ${status.running ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.5)]' : 'bg-text-3/30'}`} />
|
|
46
|
+
<span className="text-[12px] font-600 text-text-2 flex-1 text-left">
|
|
47
|
+
Daemon
|
|
48
|
+
</span>
|
|
49
|
+
{status.queueLength > 0 && (
|
|
50
|
+
<span className="text-[10px] font-mono text-amber-400/70">
|
|
51
|
+
{status.queueLength} queued
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</button>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
4
|
+
import { IconButton } from '@/components/shared/icon-button'
|
|
5
|
+
|
|
6
|
+
export function MobileHeader() {
|
|
7
|
+
const toggleSidebar = useAppStore((s) => s.toggleSidebar)
|
|
8
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
9
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
10
|
+
const session = currentSessionId ? sessions[currentSessionId] : null
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<header className="flex items-center gap-3 px-4 py-2.5 border-b border-white/[0.04] bg-bg/80 backdrop-blur-md shrink-0 min-h-[48px]"
|
|
14
|
+
style={{ paddingTop: 'max(10px, env(safe-area-inset-top))' }}>
|
|
15
|
+
<IconButton onClick={toggleSidebar} aria-label="Toggle sidebar">
|
|
16
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
17
|
+
<line x1="3" y1="7" x2="21" y2="7" />
|
|
18
|
+
<line x1="3" y1="12" x2="15" y2="12" />
|
|
19
|
+
<line x1="3" y1="17" x2="18" y2="17" />
|
|
20
|
+
</svg>
|
|
21
|
+
</IconButton>
|
|
22
|
+
<h1 className="font-display text-[14px] font-600 tracking-[-0.02em] flex-1 truncate">
|
|
23
|
+
{session ? (
|
|
24
|
+
<span className="block truncate">{session.name}</span>
|
|
25
|
+
) : (
|
|
26
|
+
<span className="font-700">SwarmClaw</span>
|
|
27
|
+
)}
|
|
28
|
+
</h1>
|
|
29
|
+
</header>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
4
|
+
|
|
5
|
+
export function NetworkBanner() {
|
|
6
|
+
const info = useAppStore((s) => s.networkInfo)
|
|
7
|
+
if (!info) return null
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="px-4 py-1.5 border-b border-white/[0.04] text-[10px] text-text-3 flex items-center gap-2 shrink-0">
|
|
11
|
+
<span className="w-[5px] h-[5px] rounded-full bg-success shrink-0" />
|
|
12
|
+
<code className="font-mono text-[10px] text-text-3/70 select-all">
|
|
13
|
+
{info.ip}:{info.port}
|
|
14
|
+
</code>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
|
+
|
|
5
|
+
const CHECK_INTERVAL = 5 * 60_000 // 5 minutes
|
|
6
|
+
|
|
7
|
+
type VersionInfo = {
|
|
8
|
+
localSha: string
|
|
9
|
+
remoteSha: string
|
|
10
|
+
updateAvailable: boolean
|
|
11
|
+
behindBy: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type UpdateState = 'idle' | 'updating' | 'done' | 'error'
|
|
15
|
+
|
|
16
|
+
export function UpdateBanner() {
|
|
17
|
+
const [version, setVersion] = useState<VersionInfo | null>(null)
|
|
18
|
+
const [updateState, setUpdateState] = useState<UpdateState>('idle')
|
|
19
|
+
const [dismissed, setDismissed] = useState<string | null>(null)
|
|
20
|
+
const [errorMsg, setErrorMsg] = useState('')
|
|
21
|
+
|
|
22
|
+
const checkVersion = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch('/api/version')
|
|
25
|
+
if (!res.ok) return
|
|
26
|
+
const data: VersionInfo = await res.json()
|
|
27
|
+
setVersion(data)
|
|
28
|
+
} catch {
|
|
29
|
+
// silently fail — no network or server issue
|
|
30
|
+
}
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
checkVersion()
|
|
35
|
+
const id = setInterval(checkVersion, CHECK_INTERVAL)
|
|
36
|
+
return () => clearInterval(id)
|
|
37
|
+
}, [checkVersion])
|
|
38
|
+
|
|
39
|
+
const handleUpdate = async () => {
|
|
40
|
+
setUpdateState('updating')
|
|
41
|
+
setErrorMsg('')
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch('/api/version/update', { method: 'POST' })
|
|
44
|
+
const data = await res.json()
|
|
45
|
+
if (data.success) {
|
|
46
|
+
setUpdateState('done')
|
|
47
|
+
} else {
|
|
48
|
+
setUpdateState('error')
|
|
49
|
+
setErrorMsg(data.error || 'Update failed')
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
setUpdateState('error')
|
|
53
|
+
setErrorMsg('Network error')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const handleDismiss = () => {
|
|
58
|
+
if (version) setDismissed(version.remoteSha)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Don't show if no update, or user dismissed this specific remote SHA
|
|
62
|
+
if (!version?.updateAvailable) return null
|
|
63
|
+
if (dismissed === version.remoteSha && updateState === 'idle') return null
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="px-4 py-1.5 border-b border-white/[0.04] text-[10px] flex items-center gap-2 shrink-0 bg-[#6366F1]/[0.04]">
|
|
67
|
+
{updateState === 'idle' && (
|
|
68
|
+
<>
|
|
69
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
|
|
70
|
+
<path d="M12 19V5M5 12l7-7 7 7" />
|
|
71
|
+
</svg>
|
|
72
|
+
<span className="text-text-3 flex-1 min-w-0 truncate">
|
|
73
|
+
<span className="text-accent-bright font-600">{version.behindBy}</span> update{version.behindBy !== 1 ? 's' : ''} available
|
|
74
|
+
</span>
|
|
75
|
+
<button
|
|
76
|
+
onClick={handleUpdate}
|
|
77
|
+
className="text-[10px] font-600 text-accent-bright hover:text-white bg-[#6366F1]/20 hover:bg-[#6366F1]/30 px-2 py-0.5 rounded-[6px] border-none cursor-pointer transition-all shrink-0"
|
|
78
|
+
style={{ fontFamily: 'inherit' }}
|
|
79
|
+
>
|
|
80
|
+
Update
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
onClick={handleDismiss}
|
|
84
|
+
className="text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer p-0 shrink-0 transition-colors"
|
|
85
|
+
>
|
|
86
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
87
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
88
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
89
|
+
</svg>
|
|
90
|
+
</button>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
{updateState === 'updating' && (
|
|
95
|
+
<>
|
|
96
|
+
<span className="w-3 h-3 border-[1.5px] border-accent-bright/30 border-t-accent-bright rounded-full shrink-0"
|
|
97
|
+
style={{ animation: 'spin 0.8s linear infinite' }} />
|
|
98
|
+
<span className="text-text-3">Updating...</span>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{updateState === 'done' && (
|
|
103
|
+
<>
|
|
104
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-success shrink-0">
|
|
105
|
+
<polyline points="20 6 9 17 4 12" />
|
|
106
|
+
</svg>
|
|
107
|
+
<span className="text-text-3 flex-1">Updated! Restart to apply.</span>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{updateState === 'error' && (
|
|
112
|
+
<>
|
|
113
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-red-400 shrink-0">
|
|
114
|
+
<circle cx="12" cy="12" r="10" />
|
|
115
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
116
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
117
|
+
</svg>
|
|
118
|
+
<span className="text-red-400/80 flex-1 truncate">{errorMsg}</span>
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => setUpdateState('idle')}
|
|
121
|
+
className="text-[10px] font-600 text-text-3 hover:text-text bg-white/[0.04] px-2 py-0.5 rounded-[6px] border-none cursor-pointer transition-all shrink-0"
|
|
122
|
+
style={{ fontFamily: 'inherit' }}
|
|
123
|
+
>
|
|
124
|
+
Retry
|
|
125
|
+
</button>
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef, useCallback } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
|
+
|
|
8
|
+
interface LogEntry {
|
|
9
|
+
time: string
|
|
10
|
+
level: string
|
|
11
|
+
tag: string
|
|
12
|
+
message: string
|
|
13
|
+
data?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LEVEL_COLORS: Record<string, string> = {
|
|
17
|
+
ERROR: 'text-red-400',
|
|
18
|
+
WARN: 'text-amber-400',
|
|
19
|
+
INFO: 'text-blue-400',
|
|
20
|
+
DEBUG: 'text-text-3',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LEVEL_BG: Record<string, string> = {
|
|
24
|
+
ERROR: 'bg-red-500/10',
|
|
25
|
+
WARN: 'bg-amber-500/10',
|
|
26
|
+
INFO: 'bg-blue-500/10',
|
|
27
|
+
DEBUG: 'bg-white/[0.02]',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function LogList() {
|
|
31
|
+
const [entries, setEntries] = useState<LogEntry[]>([])
|
|
32
|
+
const [total, setTotal] = useState(0)
|
|
33
|
+
const [loading, setLoading] = useState(true)
|
|
34
|
+
const [search, setSearch] = useState('')
|
|
35
|
+
const [levelFilter, setLevelFilter] = useState<string[]>([])
|
|
36
|
+
const [autoRefresh, setAutoRefresh] = useState(true)
|
|
37
|
+
const [selected, setSelected] = useState<LogEntry | null>(null)
|
|
38
|
+
const [creatingTask, setCreatingTask] = useState(false)
|
|
39
|
+
const [taskAgentId, setTaskAgentId] = useState('')
|
|
40
|
+
const [savedFilters, setSavedFilters] = useState<Array<{ name: string; levels: string[]; search: string }>>(() => {
|
|
41
|
+
if (typeof window === 'undefined') return []
|
|
42
|
+
try { return JSON.parse(localStorage.getItem('sc_log_filters') || '[]') } catch { return [] }
|
|
43
|
+
})
|
|
44
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
|
|
46
|
+
const agents = useAppStore((s) => s.agents)
|
|
47
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
48
|
+
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
49
|
+
|
|
50
|
+
const fetchLogs = useCallback(async () => {
|
|
51
|
+
try {
|
|
52
|
+
const params = new URLSearchParams({ lines: '300' })
|
|
53
|
+
if (levelFilter.length) params.set('level', levelFilter.join(','))
|
|
54
|
+
if (search) params.set('search', search)
|
|
55
|
+
const res = await api('GET', `/logs?${params}`) as { entries: LogEntry[]; total: number }
|
|
56
|
+
setEntries(res.entries || [])
|
|
57
|
+
setTotal(res.total || 0)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('Failed to fetch logs:', err)
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false)
|
|
62
|
+
}
|
|
63
|
+
}, [levelFilter, search])
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
fetchLogs()
|
|
67
|
+
loadAgents()
|
|
68
|
+
}, [fetchLogs])
|
|
69
|
+
|
|
70
|
+
// Auto-refresh every 3s
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!autoRefresh) return
|
|
73
|
+
const id = setInterval(fetchLogs, 3000)
|
|
74
|
+
return () => clearInterval(id)
|
|
75
|
+
}, [autoRefresh, fetchLogs])
|
|
76
|
+
|
|
77
|
+
const clearLogs = async () => {
|
|
78
|
+
try {
|
|
79
|
+
await api('DELETE', '/logs')
|
|
80
|
+
setEntries([])
|
|
81
|
+
setTotal(0)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Failed to clear logs:', err)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const toggleLevel = (level: string) => {
|
|
88
|
+
setLevelFilter((prev) =>
|
|
89
|
+
prev.includes(level) ? prev.filter((l) => l !== level) : [...prev, level]
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const formatTime = (iso: string) => {
|
|
94
|
+
if (!iso) return ''
|
|
95
|
+
try {
|
|
96
|
+
const d = new Date(iso)
|
|
97
|
+
return d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
98
|
+
} catch {
|
|
99
|
+
return iso.slice(11, 19)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const formatFullTime = (iso: string) => {
|
|
104
|
+
if (!iso) return ''
|
|
105
|
+
try {
|
|
106
|
+
const d = new Date(iso)
|
|
107
|
+
return d.toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'medium' })
|
|
108
|
+
} catch {
|
|
109
|
+
return iso
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleCreateTask = async () => {
|
|
114
|
+
if (!selected) return
|
|
115
|
+
setCreatingTask(true)
|
|
116
|
+
try {
|
|
117
|
+
const title = `[${selected.level}] ${selected.tag}: ${selected.message}`.slice(0, 120)
|
|
118
|
+
const description = [
|
|
119
|
+
`**Log Level:** ${selected.level}`,
|
|
120
|
+
`**Source:** ${selected.tag}`,
|
|
121
|
+
`**Time:** ${selected.time}`,
|
|
122
|
+
`**Message:** ${selected.message}`,
|
|
123
|
+
selected.data ? `\n**Data:**\n\`\`\`\n${selected.data}\n\`\`\`` : '',
|
|
124
|
+
].filter(Boolean).join('\n')
|
|
125
|
+
|
|
126
|
+
await api('POST', '/tasks', {
|
|
127
|
+
title,
|
|
128
|
+
description,
|
|
129
|
+
status: 'backlog',
|
|
130
|
+
agentId: taskAgentId || undefined,
|
|
131
|
+
})
|
|
132
|
+
await loadTasks()
|
|
133
|
+
setSelected(null)
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error('Failed to create task:', err)
|
|
136
|
+
} finally {
|
|
137
|
+
setCreatingTask(false)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (loading) {
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex-1 flex items-center justify-center text-text-3 text-[13px]">
|
|
144
|
+
Loading logs...
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const agentList = Object.values(agents)
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
153
|
+
{/* Controls */}
|
|
154
|
+
<div className="px-4 py-2 space-y-2 shrink-0">
|
|
155
|
+
{/* Search */}
|
|
156
|
+
<input
|
|
157
|
+
value={search}
|
|
158
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
159
|
+
placeholder="Search logs..."
|
|
160
|
+
className="w-full px-3 py-2 rounded-[8px] bg-white/[0.04] border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent/30"
|
|
161
|
+
/>
|
|
162
|
+
{/* Saved filters */}
|
|
163
|
+
{savedFilters.length > 0 && (
|
|
164
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
165
|
+
{savedFilters.map((f, i) => (
|
|
166
|
+
<button
|
|
167
|
+
key={i}
|
|
168
|
+
onClick={() => { setLevelFilter(f.levels); setSearch(f.search) }}
|
|
169
|
+
className="group flex items-center gap-1 px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-accent-soft text-accent-bright hover:bg-[#6366F1]/15"
|
|
170
|
+
>
|
|
171
|
+
{f.name}
|
|
172
|
+
<span
|
|
173
|
+
onClick={(e) => {
|
|
174
|
+
e.stopPropagation()
|
|
175
|
+
const next = savedFilters.filter((_, j) => j !== i)
|
|
176
|
+
localStorage.setItem('sc_log_filters', JSON.stringify(next))
|
|
177
|
+
setSavedFilters(next)
|
|
178
|
+
}}
|
|
179
|
+
className="text-accent-bright/50 hover:text-red-400 ml-0.5"
|
|
180
|
+
>
|
|
181
|
+
x
|
|
182
|
+
</span>
|
|
183
|
+
</button>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{/* Level filters + controls */}
|
|
188
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
189
|
+
{['ERROR', 'WARN', 'INFO', 'DEBUG'].map((level) => (
|
|
190
|
+
<button
|
|
191
|
+
key={level}
|
|
192
|
+
onClick={() => toggleLevel(level)}
|
|
193
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-700 uppercase tracking-wider cursor-pointer transition-all border-none ${
|
|
194
|
+
levelFilter.length === 0 || levelFilter.includes(level)
|
|
195
|
+
? `${LEVEL_BG[level]} ${LEVEL_COLORS[level]}`
|
|
196
|
+
: 'bg-white/[0.02] text-text-3/70'
|
|
197
|
+
}`}
|
|
198
|
+
>
|
|
199
|
+
{level}
|
|
200
|
+
</button>
|
|
201
|
+
))}
|
|
202
|
+
<div className="flex-1" />
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => setAutoRefresh(!autoRefresh)}
|
|
205
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
206
|
+
autoRefresh ? 'bg-green-500/10 text-green-400' : 'bg-white/[0.04] text-text-3'
|
|
207
|
+
}`}
|
|
208
|
+
title={autoRefresh ? 'Auto-refresh ON' : 'Auto-refresh OFF'}
|
|
209
|
+
>
|
|
210
|
+
{autoRefresh ? 'LIVE' : 'PAUSED'}
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
onClick={clearLogs}
|
|
214
|
+
className="px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-white/[0.04] text-text-3 hover:text-red-400 hover:bg-red-500/10"
|
|
215
|
+
title="Clear all logs"
|
|
216
|
+
>
|
|
217
|
+
CLEAR
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
onClick={() => {
|
|
221
|
+
const blob = new Blob([JSON.stringify(entries, null, 2)], { type: 'application/json' })
|
|
222
|
+
const url = URL.createObjectURL(blob)
|
|
223
|
+
const a = document.createElement('a')
|
|
224
|
+
a.href = url
|
|
225
|
+
a.download = `swarmclaw-logs-${new Date().toISOString().slice(0, 10)}.json`
|
|
226
|
+
a.click()
|
|
227
|
+
URL.revokeObjectURL(url)
|
|
228
|
+
}}
|
|
229
|
+
className="px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-white/[0.04] text-text-3 hover:text-accent-bright hover:bg-accent-soft"
|
|
230
|
+
title="Export logs as JSON"
|
|
231
|
+
>
|
|
232
|
+
EXPORT
|
|
233
|
+
</button>
|
|
234
|
+
<button
|
|
235
|
+
onClick={() => {
|
|
236
|
+
const name = prompt('Filter name:')
|
|
237
|
+
if (!name?.trim()) return
|
|
238
|
+
const filter = { name: name.trim(), levels: levelFilter, search }
|
|
239
|
+
const existing = JSON.parse(localStorage.getItem('sc_log_filters') || '[]')
|
|
240
|
+
existing.push(filter)
|
|
241
|
+
localStorage.setItem('sc_log_filters', JSON.stringify(existing))
|
|
242
|
+
setSavedFilters(existing)
|
|
243
|
+
}}
|
|
244
|
+
className="px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none bg-white/[0.04] text-text-3 hover:text-accent-bright hover:bg-accent-soft"
|
|
245
|
+
title="Save current filter"
|
|
246
|
+
>
|
|
247
|
+
SAVE
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* Total count */}
|
|
253
|
+
<div className="px-4 py-1 text-[10px] text-text-3/60">
|
|
254
|
+
{entries.length} of {total} entries
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Log entries */}
|
|
258
|
+
<div ref={scrollRef} className="flex-1 overflow-y-auto px-2 pb-20">
|
|
259
|
+
{entries.length === 0 ? (
|
|
260
|
+
<div className="flex items-center justify-center h-32 text-text-3 text-[12px]">
|
|
261
|
+
No log entries
|
|
262
|
+
</div>
|
|
263
|
+
) : (
|
|
264
|
+
entries.map((entry, i) => (
|
|
265
|
+
<button
|
|
266
|
+
key={i}
|
|
267
|
+
onClick={() => { setSelected(entry); setTaskAgentId('') }}
|
|
268
|
+
className={`w-full text-left px-2 py-1.5 rounded-[6px] hover:bg-white/[0.03] transition-colors cursor-pointer bg-transparent border-none block
|
|
269
|
+
${entry.level === 'ERROR' ? 'hover:bg-red-500/[0.04]' : ''}`}
|
|
270
|
+
>
|
|
271
|
+
<div className="flex items-start gap-2">
|
|
272
|
+
<span className="text-[10px] text-text-3/50 font-mono shrink-0 mt-[1px] w-[58px]">
|
|
273
|
+
{formatTime(entry.time)}
|
|
274
|
+
</span>
|
|
275
|
+
<span className={`text-[9px] font-700 uppercase tracking-wider shrink-0 mt-[2px] w-[36px] ${LEVEL_COLORS[entry.level] || 'text-text-3'}`}>
|
|
276
|
+
{entry.level}
|
|
277
|
+
</span>
|
|
278
|
+
<span className="text-[10px] font-600 text-accent/60 shrink-0 mt-[1px]">
|
|
279
|
+
{entry.tag}
|
|
280
|
+
</span>
|
|
281
|
+
<span className="text-[11px] text-text-2 truncate flex-1">
|
|
282
|
+
{entry.message}
|
|
283
|
+
</span>
|
|
284
|
+
{/* Arrow indicator */}
|
|
285
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50 shrink-0 mt-[2px]">
|
|
286
|
+
<polyline points="9 18 15 12 9 6" />
|
|
287
|
+
</svg>
|
|
288
|
+
</div>
|
|
289
|
+
</button>
|
|
290
|
+
))
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{/* Log Detail Sheet */}
|
|
295
|
+
<BottomSheet open={!!selected} onClose={() => setSelected(null)}>
|
|
296
|
+
{selected && (
|
|
297
|
+
<>
|
|
298
|
+
<div className="mb-8">
|
|
299
|
+
<div className="flex items-center gap-3 mb-3">
|
|
300
|
+
<span className={`text-[11px] font-700 uppercase tracking-wider px-2.5 py-1 rounded-[6px] ${LEVEL_BG[selected.level]} ${LEVEL_COLORS[selected.level]}`}>
|
|
301
|
+
{selected.level}
|
|
302
|
+
</span>
|
|
303
|
+
<span className="text-[12px] font-600 text-accent/80 font-mono">{selected.tag}</span>
|
|
304
|
+
</div>
|
|
305
|
+
<h2 className="font-display text-[22px] font-700 tracking-[-0.02em] mb-2 leading-snug">
|
|
306
|
+
{selected.message}
|
|
307
|
+
</h2>
|
|
308
|
+
<p className="text-[12px] text-text-3/60 font-mono">{formatFullTime(selected.time)}</p>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{/* Data payload */}
|
|
312
|
+
{selected.data && (
|
|
313
|
+
<div className="mb-8">
|
|
314
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Details</label>
|
|
315
|
+
<pre className="text-[11px] text-text-3/80 font-mono whitespace-pre-wrap break-all bg-white/[0.02] rounded-[12px] p-4 max-h-[300px] overflow-auto border border-white/[0.04]">
|
|
316
|
+
{selected.data}
|
|
317
|
+
</pre>
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{/* Create as Task */}
|
|
322
|
+
<div className="pt-4 border-t border-white/[0.04]">
|
|
323
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
324
|
+
Create as Task
|
|
325
|
+
</label>
|
|
326
|
+
<p className="text-[12px] text-text-3/60 mb-3">
|
|
327
|
+
Turn this log entry into a task and optionally assign it to an agent to investigate.
|
|
328
|
+
</p>
|
|
329
|
+
<div className="flex gap-2">
|
|
330
|
+
<select
|
|
331
|
+
value={taskAgentId}
|
|
332
|
+
onChange={(e) => setTaskAgentId(e.target.value)}
|
|
333
|
+
className="flex-1 px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none appearance-none cursor-pointer"
|
|
334
|
+
style={{ fontFamily: 'inherit' }}
|
|
335
|
+
>
|
|
336
|
+
<option value="">Unassigned</option>
|
|
337
|
+
{agentList.map((a) => (
|
|
338
|
+
<option key={a.id} value={a.id}>{a.name}</option>
|
|
339
|
+
))}
|
|
340
|
+
</select>
|
|
341
|
+
<button
|
|
342
|
+
onClick={handleCreateTask}
|
|
343
|
+
disabled={creatingTask}
|
|
344
|
+
className="px-5 py-3 rounded-[14px] border-none bg-[#6366F1] text-white text-[14px] font-600
|
|
345
|
+
cursor-pointer active:scale-[0.97] disabled:opacity-40 transition-all
|
|
346
|
+
shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110 shrink-0"
|
|
347
|
+
style={{ fontFamily: 'inherit' }}
|
|
348
|
+
>
|
|
349
|
+
{creatingTask ? 'Creating...' : 'Create Task'}
|
|
350
|
+
</button>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</>
|
|
354
|
+
)}
|
|
355
|
+
</BottomSheet>
|
|
356
|
+
</div>
|
|
357
|
+
)
|
|
358
|
+
}
|