@swarmclawai/swarmclaw 0.4.0 → 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 +13 -2
- package/next.config.ts +8 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +20 -21
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +10 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- 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 +2 -2
- 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/run/route.ts +2 -2
- 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 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -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 +2 -2
- 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 +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -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/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +28 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +2 -0
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +116 -14
- package/src/components/chat/chat-area.tsx +27 -4
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/tool-call-bubble.tsx +9 -3
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- 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 +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- 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 +2 -1
- package/src/components/tasks/task-list.tsx +5 -2
- 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/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 +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- 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 +38 -4
- 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 +392 -3
- 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/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +6 -6
- 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 +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +22 -10
- 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 +2 -2
- 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 +3 -3
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +2 -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 +33 -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.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +29 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +9 -1
- package/src/types/index.ts +27 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { VoiceConversationState } from '@/hooks/use-voice-conversation'
|
|
4
|
+
|
|
5
|
+
interface VoiceOverlayProps {
|
|
6
|
+
state: VoiceConversationState
|
|
7
|
+
interimText: string
|
|
8
|
+
transcript: string
|
|
9
|
+
onStop: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const STATE_LABELS: Record<VoiceConversationState, string> = {
|
|
13
|
+
idle: '',
|
|
14
|
+
listening: 'Listening...',
|
|
15
|
+
processing: 'Processing...',
|
|
16
|
+
speaking: 'Speaking...',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function VoiceOverlay({ state, interimText, transcript, onStop }: VoiceOverlayProps) {
|
|
20
|
+
if (state === 'idle') return null
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center gap-4 bg-bg/90 backdrop-blur-sm">
|
|
24
|
+
{/* Animated indicator */}
|
|
25
|
+
<div className="relative">
|
|
26
|
+
<div className={`w-20 h-20 rounded-full flex items-center justify-center ${
|
|
27
|
+
state === 'listening'
|
|
28
|
+
? 'bg-accent/20 animate-pulse'
|
|
29
|
+
: state === 'speaking'
|
|
30
|
+
? 'bg-green-500/20'
|
|
31
|
+
: 'bg-yellow-500/20'
|
|
32
|
+
}`}>
|
|
33
|
+
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
|
|
34
|
+
state === 'listening'
|
|
35
|
+
? 'bg-accent/30'
|
|
36
|
+
: state === 'speaking'
|
|
37
|
+
? 'bg-green-500/30'
|
|
38
|
+
: 'bg-yellow-500/30'
|
|
39
|
+
}`}>
|
|
40
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={
|
|
41
|
+
state === 'listening' ? 'text-accent-bright' : state === 'speaking' ? 'text-green-400' : 'text-yellow-400'
|
|
42
|
+
}>
|
|
43
|
+
{state === 'speaking' ? (
|
|
44
|
+
<>
|
|
45
|
+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
46
|
+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
47
|
+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
|
48
|
+
</>
|
|
49
|
+
) : (
|
|
50
|
+
<>
|
|
51
|
+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
|
52
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
53
|
+
<line x1="12" x2="12" y1="19" y2="22" />
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</svg>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="text-[14px] font-500 text-text-2">{STATE_LABELS[state]}</div>
|
|
62
|
+
|
|
63
|
+
{/* Transcript display */}
|
|
64
|
+
{(transcript || interimText) && (
|
|
65
|
+
<div className="max-w-md px-6 text-center">
|
|
66
|
+
{transcript && <p className="text-[14px] text-text-1 mb-1">{transcript}</p>}
|
|
67
|
+
{interimText && <p className="text-[13px] text-text-3/60 italic">{interimText}</p>}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{/* Stop button */}
|
|
72
|
+
<button
|
|
73
|
+
onClick={onStop}
|
|
74
|
+
className="mt-2 px-5 py-2 rounded-lg bg-red-500/10 text-red-400 text-[13px] font-600 hover:bg-red-500/20 transition-colors"
|
|
75
|
+
>
|
|
76
|
+
Stop Voice
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -104,8 +104,12 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
|
|
|
104
104
|
const agent = agents[c.agentId]
|
|
105
105
|
const isRunning = c.status === 'running'
|
|
106
106
|
const isToggling = toggling === c.id
|
|
107
|
-
// Can only toggle if connector has credentials (or
|
|
108
|
-
const hasCredentials = c.platform === 'whatsapp'
|
|
107
|
+
// Can only toggle if connector has credentials (or uses non-token auth modes).
|
|
108
|
+
const hasCredentials = c.platform === 'whatsapp'
|
|
109
|
+
|| c.platform === 'openclaw'
|
|
110
|
+
|| c.platform === 'signal'
|
|
111
|
+
|| (c.platform === 'bluebubbles' && (!!c.credentialId || !!c.config?.password))
|
|
112
|
+
|| !!c.credentialId
|
|
109
113
|
return (
|
|
110
114
|
<div
|
|
111
115
|
key={c.id}
|
|
@@ -114,7 +114,7 @@ const PLATFORMS: {
|
|
|
114
114
|
tokenHelp: 'Required when your OpenClaw gateway is auth-protected',
|
|
115
115
|
configFields: [
|
|
116
116
|
{ key: 'wsUrl', label: 'WebSocket URL', placeholder: 'ws://localhost:18789', help: 'OpenClaw gateway WebSocket endpoint (root URL, not /ws)' },
|
|
117
|
-
{ key: 'sessionKey', label: '
|
|
117
|
+
{ key: 'sessionKey', label: 'Chat Key Filter', placeholder: 'main', help: 'Optional. If set, only inbound events for this OpenClaw session are processed.' },
|
|
118
118
|
{ key: 'nodeId', label: 'Client Label', placeholder: 'swarmclaw', help: 'Optional display label shown in OpenClaw presence metadata.' },
|
|
119
119
|
{ key: 'role', label: 'Gateway Role', placeholder: 'operator', help: 'Optional role claim for connect handshake. Default is operator.' },
|
|
120
120
|
{ key: 'scopes', label: 'Scopes (CSV)', placeholder: 'operator.read,operator.write', help: 'Optional comma-separated scopes for OpenClaw connect.' },
|
|
@@ -122,6 +122,28 @@ const PLATFORMS: {
|
|
|
122
122
|
{ key: 'tickIntervalMs', label: 'Tick Interval Override (ms)', placeholder: '30000', help: 'Optional watchdog interval override when policy tick is unavailable.' },
|
|
123
123
|
],
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
id: 'bluebubbles',
|
|
127
|
+
label: 'BlueBubbles',
|
|
128
|
+
color: '#2E89FF',
|
|
129
|
+
setupSteps: [
|
|
130
|
+
'Run BlueBubbles server on your macOS host and enable the REST API',
|
|
131
|
+
'Copy the BlueBubbles server password',
|
|
132
|
+
'After saving the connector, point BlueBubbles webhook to /api/connectors/<connector-id>/webhook',
|
|
133
|
+
'Optionally set dmPolicy=pairing to require explicit sender approval for new DMs',
|
|
134
|
+
],
|
|
135
|
+
tokenLabel: 'BlueBubbles Password',
|
|
136
|
+
tokenHelp: 'Server password used for /api/v1/ping and /api/v1/message/text',
|
|
137
|
+
configFields: [
|
|
138
|
+
{ key: 'serverUrl', label: 'Server URL', placeholder: 'http://127.0.0.1:1234', help: 'BlueBubbles server URL (no trailing /api path needed)' },
|
|
139
|
+
{ key: 'chatIds', label: 'Allowed Chat IDs', placeholder: 'iMessage;-;+15551234567', help: 'Optional comma-separated chat IDs/guid fragments. Leave empty for all chats.' },
|
|
140
|
+
{ key: 'dmPolicy', label: 'DM Policy', placeholder: 'open | allowlist | pairing | disabled', help: 'Access policy for direct-message senders. Default: open.' },
|
|
141
|
+
{ key: 'allowFrom', label: 'Allowed Sender IDs', placeholder: '+15551234567,test@example.com', help: 'Optional comma-separated sender IDs for allowlist/pairing mode.' },
|
|
142
|
+
{ key: 'outboundTarget', label: 'Default Outbound Target', placeholder: 'iMessage;-;+15551234567', help: 'Used when proactive sends omit "to".' },
|
|
143
|
+
{ key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
|
|
144
|
+
{ key: 'timeoutMs', label: 'Request Timeout (ms)', placeholder: '10000', help: 'Optional BlueBubbles API timeout in milliseconds.' },
|
|
145
|
+
],
|
|
146
|
+
},
|
|
125
147
|
{
|
|
126
148
|
id: 'matrix',
|
|
127
149
|
label: 'Matrix',
|
|
@@ -146,13 +168,14 @@ const PLATFORMS: {
|
|
|
146
168
|
setupSteps: [
|
|
147
169
|
'Create a Google Cloud project and enable the Google Chat API',
|
|
148
170
|
'Create a service account and download the JSON key file',
|
|
149
|
-
'In Google Chat Admin, configure
|
|
171
|
+
'In Google Chat Admin, configure event delivery to /api/connectors/<connector-id>/webhook',
|
|
150
172
|
'Paste the full service account JSON as the bot token',
|
|
151
173
|
],
|
|
152
174
|
tokenLabel: 'Service Account JSON',
|
|
153
175
|
tokenHelp: 'Paste the full service account JSON key file contents',
|
|
154
176
|
configFields: [
|
|
155
177
|
{ key: 'spaceIds', label: 'Space IDs', placeholder: 'spaces/AAAA123', help: 'Comma-separated Google Chat space IDs' },
|
|
178
|
+
{ key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
|
|
156
179
|
],
|
|
157
180
|
},
|
|
158
181
|
{
|
|
@@ -163,13 +186,14 @@ const PLATFORMS: {
|
|
|
163
186
|
'Register a bot in the Azure Bot Framework portal',
|
|
164
187
|
'Note the Microsoft App ID and generate an App Secret',
|
|
165
188
|
'Set up a public HTTPS endpoint for webhook delivery',
|
|
166
|
-
'
|
|
189
|
+
'After saving the connector, point Azure to /api/connectors/<connector-id>/webhook',
|
|
167
190
|
],
|
|
168
191
|
tokenLabel: 'App Secret',
|
|
169
192
|
tokenHelp: 'Microsoft App Secret from Azure Bot registration',
|
|
170
193
|
configFields: [
|
|
171
194
|
{ key: 'appId', label: 'Microsoft App ID', placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', help: 'Azure Bot Framework App ID' },
|
|
172
|
-
{ key: 'notifyUrl', label: 'Notify URL', placeholder: 'https://your-server.com/api/
|
|
195
|
+
{ key: 'notifyUrl', label: 'Notify URL', placeholder: 'https://your-server.com/api/connectors/<id>/webhook', help: 'Public HTTPS endpoint for receiving messages (informational)' },
|
|
196
|
+
{ key: 'webhookSecret', label: 'Webhook Secret', placeholder: 'optional-shared-secret', help: 'Optional secret required by /api/connectors/{id}/webhook (header: x-connector-secret or ?secret=...)' },
|
|
173
197
|
],
|
|
174
198
|
},
|
|
175
199
|
{
|
|
@@ -366,7 +390,7 @@ export function ConnectorSheet() {
|
|
|
366
390
|
<div>
|
|
367
391
|
<div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
|
|
368
392
|
<div className="text-[11px] text-text-3 mt-0.5">
|
|
369
|
-
{p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : 'Bot token'}
|
|
393
|
+
{p.id === 'whatsapp' ? 'QR code pairing' : p.id === 'openclaw' ? 'WebSocket gateway' : p.id === 'bluebubbles' ? 'iMessage bridge' : p.id === 'signal' ? 'signal-cli binary' : p.id === 'matrix' ? 'Access token' : p.id === 'googlechat' ? 'Service account' : p.id === 'teams' ? 'Bot Framework' : 'Bot token'}
|
|
370
394
|
</div>
|
|
371
395
|
</div>
|
|
372
396
|
</button>
|
|
@@ -552,7 +576,7 @@ export function ConnectorSheet() {
|
|
|
552
576
|
|
|
553
577
|
{/* Platform-specific config */}
|
|
554
578
|
{platformConfig.configFields.map((field) => {
|
|
555
|
-
const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds'
|
|
579
|
+
const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
|
|
556
580
|
if (isTagField) {
|
|
557
581
|
const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
558
582
|
return (
|
|
@@ -732,7 +756,7 @@ export function ConnectorSheet() {
|
|
|
732
756
|
</div>
|
|
733
757
|
<p className="text-[11px] text-text-3">
|
|
734
758
|
{waHasCreds
|
|
735
|
-
? 'Reconnecting with saved
|
|
759
|
+
? 'Reconnecting with saved credentials, this should only take a moment'
|
|
736
760
|
: 'Connecting to WhatsApp, QR code will appear shortly'}
|
|
737
761
|
</p>
|
|
738
762
|
{waHasCreds && (
|
|
@@ -6,7 +6,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { useMediaQuery } from '@/hooks/use-media-query'
|
|
8
8
|
import { Avatar } from '@/components/shared/avatar'
|
|
9
|
-
import {
|
|
9
|
+
import { SettingsPage } from '@/components/shared/settings/settings-page'
|
|
10
10
|
import { AgentList } from '@/components/agents/agent-list'
|
|
11
11
|
import { AgentChatList } from '@/components/agents/agent-chat-list'
|
|
12
12
|
import { AgentSheet } from '@/components/agents/agent-sheet'
|
|
@@ -37,6 +37,8 @@ import { PluginList } from '@/components/plugins/plugin-list'
|
|
|
37
37
|
import { PluginSheet } from '@/components/plugins/plugin-sheet'
|
|
38
38
|
import { UsageList } from '@/components/usage/usage-list'
|
|
39
39
|
import { RunList } from '@/components/runs/run-list'
|
|
40
|
+
import { ProjectList } from '@/components/projects/project-list'
|
|
41
|
+
import { ProjectSheet } from '@/components/projects/project-sheet'
|
|
40
42
|
import { NetworkBanner } from './network-banner'
|
|
41
43
|
import { UpdateBanner } from './update-banner'
|
|
42
44
|
import { MobileHeader } from './mobile-header'
|
|
@@ -53,7 +55,6 @@ export function AppLayout() {
|
|
|
53
55
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
54
56
|
const sidebarOpen = useAppStore((s) => s.sidebarOpen)
|
|
55
57
|
const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
|
|
56
|
-
const setSettingsOpen = useAppStore((s) => s.setSettingsOpen)
|
|
57
58
|
const setUser = useAppStore((s) => s.setUser)
|
|
58
59
|
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
59
60
|
const activeView = useAppStore((s) => s.activeView)
|
|
@@ -69,6 +70,7 @@ export function AppLayout() {
|
|
|
69
70
|
const setMcpServerSheetOpen = useAppStore((s) => s.setMcpServerSheetOpen)
|
|
70
71
|
const setKnowledgeSheetOpen = useAppStore((s) => s.setKnowledgeSheetOpen)
|
|
71
72
|
const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
|
|
73
|
+
const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
72
74
|
const tasks = useAppStore((s) => s.tasks)
|
|
73
75
|
const isDesktop = useMediaQuery('(min-width: 768px)')
|
|
74
76
|
const hasSelectedSession = !!(currentSessionId && sessions[currentSessionId])
|
|
@@ -128,6 +130,7 @@ export function AppLayout() {
|
|
|
128
130
|
else if (activeView === 'mcp_servers') setMcpServerSheetOpen(true)
|
|
129
131
|
else if (activeView === 'knowledge') setKnowledgeSheetOpen(true)
|
|
130
132
|
else if (activeView === 'plugins') setPluginSheetOpen(true)
|
|
133
|
+
else if (activeView === 'projects') setProjectSheetOpen(true)
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
const handleNavClick = (view: AppView) => {
|
|
@@ -145,15 +148,11 @@ export function AppLayout() {
|
|
|
145
148
|
const agents = useAppStore((s) => s.agents)
|
|
146
149
|
const currentAgentId = useAppStore((s) => s.currentAgentId)
|
|
147
150
|
const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
|
|
148
|
-
const mainSession = Object.values(sessions).find((s) => s.name === '__main__' && s.user === currentUser)
|
|
149
|
-
|
|
150
151
|
const goToMainChat = async () => {
|
|
151
152
|
// Navigate to default agent's chat thread
|
|
152
153
|
const defaultAgent = agents['default'] || Object.values(agents)[0]
|
|
153
154
|
if (defaultAgent) {
|
|
154
155
|
await setCurrentAgent(defaultAgent.id)
|
|
155
|
-
} else if (mainSession) {
|
|
156
|
-
setCurrentSession(mainSession.id)
|
|
157
156
|
}
|
|
158
157
|
setActiveView('agents')
|
|
159
158
|
setSidebarOpen(false)
|
|
@@ -245,6 +244,11 @@ export function AppLayout() {
|
|
|
245
244
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
|
|
246
245
|
</svg>
|
|
247
246
|
</NavItem>
|
|
247
|
+
<NavItem view="projects" label="Projects" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('projects')}>
|
|
248
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
249
|
+
<path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" /><path d="M14 2v7h7" />
|
|
250
|
+
</svg>
|
|
251
|
+
</NavItem>
|
|
248
252
|
<NavItem view="schedules" label="Schedules" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('schedules')}>
|
|
249
253
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
250
254
|
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
|
@@ -355,7 +359,7 @@ export function AppLayout() {
|
|
|
355
359
|
{railExpanded && <DaemonIndicator />}
|
|
356
360
|
{railExpanded ? (
|
|
357
361
|
<button
|
|
358
|
-
onClick={() =>
|
|
362
|
+
onClick={() => handleNavClick('settings')}
|
|
359
363
|
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all
|
|
360
364
|
bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04] border-none"
|
|
361
365
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -368,7 +372,7 @@ export function AppLayout() {
|
|
|
368
372
|
</button>
|
|
369
373
|
) : (
|
|
370
374
|
<RailTooltip label="Settings" description="API keys, providers & app config">
|
|
371
|
-
<button onClick={() =>
|
|
375
|
+
<button onClick={() => handleNavClick('settings')} className="rail-btn">
|
|
372
376
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
373
377
|
<circle cx="12" cy="12" r="3" />
|
|
374
378
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
@@ -486,7 +490,7 @@ export function AppLayout() {
|
|
|
486
490
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
|
487
491
|
</svg>
|
|
488
492
|
</a>
|
|
489
|
-
<button onClick={() =>
|
|
493
|
+
<button onClick={() => handleNavClick('settings')} className="rail-btn">
|
|
490
494
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
491
495
|
<circle cx="12" cy="12" r="3" />
|
|
492
496
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
@@ -512,7 +516,7 @@ export function AppLayout() {
|
|
|
512
516
|
</button>
|
|
513
517
|
))}
|
|
514
518
|
</div>
|
|
515
|
-
{activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && (
|
|
519
|
+
{activeView !== 'logs' && activeView !== 'usage' && activeView !== 'runs' && activeView !== 'settings' && (
|
|
516
520
|
<div className="px-4 py-2.5 shrink-0">
|
|
517
521
|
<button
|
|
518
522
|
onClick={() => {
|
|
@@ -524,7 +528,7 @@ export function AppLayout() {
|
|
|
524
528
|
shadow-[0_2px_12px_rgba(99,102,241,0.15)]"
|
|
525
529
|
style={{ fontFamily: 'inherit' }}
|
|
526
530
|
>
|
|
527
|
-
+ New {activeView === 'agents' ? 'Agent' : activeView === 'schedules' ? 'Schedule' : activeView === 'tasks' ? 'Task' : activeView === 'secrets' ? 'Secret' : activeView === 'providers' ? 'Provider' : activeView === 'skills' ? 'Skill' : activeView === 'connectors' ? 'Connector' : activeView === 'webhooks' ? 'Webhook' : activeView === 'mcp_servers' ? 'MCP Server' : activeView === 'knowledge' ? 'Knowledge' : activeView === 'plugins' ? 'Plugin' : 'Entry'}
|
|
531
|
+
+ New {activeView === 'agents' ? 'Agent' : activeView === 'schedules' ? 'Schedule' : activeView === 'tasks' ? 'Task' : activeView === 'secrets' ? 'Secret' : activeView === 'providers' ? 'Provider' : activeView === 'skills' ? 'Skill' : activeView === 'connectors' ? 'Connector' : activeView === 'webhooks' ? 'Webhook' : activeView === 'mcp_servers' ? 'MCP Server' : activeView === 'knowledge' ? 'Knowledge' : activeView === 'plugins' ? 'Plugin' : activeView === 'projects' ? 'Project' : 'Entry'}
|
|
528
532
|
</button>
|
|
529
533
|
</div>
|
|
530
534
|
)}
|
|
@@ -557,6 +561,7 @@ export function AppLayout() {
|
|
|
557
561
|
{activeView === 'mcp_servers' && <McpServerList />}
|
|
558
562
|
{activeView === 'knowledge' && <KnowledgeList />}
|
|
559
563
|
{activeView === 'plugins' && <PluginList inSidebar />}
|
|
564
|
+
{activeView === 'projects' && <ProjectList />}
|
|
560
565
|
{activeView === 'usage' && <UsageList />}
|
|
561
566
|
{activeView === 'runs' && <RunList />}
|
|
562
567
|
{activeView === 'logs' && <LogList />}
|
|
@@ -591,6 +596,8 @@ export function AppLayout() {
|
|
|
591
596
|
<TaskBoard />
|
|
592
597
|
) : activeView === 'memory' ? (
|
|
593
598
|
<MemoryDetail />
|
|
599
|
+
) : activeView === 'settings' ? (
|
|
600
|
+
<SettingsPage />
|
|
594
601
|
) : !sidebarOpen && FULL_WIDTH_VIEWS.has(activeView) ? (
|
|
595
602
|
<div className="flex-1 flex flex-col h-full">
|
|
596
603
|
<div className="flex items-center px-6 pt-5 pb-3 shrink-0">
|
|
@@ -606,7 +613,7 @@ export function AppLayout() {
|
|
|
606
613
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
607
614
|
<line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" />
|
|
608
615
|
</svg>
|
|
609
|
-
{activeView === 'schedules' ? 'Schedule' : activeView === 'secrets' ? 'Secret' : activeView === 'providers' ? 'Provider' : activeView === 'skills' ? 'Skill' : activeView === 'connectors' ? 'Connector' : activeView === 'webhooks' ? 'Webhook' : activeView === 'mcp_servers' ? 'MCP Server' : activeView === 'knowledge' ? 'Knowledge' : activeView === 'plugins' ? 'Plugin' : 'New'}
|
|
616
|
+
{activeView === 'schedules' ? 'Schedule' : activeView === 'secrets' ? 'Secret' : activeView === 'providers' ? 'Provider' : activeView === 'skills' ? 'Skill' : activeView === 'connectors' ? 'Connector' : activeView === 'webhooks' ? 'Webhook' : activeView === 'mcp_servers' ? 'MCP Server' : activeView === 'knowledge' ? 'Knowledge' : activeView === 'plugins' ? 'Plugin' : activeView === 'projects' ? 'Project' : 'New'}
|
|
610
617
|
</button>
|
|
611
618
|
)}
|
|
612
619
|
</div>
|
|
@@ -619,6 +626,7 @@ export function AppLayout() {
|
|
|
619
626
|
{activeView === 'mcp_servers' && <McpServerList />}
|
|
620
627
|
{activeView === 'knowledge' && <KnowledgeList />}
|
|
621
628
|
{activeView === 'plugins' && <PluginList />}
|
|
629
|
+
{activeView === 'projects' && <ProjectList />}
|
|
622
630
|
{activeView === 'usage' && <UsageList />}
|
|
623
631
|
{activeView === 'runs' && <RunList />}
|
|
624
632
|
{activeView === 'logs' && <LogList />}
|
|
@@ -629,7 +637,6 @@ export function AppLayout() {
|
|
|
629
637
|
</div>
|
|
630
638
|
</ErrorBoundary>
|
|
631
639
|
|
|
632
|
-
<SettingsSheet />
|
|
633
640
|
<AgentSheet />
|
|
634
641
|
<ScheduleSheet />
|
|
635
642
|
<MemorySheet />
|
|
@@ -642,6 +649,7 @@ export function AppLayout() {
|
|
|
642
649
|
<McpServerSheet />
|
|
643
650
|
<KnowledgeSheet />
|
|
644
651
|
<PluginSheet />
|
|
652
|
+
<ProjectSheet />
|
|
645
653
|
|
|
646
654
|
<Dialog open={shortcutsOpen} onOpenChange={setShortcutsOpen}>
|
|
647
655
|
<DialogContent className="sm:max-w-[380px] bg-raised border-white/[0.08]">
|
|
@@ -735,13 +743,15 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
|
|
|
735
743
|
logs: 'Application logs & error tracking',
|
|
736
744
|
plugins: 'Extend agent capabilities with custom plugins',
|
|
737
745
|
usage: 'Token usage analytics & cost tracking',
|
|
738
|
-
runs: 'Live
|
|
746
|
+
runs: 'Live run monitoring & history',
|
|
747
|
+
settings: 'Manage providers, API keys & orchestrator engine',
|
|
748
|
+
projects: 'Group agents, tasks & schedules into projects',
|
|
739
749
|
}
|
|
740
750
|
|
|
741
751
|
const FULL_WIDTH_VIEWS = new Set<AppView>([
|
|
742
752
|
'schedules', 'secrets', 'providers', 'skills',
|
|
743
753
|
'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'plugins',
|
|
744
|
-
'usage', 'runs', 'logs',
|
|
754
|
+
'usage', 'runs', 'logs', 'settings', 'projects',
|
|
745
755
|
])
|
|
746
756
|
|
|
747
757
|
const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; title: string; description: string; features: string[] }> = {
|
|
@@ -754,14 +764,14 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; titl
|
|
|
754
764
|
memory: {
|
|
755
765
|
icon: 'database',
|
|
756
766
|
title: 'Memory',
|
|
757
|
-
description: 'Long-term memory store for AI agents. Orchestrators can store and retrieve knowledge across
|
|
758
|
-
features: ['Agents store findings and learnings automatically', 'Full-text search across all stored memories', 'Organized by categories and agents', 'Persists across
|
|
767
|
+
description: 'Long-term memory store for AI agents. Orchestrators can store and retrieve knowledge across conversations.',
|
|
768
|
+
features: ['Agents store findings and learnings automatically', 'Full-text search across all stored memories', 'Organized by categories and agents', 'Persists across conversations for continuity'],
|
|
759
769
|
},
|
|
760
770
|
tasks: {
|
|
761
771
|
icon: 'clipboard',
|
|
762
772
|
title: 'Task Board',
|
|
763
773
|
description: 'A Trello-style board for managing orchestrator jobs. Create tasks, assign them to orchestrators, and track progress.',
|
|
764
|
-
features: ['Kanban columns: Backlog, Queued, Running, Completed, Failed', 'Assign tasks to specific orchestrator agents', 'Sequential queue ensures orchestrators don\'t conflict', 'View results and
|
|
774
|
+
features: ['Kanban columns: Backlog, Queued, Running, Completed, Failed', 'Assign tasks to specific orchestrator agents', 'Sequential queue ensures orchestrators don\'t conflict', 'View results and logs for completed tasks'],
|
|
765
775
|
},
|
|
766
776
|
secrets: {
|
|
767
777
|
icon: 'lock',
|
|
@@ -785,7 +795,7 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; titl
|
|
|
785
795
|
icon: 'link',
|
|
786
796
|
title: 'Connectors',
|
|
787
797
|
description: 'Bridge chat platforms to your AI agents. Receive messages from Discord, Telegram, Slack, or WhatsApp and route them to agents.',
|
|
788
|
-
features: ['Connect Discord, Telegram, Slack, or WhatsApp bots', 'Route incoming messages to any agent', 'Each platform channel gets its own
|
|
798
|
+
features: ['Connect Discord, Telegram, Slack, or WhatsApp bots', 'Route incoming messages to any agent', 'Each platform channel gets its own chat thread', 'Start and stop connectors from the UI'],
|
|
789
799
|
},
|
|
790
800
|
webhooks: {
|
|
791
801
|
icon: 'webhook',
|
|
@@ -796,7 +806,7 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; titl
|
|
|
796
806
|
mcp_servers: {
|
|
797
807
|
icon: 'server',
|
|
798
808
|
title: 'MCP Servers',
|
|
799
|
-
description: 'Connect agents to external MCP (Model Context Protocol) servers, injecting their tools into
|
|
809
|
+
description: 'Connect agents to external MCP (Model Context Protocol) servers, injecting their tools into agent chats.',
|
|
800
810
|
features: ['Configure stdio, SSE, or streamable HTTP transports', 'Test connections and discover available tools', 'Assign MCP servers to specific agents', 'Tools appear alongside built-in tools in chat'],
|
|
801
811
|
},
|
|
802
812
|
knowledge: {
|
|
@@ -820,15 +830,27 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; titl
|
|
|
820
830
|
usage: {
|
|
821
831
|
icon: 'bar-chart',
|
|
822
832
|
title: 'Usage',
|
|
823
|
-
description: 'Track token usage and costs across all providers and
|
|
824
|
-
features: ['Per-provider cost breakdown', 'Token usage over time', '
|
|
833
|
+
description: 'Track token usage and costs across all providers and agents.',
|
|
834
|
+
features: ['Per-provider cost breakdown', 'Token usage over time', 'Per-agent cost tracking', 'Export usage data'],
|
|
825
835
|
},
|
|
826
836
|
runs: {
|
|
827
837
|
icon: 'activity',
|
|
828
838
|
title: 'Runs',
|
|
829
|
-
description: 'View the
|
|
839
|
+
description: 'View the run queue and execution history.',
|
|
830
840
|
features: ['Monitor queued and running tasks', 'View run results and errors', 'Cancel pending runs', 'Automatic retry tracking'],
|
|
831
841
|
},
|
|
842
|
+
settings: {
|
|
843
|
+
icon: 'settings',
|
|
844
|
+
title: 'Settings',
|
|
845
|
+
description: 'Manage providers, API keys & orchestrator engine.',
|
|
846
|
+
features: ['Configure LLM providers', 'Manage API credentials', 'Tune orchestrator settings', 'Set up voice & embedding'],
|
|
847
|
+
},
|
|
848
|
+
projects: {
|
|
849
|
+
icon: 'folder',
|
|
850
|
+
title: 'Projects',
|
|
851
|
+
description: 'Organize your work into projects. Group agents, tasks, and schedules under a common scope.',
|
|
852
|
+
features: ['Create named projects with color badges', 'Assign agents and tasks to projects', 'Filter sidebar views by project', 'Global view when no filter is active'],
|
|
853
|
+
},
|
|
832
854
|
}
|
|
833
855
|
|
|
834
856
|
function ViewEmptyState({ view }: { view: AppView }) {
|
|
@@ -1016,7 +1038,7 @@ function DesktopEmptyState({ userName }: { userName: string | null }) {
|
|
|
1016
1038
|
<span className="text-text-2">What would you like to do?</span>
|
|
1017
1039
|
</h1>
|
|
1018
1040
|
<p className="text-[15px] text-text-3 mb-12">
|
|
1019
|
-
Create a new
|
|
1041
|
+
Create a new chat to start chatting
|
|
1020
1042
|
</p>
|
|
1021
1043
|
<button
|
|
1022
1044
|
onClick={() => setNewSessionOpen(true)}
|
|
@@ -1029,7 +1051,7 @@ function DesktopEmptyState({ userName }: { userName: string | null }) {
|
|
|
1029
1051
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
1030
1052
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
1031
1053
|
</svg>
|
|
1032
|
-
New
|
|
1054
|
+
New Chat
|
|
1033
1055
|
</button>
|
|
1034
1056
|
</div>
|
|
1035
1057
|
</div>
|
|
@@ -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
|
+
}
|