@swarmclawai/swarmclaw 1.4.9 → 1.5.1
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 +11 -1
- package/package.json +1 -1
- package/src/app/home/page.tsx +62 -0
- package/src/app/protocols/page.tsx +31 -0
- package/src/app/setup/page.tsx +4 -2
- package/src/components/auth/setup-wizard/index.tsx +66 -74
- package/src/components/auth/setup-wizard/step-next.tsx +60 -48
- package/src/components/auth/setup-wizard/step-path.tsx +159 -0
- package/src/components/auth/setup-wizard/step-progress.tsx +1 -0
- package/src/components/auth/setup-wizard/step-providers.tsx +9 -0
- package/src/components/auth/setup-wizard/types.test.ts +4 -4
- package/src/components/auth/setup-wizard/types.ts +22 -5
- package/src/components/auth/setup-wizard/utils.test.ts +73 -6
- package/src/components/auth/setup-wizard/utils.ts +13 -0
- package/src/components/home/home-launchpad.tsx +135 -0
- package/src/components/protocols/builder/template-gallery.tsx +23 -16
- package/src/components/shared/launch-action-card.tsx +27 -0
- package/src/lib/home-launchpad.test.ts +49 -0
- package/src/lib/home-launchpad.ts +30 -0
- package/src/lib/server/daemon/controller.ts +55 -0
- package/src/lib/setup-defaults.ts +13 -6
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ONBOARDING_PATHS } from '@/lib/setup-defaults'
|
|
4
|
+
import type { StepPathProps } from './types'
|
|
5
|
+
import { StepShell, SkipLink } from './shared'
|
|
6
|
+
import { formatAgentCount, getStarterKitsForPath } from './utils'
|
|
7
|
+
|
|
8
|
+
export function StepPath({
|
|
9
|
+
onboardingPath,
|
|
10
|
+
starterKitId,
|
|
11
|
+
intentText,
|
|
12
|
+
onPathChange,
|
|
13
|
+
onStarterKitChange,
|
|
14
|
+
onIntentTextChange,
|
|
15
|
+
onContinue,
|
|
16
|
+
onBack,
|
|
17
|
+
onSkip,
|
|
18
|
+
}: StepPathProps) {
|
|
19
|
+
const visibleStarterKits = getStarterKitsForPath(onboardingPath)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<StepShell wide>
|
|
23
|
+
<h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
|
|
24
|
+
Choose Your Start
|
|
25
|
+
</h1>
|
|
26
|
+
<p className="text-[15px] text-text-2 mb-2">
|
|
27
|
+
Pick the setup path that matches how much guidance you want.
|
|
28
|
+
</p>
|
|
29
|
+
<p className="text-[13px] text-text-3 mb-7">
|
|
30
|
+
You can still edit providers, prompts, tools, and agent details before finishing setup.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<div className="grid gap-3 md:grid-cols-3 text-left mb-6">
|
|
34
|
+
{ONBOARDING_PATHS.map((path) => {
|
|
35
|
+
const active = path.id === onboardingPath
|
|
36
|
+
return (
|
|
37
|
+
<button
|
|
38
|
+
key={path.id}
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={() => onPathChange(path.id)}
|
|
41
|
+
className={`rounded-[18px] border px-5 py-4 text-left transition-all duration-200 cursor-pointer ${
|
|
42
|
+
active
|
|
43
|
+
? 'border-accent-bright/35 bg-accent-soft shadow-[0_0_24px_rgba(99,102,241,0.12)]'
|
|
44
|
+
: 'border-white/[0.08] bg-surface hover:border-accent-bright/20 hover:bg-white/[0.04]'
|
|
45
|
+
}`}
|
|
46
|
+
>
|
|
47
|
+
<div className="flex items-start justify-between gap-3">
|
|
48
|
+
<div className="text-[15px] font-display font-700 text-text">{path.title}</div>
|
|
49
|
+
{path.badge ? (
|
|
50
|
+
<span className={`rounded-full px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] ${
|
|
51
|
+
active ? 'bg-accent-bright text-black' : 'bg-white/[0.05] text-text-3/80'
|
|
52
|
+
}`}>
|
|
53
|
+
{path.badge}
|
|
54
|
+
</span>
|
|
55
|
+
) : null}
|
|
56
|
+
</div>
|
|
57
|
+
<p className="mt-2 text-[13px] leading-relaxed text-text-2">{path.description}</p>
|
|
58
|
+
<p className="mt-3 text-[12px] leading-relaxed text-text-3/72">{path.detail}</p>
|
|
59
|
+
</button>
|
|
60
|
+
)
|
|
61
|
+
})}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{onboardingPath === 'intent' && (
|
|
65
|
+
<div className="mb-6 rounded-[18px] border border-white/[0.08] bg-surface px-5 py-4 text-left">
|
|
66
|
+
<label className="block text-[12px] font-700 uppercase tracking-[0.12em] text-text-3/60 mb-2">
|
|
67
|
+
What Are You Setting Up SwarmClaw To Do?
|
|
68
|
+
</label>
|
|
69
|
+
<textarea
|
|
70
|
+
value={intentText}
|
|
71
|
+
onChange={(event) => onIntentTextChange(event.target.value)}
|
|
72
|
+
rows={3}
|
|
73
|
+
placeholder="e.g. Help me run product research every week, summarize findings, and turn them into follow-up tasks."
|
|
74
|
+
className="w-full rounded-[14px] border border-white/[0.08] bg-bg px-4 py-3 text-[14px] text-text outline-none transition-all duration-200 resize-none placeholder:text-text-3/45 focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
|
|
75
|
+
/>
|
|
76
|
+
<p className="mt-2 text-[12px] leading-relaxed text-text-3/72">
|
|
77
|
+
This is used only to seed the starter prompts. It does not auto-classify your workflow.
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
81
|
+
|
|
82
|
+
<div className="rounded-[20px] border border-white/[0.08] bg-surface p-5 text-left">
|
|
83
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
84
|
+
<div>
|
|
85
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Starting Shape</div>
|
|
86
|
+
<div className="mt-1 text-[13px] text-text-3/72">
|
|
87
|
+
Start from a broad team shape instead of a niche preset. You can still edit every agent before setup finishes.
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/70">
|
|
91
|
+
{visibleStarterKits.length} options
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div className="mt-4 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
96
|
+
{visibleStarterKits.map((kit) => {
|
|
97
|
+
const active = starterKitId === kit.id
|
|
98
|
+
return (
|
|
99
|
+
<button
|
|
100
|
+
key={kit.id}
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={() => onStarterKitChange(kit.id)}
|
|
103
|
+
className={`rounded-[18px] border px-4 py-4 text-left transition-all duration-200 cursor-pointer ${
|
|
104
|
+
active
|
|
105
|
+
? 'border-accent-bright/35 bg-accent-soft shadow-[0_0_24px_rgba(99,102,241,0.12)]'
|
|
106
|
+
: 'border-white/[0.08] bg-white/[0.02] hover:border-accent-bright/20 hover:bg-white/[0.04]'
|
|
107
|
+
}`}
|
|
108
|
+
>
|
|
109
|
+
<div className="flex items-start justify-between gap-3">
|
|
110
|
+
<div className="text-[15px] font-display font-700 text-text">{kit.name}</div>
|
|
111
|
+
<span className={`rounded-full px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] ${
|
|
112
|
+
active ? 'bg-accent-bright text-black' : 'bg-white/[0.05] text-text-3/80'
|
|
113
|
+
}`}>
|
|
114
|
+
{kit.badge || formatAgentCount(kit.agents.length)}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
<p className="mt-2 text-[13px] leading-relaxed text-text-2">{kit.description}</p>
|
|
118
|
+
<p className="mt-3 text-[12px] leading-relaxed text-text-3/72">{kit.detail}</p>
|
|
119
|
+
{kit.agents.length > 0 ? (
|
|
120
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
121
|
+
{kit.agents.map((agent) => (
|
|
122
|
+
<span
|
|
123
|
+
key={agent.id}
|
|
124
|
+
className="rounded-full border border-white/[0.08] bg-white/[0.03] px-2.5 py-1 text-[11px] text-text-2"
|
|
125
|
+
>
|
|
126
|
+
{agent.name}
|
|
127
|
+
</span>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="mt-4 rounded-[12px] border border-dashed border-white/[0.06] bg-white/[0.02] px-3 py-2 text-[11px] text-text-3/70">
|
|
132
|
+
Finish setup without starter agents.
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</button>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="mt-6 flex items-center justify-center gap-3">
|
|
142
|
+
<button
|
|
143
|
+
onClick={onBack}
|
|
144
|
+
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px] font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
|
|
145
|
+
>
|
|
146
|
+
Back
|
|
147
|
+
</button>
|
|
148
|
+
<button
|
|
149
|
+
onClick={onContinue}
|
|
150
|
+
className="px-8 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-display font-600 cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200 shadow-[0_6px_28px_rgba(99,102,241,0.3)]"
|
|
151
|
+
>
|
|
152
|
+
Continue to Providers
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<SkipLink onClick={onSkip} />
|
|
157
|
+
</StepShell>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -12,6 +12,7 @@ export function StepProviders({
|
|
|
12
12
|
configuredProviderIds,
|
|
13
13
|
error,
|
|
14
14
|
canContinue,
|
|
15
|
+
onBack,
|
|
15
16
|
onSelectProvider,
|
|
16
17
|
onRemoveProvider,
|
|
17
18
|
onContinue,
|
|
@@ -132,6 +133,14 @@ export function StepProviders({
|
|
|
132
133
|
{error && <p className="mt-4 text-[13px] text-red-400">{error}</p>}
|
|
133
134
|
|
|
134
135
|
<div className="mt-6 flex items-center justify-center gap-3">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onBack}
|
|
139
|
+
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
|
|
140
|
+
font-display font-500 cursor-pointer hover:bg-white/[0.03] transition-all duration-200"
|
|
141
|
+
>
|
|
142
|
+
Back
|
|
143
|
+
</button>
|
|
135
144
|
<button
|
|
136
145
|
onClick={onSkip}
|
|
137
146
|
className="px-6 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[14px]
|
|
@@ -2,10 +2,10 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { test } from 'node:test'
|
|
3
3
|
import { STEP_ORDER } from './types'
|
|
4
4
|
|
|
5
|
-
test('STEP_ORDER
|
|
6
|
-
assert.deepEqual(STEP_ORDER, ['profile', 'providers', 'agents'])
|
|
5
|
+
test('STEP_ORDER includes the new onboarding path step', () => {
|
|
6
|
+
assert.deepEqual(STEP_ORDER, ['profile', 'path', 'providers', 'agents'])
|
|
7
7
|
})
|
|
8
8
|
|
|
9
|
-
test('STEP_ORDER has exactly
|
|
10
|
-
assert.equal(STEP_ORDER.length,
|
|
9
|
+
test('STEP_ORDER has exactly 4 steps', () => {
|
|
10
|
+
assert.equal(STEP_ORDER.length, 4)
|
|
11
11
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { GatewayProfile, ProviderId } from '@/types'
|
|
2
2
|
import type { SetupProvider } from '@/lib/setup-defaults'
|
|
3
3
|
|
|
4
|
-
export type SetupStep = 'profile' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
|
|
4
|
+
export type SetupStep = 'profile' | 'path' | 'providers' | 'connect' | 'agents' | 'next' | 'done'
|
|
5
5
|
export type CheckState = 'idle' | 'checking' | 'ok' | 'error'
|
|
6
6
|
|
|
7
7
|
export interface ProviderCheckResponse {
|
|
@@ -29,7 +29,7 @@ export interface SetupDoctorResponse {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface SetupWizardProps {
|
|
32
|
-
onComplete: () => void
|
|
32
|
+
onComplete: (destination?: string) => void
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface ConfiguredProvider {
|
|
@@ -83,7 +83,7 @@ export interface CreatedAgentSummary {
|
|
|
83
83
|
providerName: string
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export const STEP_ORDER: SetupStep[] = ['profile', 'providers', 'agents']
|
|
86
|
+
export const STEP_ORDER: SetupStep[] = ['profile', 'path', 'providers', 'agents']
|
|
87
87
|
|
|
88
88
|
export const CONNECTOR_ICONS = [
|
|
89
89
|
{ name: 'Discord', icon: 'D' },
|
|
@@ -114,12 +114,25 @@ export interface StepProvidersProps {
|
|
|
114
114
|
configuredProviderIds: Set<SetupProvider>
|
|
115
115
|
error: string
|
|
116
116
|
canContinue: boolean
|
|
117
|
+
onBack: () => void
|
|
117
118
|
onSelectProvider: (provider: SetupProvider) => void
|
|
118
119
|
onRemoveProvider: (id: string) => void
|
|
119
120
|
onContinue: () => void
|
|
120
121
|
onSkip: () => void
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
export interface StepPathProps {
|
|
125
|
+
onboardingPath: import('@/lib/setup-defaults').OnboardingPath
|
|
126
|
+
starterKitId: string | null
|
|
127
|
+
intentText: string
|
|
128
|
+
onPathChange: (path: import('@/lib/setup-defaults').OnboardingPath) => void
|
|
129
|
+
onStarterKitChange: (starterKitId: string) => void
|
|
130
|
+
onIntentTextChange: (value: string) => void
|
|
131
|
+
onContinue: () => void
|
|
132
|
+
onBack: () => void
|
|
133
|
+
onSkip: () => void
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
export interface StepConnectProps {
|
|
124
137
|
provider: SetupProvider
|
|
125
138
|
selectedProvider: import('@/lib/setup-defaults').SetupProviderOption
|
|
@@ -147,9 +160,13 @@ export interface StepAgentsProps {
|
|
|
147
160
|
}
|
|
148
161
|
|
|
149
162
|
export interface StepNextProps {
|
|
150
|
-
|
|
151
|
-
onAddAgent: () => void
|
|
163
|
+
createdAgents: CreatedAgentSummary[]
|
|
152
164
|
onContinueToDashboard: () => void
|
|
165
|
+
onOpenFirstAgent: () => void
|
|
166
|
+
onOpenProtocols: () => void
|
|
167
|
+
onOpenBuilder: () => void
|
|
168
|
+
onOpenConnectors: () => void
|
|
169
|
+
onOpenUsage: () => void
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
export interface StepDoneProps {
|
|
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import { test } from 'node:test'
|
|
3
3
|
import {
|
|
4
4
|
stepIndex,
|
|
5
|
+
defaultKitForPath,
|
|
5
6
|
formatEndpointHost,
|
|
7
|
+
getStarterKitsForPath,
|
|
6
8
|
isLocalOpenClawEndpoint,
|
|
7
9
|
resolveOpenClawDashboardUrl,
|
|
8
10
|
getOpenClawErrorHint,
|
|
@@ -21,16 +23,50 @@ test('stepIndex: profile → 0', () => {
|
|
|
21
23
|
assert.equal(stepIndex('profile'), 0)
|
|
22
24
|
})
|
|
23
25
|
|
|
24
|
-
test('stepIndex:
|
|
25
|
-
assert.equal(stepIndex('
|
|
26
|
+
test('stepIndex: path → 1', () => {
|
|
27
|
+
assert.equal(stepIndex('path'), 1)
|
|
26
28
|
})
|
|
27
29
|
|
|
28
|
-
test('stepIndex:
|
|
29
|
-
assert.equal(stepIndex('
|
|
30
|
+
test('stepIndex: providers → 2', () => {
|
|
31
|
+
assert.equal(stepIndex('providers'), 2)
|
|
30
32
|
})
|
|
31
33
|
|
|
32
|
-
test('stepIndex:
|
|
33
|
-
assert.equal(stepIndex('
|
|
34
|
+
test('stepIndex: connect maps to providers index (2)', () => {
|
|
35
|
+
assert.equal(stepIndex('connect'), 2)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('stepIndex: agents → 3', () => {
|
|
39
|
+
assert.equal(stepIndex('agents'), 3)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// onboarding path defaults
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
test('defaultKitForPath returns personal assistant for quick and intent', () => {
|
|
47
|
+
assert.equal(defaultKitForPath('quick'), 'personal_assistant')
|
|
48
|
+
assert.equal(defaultKitForPath('intent'), 'personal_assistant')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('defaultKitForPath returns blank workspace for manual', () => {
|
|
52
|
+
assert.equal(defaultKitForPath('manual'), 'blank_workspace')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('getStarterKitsForPath: quick exposes a reduced starter set', () => {
|
|
56
|
+
const ids = getStarterKitsForPath('quick').map((kit) => kit.id)
|
|
57
|
+
assert.deepEqual(ids, ['personal_assistant', 'research_copilot', 'builder_studio'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('getStarterKitsForPath: intent stays focused on broad starter shapes', () => {
|
|
61
|
+
const ids = getStarterKitsForPath('intent').map((kit) => kit.id)
|
|
62
|
+
assert.deepEqual(ids, ['personal_assistant', 'research_copilot', 'builder_studio', 'operator_swarm'])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('getStarterKitsForPath: manual keeps the full catalog', () => {
|
|
66
|
+
const ids = new Set(getStarterKitsForPath('manual').map((kit) => kit.id))
|
|
67
|
+
assert.equal(ids.has('blank_workspace'), true)
|
|
68
|
+
assert.equal(ids.has('content_studio'), true)
|
|
69
|
+
assert.equal(ids.has('openclaw_fleet'), true)
|
|
34
70
|
})
|
|
35
71
|
|
|
36
72
|
// ---------------------------------------------------------------------------
|
|
@@ -261,6 +297,37 @@ test('buildStarterDrafts carries custom runtime provider ids alongside custom se
|
|
|
261
297
|
}
|
|
262
298
|
})
|
|
263
299
|
|
|
300
|
+
test('buildStarterDrafts injects current intent into starter prompts', () => {
|
|
301
|
+
const cp = makeConfiguredProvider({
|
|
302
|
+
setupProvider: 'openai',
|
|
303
|
+
provider: 'openai',
|
|
304
|
+
defaultModel: 'gpt-4o',
|
|
305
|
+
})
|
|
306
|
+
const drafts = buildStarterDrafts({
|
|
307
|
+
starterKitId: 'personal_assistant',
|
|
308
|
+
intentText: 'Help me run weekly product research and turn it into follow-up tasks.',
|
|
309
|
+
configuredProviders: [cp],
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
assert.match(drafts[0]?.systemPrompt || '', /Current user intent:/)
|
|
313
|
+
assert.match(drafts[0]?.systemPrompt || '', /weekly product research/i)
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('buildStarterDrafts creates the delegate team starter pair', () => {
|
|
317
|
+
const cp = makeConfiguredProvider({
|
|
318
|
+
setupProvider: 'openai',
|
|
319
|
+
provider: 'openai',
|
|
320
|
+
defaultModel: 'gpt-4o',
|
|
321
|
+
})
|
|
322
|
+
const drafts = buildStarterDrafts({
|
|
323
|
+
starterKitId: 'operator_swarm',
|
|
324
|
+
intentText: '',
|
|
325
|
+
configuredProviders: [cp],
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
assert.deepEqual(drafts.map((draft) => draft.name), ['Operator', 'Maker'])
|
|
329
|
+
})
|
|
330
|
+
|
|
264
331
|
test('requiresSetupProviderVerification skips custom providers', () => {
|
|
265
332
|
assert.equal(requiresSetupProviderVerification('custom'), false)
|
|
266
333
|
assert.equal(requiresSetupProviderVerification('openclaw'), false)
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
STARTER_KITS,
|
|
3
3
|
getDefaultModelForProvider,
|
|
4
4
|
type OnboardingPath,
|
|
5
|
+
type StarterKit,
|
|
5
6
|
type SetupProvider,
|
|
6
7
|
type StarterKitAgentTemplate,
|
|
7
8
|
} from '@/lib/setup-defaults'
|
|
@@ -18,6 +19,18 @@ export function defaultKitForPath(path: OnboardingPath): string {
|
|
|
18
19
|
return 'personal_assistant'
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export function getStarterKitsForPath(path: OnboardingPath): StarterKit[] {
|
|
23
|
+
if (path === 'quick') {
|
|
24
|
+
const quickIds = new Set(['personal_assistant', 'builder_studio', 'research_copilot'])
|
|
25
|
+
return STARTER_KITS.filter((kit) => quickIds.has(kit.id))
|
|
26
|
+
}
|
|
27
|
+
if (path === 'intent') {
|
|
28
|
+
const intentIds = new Set(['personal_assistant', 'builder_studio', 'research_copilot', 'operator_swarm'])
|
|
29
|
+
return STARTER_KITS.filter((kit) => intentIds.has(kit.id))
|
|
30
|
+
}
|
|
31
|
+
return STARTER_KITS
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export function applyIntentContext(prompt: string, intentText: string): string {
|
|
22
35
|
const trimmed = intentText.trim()
|
|
23
36
|
if (!trimmed) return prompt
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
4
|
+
import { LaunchActionCard } from '@/components/shared/launch-action-card'
|
|
5
|
+
import type { Agent } from '@/types'
|
|
6
|
+
|
|
7
|
+
function SnapshotItem({ label, value, hint }: { label: string; value: string; hint: string }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-4 py-3">
|
|
10
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">{label}</div>
|
|
11
|
+
<div className="mt-2 text-[24px] font-display font-700 tracking-[-0.03em] text-text">{value}</div>
|
|
12
|
+
<div className="mt-1 text-[12px] leading-relaxed text-text-3/68">{hint}</div>
|
|
13
|
+
</div>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type Props = {
|
|
18
|
+
firstAgent: Agent | null
|
|
19
|
+
agentCount: number
|
|
20
|
+
sessionCount: number
|
|
21
|
+
taskCount: number
|
|
22
|
+
scheduleCount: number
|
|
23
|
+
connectorCount: number
|
|
24
|
+
todayCost: number
|
|
25
|
+
onOpenFirstAgent: () => void
|
|
26
|
+
onOpenProtocols: () => void
|
|
27
|
+
onOpenBuilder: () => void
|
|
28
|
+
onOpenConnectors: () => void
|
|
29
|
+
onOpenUsage: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function HomeLaunchpad({
|
|
33
|
+
firstAgent,
|
|
34
|
+
agentCount,
|
|
35
|
+
sessionCount,
|
|
36
|
+
taskCount,
|
|
37
|
+
scheduleCount,
|
|
38
|
+
connectorCount,
|
|
39
|
+
todayCost,
|
|
40
|
+
onOpenFirstAgent,
|
|
41
|
+
onOpenProtocols,
|
|
42
|
+
onOpenBuilder,
|
|
43
|
+
onOpenConnectors,
|
|
44
|
+
onOpenUsage,
|
|
45
|
+
}: Props) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="max-w-[980px] mx-auto px-6 py-10">
|
|
48
|
+
<div className="rounded-[24px] border border-white/[0.06] bg-gradient-to-br from-white/[0.05] via-white/[0.02] to-transparent p-6">
|
|
49
|
+
<div className="inline-flex rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.16em] text-text-3/70">
|
|
50
|
+
Launchpad
|
|
51
|
+
</div>
|
|
52
|
+
<div className="mt-4 flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
53
|
+
<div className="max-w-[620px]">
|
|
54
|
+
<h1 className="font-display text-[34px] font-700 tracking-[-0.03em] text-text">
|
|
55
|
+
Start with the result you want, not the control plane.
|
|
56
|
+
</h1>
|
|
57
|
+
<p className="mt-3 text-[15px] leading-relaxed text-text-3/72">
|
|
58
|
+
SwarmClaw already has the building blocks. Use this workspace to start a live agent chat, launch a bounded session, wire a connector, or move straight into reusable workflows.
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="rounded-[18px] border border-white/[0.06] bg-white/[0.03] p-4 min-w-[240px]">
|
|
62
|
+
<div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Workspace Anchor</div>
|
|
63
|
+
<div className="mt-3 flex items-center gap-3">
|
|
64
|
+
{firstAgent ? (
|
|
65
|
+
<>
|
|
66
|
+
<AgentAvatar
|
|
67
|
+
seed={firstAgent.avatarSeed}
|
|
68
|
+
avatarUrl={firstAgent.avatarUrl}
|
|
69
|
+
name={firstAgent.name}
|
|
70
|
+
size={44}
|
|
71
|
+
/>
|
|
72
|
+
<div>
|
|
73
|
+
<div className="text-[14px] font-display font-700 text-text">{firstAgent.name}</div>
|
|
74
|
+
<div className="text-[12px] text-text-3/70">
|
|
75
|
+
{firstAgent.model ? firstAgent.model.split('/').pop()?.split(':')[0] : firstAgent.provider}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</>
|
|
79
|
+
) : (
|
|
80
|
+
<div className="text-[13px] leading-relaxed text-text-3/72">
|
|
81
|
+
No agents yet. Start by creating one or use the workflow tools first.
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="mt-6 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
90
|
+
<LaunchActionCard
|
|
91
|
+
title={firstAgent ? 'Open First Agent Chat' : 'Open Agents'}
|
|
92
|
+
description={firstAgent
|
|
93
|
+
? `Jump into ${firstAgent.name} and start using the workspace immediately.`
|
|
94
|
+
: 'Open the agents workspace to create or tune the first specialist agent.'}
|
|
95
|
+
actionLabel={firstAgent ? 'Open Chat' : 'Open Agents'}
|
|
96
|
+
onClick={onOpenFirstAgent}
|
|
97
|
+
tone="primary"
|
|
98
|
+
/>
|
|
99
|
+
<LaunchActionCard
|
|
100
|
+
title="Start Structured Session"
|
|
101
|
+
description="Open bounded collaboration runs for planning, review, decision-making, or focused multi-agent work."
|
|
102
|
+
actionLabel="Open Protocols"
|
|
103
|
+
onClick={onOpenProtocols}
|
|
104
|
+
/>
|
|
105
|
+
<LaunchActionCard
|
|
106
|
+
title="Open Workflow Builder"
|
|
107
|
+
description="Move straight into reusable orchestration graphs if you want a durable workflow instead of a one-off run."
|
|
108
|
+
actionLabel="Open Builder"
|
|
109
|
+
onClick={onOpenBuilder}
|
|
110
|
+
/>
|
|
111
|
+
<LaunchActionCard
|
|
112
|
+
title="Connect a Platform"
|
|
113
|
+
description="Bridge agents into chat surfaces like Discord, Slack, Telegram, and WhatsApp."
|
|
114
|
+
actionLabel="Open Connectors"
|
|
115
|
+
onClick={onOpenConnectors}
|
|
116
|
+
/>
|
|
117
|
+
<LaunchActionCard
|
|
118
|
+
title="Review Usage"
|
|
119
|
+
description="Check cost, provider health, and activity so the workspace stays observable from the start."
|
|
120
|
+
actionLabel="Open Usage"
|
|
121
|
+
onClick={onOpenUsage}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="mt-8 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
126
|
+
<SnapshotItem label="Agents" value={String(agentCount)} hint="Configured specialists available in this workspace." />
|
|
127
|
+
<SnapshotItem label="Chats" value={String(sessionCount)} hint="Durable conversations already created." />
|
|
128
|
+
<SnapshotItem label="Tasks" value={String(taskCount)} hint="Queued or archived work items in the board." />
|
|
129
|
+
<SnapshotItem label="Schedules" value={String(scheduleCount)} hint="Recurring or delayed automations ready to run." />
|
|
130
|
+
<SnapshotItem label="Connectors" value={String(connectorCount)} hint="Platform bridges currently configured." />
|
|
131
|
+
<SnapshotItem label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated usage cost for today across providers." />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
import { useProtocolTemplatesQuery } from '@/features/protocols/queries'
|
|
2
2
|
import { useRouter } from 'next/navigation'
|
|
3
|
-
import { cn } from '@/lib/utils'
|
|
4
3
|
import type { ProtocolTemplate } from '@/types'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
type Props = {
|
|
6
|
+
templates?: ProtocolTemplate[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function TemplateGallery({ templates: providedTemplates }: Props) {
|
|
10
|
+
const { data: queriedTemplates } = useProtocolTemplatesQuery({ enabled: !providedTemplates })
|
|
8
11
|
const router = useRouter()
|
|
12
|
+
const templates = providedTemplates ?? queriedTemplates ?? []
|
|
9
13
|
|
|
10
|
-
const builtInTemplates = templates
|
|
11
|
-
const customTemplates = templates
|
|
14
|
+
const builtInTemplates = templates.filter((t) => t.builtIn)
|
|
15
|
+
const customTemplates = templates.filter((t) => !t.builtIn)
|
|
12
16
|
|
|
13
17
|
const renderCard = (template: ProtocolTemplate) => (
|
|
14
18
|
<button
|
|
15
19
|
key={template.id}
|
|
16
20
|
onClick={() => router.push(`/protocols/builder/${template.id}`)}
|
|
17
|
-
className=
|
|
18
|
-
'rounded-lg border bg-card p-4 text-left transition-shadow hover:shadow-md',
|
|
19
|
-
)}
|
|
21
|
+
className="rounded-[16px] border border-white/[0.06] bg-white/[0.03] p-4 text-left transition-all hover:border-accent-bright/20 hover:bg-white/[0.05] cursor-pointer"
|
|
20
22
|
>
|
|
21
|
-
<div className="
|
|
22
|
-
|
|
23
|
+
<div className="flex items-start justify-between gap-3">
|
|
24
|
+
<div className="text-[14px] font-display font-700 text-text">{template.name}</div>
|
|
25
|
+
<span className="rounded-full border border-white/[0.08] bg-white/[0.04] px-2 py-1 text-[10px] font-700 uppercase tracking-[0.12em] text-text-3/70">
|
|
26
|
+
{template.builtIn ? 'Built-in' : 'Custom'}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="mt-2 text-[12px] leading-relaxed text-text-3/72 line-clamp-3">
|
|
23
30
|
{template.description}
|
|
24
31
|
</div>
|
|
25
32
|
{template.tags && template.tags.length > 0 && (
|
|
26
|
-
<div className="mt-
|
|
33
|
+
<div className="mt-3 flex flex-wrap gap-1.5">
|
|
27
34
|
{template.tags.slice(0, 2).map((tag) => (
|
|
28
|
-
<span key={tag} className="rounded bg-
|
|
35
|
+
<span key={tag} className="rounded-full border border-white/[0.08] bg-white/[0.03] px-2 py-1 text-[10px] text-text-2">
|
|
29
36
|
{tag}
|
|
30
37
|
</span>
|
|
31
38
|
))}
|
|
@@ -38,14 +45,14 @@ export function TemplateGallery() {
|
|
|
38
45
|
<div className="space-y-4">
|
|
39
46
|
{builtInTemplates.length > 0 && (
|
|
40
47
|
<div>
|
|
41
|
-
<h4 className="mb-2 text-
|
|
42
|
-
<div className="grid grid-cols-2
|
|
48
|
+
<h4 className="mb-2 text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Built-in</h4>
|
|
49
|
+
<div className="grid gap-3 md:grid-cols-2">{builtInTemplates.map(renderCard)}</div>
|
|
43
50
|
</div>
|
|
44
51
|
)}
|
|
45
52
|
{customTemplates.length > 0 && (
|
|
46
53
|
<div>
|
|
47
|
-
<h4 className="mb-2 text-
|
|
48
|
-
<div className="grid grid-cols-2
|
|
54
|
+
<h4 className="mb-2 text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">Custom</h4>
|
|
55
|
+
<div className="grid gap-3 md:grid-cols-2">{customTemplates.map(renderCard)}</div>
|
|
49
56
|
</div>
|
|
50
57
|
)}
|
|
51
58
|
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type LaunchActionCardProps = {
|
|
2
|
+
title: string
|
|
3
|
+
description: string
|
|
4
|
+
actionLabel: string
|
|
5
|
+
onClick: () => void
|
|
6
|
+
tone?: 'primary' | 'default'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function LaunchActionCard({ title, description, actionLabel, onClick, tone = 'default' }: LaunchActionCardProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="rounded-[18px] border border-white/[0.06] bg-white/[0.03] p-4">
|
|
12
|
+
<div className="text-[15px] font-display font-700 text-text">{title}</div>
|
|
13
|
+
<p className="mt-2 text-[13px] leading-relaxed text-text-3/72">{description}</p>
|
|
14
|
+
<button
|
|
15
|
+
type="button"
|
|
16
|
+
onClick={onClick}
|
|
17
|
+
className={`mt-4 rounded-[12px] px-4 py-2.5 text-[13px] font-display font-700 transition-all cursor-pointer ${
|
|
18
|
+
tone === 'primary'
|
|
19
|
+
? 'bg-accent-bright text-black hover:opacity-90'
|
|
20
|
+
: 'border border-white/[0.08] bg-white/[0.04] text-text-2 hover:bg-white/[0.08]'
|
|
21
|
+
}`}
|
|
22
|
+
>
|
|
23
|
+
{actionLabel}
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|