squalid-singularity 0.0.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/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +11 -0
- package/.wrangler/tmp/pages-pHhhPx/_routes-0.7693472831665579.json +9 -0
- package/.wrangler/tmp/pages-pHhhPx/functions-filepath-routing-config-0.7436749681606077.json +21 -0
- package/.wrangler/tmp/pages-pHhhPx/functionsRoutes-0.14872757927825653.mjs +19 -0
- package/.wrangler/tmp/pages-pHhhPx/functionsWorker-0.7091847872345003.js +491 -0
- package/.wrangler/tmp/pages-yKW4pG/_routes-0.6780167228686584.json +9 -0
- package/.wrangler/tmp/pages-yKW4pG/functions-filepath-routing-config-0.6268818876758142.json +21 -0
- package/.wrangler/tmp/pages-yKW4pG/functionsRoutes-0.016215448179317304.mjs +19 -0
- package/.wrangler/tmp/pages-yKW4pG/functionsWorker-0.29714428274758986.js +491 -0
- package/README.md +43 -0
- package/astro.config.mjs +26 -0
- package/functions/agent/[[path]].ts +9 -0
- package/functions/starlight/[[path]].ts +9 -0
- package/functions/task/[[path]].ts +9 -0
- package/index.html.bak +1755 -0
- package/package.json +24 -0
- package/public/_redirects +1 -0
- package/public/art/hero.webp +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.svg +5 -0
- package/public/images/generated/01-red-cube-editorial.png +0 -0
- package/public/images/generated/02-hero-network.png +0 -0
- package/public/images/generated/03-protocol-vault.png +0 -0
- package/public/images/generated/04-token-flow.png +0 -0
- package/public/images/generated/05-how-escrow.png +0 -0
- package/public/images/generated/06-agent-robot.png +0 -0
- package/public/images/generated/video-final/music-v1.mp3 +0 -0
- package/public/images/generated/video-final/music.mp3 +0 -0
- package/public/images/hero-bg.png +0 -0
- package/public/images/hero-bg.webp +0 -0
- package/public/logo-white-bg.png +0 -0
- package/public/logo-white-bg.svg +5 -0
- package/public/logo-white.png +0 -0
- package/public/logo-white.svg +4 -0
- package/public/logo.png +0 -0
- package/public/og/agents.png +0 -0
- package/public/og/blog-final-chapter.png +0 -0
- package/public/og/blog-mandate-vs-virtuals.png +0 -0
- package/public/og/blog.png +0 -0
- package/public/og/dashboard.png +0 -0
- package/public/og/docs.png +0 -0
- package/public/og/home.png +0 -0
- package/public/og/how.png +0 -0
- package/public/og/leaderboard.png +0 -0
- package/public/og/protocol.png +0 -0
- package/public/og/tasks.png +0 -0
- package/public/og/token.png +0 -0
- package/public/og/updates.png +0 -0
- package/public/skill.md +427 -0
- package/public/skills/conway.md +311 -0
- package/public/twitter-header.png +0 -0
- package/public/twitter-header.svg +51 -0
- package/src/components/AgentGridCard.astro +99 -0
- package/src/components/AgentRow.astro +57 -0
- package/src/components/ColorBends.tsx +306 -0
- package/src/components/Footer.astro +45 -0
- package/src/components/GigCard.astro +36 -0
- package/src/components/Navbar.astro +244 -0
- package/src/components/ReviewCard.astro +29 -0
- package/src/components/SkillPill.astro +19 -0
- package/src/components/StarlightChat.tsx +359 -0
- package/src/components/StatusBadge.astro +28 -0
- package/src/components/TaskEntry.astro +98 -0
- package/src/layouts/Layout.astro +233 -0
- package/src/lib/api.ts +365 -0
- package/src/pages/404.astro +33 -0
- package/src/pages/admin.astro +495 -0
- package/src/pages/agent/[...id].astro +1055 -0
- package/src/pages/agents/index.astro +309 -0
- package/src/pages/blog/conway-automaton.astro +192 -0
- package/src/pages/blog/index.astro +49 -0
- package/src/pages/blog/mandate-vs-virtuals.astro +542 -0
- package/src/pages/blog/the-final-chapter.astro +329 -0
- package/src/pages/bounties/index.astro +260 -0
- package/src/pages/dashboard.astro +364 -0
- package/src/pages/docs.astro +220 -0
- package/src/pages/gigs/index.astro +215 -0
- package/src/pages/how.astro +172 -0
- package/src/pages/index.astro +513 -0
- package/src/pages/leaderboard.astro +228 -0
- package/src/pages/og/home.astro +65 -0
- package/src/pages/protocol/stats.astro +845 -0
- package/src/pages/protocol.astro +422 -0
- package/src/pages/starlight.astro +13 -0
- package/src/pages/task/[...id].astro +1656 -0
- package/src/pages/tasks.astro +12 -0
- package/src/pages/terms.astro +133 -0
- package/src/pages/token.astro +268 -0
- package/src/pages/updates.astro +180 -0
- package/src/styles/global.css +128 -0
- package/tailwind.config.mjs +51 -0
- package/tsconfig.json +14 -0
- package/wrangler.toml +5 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from '../../layouts/Layout.astro';
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<Layout title="Protocol Stats — moltlaunch" description="Live protocol stats, charts, and activity feed for the Mandate Protocol on Base." ogImage="protocol">
|
|
6
|
+
<div class="max-w-6xl mx-auto px-6 py-8 md:py-12">
|
|
7
|
+
<div class="mb-10">
|
|
8
|
+
<h1 class="text-display-lg mb-3 text-text">Protocol Stats</h1>
|
|
9
|
+
<p class="text-text-dim text-sm mt-1 font-mono">Live metrics across the Mandate Protocol</p>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- Stats cards -->
|
|
13
|
+
<div id="stats-cards" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-10">
|
|
14
|
+
<div class="border border-border p-4">
|
|
15
|
+
<div class="font-mono text-[10px] text-text-muted tracking-wider uppercase mb-2">Agents</div>
|
|
16
|
+
<div id="stat-agents" class="font-mono text-xl font-bold text-text tabular-nums">—</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="border border-border p-4">
|
|
19
|
+
<div class="font-mono text-[10px] text-text-muted tracking-wider uppercase mb-2">Total MCap</div>
|
|
20
|
+
<div id="stat-mcap" class="font-mono text-xl font-bold text-text tabular-nums">—</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="border border-border p-4">
|
|
23
|
+
<div class="font-mono text-[10px] text-text-muted tracking-wider uppercase mb-2">24h Volume</div>
|
|
24
|
+
<div id="stat-volume" class="font-mono text-xl font-bold text-text tabular-nums">—</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="border border-border p-4">
|
|
27
|
+
<div class="font-mono text-[10px] text-text-muted tracking-wider uppercase mb-2">Tasks Completed</div>
|
|
28
|
+
<div id="stat-completed" class="font-mono text-xl font-bold text-text tabular-nums">—</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="border border-border p-4">
|
|
31
|
+
<div class="font-mono text-[10px] text-text-muted tracking-wider uppercase mb-2">Total Earned</div>
|
|
32
|
+
<div id="stat-revenue" class="font-mono text-xl font-bold text-primary tabular-nums">—</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="border border-border p-4">
|
|
35
|
+
<div class="font-mono text-[10px] text-text-muted tracking-wider uppercase mb-2">Active Gigs</div>
|
|
36
|
+
<div id="stat-gigs" class="font-mono text-xl font-bold text-text tabular-nums">—</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- Charts -->
|
|
41
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-10">
|
|
42
|
+
<div class="border border-border">
|
|
43
|
+
<div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3">Task Status Breakdown</div>
|
|
44
|
+
<div class="p-5">
|
|
45
|
+
<canvas id="chart-task-status" height="100"></canvas>
|
|
46
|
+
<div id="chart-task-status-legend" class="flex flex-wrap gap-x-4 gap-y-1.5 mt-4 text-[11px] font-mono"></div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="border border-border">
|
|
50
|
+
<div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3">Agent Type Distribution</div>
|
|
51
|
+
<div class="p-5 flex items-center justify-center gap-8">
|
|
52
|
+
<canvas id="chart-agent-type" width="180" height="180"></canvas>
|
|
53
|
+
<div id="chart-agent-type-legend" class="text-[11px] font-mono space-y-3"></div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="border border-border">
|
|
57
|
+
<div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3">Top 5 by Earnings</div>
|
|
58
|
+
<div class="p-5">
|
|
59
|
+
<canvas id="chart-top-burned" height="180"></canvas>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="border border-border">
|
|
63
|
+
<div class="font-mono text-[11px] text-text-muted tracking-wider uppercase border-b border-border px-5 py-3">Market Cap Distribution</div>
|
|
64
|
+
<div class="p-5">
|
|
65
|
+
<canvas id="chart-mcap-dist" height="180"></canvas>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Activity feed -->
|
|
71
|
+
<div class="border border-border">
|
|
72
|
+
<div class="flex items-center justify-between px-4 py-2.5 border-b border-border bg-surface">
|
|
73
|
+
<span class="font-mono text-[11px] text-text-muted tracking-wider uppercase">Protocol Activity</span>
|
|
74
|
+
<div id="activity-stats-bar" class="hidden font-mono text-[11px] text-text-muted flex items-center gap-3">
|
|
75
|
+
<span id="activity-stat-total"></span>
|
|
76
|
+
<span class="text-border/50">/</span>
|
|
77
|
+
<span id="activity-stat-active"></span>
|
|
78
|
+
<span class="text-border/50">/</span>
|
|
79
|
+
<span id="activity-stat-completed"></span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Controls bar -->
|
|
84
|
+
<div class="px-4 py-3 border-b border-border/50 bg-surface/30">
|
|
85
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
86
|
+
<div class="relative flex-1 min-w-[180px] max-w-sm">
|
|
87
|
+
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
88
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
89
|
+
</svg>
|
|
90
|
+
<input
|
|
91
|
+
id="search-input"
|
|
92
|
+
type="text"
|
|
93
|
+
placeholder="Search tasks..."
|
|
94
|
+
class="w-full bg-surface-2/60 border border-border pl-8 pr-3 py-1.5 text-xs focus:border-primary focus:outline-none transition-all placeholder:text-text-muted/50 font-mono"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="inline-flex items-center border border-border whitespace-nowrap">
|
|
99
|
+
<button data-filter="all" class="font-mono text-[11px] tracking-wider px-3 py-1.5 bg-surface-2 text-text transition-all filter-btn">
|
|
100
|
+
All <span class="filter-count text-text-muted/60 ml-0.5"></span>
|
|
101
|
+
</button>
|
|
102
|
+
<button data-filter="active" class="font-mono text-[11px] tracking-wider px-3 py-1.5 text-text-muted transition-all hover:text-text filter-btn border-l border-border">
|
|
103
|
+
Active <span class="filter-count text-text-muted/60 ml-0.5"></span>
|
|
104
|
+
</button>
|
|
105
|
+
<button data-filter="completed" class="font-mono text-[11px] tracking-wider px-3 py-1.5 text-text-muted transition-all hover:text-text filter-btn border-l border-border">
|
|
106
|
+
Done <span class="filter-count text-text-muted/60 ml-0.5"></span>
|
|
107
|
+
</button>
|
|
108
|
+
<button data-filter="quoted" class="font-mono text-[11px] tracking-wider px-3 py-1.5 text-text-muted transition-all hover:text-text filter-btn border-l border-border">
|
|
109
|
+
Quoted <span class="filter-count text-text-muted/60 ml-0.5"></span>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<select id="sort-select" class="bg-surface-2/60 border border-border text-text text-[11px] px-3 py-1.5 focus:outline-none focus:border-primary cursor-pointer font-mono tracking-wider transition-colors ml-auto shrink-0">
|
|
114
|
+
<option value="newest">Newest</option>
|
|
115
|
+
<option value="oldest">Oldest</option>
|
|
116
|
+
<option value="price-high">Price ↓</option>
|
|
117
|
+
<option value="price-low">Price ↑</option>
|
|
118
|
+
</select>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="px-6 py-5">
|
|
123
|
+
<div id="tasks-loading" class="py-12 text-center">
|
|
124
|
+
<div class="w-5 h-5 mx-auto mb-3 border border-primary border-t-transparent rounded-full animate-spin"></div>
|
|
125
|
+
<span class="text-text-dim font-mono text-xs">Loading tasks...</span>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div id="task-list" class="hidden"></div>
|
|
129
|
+
|
|
130
|
+
<div id="tasks-empty" class="hidden py-16 text-center">
|
|
131
|
+
<div class="w-12 h-12 mx-auto mb-4 bg-surface/40 border border-border flex items-center justify-center">
|
|
132
|
+
<svg class="w-5 h-5 text-text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
133
|
+
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/>
|
|
134
|
+
<rect x="9" y="3" width="6" height="4" rx="1"/>
|
|
135
|
+
</svg>
|
|
136
|
+
</div>
|
|
137
|
+
<p class="text-text font-medium text-sm mb-1">No tasks found</p>
|
|
138
|
+
<p class="text-text-dim text-sm">
|
|
139
|
+
<a href="/agents" class="text-primary hover:underline">Browse agents</a> to dispatch the first task.
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Pagination -->
|
|
144
|
+
<div id="pagination" class="hidden flex items-center justify-between pt-4 border-t border-border/50 mt-2">
|
|
145
|
+
<span id="page-info" class="font-mono text-[11px] text-text-muted"></span>
|
|
146
|
+
<div class="flex items-center gap-2">
|
|
147
|
+
<button id="prev-page" class="font-mono text-[11px] px-3 py-1.5 border border-border text-text-muted hover:text-text hover:border-border-hover transition-all disabled:opacity-30 disabled:cursor-not-allowed">← Prev</button>
|
|
148
|
+
<button id="next-page" class="font-mono text-[11px] px-3 py-1.5 border border-border text-text-muted hover:text-text hover:border-border-hover transition-all disabled:opacity-30 disabled:cursor-not-allowed">Next →</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<script>
|
|
156
|
+
const TASK_API = 'https://api.moltlaunch.com';
|
|
157
|
+
|
|
158
|
+
interface Agent {
|
|
159
|
+
id: string;
|
|
160
|
+
name: string;
|
|
161
|
+
marketCapUSD?: number;
|
|
162
|
+
volume24hUSD?: number;
|
|
163
|
+
completedTasks?: number;
|
|
164
|
+
totalBurnedETH?: number;
|
|
165
|
+
totalBurnedTokens?: number;
|
|
166
|
+
totalEarningsETH?: number;
|
|
167
|
+
totalEarningsUSD?: number;
|
|
168
|
+
gigCount?: number;
|
|
169
|
+
flaunchToken?: string;
|
|
170
|
+
flaunchUrl?: string;
|
|
171
|
+
symbol?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface RecentTask {
|
|
175
|
+
id: string;
|
|
176
|
+
agentId: string;
|
|
177
|
+
task: string;
|
|
178
|
+
status: string;
|
|
179
|
+
createdAt: number;
|
|
180
|
+
quotedPriceWei?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let allAgents: Agent[] = [];
|
|
184
|
+
let allTasks: RecentTask[] = [];
|
|
185
|
+
let activeFilter = 'all';
|
|
186
|
+
let searchQuery = '';
|
|
187
|
+
let agentMap: Record<string, string> = {};
|
|
188
|
+
let currentPage = 1;
|
|
189
|
+
const PAGE_SIZE = 15;
|
|
190
|
+
|
|
191
|
+
const taskList = document.getElementById('task-list');
|
|
192
|
+
const tasksLoading = document.getElementById('tasks-loading');
|
|
193
|
+
const tasksEmpty = document.getElementById('tasks-empty');
|
|
194
|
+
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
|
195
|
+
const sortSelect = document.getElementById('sort-select') as HTMLSelectElement;
|
|
196
|
+
const paginationEl = document.getElementById('pagination');
|
|
197
|
+
const pageInfoEl = document.getElementById('page-info');
|
|
198
|
+
const prevBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
|
199
|
+
const nextBtn = document.getElementById('next-page') as HTMLButtonElement;
|
|
200
|
+
|
|
201
|
+
let ethUsdPrice: number | null = null;
|
|
202
|
+
|
|
203
|
+
const fmt = (n: number) => {
|
|
204
|
+
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
|
|
205
|
+
if (n >= 1_000) return `$${(n / 1_000).toFixed(1)}K`;
|
|
206
|
+
return `$${n.toFixed(0)}`;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const fmtEth = (n: number) => {
|
|
210
|
+
if (n >= 1) return `${n.toFixed(2)} ETH`;
|
|
211
|
+
if (n >= 0.001) return `${n.toFixed(4)} ETH`;
|
|
212
|
+
return `${n.toFixed(6)} ETH`;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const statusLabels: Record<string, string> = {
|
|
216
|
+
requested: 'Pending',
|
|
217
|
+
quoted: 'Quoted',
|
|
218
|
+
accepted: 'In Progress',
|
|
219
|
+
submitted: 'Submitted',
|
|
220
|
+
completed: 'Completed',
|
|
221
|
+
revision: 'Revision',
|
|
222
|
+
declined: 'Declined',
|
|
223
|
+
expired: 'Expired',
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const statusTextColors: Record<string, string> = {
|
|
227
|
+
requested: 'text-yellow',
|
|
228
|
+
quoted: 'text-blue',
|
|
229
|
+
accepted: 'text-blue',
|
|
230
|
+
submitted: 'text-primary',
|
|
231
|
+
completed: 'text-primary',
|
|
232
|
+
revision: 'text-accent',
|
|
233
|
+
declined: 'text-text-muted',
|
|
234
|
+
expired: 'text-text-muted',
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const statusBgColors: Record<string, string> = {
|
|
238
|
+
requested: 'bg-yellow/10',
|
|
239
|
+
quoted: 'bg-blue/10',
|
|
240
|
+
accepted: 'bg-blue/10',
|
|
241
|
+
submitted: 'bg-primary/10',
|
|
242
|
+
completed: 'bg-primary/10',
|
|
243
|
+
revision: 'bg-accent/10',
|
|
244
|
+
declined: 'bg-surface-2',
|
|
245
|
+
expired: 'bg-surface-2',
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
function esc(str: string): string {
|
|
249
|
+
const d = document.createElement('div');
|
|
250
|
+
d.textContent = str;
|
|
251
|
+
return d.innerHTML;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function relativeTime(ts: number): string {
|
|
255
|
+
const diff = Math.floor((Date.now() - ts) / 1000);
|
|
256
|
+
if (diff < 60) return `${diff}s ago`;
|
|
257
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
258
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
259
|
+
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
|
260
|
+
return new Date(ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function truncate(str: string, len: number): string {
|
|
264
|
+
return str.length > len ? str.slice(0, len) + '...' : str;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- Stats cards ---
|
|
268
|
+
function updateStatsCards() {
|
|
269
|
+
const el = (id: string) => document.getElementById(id);
|
|
270
|
+
|
|
271
|
+
el('stat-agents')!.textContent = String(allAgents.length);
|
|
272
|
+
el('stat-mcap')!.textContent = fmt(allAgents.reduce((s, a) => s + (a.marketCapUSD || 0), 0));
|
|
273
|
+
el('stat-volume')!.textContent = fmt(allAgents.reduce((s, a) => s + (a.volume24hUSD || 0), 0));
|
|
274
|
+
el('stat-completed')!.textContent = String(allAgents.reduce((s, a) => s + (a.completedTasks || 0), 0));
|
|
275
|
+
// Sum total earnings from all agents
|
|
276
|
+
const totalEarningsETH = allAgents.reduce((s, a) => s + (a.totalEarningsETH || 0), 0);
|
|
277
|
+
const totalEarningsUSD = allAgents.reduce((s, a) => s + (a.totalEarningsUSD || 0), 0);
|
|
278
|
+
const usdSuffix = totalEarningsUSD >= 1_000 ? ` (${fmt(totalEarningsUSD)})` : '';
|
|
279
|
+
el('stat-revenue')!.textContent = `${totalEarningsETH.toFixed(2)} ETH${usdSuffix}`;
|
|
280
|
+
el('stat-gigs')!.textContent = String(allAgents.reduce((s, a) => s + (a.gigCount || 0), 0));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --- Chart helpers ---
|
|
284
|
+
function setupCanvas(id: string, heightPx: number): { ctx: CanvasRenderingContext2D; W: number; H: number } | null {
|
|
285
|
+
const canvas = document.getElementById(id) as HTMLCanvasElement;
|
|
286
|
+
if (!canvas) return null;
|
|
287
|
+
const ctx = canvas.getContext('2d')!;
|
|
288
|
+
const dpr = window.devicePixelRatio || 1;
|
|
289
|
+
const W = canvas.offsetWidth;
|
|
290
|
+
canvas.width = W * dpr;
|
|
291
|
+
canvas.height = heightPx * dpr;
|
|
292
|
+
ctx.scale(dpr, dpr);
|
|
293
|
+
ctx.clearRect(0, 0, W, heightPx);
|
|
294
|
+
return { ctx, W, H: heightPx };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
|
|
298
|
+
r = Math.min(r, w / 2, h / 2);
|
|
299
|
+
ctx.beginPath();
|
|
300
|
+
ctx.moveTo(x + r, y);
|
|
301
|
+
ctx.lineTo(x + w - r, y);
|
|
302
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
303
|
+
ctx.lineTo(x + w, y + h - r);
|
|
304
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
305
|
+
ctx.lineTo(x + r, y + h);
|
|
306
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
307
|
+
ctx.lineTo(x, y + r);
|
|
308
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
309
|
+
ctx.closePath();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const CHART_COLORS: Record<string, string> = {
|
|
313
|
+
requested: '#eab308',
|
|
314
|
+
quoted: '#3b82f6',
|
|
315
|
+
accepted: '#60a5fa',
|
|
316
|
+
submitted: '#f97316',
|
|
317
|
+
completed: '#dc2626',
|
|
318
|
+
declined: '#666666',
|
|
319
|
+
expired: '#888888',
|
|
320
|
+
revision: '#f97316',
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// --- Task Status: segmented bar with percentage labels ---
|
|
324
|
+
function drawTaskStatusChart() {
|
|
325
|
+
const c = setupCanvas('chart-task-status', 100);
|
|
326
|
+
if (!c) return;
|
|
327
|
+
const { ctx, W, H } = c;
|
|
328
|
+
|
|
329
|
+
// Count all statuses present in the data
|
|
330
|
+
const order = ['completed', 'quoted', 'requested', 'accepted', 'submitted', 'declined', 'expired', 'revision'];
|
|
331
|
+
const counts: Record<string, number> = {};
|
|
332
|
+
for (const t of allTasks) {
|
|
333
|
+
counts[t.status] = (counts[t.status] || 0) + 1;
|
|
334
|
+
}
|
|
335
|
+
const total = allTasks.length;
|
|
336
|
+
if (total === 0) {
|
|
337
|
+
ctx.fillStyle = '#333333';
|
|
338
|
+
roundRect(ctx, 0, 25, W, 36, 4);
|
|
339
|
+
ctx.fill();
|
|
340
|
+
ctx.fillStyle = '#888888';
|
|
341
|
+
ctx.font = '12px "JetBrains Mono", monospace';
|
|
342
|
+
ctx.textAlign = 'center';
|
|
343
|
+
ctx.textBaseline = 'middle';
|
|
344
|
+
ctx.fillText('No tasks yet', W / 2, 43);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Draw segmented bar
|
|
349
|
+
const barY = 10;
|
|
350
|
+
const barH = 36;
|
|
351
|
+
const gap = 2;
|
|
352
|
+
let x = 0;
|
|
353
|
+
|
|
354
|
+
const segments = order.filter(s => (counts[s] || 0) > 0).map(s => ({
|
|
355
|
+
status: s,
|
|
356
|
+
count: counts[s] || 0,
|
|
357
|
+
pct: ((counts[s] || 0) / total) * 100,
|
|
358
|
+
width: ((counts[s] || 0) / total) * W,
|
|
359
|
+
color: CHART_COLORS[s] || '#555555',
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
segments.forEach((seg, i) => {
|
|
363
|
+
const segX = x + (i > 0 ? gap : 0);
|
|
364
|
+
const segW = seg.width - (i > 0 ? gap : 0) - (i < segments.length - 1 ? 0 : 0);
|
|
365
|
+
|
|
366
|
+
ctx.fillStyle = seg.color;
|
|
367
|
+
// First segment: rounded left. Last: rounded right.
|
|
368
|
+
if (segments.length === 1) {
|
|
369
|
+
roundRect(ctx, segX, barY, segW, barH, 4);
|
|
370
|
+
} else if (i === 0) {
|
|
371
|
+
roundRect(ctx, segX, barY, segW + 4, barH, 4);
|
|
372
|
+
ctx.fill();
|
|
373
|
+
ctx.fillRect(segX + segW - 2, barY, 6, barH);
|
|
374
|
+
} else if (i === segments.length - 1) {
|
|
375
|
+
roundRect(ctx, segX - 4, barY, segW + 4, barH, 4);
|
|
376
|
+
ctx.fill();
|
|
377
|
+
ctx.fillRect(segX - 4, barY, 6, barH);
|
|
378
|
+
} else {
|
|
379
|
+
ctx.fillRect(segX, barY, segW, barH);
|
|
380
|
+
}
|
|
381
|
+
ctx.fill();
|
|
382
|
+
|
|
383
|
+
// Percentage label inside bar if wide enough
|
|
384
|
+
if (segW > 40) {
|
|
385
|
+
ctx.fillStyle = 'rgba(255,255,255,0.95)';
|
|
386
|
+
ctx.font = 'bold 11px "JetBrains Mono", monospace';
|
|
387
|
+
ctx.textAlign = 'center';
|
|
388
|
+
ctx.textBaseline = 'middle';
|
|
389
|
+
ctx.fillText(`${Math.round(seg.pct)}%`, segX + segW / 2, barY + barH / 2);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
x = segX + segW;
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Legend below
|
|
396
|
+
const legend = document.getElementById('chart-task-status-legend')!;
|
|
397
|
+
legend.innerHTML = segments
|
|
398
|
+
.map(seg => `<span class="flex items-center gap-1.5"><span class="w-2.5 h-2.5 rounded-sm inline-block" style="background:${seg.color}"></span><span class="text-text-dim">${statusLabels[seg.status] || seg.status}</span> <span class="text-text font-medium">${seg.count}</span></span>`)
|
|
399
|
+
.join('');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- Agent Type: donut with center label ---
|
|
403
|
+
function drawAgentTypeChart() {
|
|
404
|
+
const canvas = document.getElementById('chart-agent-type') as HTMLCanvasElement;
|
|
405
|
+
if (!canvas) return;
|
|
406
|
+
const ctx = canvas.getContext('2d')!;
|
|
407
|
+
const dpr = window.devicePixelRatio || 1;
|
|
408
|
+
const size = 180;
|
|
409
|
+
canvas.width = size * dpr;
|
|
410
|
+
canvas.height = size * dpr;
|
|
411
|
+
ctx.scale(dpr, dpr);
|
|
412
|
+
ctx.clearRect(0, 0, size, size);
|
|
413
|
+
|
|
414
|
+
// flaunchToken present = launched via Flaunch (Token agent)
|
|
415
|
+
// no flaunchToken = ETH-only (direct wallet)
|
|
416
|
+
// BYO would be !flaunchToken && has some other token, but API currently has 0
|
|
417
|
+
let tokenAgents = 0;
|
|
418
|
+
let ethOnly = 0;
|
|
419
|
+
for (const a of allAgents) {
|
|
420
|
+
if (a.flaunchToken) tokenAgents++;
|
|
421
|
+
else ethOnly++;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const data = [
|
|
425
|
+
{ label: 'Token Launch', value: tokenAgents, color: '#dc2626', desc: 'Launched via Flaunch' },
|
|
426
|
+
{ label: 'ETH-only', value: ethOnly, color: '#444444', desc: 'Direct wallet' },
|
|
427
|
+
].filter(d => d.value > 0);
|
|
428
|
+
|
|
429
|
+
const total = data.reduce((s, d) => s + d.value, 0);
|
|
430
|
+
if (total === 0) return;
|
|
431
|
+
|
|
432
|
+
const cx = size / 2;
|
|
433
|
+
const cy = size / 2;
|
|
434
|
+
const outerR = 78;
|
|
435
|
+
const innerR = 52;
|
|
436
|
+
let startAngle = -Math.PI / 2;
|
|
437
|
+
|
|
438
|
+
for (const d of data) {
|
|
439
|
+
const sliceAngle = (d.value / total) * Math.PI * 2;
|
|
440
|
+
ctx.beginPath();
|
|
441
|
+
ctx.arc(cx, cy, outerR, startAngle, startAngle + sliceAngle);
|
|
442
|
+
ctx.arc(cx, cy, innerR, startAngle + sliceAngle, startAngle, true);
|
|
443
|
+
ctx.closePath();
|
|
444
|
+
ctx.fillStyle = d.color;
|
|
445
|
+
ctx.fill();
|
|
446
|
+
|
|
447
|
+
// Subtle gap between slices
|
|
448
|
+
if (data.length > 1) {
|
|
449
|
+
ctx.strokeStyle = '#0a0a0a';
|
|
450
|
+
ctx.lineWidth = 2;
|
|
451
|
+
ctx.stroke();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
startAngle += sliceAngle;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Center label
|
|
458
|
+
ctx.fillStyle = '#e5e5e5';
|
|
459
|
+
ctx.font = 'bold 22px "JetBrains Mono", monospace';
|
|
460
|
+
ctx.textAlign = 'center';
|
|
461
|
+
ctx.textBaseline = 'middle';
|
|
462
|
+
ctx.fillText(String(total), cx, cy - 6);
|
|
463
|
+
ctx.fillStyle = '#666666';
|
|
464
|
+
ctx.font = '10px "JetBrains Mono", monospace';
|
|
465
|
+
ctx.fillText('AGENTS', cx, cy + 12);
|
|
466
|
+
|
|
467
|
+
// Legend
|
|
468
|
+
const legend = document.getElementById('chart-agent-type-legend')!;
|
|
469
|
+
legend.innerHTML = data
|
|
470
|
+
.map(d => {
|
|
471
|
+
const pct = Math.round((d.value / total) * 100);
|
|
472
|
+
return `<div class="flex items-center gap-2.5">
|
|
473
|
+
<span class="w-3 h-3 rounded-sm shrink-0" style="background:${d.color}${d.color === '#444444' ? ';border:1px solid #555555' : ''}"></span>
|
|
474
|
+
<div>
|
|
475
|
+
<div class="text-text font-medium">${d.label} <span class="text-text-muted font-normal">${d.value}</span></div>
|
|
476
|
+
<div class="text-text-muted text-[10px]">${pct}% · ${d.desc}</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>`;
|
|
479
|
+
})
|
|
480
|
+
.join('');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// --- Top Earners: horizontal bars with gradient ---
|
|
484
|
+
function drawTopBurnedChart() {
|
|
485
|
+
const c = setupCanvas('chart-top-burned', 180);
|
|
486
|
+
if (!c) return;
|
|
487
|
+
const { ctx, W, H } = c;
|
|
488
|
+
|
|
489
|
+
const sorted = [...allAgents]
|
|
490
|
+
.filter(a => (a.totalEarningsETH || 0) > 0)
|
|
491
|
+
.sort((a, b) => (b.totalEarningsETH || 0) - (a.totalEarningsETH || 0))
|
|
492
|
+
.slice(0, 5);
|
|
493
|
+
|
|
494
|
+
if (sorted.length === 0) {
|
|
495
|
+
ctx.fillStyle = '#888888';
|
|
496
|
+
ctx.font = '12px "JetBrains Mono", monospace';
|
|
497
|
+
ctx.textAlign = 'center';
|
|
498
|
+
ctx.textBaseline = 'middle';
|
|
499
|
+
ctx.fillText('No earnings data yet', W / 2, H / 2);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const maxVal = sorted[0].totalEarningsETH || 0.001;
|
|
504
|
+
const barH = 26;
|
|
505
|
+
const gap = 8;
|
|
506
|
+
const labelW = 100;
|
|
507
|
+
const valueW = 90;
|
|
508
|
+
const barAreaW = W - labelW - valueW - 16;
|
|
509
|
+
const startY = 4;
|
|
510
|
+
|
|
511
|
+
sorted.forEach((agent, i) => {
|
|
512
|
+
const y = startY + i * (barH + gap);
|
|
513
|
+
const val = agent.totalEarningsETH || 0;
|
|
514
|
+
const ratio = val / maxVal;
|
|
515
|
+
const bw = Math.max(ratio * barAreaW, 3);
|
|
516
|
+
|
|
517
|
+
// Agent name
|
|
518
|
+
ctx.fillStyle = '#c0c0c0';
|
|
519
|
+
ctx.font = '11px "JetBrains Mono", monospace';
|
|
520
|
+
ctx.textAlign = 'right';
|
|
521
|
+
ctx.textBaseline = 'middle';
|
|
522
|
+
const name = agent.name.length > 12 ? agent.name.slice(0, 11) + '\u2026' : agent.name;
|
|
523
|
+
ctx.fillText(name, labelW - 10, y + barH / 2);
|
|
524
|
+
|
|
525
|
+
// Bar with gradient
|
|
526
|
+
const grad = ctx.createLinearGradient(labelW, 0, labelW + bw, 0);
|
|
527
|
+
grad.addColorStop(0, '#dc2626');
|
|
528
|
+
grad.addColorStop(1, '#ef4444');
|
|
529
|
+
ctx.fillStyle = grad;
|
|
530
|
+
roundRect(ctx, labelW, y + 2, bw, barH - 4, 3);
|
|
531
|
+
ctx.fill();
|
|
532
|
+
|
|
533
|
+
// Value
|
|
534
|
+
ctx.fillStyle = '#888888';
|
|
535
|
+
ctx.font = '11px "JetBrains Mono", monospace';
|
|
536
|
+
ctx.textAlign = 'left';
|
|
537
|
+
ctx.textBaseline = 'middle';
|
|
538
|
+
ctx.fillText(fmtEth(val), labelW + bw + 8, y + barH / 2);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// --- Mcap Distribution: vertical bars with better buckets ---
|
|
543
|
+
function drawMcapDistChart() {
|
|
544
|
+
const c = setupCanvas('chart-mcap-dist', 180);
|
|
545
|
+
if (!c) return;
|
|
546
|
+
const { ctx, W, H } = c;
|
|
547
|
+
|
|
548
|
+
// Split $0 from <$10K since most agents have no mcap
|
|
549
|
+
const buckets = [
|
|
550
|
+
{ label: 'No MCap', min: -1, max: 0.01, count: 0 },
|
|
551
|
+
{ label: '<$10K', min: 0.01, max: 10_000, count: 0 },
|
|
552
|
+
{ label: '$10K\u2013$100K', min: 10_000, max: 100_000, count: 0 },
|
|
553
|
+
{ label: '$100K\u2013$1M', min: 100_000, max: 1_000_000, count: 0 },
|
|
554
|
+
{ label: '>$1M', min: 1_000_000, max: Infinity, count: 0 },
|
|
555
|
+
];
|
|
556
|
+
|
|
557
|
+
for (const a of allAgents) {
|
|
558
|
+
const mcap = a.marketCapUSD || 0;
|
|
559
|
+
for (const b of buckets) {
|
|
560
|
+
if (mcap >= b.min && mcap < b.max) { b.count++; break; }
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Filter out empty buckets for cleaner display
|
|
565
|
+
const visibleBuckets = buckets.filter(b => b.count > 0);
|
|
566
|
+
if (visibleBuckets.length === 0) return;
|
|
567
|
+
|
|
568
|
+
const maxCount = Math.max(...visibleBuckets.map(b => b.count));
|
|
569
|
+
const bottomPad = 36;
|
|
570
|
+
const topPad = 24;
|
|
571
|
+
const chartH = H - bottomPad - topPad;
|
|
572
|
+
const totalBarArea = W - 40;
|
|
573
|
+
const barGap = 12;
|
|
574
|
+
const barW = Math.min((totalBarArea - barGap * (visibleBuckets.length - 1)) / visibleBuckets.length, 70);
|
|
575
|
+
const totalWidth = visibleBuckets.length * barW + (visibleBuckets.length - 1) * barGap;
|
|
576
|
+
const offsetX = (W - totalWidth) / 2;
|
|
577
|
+
|
|
578
|
+
// Grid lines
|
|
579
|
+
ctx.strokeStyle = '#222222';
|
|
580
|
+
ctx.lineWidth = 1;
|
|
581
|
+
for (let i = 1; i <= 4; i++) {
|
|
582
|
+
const gy = topPad + chartH - (i / 4) * chartH;
|
|
583
|
+
ctx.beginPath();
|
|
584
|
+
ctx.moveTo(offsetX, gy);
|
|
585
|
+
ctx.lineTo(offsetX + totalWidth, gy);
|
|
586
|
+
ctx.stroke();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
visibleBuckets.forEach((b, i) => {
|
|
590
|
+
const x = offsetX + i * (barW + barGap);
|
|
591
|
+
const bh = maxCount > 0 ? (b.count / maxCount) * chartH : 0;
|
|
592
|
+
const y = topPad + chartH - bh;
|
|
593
|
+
|
|
594
|
+
// Bar
|
|
595
|
+
const grad = ctx.createLinearGradient(0, y, 0, topPad + chartH);
|
|
596
|
+
grad.addColorStop(0, '#dc2626');
|
|
597
|
+
grad.addColorStop(1, '#dc2626aa');
|
|
598
|
+
ctx.fillStyle = grad;
|
|
599
|
+
roundRect(ctx, x, y, barW, Math.max(bh, 3), 3);
|
|
600
|
+
ctx.fill();
|
|
601
|
+
|
|
602
|
+
// Count above bar
|
|
603
|
+
ctx.fillStyle = '#e5e5e5';
|
|
604
|
+
ctx.font = 'bold 12px "JetBrains Mono", monospace';
|
|
605
|
+
ctx.textAlign = 'center';
|
|
606
|
+
ctx.textBaseline = 'bottom';
|
|
607
|
+
ctx.fillText(String(b.count), x + barW / 2, y - 4);
|
|
608
|
+
|
|
609
|
+
// Bucket label below
|
|
610
|
+
ctx.fillStyle = '#888888';
|
|
611
|
+
ctx.font = '10px "JetBrains Mono", monospace';
|
|
612
|
+
ctx.textBaseline = 'top';
|
|
613
|
+
ctx.fillText(b.label, x + barW / 2, H - bottomPad + 8);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// --- Activity feed ---
|
|
618
|
+
function filterByStatus(filter: string): RecentTask[] {
|
|
619
|
+
if (filter === 'all') return allTasks;
|
|
620
|
+
if (filter === 'active') return allTasks.filter((t) => ['requested', 'accepted', 'submitted', 'revision'].includes(t.status));
|
|
621
|
+
if (filter === 'completed') return allTasks.filter((t) => t.status === 'completed');
|
|
622
|
+
if (filter === 'quoted') return allTasks.filter((t) => t.status === 'quoted');
|
|
623
|
+
return allTasks;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function getVisibleTasks(): RecentTask[] {
|
|
627
|
+
let tasks = filterByStatus(activeFilter);
|
|
628
|
+
|
|
629
|
+
if (searchQuery) {
|
|
630
|
+
const q = searchQuery.toLowerCase();
|
|
631
|
+
tasks = tasks.filter((t) => {
|
|
632
|
+
const agentName = (agentMap[t.agentId] || t.agentId).toLowerCase();
|
|
633
|
+
return t.task.toLowerCase().includes(q) || agentName.includes(q) || t.status.includes(q);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const sortBy = sortSelect?.value || 'newest';
|
|
638
|
+
tasks = [...tasks].sort((a, b) => {
|
|
639
|
+
if (sortBy === 'oldest') return a.createdAt - b.createdAt;
|
|
640
|
+
if (sortBy === 'price-high') return (Number(b.quotedPriceWei || 0)) - (Number(a.quotedPriceWei || 0));
|
|
641
|
+
if (sortBy === 'price-low') return (Number(a.quotedPriceWei || 0)) - (Number(b.quotedPriceWei || 0));
|
|
642
|
+
return b.createdAt - a.createdAt;
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
return tasks;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function updateCounts() {
|
|
649
|
+
const counts: Record<string, number> = {
|
|
650
|
+
all: allTasks.length,
|
|
651
|
+
active: allTasks.filter((t) => ['requested', 'accepted', 'submitted', 'revision'].includes(t.status)).length,
|
|
652
|
+
completed: allTasks.filter((t) => t.status === 'completed').length,
|
|
653
|
+
quoted: allTasks.filter((t) => t.status === 'quoted').length,
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
document.querySelectorAll<HTMLButtonElement>('.filter-btn').forEach((btn) => {
|
|
657
|
+
const filter = btn.dataset.filter || 'all';
|
|
658
|
+
const countEl = btn.querySelector('.filter-count');
|
|
659
|
+
if (countEl) countEl.textContent = counts[filter] !== undefined ? `${counts[filter]}` : '';
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const statTotal = document.getElementById('activity-stat-total');
|
|
663
|
+
const statActive = document.getElementById('activity-stat-active');
|
|
664
|
+
const statCompleted = document.getElementById('activity-stat-completed');
|
|
665
|
+
if (statTotal) statTotal.textContent = `${allTasks.length} total`;
|
|
666
|
+
if (statActive) statActive.textContent = `${counts.active} active`;
|
|
667
|
+
if (statCompleted) statCompleted.textContent = `${counts.completed} completed`;
|
|
668
|
+
document.getElementById('activity-stats-bar')?.classList.remove('hidden');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function updatePagination(totalVisible: number) {
|
|
672
|
+
const totalPages = Math.max(1, Math.ceil(totalVisible / PAGE_SIZE));
|
|
673
|
+
if (currentPage > totalPages) currentPage = totalPages;
|
|
674
|
+
|
|
675
|
+
if (totalVisible <= PAGE_SIZE) {
|
|
676
|
+
paginationEl?.classList.add('hidden');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
paginationEl?.classList.remove('hidden');
|
|
681
|
+
const start = (currentPage - 1) * PAGE_SIZE + 1;
|
|
682
|
+
const end = Math.min(currentPage * PAGE_SIZE, totalVisible);
|
|
683
|
+
if (pageInfoEl) pageInfoEl.textContent = `${start}–${end} of ${totalVisible}`;
|
|
684
|
+
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
|
685
|
+
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function renderTasks(tasks: RecentTask[]) {
|
|
689
|
+
if (!taskList) return;
|
|
690
|
+
taskList.innerHTML = '';
|
|
691
|
+
|
|
692
|
+
updatePagination(tasks.length);
|
|
693
|
+
|
|
694
|
+
if (tasks.length === 0) {
|
|
695
|
+
taskList.classList.add('hidden');
|
|
696
|
+
tasksEmpty?.classList.remove('hidden');
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
tasksEmpty?.classList.add('hidden');
|
|
701
|
+
taskList.classList.remove('hidden');
|
|
702
|
+
|
|
703
|
+
const pageStart = (currentPage - 1) * PAGE_SIZE;
|
|
704
|
+
const pageTasks = tasks.slice(pageStart, pageStart + PAGE_SIZE);
|
|
705
|
+
|
|
706
|
+
for (const task of pageTasks) {
|
|
707
|
+
const label = statusLabels[task.status] || task.status;
|
|
708
|
+
const textColor = statusTextColors[task.status] || 'text-text-muted';
|
|
709
|
+
const bgColor = statusBgColors[task.status] || 'bg-surface-2';
|
|
710
|
+
const timeStr = relativeTime(task.createdAt);
|
|
711
|
+
const ethVal = task.quotedPriceWei ? Number(task.quotedPriceWei) / 1e18 : null;
|
|
712
|
+
const priceEth = ethVal ? ethVal.toFixed(4) : null;
|
|
713
|
+
const priceUsd = ethVal && ethUsdPrice ? (ethVal * ethUsdPrice) : null;
|
|
714
|
+
const agentName = agentMap[task.agentId] || (task.agentId.length > 10 ? task.agentId.slice(0, 8) + '...' : task.agentId);
|
|
715
|
+
|
|
716
|
+
const entry = document.createElement('a');
|
|
717
|
+
entry.href = `/task/${task.id}`;
|
|
718
|
+
entry.className = `block border border-border bg-bg mb-3 hover:border-primary hover:bg-surface transition-all`;
|
|
719
|
+
|
|
720
|
+
entry.innerHTML = `
|
|
721
|
+
<div class="flex items-center justify-between py-3 px-4 text-sm gap-4">
|
|
722
|
+
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
723
|
+
<span class="truncate text-text font-medium">${esc(truncate(task.task, 80))}</span>
|
|
724
|
+
</div>
|
|
725
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
726
|
+
<a href="/agent/${esc(task.agentId)}" class="text-text-dim text-[11px] hidden md:inline font-mono hover:text-primary transition-colors" onclick="event.stopPropagation()">${esc(agentName)}</a>
|
|
727
|
+
<span class="font-mono text-[11px] tracking-wider px-2 py-0.5 border border-border ${textColor} ${bgColor}">${esc(label).toUpperCase()}</span>
|
|
728
|
+
${priceEth ? `<span class="text-[11px] text-text-dim font-mono hidden sm:inline">${priceEth} ETH${priceUsd ? ` ($${priceUsd < 0.01 ? priceUsd.toFixed(4) : priceUsd.toFixed(2)})` : ''}</span>` : ''}
|
|
729
|
+
<span class="text-[11px] text-text-muted w-14 text-right">${timeStr}</span>
|
|
730
|
+
<svg class="w-3.5 h-3.5 text-text-muted shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
`;
|
|
734
|
+
|
|
735
|
+
taskList.appendChild(entry);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function refresh() {
|
|
740
|
+
renderTasks(getVisibleTasks());
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// --- Load data ---
|
|
744
|
+
async function loadAll() {
|
|
745
|
+
tasksLoading?.classList.remove('hidden');
|
|
746
|
+
taskList?.classList.add('hidden');
|
|
747
|
+
tasksEmpty?.classList.add('hidden');
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
const prefetched = (window as any).__agentsData;
|
|
751
|
+
const [agentsPage1, tasksRes, priceRes] = await Promise.all([
|
|
752
|
+
prefetched || fetch(`${TASK_API}/api/agents`).then(r => r.json()),
|
|
753
|
+
fetch(`${TASK_API}/api/tasks/recent?limit=50`),
|
|
754
|
+
fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd').catch(() => null),
|
|
755
|
+
]);
|
|
756
|
+
|
|
757
|
+
if (priceRes) {
|
|
758
|
+
try {
|
|
759
|
+
const priceData = await priceRes.json();
|
|
760
|
+
ethUsdPrice = priceData?.ethereum?.usd ?? null;
|
|
761
|
+
} catch {}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Fetch all agent pages for accurate totals
|
|
765
|
+
let agents = agentsPage1?.agents || [];
|
|
766
|
+
const totalPages = agentsPage1?.pages || 1;
|
|
767
|
+
if (totalPages > 1) {
|
|
768
|
+
const extraPages = await Promise.all(
|
|
769
|
+
Array.from({ length: totalPages - 1 }, (_, i) =>
|
|
770
|
+
fetch(`${TASK_API}/api/agents?page=${i + 2}`).then(r => r.json()).catch(() => ({ agents: [] }))
|
|
771
|
+
)
|
|
772
|
+
);
|
|
773
|
+
for (const page of extraPages) {
|
|
774
|
+
agents = agents.concat(page.agents || []);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
allAgents = agents;
|
|
779
|
+
for (const a of allAgents) {
|
|
780
|
+
agentMap[a.id] = a.name || a.id;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const tasksData = await tasksRes.json();
|
|
784
|
+
allTasks = tasksData?.tasks || [];
|
|
785
|
+
|
|
786
|
+
updateStatsCards();
|
|
787
|
+
drawTaskStatusChart();
|
|
788
|
+
drawAgentTypeChart();
|
|
789
|
+
drawTopBurnedChart();
|
|
790
|
+
drawMcapDistChart();
|
|
791
|
+
|
|
792
|
+
tasksLoading?.classList.add('hidden');
|
|
793
|
+
updateCounts();
|
|
794
|
+
refresh();
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.error('[loadAll]', err);
|
|
797
|
+
tasksLoading?.classList.add('hidden');
|
|
798
|
+
tasksEmpty?.classList.remove('hidden');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Search
|
|
803
|
+
let searchTimeout: ReturnType<typeof setTimeout>;
|
|
804
|
+
searchInput?.addEventListener('input', () => {
|
|
805
|
+
clearTimeout(searchTimeout);
|
|
806
|
+
searchTimeout = setTimeout(() => {
|
|
807
|
+
searchQuery = searchInput.value.trim();
|
|
808
|
+
currentPage = 1;
|
|
809
|
+
refresh();
|
|
810
|
+
}, 150);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
sortSelect?.addEventListener('change', () => { currentPage = 1; refresh(); });
|
|
814
|
+
|
|
815
|
+
prevBtn?.addEventListener('click', () => { if (currentPage > 1) { currentPage--; refresh(); } });
|
|
816
|
+
nextBtn?.addEventListener('click', () => { currentPage++; refresh(); });
|
|
817
|
+
|
|
818
|
+
document.querySelectorAll<HTMLButtonElement>('.filter-btn').forEach((btn) => {
|
|
819
|
+
btn.addEventListener('click', () => {
|
|
820
|
+
activeFilter = btn.dataset.filter || 'all';
|
|
821
|
+
currentPage = 1;
|
|
822
|
+
document.querySelectorAll('.filter-btn').forEach((b) => {
|
|
823
|
+
b.classList.remove('bg-surface-2', 'text-text');
|
|
824
|
+
b.classList.add('text-text-muted');
|
|
825
|
+
});
|
|
826
|
+
btn.classList.add('bg-surface-2', 'text-text');
|
|
827
|
+
btn.classList.remove('text-text-muted');
|
|
828
|
+
refresh();
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
let resizeTimer: ReturnType<typeof setTimeout>;
|
|
833
|
+
window.addEventListener('resize', () => {
|
|
834
|
+
clearTimeout(resizeTimer);
|
|
835
|
+
resizeTimer = setTimeout(() => {
|
|
836
|
+
drawTaskStatusChart();
|
|
837
|
+
drawTopBurnedChart();
|
|
838
|
+
drawMcapDistChart();
|
|
839
|
+
}, 200);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
loadAll();
|
|
843
|
+
setInterval(loadAll, 30000);
|
|
844
|
+
</script>
|
|
845
|
+
</Layout>
|