@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,225 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
|
5
|
+
import { Input } from '@/components/ui/input'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Badge } from '@/components/ui/badge'
|
|
8
|
+
import { api } from '@/lib/api-client'
|
|
9
|
+
import { toast } from 'sonner'
|
|
10
|
+
|
|
11
|
+
interface ClawHubSkill {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
description: string
|
|
15
|
+
author: string
|
|
16
|
+
tags: string[]
|
|
17
|
+
downloads: number
|
|
18
|
+
url: string
|
|
19
|
+
version: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SearchResponse {
|
|
23
|
+
skills: ClawHubSkill[]
|
|
24
|
+
total: number
|
|
25
|
+
page: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ClawHubBrowserProps {
|
|
29
|
+
open: boolean
|
|
30
|
+
onOpenChange: (open: boolean) => void
|
|
31
|
+
onInstalled?: () => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ClawHubBrowser({ open, onOpenChange, onInstalled }: ClawHubBrowserProps) {
|
|
35
|
+
const [query, setQuery] = useState('')
|
|
36
|
+
const [skills, setSkills] = useState<ClawHubSkill[]>([])
|
|
37
|
+
const [page, setPage] = useState(1)
|
|
38
|
+
const [total, setTotal] = useState(0)
|
|
39
|
+
const [loading, setLoading] = useState(false)
|
|
40
|
+
const [error, setError] = useState<string | null>(null)
|
|
41
|
+
const [installing, setInstalling] = useState<string | null>(null)
|
|
42
|
+
const [searched, setSearched] = useState(false)
|
|
43
|
+
|
|
44
|
+
const search = useCallback(async (q: string, p: number, append = false) => {
|
|
45
|
+
setLoading(true)
|
|
46
|
+
setError(null)
|
|
47
|
+
try {
|
|
48
|
+
const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
|
|
49
|
+
if (append) {
|
|
50
|
+
setSkills(prev => [...prev, ...res.skills])
|
|
51
|
+
} else {
|
|
52
|
+
setSkills(res.skills)
|
|
53
|
+
}
|
|
54
|
+
setTotal(res.total)
|
|
55
|
+
setPage(res.page)
|
|
56
|
+
setSearched(true)
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(err instanceof Error ? err.message : 'Failed to search ClawHub')
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false)
|
|
61
|
+
}
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (open) {
|
|
66
|
+
setQuery('')
|
|
67
|
+
setSkills([])
|
|
68
|
+
setPage(1)
|
|
69
|
+
setTotal(0)
|
|
70
|
+
setError(null)
|
|
71
|
+
setSearched(false)
|
|
72
|
+
search('', 1)
|
|
73
|
+
}
|
|
74
|
+
}, [open, search])
|
|
75
|
+
|
|
76
|
+
const handleSearch = () => {
|
|
77
|
+
setSkills([])
|
|
78
|
+
search(query, 1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
82
|
+
if (e.key === 'Enter') handleSearch()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handleLoadMore = () => {
|
|
86
|
+
search(query, page + 1, true)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleInstall = async (skill: ClawHubSkill) => {
|
|
90
|
+
setInstalling(skill.id)
|
|
91
|
+
try {
|
|
92
|
+
await api('POST', '/clawhub/install', {
|
|
93
|
+
name: skill.name,
|
|
94
|
+
description: skill.description,
|
|
95
|
+
url: skill.url,
|
|
96
|
+
tags: skill.tags,
|
|
97
|
+
})
|
|
98
|
+
toast.success(`Installed "${skill.name}"`)
|
|
99
|
+
onInstalled?.()
|
|
100
|
+
} catch (err) {
|
|
101
|
+
toast.error(err instanceof Error ? err.message : 'Install failed')
|
|
102
|
+
} finally {
|
|
103
|
+
setInstalling(null)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const hasMore = skills.length < total
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
111
|
+
<SheetContent side="right" className="w-full sm:max-w-lg flex flex-col">
|
|
112
|
+
<SheetHeader>
|
|
113
|
+
<SheetTitle className="font-display text-[16px] font-600 text-text">
|
|
114
|
+
ClawHub
|
|
115
|
+
</SheetTitle>
|
|
116
|
+
<p className="text-[12px] text-text-3/60">Browse and install community skills</p>
|
|
117
|
+
</SheetHeader>
|
|
118
|
+
|
|
119
|
+
<div className="flex gap-2 px-4">
|
|
120
|
+
<Input
|
|
121
|
+
placeholder="Search skills..."
|
|
122
|
+
value={query}
|
|
123
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
124
|
+
onKeyDown={handleKeyDown}
|
|
125
|
+
className="flex-1 text-[13px]"
|
|
126
|
+
/>
|
|
127
|
+
<Button size="sm" onClick={handleSearch} disabled={loading}>
|
|
128
|
+
Search
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div className="flex-1 overflow-y-auto px-4 pb-4">
|
|
133
|
+
{error && (
|
|
134
|
+
<div className="text-center py-12">
|
|
135
|
+
<p className="text-[13px] text-red-400">{error}</p>
|
|
136
|
+
<Button size="sm" variant="ghost" className="mt-2" onClick={() => search(query, 1)}>
|
|
137
|
+
Retry
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{!error && !loading && searched && skills.length === 0 && (
|
|
143
|
+
<div className="text-center py-12">
|
|
144
|
+
<p className="text-[13px] text-text-3/60">No skills found</p>
|
|
145
|
+
{query && (
|
|
146
|
+
<p className="text-[11px] text-text-3/40 mt-1">Try a different search term</p>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{skills.length > 0 && (
|
|
152
|
+
<div className="space-y-2">
|
|
153
|
+
{skills.map((skill) => (
|
|
154
|
+
<div
|
|
155
|
+
key={skill.id}
|
|
156
|
+
className="p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all"
|
|
157
|
+
>
|
|
158
|
+
<div className="flex items-start justify-between gap-2">
|
|
159
|
+
<div className="min-w-0 flex-1">
|
|
160
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
161
|
+
<span className="font-display text-[14px] font-600 text-text truncate">
|
|
162
|
+
{skill.name}
|
|
163
|
+
</span>
|
|
164
|
+
<span className="text-[10px] font-mono text-text-3/40 shrink-0">
|
|
165
|
+
v{skill.version}
|
|
166
|
+
</span>
|
|
167
|
+
</div>
|
|
168
|
+
<p className="text-[12px] text-text-3/60 line-clamp-2 mb-2">
|
|
169
|
+
{skill.description}
|
|
170
|
+
</p>
|
|
171
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
172
|
+
{skill.tags.slice(0, 4).map((tag) => (
|
|
173
|
+
<Badge
|
|
174
|
+
key={tag}
|
|
175
|
+
variant="secondary"
|
|
176
|
+
className="text-[10px] px-1.5 py-0"
|
|
177
|
+
>
|
|
178
|
+
{tag}
|
|
179
|
+
</Badge>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
<div className="flex items-center gap-3 mt-2 text-[11px] text-text-3/50">
|
|
183
|
+
<span>{skill.author}</span>
|
|
184
|
+
<span>{skill.downloads.toLocaleString()} installs</span>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<Button
|
|
188
|
+
size="sm"
|
|
189
|
+
variant="outline"
|
|
190
|
+
className="shrink-0 text-[12px]"
|
|
191
|
+
disabled={installing === skill.id}
|
|
192
|
+
onClick={() => handleInstall(skill)}
|
|
193
|
+
>
|
|
194
|
+
{installing === skill.id ? 'Installing...' : 'Install'}
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
))}
|
|
199
|
+
|
|
200
|
+
{hasMore && (
|
|
201
|
+
<div className="pt-2 pb-4 text-center">
|
|
202
|
+
<Button
|
|
203
|
+
size="sm"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
onClick={handleLoadMore}
|
|
206
|
+
disabled={loading}
|
|
207
|
+
className="text-[12px] text-text-3/60"
|
|
208
|
+
>
|
|
209
|
+
{loading ? 'Loading...' : 'Load More'}
|
|
210
|
+
</Button>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{loading && skills.length === 0 && (
|
|
217
|
+
<div className="flex items-center justify-center py-12">
|
|
218
|
+
<div className="h-5 w-5 animate-spin rounded-full border-2 border-text-3/20 border-t-text-3/60" />
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</SheetContent>
|
|
223
|
+
</Sheet>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { ClawHubBrowser } from './clawhub-browser'
|
|
6
|
+
|
|
7
|
+
export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
8
|
+
const skills = useAppStore((s) => s.skills)
|
|
9
|
+
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
10
|
+
const setSkillSheetOpen = useAppStore((s) => s.setSkillSheetOpen)
|
|
11
|
+
const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
|
|
12
|
+
const [clawHubOpen, setClawHubOpen] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
loadSkills()
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
const skillList = Object.values(skills)
|
|
19
|
+
|
|
20
|
+
const handleEdit = (id: string) => {
|
|
21
|
+
setEditingSkillId(id)
|
|
22
|
+
setSkillSheetOpen(true)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
|
|
27
|
+
<button
|
|
28
|
+
onClick={() => setClawHubOpen(true)}
|
|
29
|
+
className="w-full mb-3 py-2.5 px-4 rounded-[12px] border border-dashed border-white/[0.1] text-[13px] font-600 text-text-3 hover:text-accent-bright hover:border-accent-bright/30 transition-all cursor-pointer bg-transparent"
|
|
30
|
+
style={{ fontFamily: 'inherit' }}
|
|
31
|
+
>
|
|
32
|
+
Browse ClawHub Skills
|
|
33
|
+
</button>
|
|
34
|
+
<ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
|
|
35
|
+
{skillList.length === 0 ? (
|
|
36
|
+
<div className="text-center py-12">
|
|
37
|
+
<p className="text-[13px] text-text-3/60">No skills yet</p>
|
|
38
|
+
<button
|
|
39
|
+
onClick={() => setSkillSheetOpen(true)}
|
|
40
|
+
className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
|
|
41
|
+
style={{ fontFamily: 'inherit' }}
|
|
42
|
+
>
|
|
43
|
+
+ Add Skill
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
) : (
|
|
47
|
+
<div className="space-y-2">
|
|
48
|
+
{skillList.map((skill) => (
|
|
49
|
+
<button
|
|
50
|
+
key={skill.id}
|
|
51
|
+
onClick={() => handleEdit(skill.id)}
|
|
52
|
+
className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
53
|
+
>
|
|
54
|
+
<div className="flex items-center justify-between mb-1">
|
|
55
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
|
|
56
|
+
<span className="text-[10px] font-mono text-text-3/50 shrink-0 ml-2">{skill.filename}</span>
|
|
57
|
+
</div>
|
|
58
|
+
{skill.description && (
|
|
59
|
+
<p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
|
|
60
|
+
)}
|
|
61
|
+
<div className="text-[11px] text-text-3/70 mt-1.5">
|
|
62
|
+
{skill.content.length} chars
|
|
63
|
+
</div>
|
|
64
|
+
</button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
|
+
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
7
|
+
import { api } from '@/lib/api-client'
|
|
8
|
+
|
|
9
|
+
export function SkillSheet() {
|
|
10
|
+
const open = useAppStore((s) => s.skillSheetOpen)
|
|
11
|
+
const setOpen = useAppStore((s) => s.setSkillSheetOpen)
|
|
12
|
+
const editingId = useAppStore((s) => s.editingSkillId)
|
|
13
|
+
const setEditingId = useAppStore((s) => s.setEditingSkillId)
|
|
14
|
+
const skills = useAppStore((s) => s.skills)
|
|
15
|
+
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
16
|
+
const fileRef = useRef<HTMLInputElement>(null)
|
|
17
|
+
|
|
18
|
+
const [name, setName] = useState('')
|
|
19
|
+
const [filename, setFilename] = useState('')
|
|
20
|
+
const [description, setDescription] = useState('')
|
|
21
|
+
const [content, setContent] = useState('')
|
|
22
|
+
const [importUrl, setImportUrl] = useState('')
|
|
23
|
+
const [importingUrl, setImportingUrl] = useState(false)
|
|
24
|
+
const [importError, setImportError] = useState('')
|
|
25
|
+
const [importNotice, setImportNotice] = useState('')
|
|
26
|
+
|
|
27
|
+
// AI generation state
|
|
28
|
+
const [aiPrompt, setAiPrompt] = useState('')
|
|
29
|
+
const [generating, setGenerating] = useState(false)
|
|
30
|
+
const [generated, setGenerated] = useState(false)
|
|
31
|
+
const [genError, setGenError] = useState('')
|
|
32
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
33
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
34
|
+
|
|
35
|
+
const editing = editingId ? skills[editingId] : null
|
|
36
|
+
|
|
37
|
+
const handleGenerate = async () => {
|
|
38
|
+
if (!aiPrompt.trim()) return
|
|
39
|
+
setGenerating(true)
|
|
40
|
+
setGenError('')
|
|
41
|
+
try {
|
|
42
|
+
const result = await api<{ name?: string; description?: string; content?: string; error?: string }>('POST', '/generate', { type: 'skill', prompt: aiPrompt })
|
|
43
|
+
if (result.error) {
|
|
44
|
+
setGenError(result.error)
|
|
45
|
+
} else if (result.name || result.content) {
|
|
46
|
+
if (result.name) { setName(result.name); setFilename(`${result.name.toLowerCase().replace(/\s+/g, '-')}.md`) }
|
|
47
|
+
if (result.description) setDescription(result.description)
|
|
48
|
+
if (result.content) setContent(result.content)
|
|
49
|
+
setGenerated(true)
|
|
50
|
+
} else {
|
|
51
|
+
setGenError('AI returned empty response — try again')
|
|
52
|
+
}
|
|
53
|
+
} catch (err: unknown) {
|
|
54
|
+
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
55
|
+
}
|
|
56
|
+
setGenerating(false)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleImportFromUrl = async () => {
|
|
60
|
+
if (!importUrl.trim()) return
|
|
61
|
+
setImportingUrl(true)
|
|
62
|
+
setImportError('')
|
|
63
|
+
setImportNotice('')
|
|
64
|
+
try {
|
|
65
|
+
const result = await api<{ name: string; filename: string; description?: string; content: string; sourceFormat?: 'openclaw' | 'plain' }>('POST', '/skills/import', { url: importUrl.trim() })
|
|
66
|
+
setName(result.name || '')
|
|
67
|
+
setFilename(result.filename || '')
|
|
68
|
+
setDescription(result.description || '')
|
|
69
|
+
setContent(result.content || '')
|
|
70
|
+
if (result.sourceFormat === 'openclaw') {
|
|
71
|
+
setImportNotice('Imported OpenClaw SKILL.md format and stripped frontmatter automatically.')
|
|
72
|
+
} else {
|
|
73
|
+
setImportNotice('Skill imported from URL.')
|
|
74
|
+
}
|
|
75
|
+
setGenerated(false)
|
|
76
|
+
} catch (err: unknown) {
|
|
77
|
+
setImportError(err instanceof Error ? err.message : 'Failed to import skill URL')
|
|
78
|
+
} finally {
|
|
79
|
+
setImportingUrl(false)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (open) {
|
|
85
|
+
loadSettings()
|
|
86
|
+
setAiPrompt('')
|
|
87
|
+
setGenerating(false)
|
|
88
|
+
setGenerated(false)
|
|
89
|
+
setGenError('')
|
|
90
|
+
setImportUrl('')
|
|
91
|
+
setImportingUrl(false)
|
|
92
|
+
setImportError('')
|
|
93
|
+
setImportNotice('')
|
|
94
|
+
if (editing) {
|
|
95
|
+
setName(editing.name)
|
|
96
|
+
setFilename(editing.filename)
|
|
97
|
+
setDescription(editing.description || '')
|
|
98
|
+
setContent(editing.content)
|
|
99
|
+
} else {
|
|
100
|
+
setName('')
|
|
101
|
+
setFilename('')
|
|
102
|
+
setDescription('')
|
|
103
|
+
setContent('')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [open, editingId])
|
|
107
|
+
|
|
108
|
+
const onClose = () => {
|
|
109
|
+
setOpen(false)
|
|
110
|
+
setEditingId(null)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
114
|
+
const file = e.target.files?.[0]
|
|
115
|
+
if (!file) return
|
|
116
|
+
const reader = new FileReader()
|
|
117
|
+
reader.onload = (ev) => {
|
|
118
|
+
const text = ev.target?.result as string
|
|
119
|
+
setContent(text)
|
|
120
|
+
if (!name) setName(file.name.replace(/\.\w+$/, '').replace(/[-_]/g, ' '))
|
|
121
|
+
if (!filename) setFilename(file.name)
|
|
122
|
+
}
|
|
123
|
+
reader.readAsText(file)
|
|
124
|
+
e.target.value = ''
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const handleSave = async () => {
|
|
128
|
+
const data = {
|
|
129
|
+
name: name.trim() || 'Unnamed Skill',
|
|
130
|
+
filename: filename.trim() || `${name.trim().toLowerCase().replace(/\s+/g, '-')}.md`,
|
|
131
|
+
description,
|
|
132
|
+
content,
|
|
133
|
+
}
|
|
134
|
+
if (editing) {
|
|
135
|
+
await api('PUT', `/skills/${editing.id}`, data)
|
|
136
|
+
} else {
|
|
137
|
+
await api('POST', '/skills', data)
|
|
138
|
+
}
|
|
139
|
+
await loadSkills()
|
|
140
|
+
onClose()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const handleDelete = async () => {
|
|
144
|
+
if (editing) {
|
|
145
|
+
await api('DELETE', `/skills/${editing.id}`)
|
|
146
|
+
await loadSkills()
|
|
147
|
+
onClose()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<BottomSheet open={open} onClose={onClose} wide>
|
|
155
|
+
<div className="mb-10">
|
|
156
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
157
|
+
{editing ? 'Edit Skill' : 'New Skill'}
|
|
158
|
+
</h2>
|
|
159
|
+
<p className="text-[14px] text-text-3">Upload or write a reusable instruction set for agents</p>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* AI Generation */}
|
|
163
|
+
{!editing && <AiGenBlock
|
|
164
|
+
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
165
|
+
generating={generating} generated={generated} genError={genError}
|
|
166
|
+
onGenerate={handleGenerate} appSettings={appSettings}
|
|
167
|
+
placeholder='Describe the skill, e.g. "A frontend design skill for building polished React components with Tailwind"'
|
|
168
|
+
/>}
|
|
169
|
+
|
|
170
|
+
{/* File upload */}
|
|
171
|
+
{!editing && (
|
|
172
|
+
<div className="mb-8">
|
|
173
|
+
<label
|
|
174
|
+
onClick={() => fileRef.current?.click()}
|
|
175
|
+
className="flex items-center justify-center gap-2.5 w-full py-4 rounded-[14px] border border-dashed border-white/[0.1] bg-transparent text-text-3 text-[14px] font-600 cursor-pointer hover:border-accent-bright/30 hover:text-accent-bright hover:bg-accent-soft transition-all duration-200"
|
|
176
|
+
>
|
|
177
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
178
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
179
|
+
<polyline points="17 8 12 3 7 8" />
|
|
180
|
+
<line x1="12" y1="3" x2="12" y2="15" />
|
|
181
|
+
</svg>
|
|
182
|
+
Upload .md file
|
|
183
|
+
</label>
|
|
184
|
+
<input ref={fileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload} className="hidden" />
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{!editing && (
|
|
189
|
+
<div className="mb-8 p-4 rounded-[14px] border border-white/[0.08] bg-surface">
|
|
190
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Import from URL</label>
|
|
191
|
+
<div className="flex gap-2">
|
|
192
|
+
<input
|
|
193
|
+
type="url"
|
|
194
|
+
value={importUrl}
|
|
195
|
+
onChange={(e) => setImportUrl(e.target.value)}
|
|
196
|
+
placeholder="https://.../SKILL.md"
|
|
197
|
+
className={`${inputClass} flex-1`}
|
|
198
|
+
style={{ fontFamily: 'inherit' }}
|
|
199
|
+
/>
|
|
200
|
+
<button
|
|
201
|
+
onClick={handleImportFromUrl}
|
|
202
|
+
disabled={importingUrl || !importUrl.trim()}
|
|
203
|
+
className="px-4 py-3 rounded-[12px] border-none bg-[#6366F1] text-white text-[13px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
204
|
+
style={{ fontFamily: 'inherit' }}
|
|
205
|
+
>
|
|
206
|
+
{importingUrl ? 'Importing...' : 'Import'}
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
{importError && <p className="mt-2 text-[12px] text-red-400/80">{importError}</p>}
|
|
210
|
+
{importNotice && <p className="mt-2 text-[12px] text-emerald-400/80">{importNotice}</p>}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
<div className="mb-8">
|
|
215
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
|
|
216
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Frontend Design" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
<div className="mb-8">
|
|
220
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
221
|
+
Description <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
222
|
+
</label>
|
|
223
|
+
<input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Short summary of what this skill does" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="mb-8">
|
|
227
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Content</label>
|
|
228
|
+
<textarea
|
|
229
|
+
value={content}
|
|
230
|
+
onChange={(e) => setContent(e.target.value)}
|
|
231
|
+
placeholder="# Skill Instructions Write your skill content in markdown..."
|
|
232
|
+
rows={10}
|
|
233
|
+
className={`${inputClass} resize-y min-h-[200px] font-mono text-[13px]`}
|
|
234
|
+
style={{ fontFamily: 'inherit' }}
|
|
235
|
+
/>
|
|
236
|
+
<p className="text-[11px] text-text-3/70 mt-2">{content.length} characters</p>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="flex gap-3 pt-2 border-t border-white/[0.04]">
|
|
240
|
+
{editing && (
|
|
241
|
+
<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' }}>
|
|
242
|
+
Delete
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
<button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
246
|
+
Cancel
|
|
247
|
+
</button>
|
|
248
|
+
<button onClick={handleSave} disabled={!name.trim() || !content.trim()} 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" style={{ fontFamily: 'inherit' }}>
|
|
249
|
+
{editing ? 'Save' : 'Create'}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</BottomSheet>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useCallback, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { updateTask } from '@/lib/tasks'
|
|
6
|
+
import { TaskColumn } from './task-column'
|
|
7
|
+
import type { BoardTaskStatus } from '@/types'
|
|
8
|
+
|
|
9
|
+
const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
|
|
10
|
+
|
|
11
|
+
export function TaskBoard() {
|
|
12
|
+
const tasks = useAppStore((s) => s.tasks)
|
|
13
|
+
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
14
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
15
|
+
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
16
|
+
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
17
|
+
const agents = useAppStore((s) => s.agents)
|
|
18
|
+
const showArchived = useAppStore((s) => s.showArchivedTasks)
|
|
19
|
+
const setShowArchived = useAppStore((s) => s.setShowArchivedTasks)
|
|
20
|
+
const [filterAgentId, setFilterAgentId] = useState<string>('')
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
loadTasks()
|
|
24
|
+
loadAgents()
|
|
25
|
+
const interval = setInterval(loadTasks, 5000)
|
|
26
|
+
return () => clearInterval(interval)
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
|
|
30
|
+
|
|
31
|
+
const tasksByStatus = (status: BoardTaskStatus) =>
|
|
32
|
+
Object.values(tasks)
|
|
33
|
+
.filter((t) => t.status === status && (!filterAgentId || t.agentId === filterAgentId))
|
|
34
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
35
|
+
|
|
36
|
+
const handleDrop = useCallback(async (taskId: string, newStatus: BoardTaskStatus) => {
|
|
37
|
+
const task = tasks[taskId]
|
|
38
|
+
if (!task || task.status === newStatus) return
|
|
39
|
+
await updateTask(taskId, { status: newStatus })
|
|
40
|
+
await loadTasks()
|
|
41
|
+
}, [tasks, loadTasks])
|
|
42
|
+
|
|
43
|
+
const archivedCount = Object.values(tasks).filter((t) => t.status === 'archived').length
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
|
47
|
+
<div className="flex items-center justify-between px-8 pt-6 pb-4 shrink-0">
|
|
48
|
+
<div>
|
|
49
|
+
<h1 className="font-display text-[28px] font-800 tracking-[-0.03em]">Task Board</h1>
|
|
50
|
+
<p className="text-[13px] text-text-3 mt-1">Create tasks and assign orchestrators to run them sequentially</p>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="flex items-center gap-3">
|
|
53
|
+
<select
|
|
54
|
+
value={filterAgentId}
|
|
55
|
+
onChange={(e) => setFilterAgentId(e.target.value)}
|
|
56
|
+
className="px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
57
|
+
bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03] appearance-none"
|
|
58
|
+
style={{ fontFamily: 'inherit', minWidth: 130 }}
|
|
59
|
+
>
|
|
60
|
+
<option value="">All Agents</option>
|
|
61
|
+
{Object.values(agents).map((a) => (
|
|
62
|
+
<option key={a.id} value={a.id}>{a.name}</option>
|
|
63
|
+
))}
|
|
64
|
+
</select>
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => setShowArchived(!showArchived)}
|
|
67
|
+
className={`px-4 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
68
|
+
${showArchived
|
|
69
|
+
? 'bg-white/[0.06] border-white/[0.1] text-text-2'
|
|
70
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
71
|
+
style={{ fontFamily: 'inherit' }}
|
|
72
|
+
>
|
|
73
|
+
{showArchived ? 'Hide' : 'Show'} Archived{!showArchived && archivedCount > 0 ? '' : ''}
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => {
|
|
77
|
+
setEditingTaskId(null)
|
|
78
|
+
setTaskSheetOpen(true)
|
|
79
|
+
}}
|
|
80
|
+
className="px-5 py-2.5 rounded-[12px] border-none bg-[#6366F1] text-white text-[14px] font-600 cursor-pointer
|
|
81
|
+
hover:brightness-110 active:scale-[0.97] transition-all shadow-[0_2px_12px_rgba(99,102,241,0.2)]"
|
|
82
|
+
style={{ fontFamily: 'inherit' }}
|
|
83
|
+
>
|
|
84
|
+
+ New Task
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="flex-1 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden">
|
|
90
|
+
{columns.map((status) => (
|
|
91
|
+
<TaskColumn key={status} status={status} tasks={tasksByStatus(status)} onDrop={handleDrop} />
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|