@swarmclawai/swarmclaw 1.4.0 → 1.4.3
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 -71
- package/next.config.ts +9 -4
- package/package.json +10 -8
- package/scripts/build-bootstrap-env.mjs +24 -0
- package/scripts/run-next-build.mjs +120 -0
- package/scripts/run-next-typegen.mjs +61 -0
- package/src/app/api/approvals/route.test.ts +29 -3
- package/src/app/api/approvals/route.ts +13 -7
- package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
- package/src/app/api/chats/[id]/chat/route.ts +24 -8
- package/src/app/api/chats/chat-route.test.ts +68 -0
- package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
- package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
- package/src/app/api/logs/route.test.ts +61 -0
- package/src/app/api/logs/route.ts +35 -0
- package/src/app/api/swarmdock/route.ts +25 -0
- package/src/app/api/swarmfeed/posts/route.ts +44 -6
- package/src/app/api/swarmfeed/route.ts +4 -0
- package/src/app/api/tts/route.test.ts +82 -0
- package/src/app/api/tts/route.ts +13 -6
- package/src/app/api/tts/stream/route.ts +12 -5
- package/src/app/error.tsx +32 -0
- package/src/app/global-error.tsx +33 -0
- package/src/app/marketplace/page.tsx +7 -0
- package/src/cli/index.js +10 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/layout/error-boundary.tsx +12 -30
- package/src/components/layout/error-fallback.tsx +61 -0
- package/src/components/layout/sidebar-rail.tsx +5 -0
- package/src/features/swarmdock/agent-marketplace-settings.tsx +303 -0
- package/src/features/swarmdock/marketplace-page.tsx +189 -0
- package/src/features/swarmfeed/feed-page.tsx +3 -33
- package/src/features/swarmfeed/queries.ts +3 -3
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/report-client-error.ts +52 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +9 -1
- package/src/lib/providers/ollama.ts +34 -14
- package/src/lib/providers/openai.ts +9 -1
- package/src/lib/providers/openclaw.ts +3 -3
- package/src/lib/server/agents/agent-service.ts +18 -0
- package/src/lib/server/agents/agent-swarm-registration.ts +35 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
- package/src/lib/server/connectors/swarmdock.ts +29 -7
- package/src/lib/server/messages/message-repository.ts +31 -0
- package/src/lib/server/provider-health.ts +19 -3
- package/src/lib/server/safe-parse-body.test.ts +32 -0
- package/src/lib/server/safe-parse-body.ts +20 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/swarmdock.ts +104 -0
- package/src/lib/server/session-tools/swarmfeed.ts +150 -0
- package/src/lib/server/storage-normalization.ts +10 -0
- package/src/lib/server/storage.ts +13 -4
- package/src/lib/swarmfeed-client.ts +1 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/types/agent.ts +23 -0
- package/src/types/session.ts +1 -1
- package/tsconfig.json +1 -2
- package/src/.env.local +0 -4
package/src/cli/spec.js
CHANGED
|
@@ -215,6 +215,7 @@ const COMMAND_GROUPS = {
|
|
|
215
215
|
commands: {
|
|
216
216
|
list: { description: 'Fetch logs (supports --query lines=200,level=INFO)', method: 'GET', path: '/logs' },
|
|
217
217
|
clear: { description: 'Clear log file', method: 'DELETE', path: '/logs' },
|
|
218
|
+
report: { description: 'Write a client/browser error entry to the application log', method: 'POST', path: '/logs' },
|
|
218
219
|
},
|
|
219
220
|
},
|
|
220
221
|
|
|
@@ -29,6 +29,7 @@ import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
|
|
|
29
29
|
import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
|
|
30
30
|
import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
|
|
31
31
|
import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
|
|
32
|
+
import { AgentMarketplaceSettings } from '@/features/swarmdock/agent-marketplace-settings'
|
|
32
33
|
|
|
33
34
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
34
35
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
@@ -1983,6 +1984,15 @@ export function AgentSheet() {
|
|
|
1983
1984
|
</SectionCard>
|
|
1984
1985
|
)}
|
|
1985
1986
|
|
|
1987
|
+
{editing && (
|
|
1988
|
+
<SectionCard
|
|
1989
|
+
title="Marketplace"
|
|
1990
|
+
description="SwarmDock integration — list this agent on the AI marketplace to accept tasks and earn USDC."
|
|
1991
|
+
>
|
|
1992
|
+
<AgentMarketplaceSettings agent={editing} />
|
|
1993
|
+
</SectionCard>
|
|
1994
|
+
)}
|
|
1995
|
+
|
|
1986
1996
|
{!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
|
|
1987
1997
|
<AdvancedSettingsSection
|
|
1988
1998
|
open={showAdvancedSettings}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
import { Component } from 'react'
|
|
4
4
|
import type { ReactNode, ErrorInfo } from 'react'
|
|
5
5
|
|
|
6
|
+
import { reportClientError } from '@/lib/app/report-client-error'
|
|
7
|
+
import { ErrorFallback } from '@/components/layout/error-fallback'
|
|
8
|
+
|
|
6
9
|
export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
|
|
7
10
|
constructor(props: { children: ReactNode }) {
|
|
8
11
|
super(props)
|
|
@@ -15,41 +18,20 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError
|
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
18
|
-
|
|
21
|
+
reportClientError({
|
|
22
|
+
source: 'error-boundary',
|
|
23
|
+
error,
|
|
24
|
+
componentStack: info.componentStack,
|
|
25
|
+
})
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
render() {
|
|
22
29
|
if (this.state.hasError) {
|
|
23
30
|
return (
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<circle cx="12" cy="12" r="10" />
|
|
29
|
-
<line x1="12" y1="8" x2="12" y2="12" />
|
|
30
|
-
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
31
|
-
</svg>
|
|
32
|
-
</div>
|
|
33
|
-
<h2 className="font-display text-[22px] font-700 text-text mb-2 tracking-[-0.02em]">
|
|
34
|
-
Something went wrong
|
|
35
|
-
</h2>
|
|
36
|
-
<p className="text-[14px] text-text-3 mb-6">
|
|
37
|
-
An unexpected error occurred. Try reloading the page.
|
|
38
|
-
</p>
|
|
39
|
-
<button
|
|
40
|
-
onClick={() => window.location.reload()}
|
|
41
|
-
className="inline-flex items-center gap-2 px-6 py-3 rounded-[12px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer
|
|
42
|
-
hover:brightness-110 active:scale-[0.97] transition-all shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
|
|
43
|
-
style={{ fontFamily: 'inherit' }}
|
|
44
|
-
>
|
|
45
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
46
|
-
<polyline points="23 4 23 10 17 10" />
|
|
47
|
-
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
48
|
-
</svg>
|
|
49
|
-
Reload
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
31
|
+
<ErrorFallback
|
|
32
|
+
message="An unexpected dashboard error occurred. Reload the page to recover."
|
|
33
|
+
onPrimaryAction={() => window.location.reload()}
|
|
34
|
+
/>
|
|
53
35
|
)
|
|
54
36
|
}
|
|
55
37
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
type ErrorFallbackProps = {
|
|
4
|
+
title?: string
|
|
5
|
+
message?: string
|
|
6
|
+
primaryLabel?: string
|
|
7
|
+
onPrimaryAction?: () => void
|
|
8
|
+
secondaryLabel?: string
|
|
9
|
+
onSecondaryAction?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ErrorFallback({
|
|
13
|
+
title = 'Something went wrong',
|
|
14
|
+
message = 'An unexpected error occurred. Try again or reload the page.',
|
|
15
|
+
primaryLabel = 'Reload',
|
|
16
|
+
onPrimaryAction,
|
|
17
|
+
secondaryLabel,
|
|
18
|
+
onSecondaryAction,
|
|
19
|
+
}: ErrorFallbackProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex min-h-[50vh] flex-1 flex-col items-center justify-center bg-bg px-8">
|
|
22
|
+
<div className="max-w-[420px] text-center">
|
|
23
|
+
<div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-[16px] border border-red-500/20 bg-red-500/10">
|
|
24
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-red-400">
|
|
25
|
+
<circle cx="12" cy="12" r="10" />
|
|
26
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
27
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
28
|
+
</svg>
|
|
29
|
+
</div>
|
|
30
|
+
<h2 className="mb-2 font-display text-[22px] font-700 tracking-[-0.02em] text-text">
|
|
31
|
+
{title}
|
|
32
|
+
</h2>
|
|
33
|
+
<p className="mb-6 text-[14px] text-text-3">
|
|
34
|
+
{message}
|
|
35
|
+
</p>
|
|
36
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
37
|
+
<button
|
|
38
|
+
onClick={onPrimaryAction}
|
|
39
|
+
className="inline-flex cursor-pointer items-center gap-2 rounded-[12px] border-none bg-accent-bright px-6 py-3 text-[14px] font-600 text-white shadow-[0_4px_16px_rgba(99,102,241,0.2)] transition-all hover:brightness-110 active:scale-[0.97]"
|
|
40
|
+
style={{ fontFamily: 'inherit' }}
|
|
41
|
+
>
|
|
42
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
43
|
+
<polyline points="23 4 23 10 17 10" />
|
|
44
|
+
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
45
|
+
</svg>
|
|
46
|
+
{primaryLabel}
|
|
47
|
+
</button>
|
|
48
|
+
{secondaryLabel && onSecondaryAction ? (
|
|
49
|
+
<button
|
|
50
|
+
onClick={onSecondaryAction}
|
|
51
|
+
className="inline-flex cursor-pointer items-center rounded-[12px] border border-border bg-transparent px-5 py-3 text-[14px] font-600 text-text transition-colors hover:bg-panel/60"
|
|
52
|
+
style={{ fontFamily: 'inherit' }}
|
|
53
|
+
>
|
|
54
|
+
{secondaryLabel}
|
|
55
|
+
</button>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -267,6 +267,11 @@ export function SidebarRail({
|
|
|
267
267
|
<path d="M4 11a9 9 0 0 1 9 9" /><path d="M4 4a16 16 0 0 1 16 16" /><circle cx="5" cy="19" r="1" />
|
|
268
268
|
</svg>
|
|
269
269
|
</NavItem>
|
|
270
|
+
<NavItem view="marketplace" label="Marketplace" expanded={railExpanded} isActive={isNavActive('marketplace')} onClick={() => handleNavClick('marketplace')}>
|
|
271
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
272
|
+
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z" /><line x1="3" x2="21" y1="6" y2="6" /><path d="M16 10a4 4 0 0 1-8 0" />
|
|
273
|
+
</svg>
|
|
274
|
+
</NavItem>
|
|
270
275
|
</div>
|
|
271
276
|
|
|
272
277
|
<div className={`flex flex-col gap-0.5 ${railExpanded ? '' : 'items-center'}`}>
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useMemo } from 'react'
|
|
4
|
+
import { updateAgent } from '@/lib/agents'
|
|
5
|
+
import { toast } from 'sonner'
|
|
6
|
+
import { HintTip } from '@/components/shared/hint-tip'
|
|
7
|
+
import { AdvancedSettingsSection } from '@/components/shared/advanced-settings-section'
|
|
8
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
9
|
+
import type { Agent, SwarmDockMarketplaceConfig } from '@/types'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MARKETPLACE: SwarmDockMarketplaceConfig = {
|
|
12
|
+
enabled: false,
|
|
13
|
+
autoDiscover: false,
|
|
14
|
+
maxBudgetUsdc: '5000000',
|
|
15
|
+
autoBid: false,
|
|
16
|
+
autoBidMaxPrice: '1000000',
|
|
17
|
+
taskNotifications: true,
|
|
18
|
+
preferredCategories: [],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AgentMarketplaceSettings({ agent, onUpdate }: {
|
|
22
|
+
agent: Agent
|
|
23
|
+
onUpdate?: (agent: Agent) => void
|
|
24
|
+
}) {
|
|
25
|
+
const [enabled, setEnabled] = useState(agent.swarmdockEnabled || false)
|
|
26
|
+
const [description, setDescription] = useState(agent.swarmdockDescription || '')
|
|
27
|
+
const [skills, setSkills] = useState<string[]>(agent.swarmdockSkills || [])
|
|
28
|
+
const [walletId, setWalletId] = useState<string | null>(agent.swarmdockWalletId || null)
|
|
29
|
+
const [marketplace, setMarketplace] = useState<SwarmDockMarketplaceConfig>(agent.swarmdockMarketplace || DEFAULT_MARKETPLACE)
|
|
30
|
+
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
31
|
+
const [skillInput, setSkillInput] = useState('')
|
|
32
|
+
const [saving, setSaving] = useState(false)
|
|
33
|
+
|
|
34
|
+
const wallets = useAppStore((s) => s.wallets)
|
|
35
|
+
const loadWallets = useAppStore((s) => s.loadWallets)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
loadWallets()
|
|
39
|
+
}, [loadWallets])
|
|
40
|
+
|
|
41
|
+
const agentWallets = useMemo(() =>
|
|
42
|
+
Object.values(wallets).filter((w) => w.agentId === agent.id),
|
|
43
|
+
[wallets, agent.id])
|
|
44
|
+
|
|
45
|
+
const handleSave = useCallback(async () => {
|
|
46
|
+
setSaving(true)
|
|
47
|
+
try {
|
|
48
|
+
const updated = await updateAgent(agent.id, {
|
|
49
|
+
swarmdockEnabled: enabled,
|
|
50
|
+
swarmdockDescription: description.trim() || null,
|
|
51
|
+
swarmdockSkills: skills,
|
|
52
|
+
swarmdockWalletId: walletId,
|
|
53
|
+
swarmdockListedAt: enabled && !agent.swarmdockListedAt ? Date.now() : agent.swarmdockListedAt,
|
|
54
|
+
swarmdockMarketplace: marketplace.enabled ? marketplace : null,
|
|
55
|
+
})
|
|
56
|
+
toast.success('Marketplace settings saved')
|
|
57
|
+
onUpdate?.(updated)
|
|
58
|
+
} catch {
|
|
59
|
+
toast.error('Failed to save marketplace settings')
|
|
60
|
+
} finally {
|
|
61
|
+
setSaving(false)
|
|
62
|
+
}
|
|
63
|
+
}, [agent.id, agent.swarmdockListedAt, enabled, description, skills, walletId, marketplace, onUpdate])
|
|
64
|
+
|
|
65
|
+
const addSkill = useCallback(() => {
|
|
66
|
+
const trimmed = skillInput.trim().toLowerCase().replace(/\s+/g, '-')
|
|
67
|
+
if (trimmed && !skills.includes(trimmed)) {
|
|
68
|
+
setSkills((prev) => [...prev, trimmed])
|
|
69
|
+
}
|
|
70
|
+
setSkillInput('')
|
|
71
|
+
}, [skillInput, skills])
|
|
72
|
+
|
|
73
|
+
const removeSkill = useCallback((skill: string) => {
|
|
74
|
+
setSkills((prev) => prev.filter((s) => s !== skill))
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
77
|
+
const truncateAddr = (addr: string) =>
|
|
78
|
+
addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="space-y-5">
|
|
82
|
+
{/* Enable/Disable toggle */}
|
|
83
|
+
<div className="flex items-center justify-between gap-4 rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-4">
|
|
84
|
+
<div className="min-w-0">
|
|
85
|
+
<div className="flex items-center gap-2">
|
|
86
|
+
<p className="text-[14px] font-600 text-text">SwarmDock</p>
|
|
87
|
+
<HintTip text="Enable this agent to list on the SwarmDock AI marketplace" />
|
|
88
|
+
</div>
|
|
89
|
+
<p className="mt-1 text-[12px] leading-[1.6] text-text-3/75">
|
|
90
|
+
List this agent on the marketplace to accept tasks and earn USDC.
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
onClick={() => setEnabled((c) => !c)}
|
|
96
|
+
className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
97
|
+
aria-pressed={enabled}
|
|
98
|
+
>
|
|
99
|
+
<span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${enabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{enabled && (
|
|
104
|
+
<>
|
|
105
|
+
{/* Description */}
|
|
106
|
+
<div>
|
|
107
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
108
|
+
Marketplace Description <HintTip text="A short description shown on the agent's marketplace profile" />
|
|
109
|
+
</label>
|
|
110
|
+
<textarea
|
|
111
|
+
value={description}
|
|
112
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
113
|
+
placeholder="Describe what this agent specializes in..."
|
|
114
|
+
className="w-full min-h-[80px] px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow resize-y"
|
|
115
|
+
style={{ fontFamily: 'inherit' }}
|
|
116
|
+
maxLength={500}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Skills */}
|
|
121
|
+
<div>
|
|
122
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
123
|
+
Skills <HintTip text="Skill tags for task matching on the marketplace" />
|
|
124
|
+
</label>
|
|
125
|
+
{skills.length > 0 && (
|
|
126
|
+
<div className="flex flex-wrap gap-2 mb-2">
|
|
127
|
+
{skills.map((skill) => (
|
|
128
|
+
<button
|
|
129
|
+
key={skill}
|
|
130
|
+
onClick={() => removeSkill(skill)}
|
|
131
|
+
className="px-3 py-1.5 rounded-[10px] border border-accent-bright/40 bg-accent-bright/10 text-accent-bright text-[12px] font-500 transition-all cursor-pointer bg-transparent hover:bg-red-500/10 hover:border-red-500/40 hover:text-red-400"
|
|
132
|
+
>
|
|
133
|
+
{skill} ×
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
<div className="flex gap-2">
|
|
139
|
+
<input
|
|
140
|
+
value={skillInput}
|
|
141
|
+
onChange={(e) => setSkillInput(e.target.value)}
|
|
142
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addSkill() } }}
|
|
143
|
+
placeholder="e.g. data-analysis, web-design"
|
|
144
|
+
className="flex-1 px-4 py-2.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
145
|
+
style={{ fontFamily: 'inherit' }}
|
|
146
|
+
/>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={addSkill}
|
|
150
|
+
disabled={!skillInput.trim()}
|
|
151
|
+
className="px-4 py-2.5 rounded-[12px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 transition-all hover:bg-white/[0.08] disabled:opacity-30 disabled:cursor-not-allowed cursor-pointer"
|
|
152
|
+
>
|
|
153
|
+
Add
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Wallet picker */}
|
|
159
|
+
<div>
|
|
160
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
161
|
+
Payout Wallet <HintTip text="Base L2 wallet for receiving USDC payments" />
|
|
162
|
+
</label>
|
|
163
|
+
{agentWallets.length > 0 ? (
|
|
164
|
+
<select
|
|
165
|
+
value={walletId || ''}
|
|
166
|
+
onChange={(e) => setWalletId(e.target.value || null)}
|
|
167
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none cursor-pointer"
|
|
168
|
+
style={{ fontFamily: 'inherit' }}
|
|
169
|
+
>
|
|
170
|
+
<option value="">No wallet selected</option>
|
|
171
|
+
{agentWallets.map((w) => (
|
|
172
|
+
<option key={w.id} value={w.id}>
|
|
173
|
+
{w.label ? `${w.label} (${truncateAddr(w.walletAddress)})` : truncateAddr(w.walletAddress)}
|
|
174
|
+
</option>
|
|
175
|
+
))}
|
|
176
|
+
</select>
|
|
177
|
+
) : (
|
|
178
|
+
<p className="text-[13px] text-text-3/75">
|
|
179
|
+
No wallets linked to this agent. Add a wallet in the Wallets section first.
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Advanced: Marketplace config */}
|
|
185
|
+
<AdvancedSettingsSection
|
|
186
|
+
open={showAdvanced}
|
|
187
|
+
onToggle={() => setShowAdvanced((c) => !c)}
|
|
188
|
+
summary={marketplace.enabled ? 'Active' : undefined}
|
|
189
|
+
badges={marketplace.enabled ? [
|
|
190
|
+
...(marketplace.autoDiscover ? ['auto-discover'] : []),
|
|
191
|
+
...(marketplace.autoBid ? ['auto-bid'] : []),
|
|
192
|
+
] : []}
|
|
193
|
+
>
|
|
194
|
+
<div className="space-y-4">
|
|
195
|
+
<div className="flex items-center justify-between gap-4">
|
|
196
|
+
<div className="min-w-0">
|
|
197
|
+
<div className="flex items-center gap-2">
|
|
198
|
+
<p className="text-[13px] font-600 text-text">Marketplace Automation</p>
|
|
199
|
+
<HintTip text="Enable automated task discovery and bidding on the marketplace" />
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => setMarketplace((m) => ({ ...m, enabled: !m.enabled }))}
|
|
205
|
+
className={`relative h-6 w-11 shrink-0 rounded-full border-none transition-colors duration-200 ${marketplace.enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
206
|
+
aria-pressed={marketplace.enabled}
|
|
207
|
+
>
|
|
208
|
+
<span className={`absolute top-0.5 left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200 ${marketplace.enabled ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{marketplace.enabled && (
|
|
213
|
+
<>
|
|
214
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
215
|
+
<div
|
|
216
|
+
onClick={() => setMarketplace((m) => ({ ...m, autoDiscover: !m.autoDiscover }))}
|
|
217
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoDiscover ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
218
|
+
>
|
|
219
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoDiscover ? 'left-[22px]' : 'left-0.5'}`} />
|
|
220
|
+
</div>
|
|
221
|
+
<span className="flex items-center gap-2 text-[13px] text-text-2">
|
|
222
|
+
Auto-discover <HintTip text="Automatically scan for matching tasks" />
|
|
223
|
+
</span>
|
|
224
|
+
</label>
|
|
225
|
+
|
|
226
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
227
|
+
<div
|
|
228
|
+
onClick={() => setMarketplace((m) => ({ ...m, autoBid: !m.autoBid }))}
|
|
229
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.autoBid ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
230
|
+
>
|
|
231
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.autoBid ? 'left-[22px]' : 'left-0.5'}`} />
|
|
232
|
+
</div>
|
|
233
|
+
<span className="flex items-center gap-2 text-[13px] text-text-2">
|
|
234
|
+
Auto-bid <HintTip text="Automatically bid on matching tasks" />
|
|
235
|
+
</span>
|
|
236
|
+
</label>
|
|
237
|
+
|
|
238
|
+
<label className="flex items-center gap-3 cursor-pointer">
|
|
239
|
+
<div
|
|
240
|
+
onClick={() => setMarketplace((m) => ({ ...m, taskNotifications: !m.taskNotifications }))}
|
|
241
|
+
className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0 ${marketplace.taskNotifications ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
|
|
242
|
+
>
|
|
243
|
+
<div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200 ${marketplace.taskNotifications ? 'left-[22px]' : 'left-0.5'}`} />
|
|
244
|
+
</div>
|
|
245
|
+
<span className="flex items-center gap-2 text-[13px] text-text-2">
|
|
246
|
+
Task notifications <HintTip text="Show notifications for new matching tasks" />
|
|
247
|
+
</span>
|
|
248
|
+
</label>
|
|
249
|
+
|
|
250
|
+
<div>
|
|
251
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
|
|
252
|
+
Max budget (USDC) <HintTip text="Maximum budget cap per task. 1000000 = $1.00" />
|
|
253
|
+
</label>
|
|
254
|
+
<input
|
|
255
|
+
value={marketplace.maxBudgetUsdc}
|
|
256
|
+
onChange={(e) => setMarketplace((m) => ({ ...m, maxBudgetUsdc: e.target.value.replace(/[^0-9]/g, '') }))}
|
|
257
|
+
placeholder="5000000"
|
|
258
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
259
|
+
style={{ fontFamily: 'inherit' }}
|
|
260
|
+
/>
|
|
261
|
+
<p className="mt-1 text-[11px] text-text-3/60">
|
|
262
|
+
= ${(parseInt(marketplace.maxBudgetUsdc || '0', 10) / 1_000_000).toFixed(2)} USDC
|
|
263
|
+
</p>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
{marketplace.autoBid && (
|
|
267
|
+
<div>
|
|
268
|
+
<label className="flex items-center gap-2 text-[12px] font-600 text-text-2 mb-1.5">
|
|
269
|
+
Auto-bid max price (USDC) <HintTip text="Maximum amount to auto-bid per task. 1000000 = $1.00" />
|
|
270
|
+
</label>
|
|
271
|
+
<input
|
|
272
|
+
value={marketplace.autoBidMaxPrice}
|
|
273
|
+
onChange={(e) => setMarketplace((m) => ({ ...m, autoBidMaxPrice: e.target.value.replace(/[^0-9]/g, '') }))}
|
|
274
|
+
placeholder="1000000"
|
|
275
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none transition-all placeholder:text-text-3/50 focus-glow"
|
|
276
|
+
style={{ fontFamily: 'inherit' }}
|
|
277
|
+
/>
|
|
278
|
+
<p className="mt-1 text-[11px] text-text-3/60">
|
|
279
|
+
= ${(parseInt(marketplace.autoBidMaxPrice || '0', 10) / 1_000_000).toFixed(2)} USDC
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</AdvancedSettingsSection>
|
|
287
|
+
</>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{/* Save button */}
|
|
291
|
+
<div className="flex justify-end pt-2">
|
|
292
|
+
<button
|
|
293
|
+
onClick={handleSave}
|
|
294
|
+
disabled={saving}
|
|
295
|
+
className="px-6 py-2.5 rounded-[12px] bg-accent-bright text-white text-[14px] font-600 transition-all
|
|
296
|
+
hover:bg-accent-bright/90 disabled:opacity-40 disabled:cursor-not-allowed border-none cursor-pointer"
|
|
297
|
+
>
|
|
298
|
+
{saving ? 'Saving...' : 'Save Marketplace Settings'}
|
|
299
|
+
</button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
)
|
|
303
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { MainContent } from '@/components/layout/main-content'
|
|
5
|
+
import { PageLoader } from '@/components/ui/page-loader'
|
|
6
|
+
|
|
7
|
+
interface MarketplaceTask {
|
|
8
|
+
id: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
status: string
|
|
12
|
+
budgetMin: string
|
|
13
|
+
budgetMax: string
|
|
14
|
+
skillRequirements: string[]
|
|
15
|
+
bidCount: number
|
|
16
|
+
createdAt: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface MarketplaceAgent {
|
|
20
|
+
id: string
|
|
21
|
+
displayName: string
|
|
22
|
+
description: string | null
|
|
23
|
+
framework: string | null
|
|
24
|
+
trustLevel: number
|
|
25
|
+
status: string
|
|
26
|
+
createdAt: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type Tab = 'tasks' | 'agents'
|
|
30
|
+
|
|
31
|
+
// Proxy through SwarmClaw API to avoid CORS
|
|
32
|
+
const API_PREFIX = '/api/swarmdock'
|
|
33
|
+
|
|
34
|
+
function formatUsdc(microUnits: string): string {
|
|
35
|
+
const dollars = Number(microUnits) / 1_000_000
|
|
36
|
+
return `$${dollars.toFixed(2)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function timeAgo(dateStr: string): string {
|
|
40
|
+
const diff = Date.now() - new Date(dateStr).getTime()
|
|
41
|
+
const mins = Math.floor(diff / 60000)
|
|
42
|
+
if (mins < 60) return `${mins}m ago`
|
|
43
|
+
const hrs = Math.floor(mins / 60)
|
|
44
|
+
if (hrs < 24) return `${hrs}h ago`
|
|
45
|
+
const days = Math.floor(hrs / 24)
|
|
46
|
+
return `${days}d ago`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function MarketplacePage() {
|
|
50
|
+
const [tab, setTab] = useState<Tab>('tasks')
|
|
51
|
+
const [tasks, setTasks] = useState<MarketplaceTask[]>([])
|
|
52
|
+
const [agents, setAgents] = useState<MarketplaceAgent[]>([])
|
|
53
|
+
const [loading, setLoading] = useState(true)
|
|
54
|
+
const [error, setError] = useState<string | null>(null)
|
|
55
|
+
|
|
56
|
+
const loadData = useCallback(async (activeTab: Tab) => {
|
|
57
|
+
setLoading(true)
|
|
58
|
+
setError(null)
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${API_PREFIX}?type=${activeTab}&limit=50`)
|
|
61
|
+
if (!res.ok) throw new Error(`API error ${res.status}`)
|
|
62
|
+
const data = await res.json()
|
|
63
|
+
if (activeTab === 'tasks') {
|
|
64
|
+
setTasks(data.tasks || data)
|
|
65
|
+
} else {
|
|
66
|
+
setAgents(data.agents || data)
|
|
67
|
+
}
|
|
68
|
+
} catch (err: unknown) {
|
|
69
|
+
setError(err instanceof Error ? err.message : 'Failed to load')
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
void loadData(tab)
|
|
77
|
+
}, [tab, loadData])
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<MainContent>
|
|
81
|
+
<div className="flex-1 overflow-y-auto overscroll-contain">
|
|
82
|
+
<div className="mx-auto max-w-4xl px-4 sm:px-6 py-8">
|
|
83
|
+
<div className="mb-6">
|
|
84
|
+
<h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text">Marketplace</h1>
|
|
85
|
+
<p className="mt-1 text-[13px] text-text-3/75">Browse the SwarmDock agent marketplace</p>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Tab bar */}
|
|
89
|
+
<div className="flex gap-1 mb-6 rounded-[14px] border border-white/[0.06] bg-surface/50 p-1">
|
|
90
|
+
{(['tasks', 'agents'] as Tab[]).map((t) => (
|
|
91
|
+
<button
|
|
92
|
+
key={t}
|
|
93
|
+
onClick={() => setTab(t)}
|
|
94
|
+
className={`flex-1 px-4 py-2.5 rounded-[10px] text-[13px] font-600 transition-all border-none cursor-pointer
|
|
95
|
+
${tab === t
|
|
96
|
+
? 'bg-accent-bright/15 text-accent-bright'
|
|
97
|
+
: 'bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04]'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
{t === 'tasks' ? 'Tasks' : 'Agents'}
|
|
101
|
+
</button>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Content */}
|
|
106
|
+
{loading ? (
|
|
107
|
+
<PageLoader />
|
|
108
|
+
) : error ? (
|
|
109
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
|
|
110
|
+
<p className="text-[14px] text-text-3/75 mb-3">{error}</p>
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => loadData(tab)}
|
|
113
|
+
className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-white/[0.04] text-text-2 text-[13px] font-500 cursor-pointer hover:bg-white/[0.08] transition-all"
|
|
114
|
+
>
|
|
115
|
+
Retry
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
) : tab === 'tasks' ? (
|
|
119
|
+
<div className="space-y-3">
|
|
120
|
+
{tasks.length === 0 ? (
|
|
121
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
|
|
122
|
+
<p className="text-[14px] font-600 text-text mb-1">No tasks yet</p>
|
|
123
|
+
<p className="text-[13px] text-text-3/75">Tasks will appear here when posted on SwarmDock.</p>
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
tasks.map((task) => (
|
|
127
|
+
<div key={task.id} className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-4">
|
|
128
|
+
<div className="flex items-start justify-between gap-3">
|
|
129
|
+
<div className="min-w-0 flex-1">
|
|
130
|
+
<div className="flex items-center gap-2 mb-1">
|
|
131
|
+
<h3 className="text-[14px] font-600 text-text truncate">{task.title}</h3>
|
|
132
|
+
<span className={`px-2 py-0.5 rounded-full text-[11px] font-600 shrink-0
|
|
133
|
+
${task.status === 'open' ? 'bg-green-500/15 text-green-400' :
|
|
134
|
+
task.status === 'bidding' ? 'bg-amber-500/15 text-amber-400' :
|
|
135
|
+
task.status === 'completed' ? 'bg-white/[0.08] text-text-3' :
|
|
136
|
+
'bg-accent-bright/15 text-accent-bright'}`}>
|
|
137
|
+
{task.status}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
<p className="text-[12px] text-text-3/75 line-clamp-2 mb-2">{task.description}</p>
|
|
141
|
+
<div className="flex items-center gap-3 text-[11px] text-text-3/60">
|
|
142
|
+
<span>{formatUsdc(task.budgetMin)}–{formatUsdc(task.budgetMax)}</span>
|
|
143
|
+
<span>{task.bidCount} bid{task.bidCount !== 1 ? 's' : ''}</span>
|
|
144
|
+
<span>{task.skillRequirements.join(', ')}</span>
|
|
145
|
+
<span>{timeAgo(task.createdAt)}</span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
))
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
) : (
|
|
154
|
+
<div className="space-y-3">
|
|
155
|
+
{agents.length === 0 ? (
|
|
156
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-8 text-center">
|
|
157
|
+
<p className="text-[14px] font-600 text-text mb-1">No agents registered</p>
|
|
158
|
+
<p className="text-[13px] text-text-3/75">Agents will appear here when registered on SwarmDock.</p>
|
|
159
|
+
</div>
|
|
160
|
+
) : (
|
|
161
|
+
agents.map((agent) => (
|
|
162
|
+
<div key={agent.id} className="rounded-[14px] border border-white/[0.06] bg-surface/50 p-4">
|
|
163
|
+
<div className="flex items-start justify-between gap-3">
|
|
164
|
+
<div className="min-w-0 flex-1">
|
|
165
|
+
<div className="flex items-center gap-2 mb-1">
|
|
166
|
+
<h3 className="text-[14px] font-600 text-text">{agent.displayName}</h3>
|
|
167
|
+
{agent.framework && (
|
|
168
|
+
<span className="px-2 py-0.5 rounded-full text-[11px] font-500 bg-white/[0.06] text-text-3">
|
|
169
|
+
{agent.framework}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span className="px-2 py-0.5 rounded-full text-[11px] font-600 bg-accent-bright/15 text-accent-bright">
|
|
173
|
+
L{agent.trustLevel}
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
<p className="text-[12px] text-text-3/75">{agent.description || 'No description'}</p>
|
|
177
|
+
<p className="text-[11px] text-text-3/50 mt-1">{timeAgo(agent.createdAt)}</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
))
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</MainContent>
|
|
188
|
+
)
|
|
189
|
+
}
|