@swarmclawai/swarmclaw 0.5.2 → 0.6.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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -2,20 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import type { Agent, BoardTask, Schedule } from '@/types'
|
|
6
|
+
|
|
7
|
+
function relativeDate(ts: number): string {
|
|
8
|
+
const diff = Date.now() - ts
|
|
9
|
+
if (diff < 60_000) return 'just now'
|
|
10
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
11
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
12
|
+
if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)}d ago`
|
|
13
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ProjectStats {
|
|
17
|
+
agents: number
|
|
18
|
+
tasks: number
|
|
19
|
+
completedTasks: number
|
|
20
|
+
schedules: number
|
|
21
|
+
lastActivity: number
|
|
22
|
+
}
|
|
5
23
|
|
|
6
24
|
export function ProjectList() {
|
|
7
25
|
const projects = useAppStore((s) => s.projects)
|
|
8
26
|
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
9
|
-
const agents = useAppStore((s) => s.agents)
|
|
10
|
-
const tasks = useAppStore((s) => s.tasks)
|
|
27
|
+
const agents = useAppStore((s) => s.agents) as Record<string, Agent>
|
|
28
|
+
const tasks = useAppStore((s) => s.tasks) as Record<string, BoardTask>
|
|
29
|
+
const schedules = useAppStore((s) => s.schedules) as Record<string, Schedule>
|
|
11
30
|
const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
12
31
|
const setEditingProjectId = useAppStore((s) => s.setEditingProjectId)
|
|
13
32
|
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
14
33
|
const setActiveProjectFilter = useAppStore((s) => s.setActiveProjectFilter)
|
|
34
|
+
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
35
|
+
const loadSchedules = useAppStore((s) => s.loadSchedules)
|
|
15
36
|
const [search, setSearch] = useState('')
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
loadProjects()
|
|
40
|
+
loadTasks()
|
|
41
|
+
loadSchedules()
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [])
|
|
19
44
|
|
|
20
45
|
const filtered = useMemo(() => {
|
|
21
46
|
return Object.values(projects)
|
|
@@ -26,97 +51,208 @@ export function ProjectList() {
|
|
|
26
51
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
27
52
|
}, [projects, search])
|
|
28
53
|
|
|
29
|
-
const
|
|
30
|
-
const
|
|
54
|
+
const statsMap = useMemo(() => {
|
|
55
|
+
const map: Record<string, ProjectStats> = {}
|
|
31
56
|
for (const p of Object.values(projects)) {
|
|
32
|
-
|
|
57
|
+
map[p.id] = { agents: 0, tasks: 0, completedTasks: 0, schedules: 0, lastActivity: p.updatedAt }
|
|
33
58
|
}
|
|
34
59
|
for (const a of Object.values(agents)) {
|
|
35
|
-
if (a.projectId &&
|
|
60
|
+
if (a.projectId && map[a.projectId]) {
|
|
61
|
+
map[a.projectId].agents++
|
|
62
|
+
if (a.updatedAt && a.updatedAt > map[a.projectId].lastActivity) {
|
|
63
|
+
map[a.projectId].lastActivity = a.updatedAt
|
|
64
|
+
}
|
|
65
|
+
}
|
|
36
66
|
}
|
|
37
67
|
for (const t of Object.values(tasks)) {
|
|
38
|
-
if (t.projectId &&
|
|
68
|
+
if (t.projectId && map[t.projectId]) {
|
|
69
|
+
map[t.projectId].tasks++
|
|
70
|
+
if (t.status === 'completed') map[t.projectId].completedTasks++
|
|
71
|
+
if (t.updatedAt && t.updatedAt > map[t.projectId].lastActivity) {
|
|
72
|
+
map[t.projectId].lastActivity = t.updatedAt
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const s of Object.values(schedules)) {
|
|
77
|
+
if (s.projectId && map[s.projectId]) {
|
|
78
|
+
map[s.projectId].schedules++
|
|
79
|
+
}
|
|
39
80
|
}
|
|
40
|
-
return
|
|
41
|
-
}, [projects, agents, tasks])
|
|
81
|
+
return map
|
|
82
|
+
}, [projects, agents, tasks, schedules])
|
|
83
|
+
|
|
84
|
+
// Summary stats
|
|
85
|
+
const totalProjects = Object.keys(projects).length
|
|
86
|
+
const totalTasks = Object.values(tasks).filter((t) => t.projectId).length
|
|
87
|
+
const totalCompleted = Object.values(tasks).filter((t) => t.projectId && t.status === 'completed').length
|
|
42
88
|
|
|
43
89
|
if (!filtered.length && !search) {
|
|
44
90
|
return (
|
|
45
91
|
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
|
|
46
|
-
<div className="w-
|
|
47
|
-
<svg width="
|
|
92
|
+
<div className="w-14 h-14 rounded-[16px] bg-accent-soft flex items-center justify-center mb-1">
|
|
93
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-accent-bright">
|
|
48
94
|
<path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" />
|
|
49
95
|
<path d="M14 2v7h7" />
|
|
50
96
|
</svg>
|
|
51
97
|
</div>
|
|
52
|
-
<p className="font-display text-[
|
|
53
|
-
<p className="text-[13px] text-text-3/
|
|
98
|
+
<p className="font-display text-[16px] font-600 text-text-2">No projects yet</p>
|
|
99
|
+
<p className="text-[13px] text-text-3/60 max-w-[280px]">
|
|
100
|
+
Projects group your agents, tasks, and schedules together. Create one to get organized.
|
|
101
|
+
</p>
|
|
54
102
|
<button
|
|
55
103
|
onClick={() => { setEditingProjectId(null); setProjectSheetOpen(true) }}
|
|
56
|
-
className="inline-flex items-center gap-1.5 px-
|
|
104
|
+
className="inline-flex items-center gap-1.5 px-5 py-2.5 text-[13px] font-600 text-white bg-accent-bright rounded-[10px] hover:brightness-110 transition-all cursor-pointer border-none"
|
|
105
|
+
style={{ fontFamily: 'inherit' }}
|
|
57
106
|
>
|
|
58
|
-
<
|
|
107
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
108
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
109
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
110
|
+
</svg>
|
|
111
|
+
New Project
|
|
59
112
|
</button>
|
|
60
113
|
</div>
|
|
61
114
|
)
|
|
62
115
|
}
|
|
63
116
|
|
|
64
117
|
return (
|
|
65
|
-
<div className="flex-1 flex flex-col h-full overflow-
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
|
119
|
+
{/* Header with search and new button */}
|
|
120
|
+
<div className="px-5 pt-5 pb-3 shrink-0">
|
|
121
|
+
<div className="flex items-center justify-between mb-4">
|
|
122
|
+
<div>
|
|
123
|
+
<h2 className="font-display text-[20px] font-700 text-text tracking-[-0.02em]">Projects</h2>
|
|
124
|
+
<p className="text-[12px] text-text-3/60 mt-0.5">
|
|
125
|
+
{totalProjects} project{totalProjects !== 1 ? 's' : ''}
|
|
126
|
+
{totalTasks > 0 && <> · {totalCompleted}/{totalTasks} tasks done</>}
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => { setEditingProjectId(null); setProjectSheetOpen(true) }}
|
|
131
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 text-[12px] font-600 text-white bg-accent-bright rounded-[10px] hover:brightness-110 transition-all cursor-pointer border-none"
|
|
132
|
+
style={{ fontFamily: 'inherit' }}
|
|
133
|
+
>
|
|
134
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
135
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
136
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
137
|
+
</svg>
|
|
138
|
+
New
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Search */}
|
|
143
|
+
<div className="relative">
|
|
144
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="absolute left-3 top-1/2 -translate-y-1/2 text-text-3/50">
|
|
145
|
+
<circle cx="11" cy="11" r="8" />
|
|
146
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
147
|
+
</svg>
|
|
68
148
|
<input
|
|
69
149
|
type="text"
|
|
70
150
|
value={search}
|
|
71
151
|
onChange={(e) => setSearch(e.target.value)}
|
|
72
152
|
placeholder="Search projects..."
|
|
73
|
-
className="
|
|
153
|
+
className="w-full pl-9 pr-3 py-2.5 rounded-[10px] bg-white/[0.04] border border-white/[0.06] text-[13px] text-text placeholder:text-text-3/40 focus:outline-none focus:border-accent-bright/30 transition-colors"
|
|
74
154
|
style={{ fontFamily: 'inherit' }}
|
|
75
155
|
/>
|
|
76
156
|
</div>
|
|
77
157
|
</div>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
158
|
+
|
|
159
|
+
{/* Project cards */}
|
|
160
|
+
<div className="flex-1 overflow-y-auto px-5 pb-5">
|
|
161
|
+
<div className="grid gap-3">
|
|
162
|
+
{filtered.map((project) => {
|
|
163
|
+
const stats = statsMap[project.id] || { agents: 0, tasks: 0, completedTasks: 0, schedules: 0, lastActivity: project.updatedAt }
|
|
164
|
+
const isActive = activeProjectFilter === project.id
|
|
165
|
+
const progressPct = stats.tasks > 0 ? Math.round((stats.completedTasks / stats.tasks) * 100) : 0
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
key={project.id}
|
|
170
|
+
className={`group relative rounded-[14px] border transition-all duration-200 cursor-pointer overflow-hidden
|
|
171
|
+
${isActive
|
|
172
|
+
? 'bg-white/[0.06] border-accent-bright/30 shadow-[0_0_20px_rgba(99,102,241,0.08)]'
|
|
173
|
+
: 'bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05] hover:border-white/[0.1]'}`}
|
|
174
|
+
onClick={() => setActiveProjectFilter(isActive ? null : project.id)}
|
|
175
|
+
>
|
|
176
|
+
{/* Color accent stripe */}
|
|
177
|
+
<div className="absolute left-0 top-0 bottom-0 w-1 rounded-l-[14px]" style={{ backgroundColor: project.color || '#6B7280' }} />
|
|
178
|
+
|
|
179
|
+
<div className="pl-5 pr-4 py-4">
|
|
180
|
+
<div className="flex items-start justify-between gap-3">
|
|
181
|
+
<div className="min-w-0 flex-1">
|
|
182
|
+
<div className="flex items-center gap-2">
|
|
183
|
+
<h3 className="font-display text-[14px] font-600 text-text truncate">{project.name}</h3>
|
|
184
|
+
{isActive && (
|
|
185
|
+
<span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright bg-accent-soft px-1.5 py-0.5 rounded-[5px]">
|
|
186
|
+
active filter
|
|
187
|
+
</span>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
{project.description && (
|
|
191
|
+
<p className="text-[12px] text-text-3/60 mt-1 line-clamp-2 leading-relaxed">{project.description}</p>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
<button
|
|
195
|
+
onClick={(e) => { e.stopPropagation(); setEditingProjectId(project.id); setProjectSheetOpen(true) }}
|
|
196
|
+
className="opacity-0 group-hover:opacity-100 p-1.5 rounded-[8px] hover:bg-white/[0.08] transition-all text-text-3/50 hover:text-text-2 cursor-pointer bg-transparent border-none shrink-0"
|
|
197
|
+
>
|
|
198
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
199
|
+
<path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
|
200
|
+
</svg>
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Stats row */}
|
|
205
|
+
<div className="flex items-center gap-4 mt-3 text-[11px] text-text-3/50">
|
|
206
|
+
<span className="flex items-center gap-1.5">
|
|
207
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
208
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
209
|
+
<circle cx="9" cy="7" r="4" />
|
|
210
|
+
</svg>
|
|
211
|
+
{stats.agents} agent{stats.agents !== 1 ? 's' : ''}
|
|
212
|
+
</span>
|
|
213
|
+
<span className="flex items-center gap-1.5">
|
|
214
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
215
|
+
<path d="M9 11l3 3L22 4" />
|
|
216
|
+
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
|
217
|
+
</svg>
|
|
218
|
+
{stats.completedTasks}/{stats.tasks} task{stats.tasks !== 1 ? 's' : ''}
|
|
219
|
+
</span>
|
|
220
|
+
{stats.schedules > 0 && (
|
|
221
|
+
<span className="flex items-center gap-1.5">
|
|
222
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
223
|
+
<circle cx="12" cy="12" r="10" />
|
|
224
|
+
<polyline points="12 6 12 12 16 14" />
|
|
225
|
+
</svg>
|
|
226
|
+
{stats.schedules} schedule{stats.schedules !== 1 ? 's' : ''}
|
|
227
|
+
</span>
|
|
101
228
|
)}
|
|
229
|
+
<span className="ml-auto text-text-3/40">
|
|
230
|
+
{relativeDate(stats.lastActivity)}
|
|
231
|
+
</span>
|
|
102
232
|
</div>
|
|
233
|
+
|
|
234
|
+
{/* Progress bar */}
|
|
235
|
+
{stats.tasks > 0 && (
|
|
236
|
+
<div className="mt-3 flex items-center gap-2.5">
|
|
237
|
+
<div className="flex-1 h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
|
|
238
|
+
<div
|
|
239
|
+
className="h-full rounded-full transition-all duration-500"
|
|
240
|
+
style={{
|
|
241
|
+
width: `${progressPct}%`,
|
|
242
|
+
backgroundColor: progressPct === 100 ? '#22C55E' : (project.color || '#6366F1'),
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
</div>
|
|
246
|
+
<span className={`text-[10px] font-mono font-600 ${progressPct === 100 ? 'text-emerald-400' : 'text-text-3/50'}`}>
|
|
247
|
+
{progressPct}%
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
103
251
|
</div>
|
|
104
|
-
<button
|
|
105
|
-
onClick={(e) => { e.stopPropagation(); setEditingProjectId(project.id); setProjectSheetOpen(true) }}
|
|
106
|
-
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"
|
|
107
|
-
>
|
|
108
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
109
|
-
<path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
|
110
|
-
</svg>
|
|
111
|
-
</button>
|
|
112
252
|
</div>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
)
|
|
119
|
-
})}
|
|
253
|
+
)
|
|
254
|
+
})}
|
|
255
|
+
</div>
|
|
120
256
|
</div>
|
|
121
257
|
</div>
|
|
122
258
|
)
|
|
@@ -86,7 +86,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
86
86
|
<span className="font-display text-[14px] font-600 text-text truncate">{item.name}</span>
|
|
87
87
|
<div className="flex items-center gap-2 shrink-0">
|
|
88
88
|
<span className={`text-[10px] font-600 px-2 py-0.5 rounded-[5px] uppercase tracking-wider
|
|
89
|
-
${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-
|
|
89
|
+
${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-accent-bright/10 text-[#6366F1]'}`}>
|
|
90
90
|
{item.type === 'builtin' ? 'Built-in' : 'Custom'}
|
|
91
91
|
</span>
|
|
92
92
|
{!inSidebar && item.type === 'custom' && (
|
|
@@ -94,7 +94,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
94
94
|
<div
|
|
95
95
|
onClick={(e) => handleToggle(e, item.id, item.isEnabled)}
|
|
96
96
|
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
97
|
-
${item.isEnabled ? 'bg-
|
|
97
|
+
${item.isEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
98
98
|
>
|
|
99
99
|
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
100
100
|
${item.isEnabled ? 'left-[18px]' : 'left-0.5'}`} />
|
|
@@ -342,7 +342,7 @@ export function ProviderSheet() {
|
|
|
342
342
|
<div
|
|
343
343
|
onClick={() => setRequiresApiKey(!requiresApiKey)}
|
|
344
344
|
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
|
|
345
|
-
${requiresApiKey ? 'bg-
|
|
345
|
+
${requiresApiKey ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
346
346
|
>
|
|
347
347
|
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
348
348
|
${requiresApiKey ? 'left-[22px]' : 'left-0.5'}`} />
|
|
@@ -441,7 +441,7 @@ export function ProviderSheet() {
|
|
|
441
441
|
<div
|
|
442
442
|
onClick={() => setIsEnabled(!isEnabled)}
|
|
443
443
|
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
|
|
444
|
-
${isEnabled ? 'bg-
|
|
444
|
+
${isEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
445
445
|
>
|
|
446
446
|
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
|
|
447
447
|
${isEnabled ? 'left-[22px]' : 'left-0.5'}`} />
|
|
@@ -485,7 +485,7 @@ export function ProviderSheet() {
|
|
|
485
485
|
<button
|
|
486
486
|
onClick={handleSave}
|
|
487
487
|
disabled={isBuiltin ? false : (!name.trim() || !baseUrl.trim())}
|
|
488
|
-
className="flex-1 py-3.5 rounded-[14px] border-none bg-
|
|
488
|
+
className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright 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"
|
|
489
489
|
style={{ fontFamily: 'inherit' }}
|
|
490
490
|
>
|
|
491
491
|
{editing ? 'Save' : 'Create'}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Schedule } from '@/types'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
|
+
import { cronToHuman } from '@/lib/cron-human'
|
|
6
7
|
|
|
7
8
|
const STATUS_COLORS: Record<string, string> = {
|
|
8
9
|
active: 'text-emerald-400 bg-emerald-400/[0.08]',
|
|
@@ -70,7 +71,7 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
|
|
|
70
71
|
<div
|
|
71
72
|
onClick={handleToggle}
|
|
72
73
|
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
|
|
73
|
-
${schedule.status === 'active' ? 'bg-
|
|
74
|
+
${schedule.status === 'active' ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
74
75
|
>
|
|
75
76
|
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
|
|
76
77
|
${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`} />
|
|
@@ -95,7 +96,7 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
|
|
|
95
96
|
<div className="text-[12px] text-text-3/70 mt-1.5 truncate">
|
|
96
97
|
{agent?.name || 'Unknown agent'} · {schedule.scheduleType}
|
|
97
98
|
{!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
|
|
98
|
-
<span className="
|
|
99
|
+
<span className="text-text-3/50 ml-1" title={schedule.cron}>({cronToHuman(schedule.cron)})</span>
|
|
99
100
|
)}
|
|
100
101
|
{!inSidebar && schedule.scheduleType === 'interval' && schedule.intervalMs && (
|
|
101
102
|
<span className="text-text-3/50 ml-1">
|
|
@@ -41,7 +41,7 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
41
41
|
{!inSidebar && (
|
|
42
42
|
<button
|
|
43
43
|
onClick={() => setScheduleSheetOpen(true)}
|
|
44
|
-
className="mt-3 px-8 py-3 rounded-[14px] border-none bg-
|
|
44
|
+
className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
|
|
45
45
|
text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
|
|
46
46
|
shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
|
|
47
47
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -4,8 +4,12 @@ import { useEffect, useState, useMemo } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules'
|
|
6
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
|
+
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
8
|
+
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
9
|
+
import { inputClass } from '@/components/shared/form-styles'
|
|
7
10
|
import type { ScheduleType, ScheduleStatus } from '@/types'
|
|
8
11
|
import cronstrue from 'cronstrue'
|
|
12
|
+
import { SectionLabel } from '@/components/shared/section-label'
|
|
9
13
|
|
|
10
14
|
const CRON_PRESETS = [
|
|
11
15
|
{ label: 'Every hour', cron: '0 * * * *' },
|
|
@@ -62,7 +66,7 @@ export function ScheduleSheet() {
|
|
|
62
66
|
const [customCron, setCustomCron] = useState(false)
|
|
63
67
|
|
|
64
68
|
const editing = editingId ? schedules[editingId] : null
|
|
65
|
-
const agentList = Object.values(agents)
|
|
69
|
+
const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
66
70
|
|
|
67
71
|
useEffect(() => {
|
|
68
72
|
if (open) {
|
|
@@ -125,8 +129,6 @@ export function ScheduleSheet() {
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
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"
|
|
129
|
-
|
|
130
132
|
return (
|
|
131
133
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
132
134
|
<div className="mb-10">
|
|
@@ -137,22 +139,22 @@ export function ScheduleSheet() {
|
|
|
137
139
|
</div>
|
|
138
140
|
|
|
139
141
|
<div className="mb-8">
|
|
140
|
-
<
|
|
142
|
+
<SectionLabel>Name</SectionLabel>
|
|
141
143
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
142
144
|
</div>
|
|
143
145
|
|
|
144
146
|
<div className="mb-8">
|
|
145
|
-
<
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
{
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
<SectionLabel>Agent</SectionLabel>
|
|
148
|
+
<AgentPickerList
|
|
149
|
+
agents={agentList}
|
|
150
|
+
selected={agentId}
|
|
151
|
+
onSelect={(id) => setAgentId(id)}
|
|
152
|
+
showOrchBadge={true}
|
|
153
|
+
/>
|
|
152
154
|
</div>
|
|
153
155
|
|
|
154
156
|
<div className="mb-8">
|
|
155
|
-
<
|
|
157
|
+
<SectionLabel>Task Prompt</SectionLabel>
|
|
156
158
|
<textarea
|
|
157
159
|
value={taskPrompt}
|
|
158
160
|
onChange={(e) => setTaskPrompt(e.target.value)}
|
|
@@ -164,7 +166,7 @@ export function ScheduleSheet() {
|
|
|
164
166
|
</div>
|
|
165
167
|
|
|
166
168
|
<div className="mb-8">
|
|
167
|
-
<
|
|
169
|
+
<SectionLabel>Schedule Type</SectionLabel>
|
|
168
170
|
<div className="grid grid-cols-3 gap-3">
|
|
169
171
|
{(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => (
|
|
170
172
|
<button
|
|
@@ -185,7 +187,7 @@ export function ScheduleSheet() {
|
|
|
185
187
|
|
|
186
188
|
{scheduleType === 'cron' && (
|
|
187
189
|
<div className="mb-8">
|
|
188
|
-
<
|
|
190
|
+
<SectionLabel>Schedule</SectionLabel>
|
|
189
191
|
|
|
190
192
|
{/* Preset buttons */}
|
|
191
193
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
@@ -239,7 +241,7 @@ export function ScheduleSheet() {
|
|
|
239
241
|
|
|
240
242
|
{scheduleType === 'interval' && (
|
|
241
243
|
<div className="mb-8">
|
|
242
|
-
<
|
|
244
|
+
<SectionLabel>Interval (minutes)</SectionLabel>
|
|
243
245
|
<input
|
|
244
246
|
type="number"
|
|
245
247
|
value={Math.round(intervalMs / 60000)}
|
|
@@ -252,7 +254,7 @@ export function ScheduleSheet() {
|
|
|
252
254
|
|
|
253
255
|
{editing && (
|
|
254
256
|
<div className="mb-8">
|
|
255
|
-
<
|
|
257
|
+
<SectionLabel>Status</SectionLabel>
|
|
256
258
|
<div className="flex gap-2">
|
|
257
259
|
{(['active', 'paused'] as ScheduleStatus[]).map((s) => (
|
|
258
260
|
<button
|
|
@@ -271,19 +273,17 @@ export function ScheduleSheet() {
|
|
|
271
273
|
</div>
|
|
272
274
|
)}
|
|
273
275
|
|
|
274
|
-
<
|
|
275
|
-
{
|
|
276
|
+
<SheetFooter
|
|
277
|
+
onCancel={onClose}
|
|
278
|
+
onSave={handleSave}
|
|
279
|
+
saveLabel={editing ? 'Save' : 'Create'}
|
|
280
|
+
saveDisabled={!name.trim() || !agentId}
|
|
281
|
+
left={editing && (
|
|
276
282
|
<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' }}>
|
|
277
283
|
Delete
|
|
278
284
|
</button>
|
|
279
285
|
)}
|
|
280
|
-
|
|
281
|
-
Cancel
|
|
282
|
-
</button>
|
|
283
|
-
<button onClick={handleSave} disabled={!name.trim() || !agentId} 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' }}>
|
|
284
|
-
{editing ? 'Save' : 'Create'}
|
|
285
|
-
</button>
|
|
286
|
-
</div>
|
|
286
|
+
/>
|
|
287
287
|
</BottomSheet>
|
|
288
288
|
)
|
|
289
289
|
}
|