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,359 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
const API = 'https://api.moltlaunch.com';
|
|
4
|
+
const MAX_MESSAGES = 20;
|
|
5
|
+
|
|
6
|
+
interface AgentCard {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
skills: string[];
|
|
11
|
+
reputation: number;
|
|
12
|
+
marketCapUSD: number;
|
|
13
|
+
image?: string;
|
|
14
|
+
symbol?: string;
|
|
15
|
+
completedTasks: number;
|
|
16
|
+
totalEarningsUSD: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ChatMsg {
|
|
20
|
+
id: string;
|
|
21
|
+
role: 'user' | 'assistant';
|
|
22
|
+
content: string;
|
|
23
|
+
agents?: AgentCard[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SUGGESTIONS = [
|
|
27
|
+
'Find me a smart contract auditor',
|
|
28
|
+
'Who has the best reputation?',
|
|
29
|
+
'Show me agents that build frontends',
|
|
30
|
+
'What gigs are available right now?',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ─── Markdown ────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function renderMarkdown(text: string): React.ReactNode[] {
|
|
36
|
+
// Strip image tags — agent images shown in cards
|
|
37
|
+
const cleaned = text.replace(/!\[.*?\]\(.*?\)/g, '').replace(/\n{3,}/g, '\n\n');
|
|
38
|
+
return cleaned.split('\n').map((line, li) => (
|
|
39
|
+
<span key={li}>
|
|
40
|
+
{li > 0 && <br />}
|
|
41
|
+
{parseLine(line)}
|
|
42
|
+
</span>
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseLine(line: string): React.ReactNode[] {
|
|
47
|
+
const parts: React.ReactNode[] = [];
|
|
48
|
+
const re = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`|\[(.+?)\]\((.+?)\))/g;
|
|
49
|
+
let last = 0;
|
|
50
|
+
let match: RegExpExecArray | null;
|
|
51
|
+
while ((match = re.exec(line)) !== null) {
|
|
52
|
+
if (match.index > last) parts.push(line.slice(last, match.index));
|
|
53
|
+
if (match[2]) parts.push(<strong key={match.index} className="text-text font-semibold">{match[2]}</strong>);
|
|
54
|
+
else if (match[3]) parts.push(<em key={match.index}>{match[3]}</em>);
|
|
55
|
+
else if (match[4]) parts.push(<code key={match.index} className="text-[11px] px-1 py-0.5 bg-surface-2 border border-border/40 font-mono">{match[4]}</code>);
|
|
56
|
+
else if (match[5] && match[6]) parts.push(<a key={match.index} href={match[6]} target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary-hover underline underline-offset-2">{match[5]}</a>);
|
|
57
|
+
last = match.index + match[0].length;
|
|
58
|
+
}
|
|
59
|
+
if (last < line.length) parts.push(line.slice(last));
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function formatUSD(n: number): string {
|
|
66
|
+
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
|
|
67
|
+
if (n >= 1_000) return `$${(n / 1_000).toFixed(1)}K`;
|
|
68
|
+
return `$${n.toFixed(0)}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatNum(n: number): string {
|
|
72
|
+
return n.toLocaleString();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Agent Card ──────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function AgentResultCard({ agent }: { agent: AgentCard }) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="border border-border/60 bg-surface/20 hover:border-border-hover transition-all group">
|
|
80
|
+
<div className="flex items-start gap-3 px-4 py-3">
|
|
81
|
+
{agent.image ? (
|
|
82
|
+
<img src={agent.image} alt="" className="w-10 h-10 rounded-full object-cover shrink-0 bg-surface-2 mt-0.5" />
|
|
83
|
+
) : (
|
|
84
|
+
<div className="w-10 h-10 rounded-full bg-surface-2 shrink-0 flex items-center justify-center text-text-muted text-sm font-bold font-mono mt-0.5">
|
|
85
|
+
{agent.name.charAt(0).toUpperCase()}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
<div className="flex-1 min-w-0">
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<span className="text-[13px] font-bold text-text truncate">{agent.name}</span>
|
|
91
|
+
{agent.symbol && <span className="text-[10px] font-mono text-text-muted">${agent.symbol}</span>}
|
|
92
|
+
</div>
|
|
93
|
+
<p className="text-[11px] text-text-muted mt-0.5 line-clamp-2 leading-relaxed">{agent.description}</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Stats */}
|
|
98
|
+
<div className="flex border-t border-border/40 text-[10px] font-mono tracking-wider text-text-muted">
|
|
99
|
+
<div className="flex-1 px-3 py-1.5 text-center">REP <span className="text-text font-bold">{agent.reputation}</span></div>
|
|
100
|
+
<div className="flex-1 px-3 py-1.5 text-center border-l border-border/40">MCAP <span className="text-text font-bold">{formatUSD(agent.marketCapUSD)}</span></div>
|
|
101
|
+
<div className="flex-1 px-3 py-1.5 text-center border-l border-border/40">TASKS <span className="text-text font-bold">{formatNum(agent.completedTasks)}</span></div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Skills + actions */}
|
|
105
|
+
<div className="flex items-center justify-between px-4 py-2 border-t border-border/40">
|
|
106
|
+
<div className="flex items-center gap-1 flex-1 min-w-0 overflow-hidden">
|
|
107
|
+
{agent.skills.slice(0, 3).map(s => (
|
|
108
|
+
<span key={s} className="text-[9px] font-mono text-text-muted uppercase tracking-wider px-1.5 py-0.5 bg-surface-2/60 border border-border/30 shrink-0">{s}</span>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
<div className="flex items-center gap-2 shrink-0 ml-3">
|
|
112
|
+
<a href={`/agent/${agent.id}`} className="text-[10px] font-mono text-text-muted hover:text-text tracking-wider transition-colors">VIEW</a>
|
|
113
|
+
<a href={`/agent/${agent.id}#hire`} className="text-[10px] font-mono font-bold text-primary hover:text-primary-hover tracking-wider transition-colors">HIRE →</a>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Chat Component ──────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export default function StarlightChat() {
|
|
123
|
+
const [messages, setMessages] = useState<ChatMsg[]>([]);
|
|
124
|
+
const [input, setInput] = useState('');
|
|
125
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
126
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
127
|
+
const messagesRef = useRef<ChatMsg[]>([]);
|
|
128
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
129
|
+
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
130
|
+
|
|
131
|
+
messagesRef.current = messages;
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
inputRef.current?.focus();
|
|
135
|
+
return () => { abortRef.current?.abort(); };
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const scrollToBottom = useCallback(() => {
|
|
139
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
scrollToBottom();
|
|
144
|
+
}, [messages, scrollToBottom]);
|
|
145
|
+
|
|
146
|
+
const sendMessage = useCallback(async (text: string) => {
|
|
147
|
+
if (!text.trim() || isStreaming) return;
|
|
148
|
+
|
|
149
|
+
const userMsg: ChatMsg = { id: crypto.randomUUID(), role: 'user', content: text.trim() };
|
|
150
|
+
const currentMessages = messagesRef.current;
|
|
151
|
+
const newMessages = [...currentMessages, userMsg];
|
|
152
|
+
|
|
153
|
+
if (newMessages.length >= MAX_MESSAGES) {
|
|
154
|
+
setMessages(prev => [...prev, userMsg, {
|
|
155
|
+
id: crypto.randomUUID(), role: 'assistant',
|
|
156
|
+
content: 'Conversation limit reached. Start a new chat to continue.',
|
|
157
|
+
}]);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const assistantMsg: ChatMsg = { id: crypto.randomUUID(), role: 'assistant', content: '' };
|
|
162
|
+
setMessages([...newMessages, assistantMsg]);
|
|
163
|
+
setInput('');
|
|
164
|
+
setIsStreaming(true);
|
|
165
|
+
|
|
166
|
+
abortRef.current?.abort();
|
|
167
|
+
const controller = new AbortController();
|
|
168
|
+
abortRef.current = controller;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(`${API}/api/starlight/chat`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
body: JSON.stringify({ messages: newMessages.map(m => ({ role: m.role, content: m.content })) }),
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
const err = await res.json().catch(() => ({ error: 'Request failed' })) as { error?: string };
|
|
180
|
+
setMessages(prev => {
|
|
181
|
+
const updated = [...prev];
|
|
182
|
+
updated[updated.length - 1] = { ...updated[updated.length - 1], content: err.error ?? 'Something went wrong.' };
|
|
183
|
+
return updated;
|
|
184
|
+
});
|
|
185
|
+
setIsStreaming(false);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const reader = res.body?.getReader();
|
|
190
|
+
if (!reader) throw new Error('No reader');
|
|
191
|
+
|
|
192
|
+
const decoder = new TextDecoder();
|
|
193
|
+
let buffer = '';
|
|
194
|
+
let fullContent = '';
|
|
195
|
+
let agentCards: AgentCard[] | undefined;
|
|
196
|
+
|
|
197
|
+
while (true) {
|
|
198
|
+
const { done, value } = await reader.read();
|
|
199
|
+
if (done) break;
|
|
200
|
+
buffer += decoder.decode(value, { stream: true });
|
|
201
|
+
const lines = buffer.split('\n');
|
|
202
|
+
buffer = lines.pop() ?? '';
|
|
203
|
+
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
if (!line.startsWith('data: ')) continue;
|
|
206
|
+
const raw = line.slice(6);
|
|
207
|
+
if (!raw) continue;
|
|
208
|
+
try {
|
|
209
|
+
const event = JSON.parse(raw) as { type: string; content?: string; agents?: AgentCard[] };
|
|
210
|
+
if (event.type === 'token' && event.content) {
|
|
211
|
+
fullContent += event.content;
|
|
212
|
+
setMessages(prev => { const u = [...prev]; u[u.length - 1] = { ...u[u.length - 1], content: fullContent, agents: agentCards }; return u; });
|
|
213
|
+
} else if (event.type === 'agents' && event.agents) {
|
|
214
|
+
agentCards = event.agents;
|
|
215
|
+
setMessages(prev => { const u = [...prev]; u[u.length - 1] = { ...u[u.length - 1], content: fullContent, agents: agentCards }; return u; });
|
|
216
|
+
}
|
|
217
|
+
} catch { /* malformed */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err instanceof Error && err.name === 'AbortError') return;
|
|
222
|
+
setMessages(prev => { const u = [...prev]; u[u.length - 1] = { ...u[u.length - 1], content: 'Connection error. Try again.' }; return u; });
|
|
223
|
+
} finally {
|
|
224
|
+
setIsStreaming(false);
|
|
225
|
+
inputRef.current?.focus();
|
|
226
|
+
}
|
|
227
|
+
}, [isStreaming]);
|
|
228
|
+
|
|
229
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
230
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(input); }
|
|
231
|
+
}, [sendMessage, input]);
|
|
232
|
+
|
|
233
|
+
const isEmpty = messages.length === 0;
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div className="fixed inset-0 top-14 flex flex-col bg-bg">
|
|
237
|
+
|
|
238
|
+
{/* Scrollable content */}
|
|
239
|
+
<div className="flex-1 overflow-y-auto">
|
|
240
|
+
<div className="max-w-2xl mx-auto px-5">
|
|
241
|
+
|
|
242
|
+
{/* Empty state — centered vertically */}
|
|
243
|
+
{isEmpty && (
|
|
244
|
+
<div className="flex flex-col items-center justify-center" style={{ minHeight: 'calc(100vh - 3.5rem - 80px)' }}>
|
|
245
|
+
<div className="text-center">
|
|
246
|
+
<div className="flex items-center justify-center gap-2 mb-2">
|
|
247
|
+
<span className="w-2 h-2 bg-primary rounded-full" />
|
|
248
|
+
<span className="font-mono text-[11px] text-text-muted tracking-[0.2em] uppercase">Starlight</span>
|
|
249
|
+
</div>
|
|
250
|
+
<h1 className="text-[clamp(1.8rem,5vw,2.8rem)] font-extrabold text-text tracking-tight leading-tight mb-3">
|
|
251
|
+
Find your next agent.
|
|
252
|
+
</h1>
|
|
253
|
+
<p className="text-text-muted text-sm max-w-md mx-auto mb-10">
|
|
254
|
+
Describe what you need — Starlight searches agents, gigs, and stats to find a match.
|
|
255
|
+
</p>
|
|
256
|
+
<div className="flex flex-wrap justify-center gap-2">
|
|
257
|
+
{SUGGESTIONS.map(s => (
|
|
258
|
+
<button
|
|
259
|
+
key={s}
|
|
260
|
+
onClick={() => sendMessage(s)}
|
|
261
|
+
className="px-3.5 py-2 border border-border text-text-muted text-[11px] font-mono tracking-wider hover:border-border-hover hover:text-text hover:bg-surface/30 transition-all"
|
|
262
|
+
>
|
|
263
|
+
{s}
|
|
264
|
+
</button>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
{/* Messages */}
|
|
272
|
+
{!isEmpty && (
|
|
273
|
+
<div className="pt-8 pb-4 space-y-6">
|
|
274
|
+
{messages.map(msg => (
|
|
275
|
+
<div key={msg.id}>
|
|
276
|
+
{/* Role */}
|
|
277
|
+
<div className="flex items-center gap-1.5 mb-1.5">
|
|
278
|
+
{msg.role === 'user' ? (
|
|
279
|
+
<span className="font-mono text-[10px] text-text-muted tracking-[0.15em] uppercase">You</span>
|
|
280
|
+
) : (
|
|
281
|
+
<>
|
|
282
|
+
<span className="w-1.5 h-1.5 bg-primary rounded-full" />
|
|
283
|
+
<span className="font-mono text-[10px] text-primary tracking-[0.15em] uppercase">Starlight</span>
|
|
284
|
+
</>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Content */}
|
|
289
|
+
{msg.role === 'assistant' && !msg.content ? (
|
|
290
|
+
<div className="flex items-center gap-2 py-1">
|
|
291
|
+
<div className="w-3.5 h-3.5 border border-primary border-t-transparent rounded-full animate-spin" />
|
|
292
|
+
<span className="text-text-muted font-mono text-[11px]">Searching agents...</span>
|
|
293
|
+
</div>
|
|
294
|
+
) : (
|
|
295
|
+
<div className={`text-[13px] leading-relaxed ${msg.role === 'user' ? 'text-text' : 'text-text-dim'}`}>
|
|
296
|
+
{msg.role === 'assistant' ? renderMarkdown(msg.content) : msg.content}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
|
|
300
|
+
{/* Agent cards */}
|
|
301
|
+
{msg.agents && msg.agents.length > 0 && (
|
|
302
|
+
<div className="mt-3 grid grid-cols-1 gap-2">
|
|
303
|
+
{msg.agents.map(a => (
|
|
304
|
+
<AgentResultCard key={a.id} agent={a} />
|
|
305
|
+
))}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
))}
|
|
310
|
+
<div ref={messagesEndRef} />
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
{/* Input — pinned to bottom */}
|
|
317
|
+
<div className="shrink-0 border-t border-border/50 bg-bg">
|
|
318
|
+
<div className="max-w-2xl mx-auto px-5 py-4">
|
|
319
|
+
<div className="flex items-center gap-3">
|
|
320
|
+
<div className="relative flex-1">
|
|
321
|
+
<textarea
|
|
322
|
+
ref={inputRef}
|
|
323
|
+
value={input}
|
|
324
|
+
onChange={e => setInput(e.target.value)}
|
|
325
|
+
onKeyDown={handleKeyDown}
|
|
326
|
+
placeholder={isEmpty ? 'What kind of agent are you looking for?' : 'Ask a follow-up...'}
|
|
327
|
+
rows={1}
|
|
328
|
+
disabled={isStreaming}
|
|
329
|
+
className="w-full bg-surface/40 border border-border px-4 py-2.5 text-[13px] text-text resize-none focus:border-primary focus:outline-none transition-all placeholder:text-text-muted/40 font-mono disabled:opacity-40"
|
|
330
|
+
/>
|
|
331
|
+
</div>
|
|
332
|
+
<button
|
|
333
|
+
onClick={() => sendMessage(input)}
|
|
334
|
+
disabled={isStreaming || !input.trim()}
|
|
335
|
+
className="px-5 py-2.5 bg-primary text-white font-mono text-[11px] font-bold tracking-wider hover:bg-primary-hover transition-all disabled:opacity-20 shrink-0"
|
|
336
|
+
>
|
|
337
|
+
{isStreaming ? (
|
|
338
|
+
<span className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin inline-block" />
|
|
339
|
+
) : '→'}
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
{messages.length > 0 && (
|
|
343
|
+
<div className="flex items-center justify-between mt-2">
|
|
344
|
+
<span className="font-mono text-[10px] text-text-muted/40 tracking-wider">
|
|
345
|
+
{messages.filter(m => m.role === 'user').length}/{MAX_MESSAGES / 2} queries
|
|
346
|
+
</span>
|
|
347
|
+
<button
|
|
348
|
+
onClick={() => { setMessages([]); setInput(''); inputRef.current?.focus(); }}
|
|
349
|
+
className="font-mono text-[10px] text-text-muted/40 hover:text-text-muted tracking-wider transition-colors"
|
|
350
|
+
>
|
|
351
|
+
clear chat
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
status: string;
|
|
4
|
+
pulse?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { status } = Astro.props;
|
|
8
|
+
|
|
9
|
+
const statusConfig: Record<string, { label: string; color: string; dot: string; bg: string }> = {
|
|
10
|
+
requested: { label: 'Pending', color: 'text-yellow', dot: 'bg-yellow', bg: 'bg-yellow/[0.06]' },
|
|
11
|
+
quoted: { label: 'Quoted', color: 'text-blue', dot: 'bg-blue', bg: 'bg-blue/[0.06]' },
|
|
12
|
+
accepted: { label: 'In Progress', color: 'text-blue', dot: 'bg-blue', bg: 'bg-blue/[0.06]' },
|
|
13
|
+
submitted: { label: 'Submitted', color: 'text-primary', dot: 'bg-primary', bg: 'bg-primary/[0.06]' },
|
|
14
|
+
revision: { label: 'Revision', color: 'text-accent', dot: 'bg-accent', bg: 'bg-accent/[0.06]' },
|
|
15
|
+
completed: { label: 'Completed', color: 'text-green', dot: 'bg-green', bg: 'bg-green/[0.06]' },
|
|
16
|
+
declined: { label: 'Declined', color: 'text-text-muted', dot: 'bg-text-muted', bg: 'bg-surface-2' },
|
|
17
|
+
expired: { label: 'Expired', color: 'text-text-muted', dot: 'bg-text-muted', bg: 'bg-surface-2' },
|
|
18
|
+
disputed: { label: 'Disputed', color: 'text-yellow', dot: 'bg-yellow', bg: 'bg-yellow/[0.06]' },
|
|
19
|
+
resolved: { label: 'Resolved', color: 'text-primary', dot: 'bg-primary', bg: 'bg-primary/[0.06]' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const config = statusConfig[status] || { label: status, color: 'text-text-muted', dot: 'bg-text-muted', bg: 'bg-surface-2' };
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<span class:list={['inline-flex items-center gap-1.5 text-[11px] font-medium font-mono uppercase tracking-wider px-2 py-0.5 border border-transparent', config.color, config.bg]}>
|
|
26
|
+
<span class:list={['w-1.5 h-1.5 rounded-full', config.dot]}></span>
|
|
27
|
+
{config.label}
|
|
28
|
+
</span>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
import StatusBadge from './StatusBadge.astro';
|
|
3
|
+
import type { Task } from '../lib/api';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
task: Task;
|
|
7
|
+
showAgent?: boolean;
|
|
8
|
+
agentName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { task, showAgent = false, agentName } = Astro.props;
|
|
12
|
+
|
|
13
|
+
const date = new Date(task.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
14
|
+
const shortAddr = `${task.clientAddress.slice(0, 6)}...${task.clientAddress.slice(-4)}`;
|
|
15
|
+
const priceEth = task.quotedPriceWei ? (Number(task.quotedPriceWei) / 1e18).toFixed(4) : null;
|
|
16
|
+
const hasExpandedContent = !!(task.quotedMessage || task.result || task.txHash);
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div class="bg-surface border border-border/50 shadow-card mb-3 transition-all duration-200 hover:border-border-hover hover:shadow-card-hover hover:-translate-y-px" data-task-entry>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class:list={[
|
|
23
|
+
'w-full flex items-center justify-between py-3.5 px-5 text-sm gap-4 text-left task-toggle transition-colors',
|
|
24
|
+
hasExpandedContent ? 'cursor-pointer hover:bg-surface-2/50' : 'cursor-default',
|
|
25
|
+
]}
|
|
26
|
+
data-expanded="false"
|
|
27
|
+
>
|
|
28
|
+
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
29
|
+
{hasExpandedContent && (
|
|
30
|
+
<svg class="w-3.5 h-3.5 text-text-muted shrink-0 toggle-arrow transition-transform duration-200" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
31
|
+
<path d="M9 18l6-6-6-6"/>
|
|
32
|
+
</svg>
|
|
33
|
+
)}
|
|
34
|
+
<span class="truncate text-text font-medium">{task.task}</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="flex items-center gap-3 shrink-0">
|
|
37
|
+
{showAgent && agentName && (
|
|
38
|
+
<span class="text-text-dim text-xs hidden md:inline">{agentName}</span>
|
|
39
|
+
)}
|
|
40
|
+
<StatusBadge status={task.status} />
|
|
41
|
+
{priceEth && <span class="text-xs text-text-dim font-mono">{priceEth} ETH</span>}
|
|
42
|
+
<span class="text-xs text-text-muted w-14 text-right">{date}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
{hasExpandedContent && (
|
|
47
|
+
<div class="task-detail hidden border-t border-border/30 mx-5 py-4 space-y-3 text-sm">
|
|
48
|
+
<div class="text-text-muted text-xs">
|
|
49
|
+
Requested · {date} · <span class="font-mono">{shortAddr}</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{task.quotedMessage && (
|
|
53
|
+
<div>
|
|
54
|
+
<div class="text-text-muted text-xs mb-1">
|
|
55
|
+
Quoted{task.quotedAt ? ` · ${new Date(task.quotedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : ''}
|
|
56
|
+
</div>
|
|
57
|
+
<div class="text-text-dim text-sm quoted-text" data-full={task.quotedMessage}>
|
|
58
|
+
{task.quotedMessage.length > 300 ? task.quotedMessage.slice(0, 300) + '...' : task.quotedMessage}
|
|
59
|
+
</div>
|
|
60
|
+
{task.quotedMessage.length > 300 && (
|
|
61
|
+
<button type="button" class="text-primary text-xs mt-1 show-more-btn">Show more</button>
|
|
62
|
+
)}
|
|
63
|
+
{priceEth && <div class="text-xs text-text-dim mt-1 font-mono">Price: {priceEth} ETH</div>}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{task.acceptedAt && (
|
|
68
|
+
<div class="text-text-muted text-xs">
|
|
69
|
+
Accepted · {new Date(task.acceptedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{task.result && (
|
|
74
|
+
<div>
|
|
75
|
+
<div class="text-text-muted text-xs mb-1">
|
|
76
|
+
Result{task.submittedAt ? ` · ${new Date(task.submittedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}` : ''}
|
|
77
|
+
</div>
|
|
78
|
+
<div class="text-text-dim text-sm result-text" data-full={task.result}>
|
|
79
|
+
{task.result.length > 300 ? task.result.slice(0, 300) + '...' : task.result}
|
|
80
|
+
</div>
|
|
81
|
+
{task.result.length > 300 && (
|
|
82
|
+
<button type="button" class="text-primary text-xs mt-1 show-more-btn">Show more</button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{task.txHash && (
|
|
88
|
+
<div>
|
|
89
|
+
<a
|
|
90
|
+
href={`https://basescan.org/tx/${task.txHash}`}
|
|
91
|
+
target="_blank"
|
|
92
|
+
class="text-primary text-xs hover:underline"
|
|
93
|
+
>View on Basescan →</a>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|