@swarmclawai/swarmclaw 0.3.1 → 0.4.5
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 +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +133 -90
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +9 -4
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -59,8 +59,19 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
59
59
|
await loadMcpServers()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
const handleRetest = async (e: React.MouseEvent, id: string) => {
|
|
63
|
+
e.stopPropagation()
|
|
64
|
+
setStatuses((prev) => ({ ...prev, [id]: { ok: false, loading: true } }))
|
|
65
|
+
try {
|
|
66
|
+
const res = await api<{ ok: boolean; tools?: string[]; error?: string }>('POST', `/mcp-servers/${id}/test`)
|
|
67
|
+
setStatuses((prev) => ({ ...prev, [id]: { ok: res.ok, tools: res.tools, error: res.error, loading: false } }))
|
|
68
|
+
} catch {
|
|
69
|
+
setStatuses((prev) => ({ ...prev, [id]: { ok: false, error: 'Test failed', loading: false } }))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
return (
|
|
63
|
-
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-
|
|
74
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
64
75
|
{serverList.length === 0 ? (
|
|
65
76
|
<div className="text-center py-12">
|
|
66
77
|
<p className="text-[13px] text-text-3/60">No MCP servers configured</p>
|
|
@@ -73,7 +84,7 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
73
84
|
</button>
|
|
74
85
|
</div>
|
|
75
86
|
) : (
|
|
76
|
-
<div className=
|
|
87
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
77
88
|
{serverList.map((server) => (
|
|
78
89
|
<button
|
|
79
90
|
key={server.id}
|
|
@@ -96,6 +107,17 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
96
107
|
<span className="font-display text-[14px] font-600 text-text truncate">{server.name}</span>
|
|
97
108
|
</div>
|
|
98
109
|
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
110
|
+
{!inSidebar && (
|
|
111
|
+
<button
|
|
112
|
+
onClick={(e) => handleRetest(e, server.id)}
|
|
113
|
+
className="text-text-3/40 hover:text-text-2 transition-colors p-0.5"
|
|
114
|
+
title="Re-test connection"
|
|
115
|
+
>
|
|
116
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
117
|
+
<path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" />
|
|
118
|
+
</svg>
|
|
119
|
+
</button>
|
|
120
|
+
)}
|
|
99
121
|
<span className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${transportColors[server.transport] || 'bg-white/10 text-text-3'}`}>
|
|
100
122
|
{server.transport}
|
|
101
123
|
</span>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect } from 'react'
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
import type { MarketplacePlugin } from '@/types'
|
|
5
7
|
|
|
6
8
|
export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
7
9
|
const plugins = useAppStore((s) => s.plugins)
|
|
@@ -9,10 +11,31 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
9
11
|
const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
|
|
10
12
|
const setEditingPluginFilename = useAppStore((s) => s.setEditingPluginFilename)
|
|
11
13
|
|
|
14
|
+
const [tab, setTab] = useState<'installed' | 'marketplace'>('installed')
|
|
15
|
+
const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
|
|
16
|
+
const [mpLoading, setMpLoading] = useState(false)
|
|
17
|
+
const [installing, setInstalling] = useState<string | null>(null)
|
|
18
|
+
const [search, setSearch] = useState('')
|
|
19
|
+
const [activeTag, setActiveTag] = useState<string | null>(null)
|
|
20
|
+
const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
|
|
21
|
+
|
|
12
22
|
useEffect(() => {
|
|
13
23
|
loadPlugins()
|
|
14
24
|
}, [])
|
|
15
25
|
|
|
26
|
+
const loadMarketplace = useCallback(async () => {
|
|
27
|
+
setMpLoading(true)
|
|
28
|
+
try {
|
|
29
|
+
const data = await api<MarketplacePlugin[]>('GET', '/plugins/marketplace')
|
|
30
|
+
if (Array.isArray(data)) setMarketplace(data)
|
|
31
|
+
} catch { /* ignore */ }
|
|
32
|
+
setMpLoading(false)
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!inSidebar && tab === 'marketplace') loadMarketplace()
|
|
37
|
+
}, [tab, inSidebar, loadMarketplace])
|
|
38
|
+
|
|
16
39
|
const pluginList = Object.values(plugins)
|
|
17
40
|
|
|
18
41
|
const handleEdit = (filename: string) => {
|
|
@@ -20,41 +43,218 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
20
43
|
setPluginSheetOpen(true)
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
const handleToggle = async (e: React.MouseEvent, filename: string, enabled: boolean) => {
|
|
47
|
+
e.stopPropagation()
|
|
48
|
+
await api('POST', '/plugins', { filename, enabled: !enabled })
|
|
49
|
+
loadPlugins()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleDelete = async (e: React.MouseEvent, filename: string) => {
|
|
53
|
+
e.stopPropagation()
|
|
54
|
+
await api('DELETE', `/plugins/${encodeURIComponent(filename)}`)
|
|
55
|
+
loadPlugins()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const installFromMarketplace = async (p: MarketplacePlugin) => {
|
|
59
|
+
setInstalling(p.id)
|
|
60
|
+
try {
|
|
61
|
+
await api('POST', '/plugins/install', { url: p.url, filename: `${p.id}.js` })
|
|
62
|
+
await loadPlugins()
|
|
63
|
+
} catch { /* ignore */ }
|
|
64
|
+
setInstalling(null)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const installedFilenames = new Set(Object.keys(plugins))
|
|
68
|
+
|
|
69
|
+
const tabClass = (t: string) =>
|
|
70
|
+
`py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
71
|
+
${tab === t
|
|
72
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
73
|
+
: 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
|
|
74
|
+
|
|
75
|
+
// Marketplace tab content (full-width only)
|
|
76
|
+
const renderMarketplace = () => {
|
|
77
|
+
if (mpLoading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
|
|
78
|
+
if (marketplace.length === 0) return <p className="text-[12px] text-text-3/70 py-8 text-center">No plugins available</p>
|
|
79
|
+
|
|
80
|
+
const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags))).sort()
|
|
81
|
+
const q = search.toLowerCase()
|
|
82
|
+
const filtered = marketplace
|
|
83
|
+
.filter((p) => {
|
|
84
|
+
if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !p.tags.some((t) => t.toLowerCase().includes(q))) return false
|
|
85
|
+
if (activeTag && !p.tags.includes(activeTag)) return false
|
|
86
|
+
return true
|
|
87
|
+
})
|
|
88
|
+
.sort((a, b) => sort === 'downloads' ? b.downloads - a.downloads : a.name.localeCompare(b.name))
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="space-y-3">
|
|
92
|
+
<input
|
|
93
|
+
value={search}
|
|
94
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
95
|
+
placeholder="Search plugins..."
|
|
96
|
+
className="w-full px-3 py-2.5 rounded-[10px] bg-surface border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30"
|
|
97
|
+
style={{ fontFamily: 'inherit' }}
|
|
98
|
+
/>
|
|
99
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
28
100
|
<button
|
|
29
|
-
onClick={() =>
|
|
30
|
-
className=
|
|
31
|
-
|
|
101
|
+
onClick={() => setActiveTag(null)}
|
|
102
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
103
|
+
!activeTag ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
104
|
+
}`}
|
|
32
105
|
>
|
|
33
|
-
|
|
106
|
+
All
|
|
34
107
|
</button>
|
|
35
|
-
|
|
36
|
-
) : (
|
|
37
|
-
<div className="space-y-2">
|
|
38
|
-
{pluginList.map((plugin) => (
|
|
108
|
+
{allTags.map((t) => (
|
|
39
109
|
<button
|
|
40
|
-
key={
|
|
41
|
-
onClick={() =>
|
|
42
|
-
className=
|
|
110
|
+
key={t}
|
|
111
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
112
|
+
className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
|
|
113
|
+
activeTag === t ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
|
|
114
|
+
}`}
|
|
43
115
|
>
|
|
44
|
-
|
|
45
|
-
<span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
|
|
46
|
-
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full shrink-0 ml-2 ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
|
|
47
|
-
{plugin.enabled ? 'Enabled' : 'Disabled'}
|
|
48
|
-
</span>
|
|
49
|
-
</div>
|
|
50
|
-
<div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
|
|
51
|
-
{plugin.description && (
|
|
52
|
-
<p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
|
|
53
|
-
)}
|
|
116
|
+
{t}
|
|
54
117
|
</button>
|
|
55
118
|
))}
|
|
119
|
+
<div className="flex-1" />
|
|
120
|
+
<select
|
|
121
|
+
value={sort}
|
|
122
|
+
onChange={(e) => setSort(e.target.value as 'name' | 'downloads')}
|
|
123
|
+
className="px-2 py-1 rounded-[6px] bg-surface border border-white/[0.06] text-[10px] text-text-3 outline-none cursor-pointer appearance-none"
|
|
124
|
+
style={{ fontFamily: 'inherit' }}
|
|
125
|
+
>
|
|
126
|
+
<option value="downloads">Popular</option>
|
|
127
|
+
<option value="name">A-Z</option>
|
|
128
|
+
</select>
|
|
129
|
+
</div>
|
|
130
|
+
{filtered.length === 0 ? (
|
|
131
|
+
<p className="text-[12px] text-text-3/50 text-center py-4">No plugins match your search</p>
|
|
132
|
+
) : (
|
|
133
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
|
134
|
+
{filtered.map((p) => {
|
|
135
|
+
const isInstalled = installedFilenames.has(`${p.id}.js`)
|
|
136
|
+
return (
|
|
137
|
+
<div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
138
|
+
<div className="flex items-start gap-3">
|
|
139
|
+
<div className="flex-1 min-w-0">
|
|
140
|
+
<div className="flex items-center gap-2">
|
|
141
|
+
<span className="text-[14px] font-600 text-text">{p.name}</span>
|
|
142
|
+
<span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
|
|
143
|
+
{p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
|
|
144
|
+
</div>
|
|
145
|
+
<div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
|
|
146
|
+
<div className="flex items-center gap-2 mt-2">
|
|
147
|
+
<span className="text-[10px] text-text-3/70">by {p.author}</span>
|
|
148
|
+
<span className="text-[10px] text-text-3/50">·</span>
|
|
149
|
+
{p.tags.slice(0, 3).map((t) => (
|
|
150
|
+
<button
|
|
151
|
+
key={t}
|
|
152
|
+
onClick={() => setActiveTag(activeTag === t ? null : t)}
|
|
153
|
+
className={`text-[9px] font-600 px-1.5 py-0.5 rounded-full cursor-pointer transition-all border-none ${
|
|
154
|
+
activeTag === t ? 'text-accent-bright bg-accent-soft' : 'text-text-3/50 bg-white/[0.04] hover:text-text-3'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
{t}
|
|
158
|
+
</button>
|
|
159
|
+
))}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<button
|
|
163
|
+
onClick={() => !isInstalled && installFromMarketplace(p)}
|
|
164
|
+
disabled={isInstalled || installing === p.id}
|
|
165
|
+
className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
|
|
166
|
+
${isInstalled
|
|
167
|
+
? 'bg-white/[0.04] text-text-3/70 cursor-default'
|
|
168
|
+
: installing === p.id
|
|
169
|
+
? 'bg-accent-soft text-accent-bright animate-pulse'
|
|
170
|
+
: 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
|
|
171
|
+
style={{ fontFamily: 'inherit' }}
|
|
172
|
+
>
|
|
173
|
+
{isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
187
|
+
{/* Tabs — full-width only */}
|
|
188
|
+
{!inSidebar && (
|
|
189
|
+
<div className="flex gap-1 mb-4">
|
|
190
|
+
<button onClick={() => setTab('installed')} className={tabClass('installed')} style={{ fontFamily: 'inherit' }}>
|
|
191
|
+
Installed
|
|
192
|
+
</button>
|
|
193
|
+
<button onClick={() => setTab('marketplace')} className={tabClass('marketplace')} style={{ fontFamily: 'inherit' }}>
|
|
194
|
+
Marketplace
|
|
195
|
+
</button>
|
|
56
196
|
</div>
|
|
57
197
|
)}
|
|
198
|
+
|
|
199
|
+
{(!inSidebar && tab === 'marketplace') ? renderMarketplace() : (
|
|
200
|
+
pluginList.length === 0 ? (
|
|
201
|
+
<div className="text-center py-12">
|
|
202
|
+
<p className="text-[13px] text-text-3/60">No plugins installed</p>
|
|
203
|
+
<button
|
|
204
|
+
onClick={() => { setEditingPluginFilename(null); setPluginSheetOpen(true) }}
|
|
205
|
+
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"
|
|
206
|
+
style={{ fontFamily: 'inherit' }}
|
|
207
|
+
>
|
|
208
|
+
+ Add Plugin
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
) : (
|
|
212
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
213
|
+
{pluginList.map((plugin) => (
|
|
214
|
+
<button
|
|
215
|
+
key={plugin.filename}
|
|
216
|
+
onClick={() => handleEdit(plugin.filename)}
|
|
217
|
+
className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
|
|
218
|
+
>
|
|
219
|
+
<div className="flex items-center justify-between mb-1">
|
|
220
|
+
<span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
|
|
221
|
+
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
222
|
+
{!inSidebar ? (
|
|
223
|
+
<>
|
|
224
|
+
<div
|
|
225
|
+
onClick={(e) => handleToggle(e, plugin.filename, plugin.enabled)}
|
|
226
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
227
|
+
${plugin.enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
|
|
228
|
+
>
|
|
229
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
230
|
+
${plugin.enabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
231
|
+
</div>
|
|
232
|
+
<button
|
|
233
|
+
onClick={(e) => handleDelete(e, plugin.filename)}
|
|
234
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
|
|
235
|
+
title="Delete"
|
|
236
|
+
>
|
|
237
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
238
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
239
|
+
</svg>
|
|
240
|
+
</button>
|
|
241
|
+
</>
|
|
242
|
+
) : (
|
|
243
|
+
<span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
|
|
244
|
+
{plugin.enabled ? 'Enabled' : 'Disabled'}
|
|
245
|
+
</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
|
|
250
|
+
{plugin.description && (
|
|
251
|
+
<p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
|
|
252
|
+
)}
|
|
253
|
+
</button>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
)}
|
|
58
258
|
</div>
|
|
59
259
|
)
|
|
60
260
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
|
|
6
|
+
export function ProjectList() {
|
|
7
|
+
const projects = useAppStore((s) => s.projects)
|
|
8
|
+
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
9
|
+
const agents = useAppStore((s) => s.agents)
|
|
10
|
+
const tasks = useAppStore((s) => s.tasks)
|
|
11
|
+
const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
12
|
+
const setEditingProjectId = useAppStore((s) => s.setEditingProjectId)
|
|
13
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
14
|
+
const setActiveProjectFilter = useAppStore((s) => s.setActiveProjectFilter)
|
|
15
|
+
const [search, setSearch] = useState('')
|
|
16
|
+
|
|
17
|
+
useEffect(() => { loadProjects() }, [])
|
|
18
|
+
|
|
19
|
+
const filtered = useMemo(() => {
|
|
20
|
+
return Object.values(projects)
|
|
21
|
+
.filter((p) => {
|
|
22
|
+
if (search && !p.name.toLowerCase().includes(search.toLowerCase())) return false
|
|
23
|
+
return true
|
|
24
|
+
})
|
|
25
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
26
|
+
}, [projects, search])
|
|
27
|
+
|
|
28
|
+
const entityCounts = useMemo(() => {
|
|
29
|
+
const counts: Record<string, { agents: number; tasks: number }> = {}
|
|
30
|
+
for (const p of Object.values(projects)) {
|
|
31
|
+
counts[p.id] = { agents: 0, tasks: 0 }
|
|
32
|
+
}
|
|
33
|
+
for (const a of Object.values(agents)) {
|
|
34
|
+
if (a.projectId && counts[a.projectId]) counts[a.projectId].agents++
|
|
35
|
+
}
|
|
36
|
+
for (const t of Object.values(tasks)) {
|
|
37
|
+
if (t.projectId && counts[t.projectId]) counts[t.projectId].tasks++
|
|
38
|
+
}
|
|
39
|
+
return counts
|
|
40
|
+
}, [projects, agents, tasks])
|
|
41
|
+
|
|
42
|
+
if (!filtered.length && !search) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
|
|
45
|
+
<div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
|
|
46
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
|
|
47
|
+
<path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" />
|
|
48
|
+
<path d="M14 2v7h7" />
|
|
49
|
+
</svg>
|
|
50
|
+
</div>
|
|
51
|
+
<p className="font-display text-[15px] font-600 text-text-2">No projects yet</p>
|
|
52
|
+
<p className="text-[13px] text-text-3/50">Group agents, tasks, and schedules into projects</p>
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => { setEditingProjectId(null); setProjectSheetOpen(true) }}
|
|
55
|
+
className="inline-flex items-center gap-1.5 px-4 py-2 text-[13px] font-500 text-white bg-accent rounded-lg hover:bg-accent-bright transition-colors"
|
|
56
|
+
>
|
|
57
|
+
<span className="text-lg leading-none">+</span> New Project
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex-1 flex flex-col h-full overflow-y-auto">
|
|
65
|
+
<div className="p-4 pb-0">
|
|
66
|
+
<div className="flex items-center gap-2 mb-4">
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
value={search}
|
|
70
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
71
|
+
placeholder="Search projects..."
|
|
72
|
+
className="flex-1 px-3 py-2 rounded-lg bg-white/[0.06] border border-white/[0.06] text-[13px] text-text-1 placeholder:text-text-3/40 focus:outline-none focus:border-accent/40"
|
|
73
|
+
style={{ fontFamily: 'inherit' }}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex-1 overflow-y-auto px-4 pb-4 space-y-2">
|
|
78
|
+
{filtered.map((project) => {
|
|
79
|
+
const counts = entityCounts[project.id] || { agents: 0, tasks: 0 }
|
|
80
|
+
const isActive = activeProjectFilter === project.id
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
key={project.id}
|
|
84
|
+
className={`group relative p-4 rounded-xl border transition-colors cursor-pointer ${
|
|
85
|
+
isActive
|
|
86
|
+
? 'bg-accent/10 border-accent/30'
|
|
87
|
+
: 'bg-white/[0.03] border-white/[0.06] hover:bg-white/[0.06]'
|
|
88
|
+
}`}
|
|
89
|
+
onClick={() => setActiveProjectFilter(isActive ? null : project.id)}
|
|
90
|
+
>
|
|
91
|
+
<div className="flex items-start justify-between gap-3">
|
|
92
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
93
|
+
{project.color && (
|
|
94
|
+
<div className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: project.color }} />
|
|
95
|
+
)}
|
|
96
|
+
<div className="min-w-0">
|
|
97
|
+
<div className="font-display text-[14px] font-600 text-text-1 truncate">{project.name}</div>
|
|
98
|
+
{project.description && (
|
|
99
|
+
<p className="text-[12px] text-text-3/60 mt-0.5 line-clamp-2">{project.description}</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<button
|
|
104
|
+
onClick={(e) => { e.stopPropagation(); setEditingProjectId(project.id); setProjectSheetOpen(true) }}
|
|
105
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-md hover:bg-white/[0.08] transition-all text-text-3/50 hover:text-text-2"
|
|
106
|
+
>
|
|
107
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
108
|
+
<path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
|
109
|
+
</svg>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex items-center gap-3 mt-2.5 text-[11px] text-text-3/50">
|
|
113
|
+
<span>{counts.agents} agent{counts.agents !== 1 ? 's' : ''}</span>
|
|
114
|
+
<span>{counts.tasks} task{counts.tasks !== 1 ? 's' : ''}</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { createProject, updateProject, deleteProject } from '@/lib/projects'
|
|
6
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
|
+
import { toast } from 'sonner'
|
|
8
|
+
|
|
9
|
+
const PROJECT_COLORS = [
|
|
10
|
+
'#EF4444', '#F97316', '#EAB308', '#22C55E', '#06B6D4',
|
|
11
|
+
'#3B82F6', '#8B5CF6', '#EC4899', '#6B7280',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const inputClass = 'w-full px-3 py-2.5 rounded-lg bg-white/[0.06] border border-white/[0.06] text-[13px] text-text-1 placeholder:text-text-3/40 focus:outline-none focus:border-accent/40 transition-colors'
|
|
15
|
+
|
|
16
|
+
export function ProjectSheet() {
|
|
17
|
+
const open = useAppStore((s) => s.projectSheetOpen)
|
|
18
|
+
const setOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
19
|
+
const editingId = useAppStore((s) => s.editingProjectId)
|
|
20
|
+
const setEditingId = useAppStore((s) => s.setEditingProjectId)
|
|
21
|
+
const projects = useAppStore((s) => s.projects)
|
|
22
|
+
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
23
|
+
|
|
24
|
+
const [name, setName] = useState('')
|
|
25
|
+
const [description, setDescription] = useState('')
|
|
26
|
+
const [color, setColor] = useState<string | undefined>(undefined)
|
|
27
|
+
|
|
28
|
+
const editing = editingId ? projects[editingId] : null
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (open) {
|
|
32
|
+
if (editing) {
|
|
33
|
+
setName(editing.name)
|
|
34
|
+
setDescription(editing.description)
|
|
35
|
+
setColor(editing.color)
|
|
36
|
+
} else {
|
|
37
|
+
setName('')
|
|
38
|
+
setDescription('')
|
|
39
|
+
setColor(PROJECT_COLORS[0])
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [open, editingId])
|
|
44
|
+
|
|
45
|
+
const onClose = () => {
|
|
46
|
+
setOpen(false)
|
|
47
|
+
setEditingId(null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleSave = async () => {
|
|
51
|
+
const data = {
|
|
52
|
+
name: name.trim() || 'Unnamed Project',
|
|
53
|
+
description,
|
|
54
|
+
color,
|
|
55
|
+
}
|
|
56
|
+
if (editing) {
|
|
57
|
+
await updateProject(editing.id, data)
|
|
58
|
+
} else {
|
|
59
|
+
await createProject(data)
|
|
60
|
+
}
|
|
61
|
+
await loadProjects()
|
|
62
|
+
onClose()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleDelete = async () => {
|
|
66
|
+
if (editing) {
|
|
67
|
+
await deleteProject(editing.id)
|
|
68
|
+
await loadProjects()
|
|
69
|
+
onClose()
|
|
70
|
+
toast.success('Project deleted')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<BottomSheet open={open} onClose={onClose}>
|
|
76
|
+
<h2 className="font-display text-[18px] font-700 text-text mb-6">{editing ? 'Edit Project' : 'New Project'}</h2>
|
|
77
|
+
<div className="mb-6">
|
|
78
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Name</label>
|
|
79
|
+
<input
|
|
80
|
+
type="text"
|
|
81
|
+
value={name}
|
|
82
|
+
onChange={(e) => setName(e.target.value)}
|
|
83
|
+
placeholder="e.g. Marketing Site"
|
|
84
|
+
className={inputClass}
|
|
85
|
+
style={{ fontFamily: 'inherit' }}
|
|
86
|
+
autoFocus
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="mb-6">
|
|
91
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Description</label>
|
|
92
|
+
<textarea
|
|
93
|
+
value={description}
|
|
94
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
95
|
+
placeholder="What is this project about?"
|
|
96
|
+
className={inputClass + ' min-h-[80px] resize-y'}
|
|
97
|
+
style={{ fontFamily: 'inherit' }}
|
|
98
|
+
rows={3}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="mb-8">
|
|
103
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Color</label>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
{PROJECT_COLORS.map((c) => (
|
|
106
|
+
<button
|
|
107
|
+
key={c}
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setColor(c)}
|
|
110
|
+
className={`w-7 h-7 rounded-full transition-all ${color === c ? 'ring-2 ring-offset-2 ring-offset-surface ring-accent scale-110' : 'hover:scale-105'}`}
|
|
111
|
+
style={{ backgroundColor: c }}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
<button
|
|
119
|
+
onClick={handleSave}
|
|
120
|
+
className="flex-1 py-2.5 rounded-lg bg-accent text-white text-[13px] font-600 hover:bg-accent-bright transition-colors"
|
|
121
|
+
>
|
|
122
|
+
{editing ? 'Update' : 'Create'} Project
|
|
123
|
+
</button>
|
|
124
|
+
{editing && (
|
|
125
|
+
<button
|
|
126
|
+
onClick={handleDelete}
|
|
127
|
+
className="px-4 py-2.5 rounded-lg bg-red-500/10 text-red-400 text-[13px] font-600 hover:bg-red-500/20 transition-colors"
|
|
128
|
+
>
|
|
129
|
+
Delete
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</BottomSheet>
|
|
134
|
+
)
|
|
135
|
+
}
|