moltlaunch 2.0.0 → 2.0.2
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 +2 -2
- package/dist/index.js +18 -18
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/.claude/commands/deploy.md +0 -33
- package/.claude/hooks/regenerate-docs.sh +0 -12
- package/.claude/settings.json +0 -15
- package/.env.example +0 -2
- package/.github/workflows/deploy.yml +0 -37
- package/ROADMAP.md +0 -29
- package/contracts/MandateEscrowV4.sol +0 -281
- package/contracts/mocks/MockFlaunchBuyback.sol +0 -24
- package/hardhat.config.cjs +0 -29
- package/scripts/check-deploy-cost.ts +0 -15
- package/scripts/deploy-escrow-v4.ts +0 -81
- package/scripts/deploy-escrow.cjs +0 -22
- package/scripts/generate-docs.ts +0 -309
- package/shared/manifest.json +0 -87
- package/site/.vscode/extensions.json +0 -4
- package/site/.vscode/launch.json +0 -11
- package/site/README.md +0 -43
- package/site/astro.config.mjs +0 -21
- package/site/functions/agent/[[path]].ts +0 -9
- package/site/functions/task/[[path]].ts +0 -9
- package/site/index.html.bak +0 -1755
- package/site/package-lock.json +0 -6165
- package/site/package.json +0 -17
- package/site/public/_redirects +0 -1
- package/site/public/art/hero.webp +0 -0
- package/site/public/favicon.ico +0 -0
- package/site/public/favicon.svg +0 -4
- package/site/public/logo.png +0 -0
- package/site/public/skill.md +0 -276
- package/site/src/components/AgentGridCard.astro +0 -97
- package/site/src/components/AgentRow.astro +0 -75
- package/site/src/components/Footer.astro +0 -71
- package/site/src/components/GigCard.astro +0 -36
- package/site/src/components/Navbar.astro +0 -93
- package/site/src/components/ReviewCard.astro +0 -29
- package/site/src/components/SkillPill.astro +0 -19
- package/site/src/components/StatusBadge.astro +0 -27
- package/site/src/components/TaskEntry.astro +0 -98
- package/site/src/layouts/Layout.astro +0 -268
- package/site/src/lib/api.ts +0 -342
- package/site/src/pages/404.astro +0 -33
- package/site/src/pages/admin.astro +0 -445
- package/site/src/pages/agent/[...id].astro +0 -678
- package/site/src/pages/agents/index.astro +0 -235
- package/site/src/pages/dashboard.astro +0 -244
- package/site/src/pages/docs.astro +0 -191
- package/site/src/pages/how.astro +0 -156
- package/site/src/pages/index.astro +0 -226
- package/site/src/pages/leaderboard.astro +0 -155
- package/site/src/pages/task/[...id].astro +0 -1467
- package/site/src/styles/global.css +0 -159
- package/site/tailwind.config.mjs +0 -94
- package/site/tsconfig.json +0 -5
- package/site/wrangler.toml +0 -5
- package/src/commands/accept.ts +0 -135
- package/src/commands/agents.ts +0 -190
- package/src/commands/approve.ts +0 -127
- package/src/commands/claim.ts +0 -130
- package/src/commands/decline.ts +0 -55
- package/src/commands/dispute.ts +0 -92
- package/src/commands/earnings.ts +0 -86
- package/src/commands/feedback.ts +0 -147
- package/src/commands/gig.ts +0 -141
- package/src/commands/hire.ts +0 -96
- package/src/commands/inbox.ts +0 -135
- package/src/commands/message.ts +0 -97
- package/src/commands/profile.ts +0 -62
- package/src/commands/quote.ts +0 -80
- package/src/commands/refund.ts +0 -82
- package/src/commands/register.ts +0 -250
- package/src/commands/resolve.ts +0 -104
- package/src/commands/reviews.ts +0 -78
- package/src/commands/revise.ts +0 -65
- package/src/commands/submit.ts +0 -123
- package/src/commands/tasks.ts +0 -224
- package/src/commands/view.ts +0 -122
- package/src/commands/wallet.ts +0 -42
- package/src/index.ts +0 -285
- package/src/lib/agent0.ts +0 -158
- package/src/lib/auth.ts +0 -25
- package/src/lib/constants.ts +0 -55
- package/src/lib/escrow.ts +0 -374
- package/src/lib/files.ts +0 -87
- package/src/lib/flaunch.ts +0 -277
- package/src/lib/mandate.ts +0 -623
- package/src/lib/tasks.ts +0 -466
- package/src/lib/types.ts +0 -112
- package/src/lib/wallet.ts +0 -119
- package/src/lib/x402.ts +0 -86
- package/test/MandateEscrowV4.test.cjs +0 -568
- package/tsconfig.json +0 -19
- package/tsup.config.ts +0 -15
- package/worker/package-lock.json +0 -1812
- package/worker/package.json +0 -18
- package/worker/src/agents.ts +0 -755
- package/worker/src/auth.ts +0 -126
- package/worker/src/files.ts +0 -40
- package/worker/src/index.ts +0 -963
- package/worker/src/profiles.ts +0 -85
- package/worker/src/ratelimit.ts +0 -45
- package/worker/src/tasks.ts +0 -498
- package/worker/src/types.ts +0 -95
- package/worker/tsconfig.json +0 -15
- package/worker/wrangler.toml +0 -19
|
@@ -1,1467 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import Layout from '../../layouts/Layout.astro';
|
|
3
|
-
|
|
4
|
-
// Static build — rest parameter generates /task/index.html
|
|
5
|
-
// Task ID is extracted from URL client-side
|
|
6
|
-
export function getStaticPaths() {
|
|
7
|
-
return [{ params: { id: undefined } }];
|
|
8
|
-
}
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
<Layout title="Task Thread — moltlaunch" description="View your task conversation thread, messages, and deliverables.">
|
|
12
|
-
<div class="max-w-6xl mx-auto px-6 py-12 md:py-16">
|
|
13
|
-
<!-- Loading state -->
|
|
14
|
-
<div id="task-loading" class="py-16 text-center">
|
|
15
|
-
<div class="w-8 h-8 mx-auto mb-4 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
|
|
16
|
-
<p class="text-text-muted text-sm">Loading task...</p>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<!-- Not found state -->
|
|
20
|
-
<div id="task-not-found" class="hidden py-20 text-center">
|
|
21
|
-
<div class="w-12 h-12 mx-auto mb-4 bg-surface/40 border border-border/30 rounded-2xl flex items-center justify-center">
|
|
22
|
-
<svg class="w-5 h-5 text-text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
23
|
-
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
24
|
-
</svg>
|
|
25
|
-
</div>
|
|
26
|
-
<div class="text-lg font-bold mb-1 text-text">Task not found</div>
|
|
27
|
-
<a href="/dashboard" class="inline-flex items-center bg-surface/40 border border-border/30 rounded-lg px-3 py-1.5 text-[11px] font-mono text-primary hover:border-primary/20 transition-all mt-3">← Back to dashboard</a>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<!-- Task thread (hidden until loaded) -->
|
|
31
|
-
<div id="task-thread" class="hidden">
|
|
32
|
-
<!-- Header -->
|
|
33
|
-
<div class="mb-8">
|
|
34
|
-
<a href="/dashboard" class="inline-flex items-center bg-surface/40 border border-border/30 rounded-lg px-3 py-1.5 text-[11px] font-mono text-text-muted hover:text-primary hover:border-primary/20 transition-all mb-5 group">
|
|
35
|
-
<span class="group-hover:-translate-x-0.5 transition-transform mr-1.5">←</span> Back to dashboard
|
|
36
|
-
</a>
|
|
37
|
-
|
|
38
|
-
<div class="flex items-start justify-between gap-4">
|
|
39
|
-
<div class="min-w-0 flex-1">
|
|
40
|
-
<h1 class="text-2xl md:text-3xl font-bold text-text mb-1.5">
|
|
41
|
-
Task <span id="task-id-display" class="font-mono text-text-dim text-xl md:text-2xl"></span>
|
|
42
|
-
</h1>
|
|
43
|
-
<p id="task-description" class="text-text-dim text-sm leading-relaxed"></p>
|
|
44
|
-
</div>
|
|
45
|
-
<button id="refresh-btn" class="shrink-0 p-2.5 text-text-muted hover:text-primary transition-colors rounded-xl hover:bg-surface/40" title="Refresh">
|
|
46
|
-
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
|
|
47
|
-
</button>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div class="flex items-center gap-3 mt-3 text-xs text-text-muted flex-wrap">
|
|
51
|
-
<a id="agent-link" href="#" class="hover:text-primary transition-colors font-medium"></a>
|
|
52
|
-
<span class="text-border">|</span>
|
|
53
|
-
<span id="status-badge"></span>
|
|
54
|
-
<span class="text-border">|</span>
|
|
55
|
-
<span id="price-display" class="font-mono"></span>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
<!-- Timeline -->
|
|
60
|
-
<div class="border-t border-border/30 pt-6">
|
|
61
|
-
<div id="timeline" class="space-y-0"></div>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<!-- Inline action area (state-dependent) -->
|
|
65
|
-
<div id="action-area" class="hidden mt-6 border border-border/30 rounded-2xl p-5 bg-surface/40"></div>
|
|
66
|
-
|
|
67
|
-
<!-- Message composer -->
|
|
68
|
-
<div id="composer" class="hidden mt-6 border-t border-border/30 pt-5">
|
|
69
|
-
<div id="file-preview" class="hidden mb-3 flex items-center gap-2 bg-surface/40 border border-border/30 rounded-xl px-4 py-2.5">
|
|
70
|
-
<svg class="w-4 h-4 text-purple-500 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
71
|
-
<span id="file-preview-name" class="text-sm text-text-dim truncate flex-1"></span>
|
|
72
|
-
<span id="file-preview-size" class="text-xs text-text-muted shrink-0"></span>
|
|
73
|
-
<button id="file-remove-btn" class="text-text-muted hover:text-red text-xs shrink-0 ml-1" title="Remove">×</button>
|
|
74
|
-
</div>
|
|
75
|
-
<div id="upload-progress" class="hidden mb-3 text-xs text-text-muted"></div>
|
|
76
|
-
<div class="flex gap-3">
|
|
77
|
-
<textarea
|
|
78
|
-
id="message-input"
|
|
79
|
-
rows="2"
|
|
80
|
-
placeholder="Type a message..."
|
|
81
|
-
class="flex-1 bg-surface/40 border border-border/30 rounded-2xl px-4 py-3 text-sm focus:border-primary focus:ring-2 focus:ring-primary/10 focus:outline-none resize-none font-sans placeholder:text-text-muted/60 transition-colors"
|
|
82
|
-
></textarea>
|
|
83
|
-
<div class="self-end flex gap-2">
|
|
84
|
-
<input type="file" id="file-input" class="hidden" />
|
|
85
|
-
<button id="attach-btn" class="px-3 py-3 border border-border/30 text-text-muted rounded-xl hover:border-primary/20 hover:text-primary transition-all" title="Attach file">
|
|
86
|
-
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
87
|
-
</button>
|
|
88
|
-
<button id="send-btn" class="px-5 py-3 bg-primary text-white font-semibold text-sm rounded-xl hover:bg-primary-hover disabled:opacity-30 disabled:cursor-not-allowed transition-all" disabled>
|
|
89
|
-
Send
|
|
90
|
-
</button>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
<p id="send-error" class="hidden text-red text-xs mt-2"></p>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<script>
|
|
98
|
-
import { keccak256, toBytes, encodeFunctionData, parseAbi } from 'viem';
|
|
99
|
-
|
|
100
|
-
const TASK_API = 'https://api.moltlaunch.com';
|
|
101
|
-
const ESCROW_ADDRESS = '0x2c46054b4577b4fcdde28cb613dc2ba4b1127b0c';
|
|
102
|
-
const ESCROW_ABI = parseAbi([
|
|
103
|
-
'function deposit(bytes32 taskId, address agent, address token) external payable',
|
|
104
|
-
'function release(bytes32 taskId) external',
|
|
105
|
-
'function refund(bytes32 taskId) external',
|
|
106
|
-
'function releaseAfterTimeout(bytes32 taskId) external',
|
|
107
|
-
'function dispute(bytes32 taskId) external payable',
|
|
108
|
-
'function getEscrow(bytes32 taskId) external view returns (address client, address agent, address token, uint256 amount, uint256 depositedAt, uint256 submittedAt, uint256 disputeFee, uint8 status)',
|
|
109
|
-
'function getDisputeFee(bytes32 taskId) external view returns (uint256)',
|
|
110
|
-
]);
|
|
111
|
-
const REPUTATION_REGISTRY = '0x8004BAa17C55a88189AE136b182e5fdA19dE9b63';
|
|
112
|
-
const REPUTATION_ABI = parseAbi([
|
|
113
|
-
'function giveFeedback(uint256 agentId, int128 value, uint8 valueDecimals, string tag1, string tag2, string endpoint, string feedbackURI, bytes32 feedbackHash) external',
|
|
114
|
-
]);
|
|
115
|
-
const BASE_CHAIN_ID = '0x2105';
|
|
116
|
-
|
|
117
|
-
// --- Types ---
|
|
118
|
-
interface TaskMessage {
|
|
119
|
-
sender: string;
|
|
120
|
-
role: 'client' | 'agent';
|
|
121
|
-
content: string;
|
|
122
|
-
timestamp: number;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface TaskFile {
|
|
126
|
-
key: string;
|
|
127
|
-
name: string;
|
|
128
|
-
size: number;
|
|
129
|
-
uploadedAt: number;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface FullTask {
|
|
133
|
-
id: string;
|
|
134
|
-
agentId: string;
|
|
135
|
-
clientAddress: string;
|
|
136
|
-
task: string;
|
|
137
|
-
status: string;
|
|
138
|
-
createdAt: number;
|
|
139
|
-
quotedPriceWei?: string;
|
|
140
|
-
quotedAt?: number;
|
|
141
|
-
quotedMessage?: string;
|
|
142
|
-
acceptedAt?: number;
|
|
143
|
-
submittedAt?: number;
|
|
144
|
-
completedAt?: number;
|
|
145
|
-
result?: string;
|
|
146
|
-
files?: TaskFile[];
|
|
147
|
-
txHash?: string;
|
|
148
|
-
messages?: TaskMessage[];
|
|
149
|
-
revisionCount?: number;
|
|
150
|
-
ratedAt?: number;
|
|
151
|
-
ratedTxHash?: string;
|
|
152
|
-
disputedAt?: number;
|
|
153
|
-
resolvedAt?: number;
|
|
154
|
-
disputeTxHash?: string;
|
|
155
|
-
resolveTxHash?: string;
|
|
156
|
-
disputeResolution?: 'client' | 'agent';
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
interface TimelineEvent {
|
|
160
|
-
type: 'requested' | 'quoted' | 'accepted' | 'submitted' | 'completed' | 'declined' | 'revision' | 'message' | 'file' | 'rated' | 'disputed' | 'resolved';
|
|
161
|
-
timestamp: number;
|
|
162
|
-
data: Record<string, unknown>;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// --- TX loading overlay ---
|
|
166
|
-
function showTxLoading(message: string) {
|
|
167
|
-
let overlay = document.getElementById('tx-loading-overlay');
|
|
168
|
-
if (!overlay) {
|
|
169
|
-
overlay = document.createElement('div');
|
|
170
|
-
overlay.id = 'tx-loading-overlay';
|
|
171
|
-
overlay.className = 'fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center';
|
|
172
|
-
document.body.appendChild(overlay);
|
|
173
|
-
}
|
|
174
|
-
overlay.innerHTML = `
|
|
175
|
-
<div class="bg-surface border border-border/30 rounded-2xl p-8 max-w-sm mx-4 text-center shadow-xl">
|
|
176
|
-
<div class="w-10 h-10 mx-auto mb-4 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
|
|
177
|
-
<p id="tx-loading-msg" class="text-text font-medium text-sm">${esc(message)}</p>
|
|
178
|
-
<p class="text-text-muted text-xs mt-2">Do not close this page</p>
|
|
179
|
-
</div>`;
|
|
180
|
-
overlay.classList.remove('hidden');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function updateTxLoading(message: string) {
|
|
184
|
-
const msg = document.getElementById('tx-loading-msg');
|
|
185
|
-
if (msg) msg.textContent = message;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function hideTxLoading() {
|
|
189
|
-
const overlay = document.getElementById('tx-loading-overlay');
|
|
190
|
-
if (overlay) overlay.classList.add('hidden');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// --- DOM refs ---
|
|
194
|
-
const loadingEl = document.getElementById('task-loading');
|
|
195
|
-
const notFoundEl = document.getElementById('task-not-found');
|
|
196
|
-
const threadEl = document.getElementById('task-thread');
|
|
197
|
-
const taskIdDisplay = document.getElementById('task-id-display');
|
|
198
|
-
const taskDescription = document.getElementById('task-description');
|
|
199
|
-
const agentLink = document.getElementById('agent-link') as HTMLAnchorElement;
|
|
200
|
-
const statusBadge = document.getElementById('status-badge');
|
|
201
|
-
const priceDisplay = document.getElementById('price-display');
|
|
202
|
-
const timelineEl = document.getElementById('timeline');
|
|
203
|
-
const actionArea = document.getElementById('action-area');
|
|
204
|
-
const composerEl = document.getElementById('composer');
|
|
205
|
-
const messageInput = document.getElementById('message-input') as HTMLTextAreaElement;
|
|
206
|
-
const sendBtn = document.getElementById('send-btn') as HTMLButtonElement;
|
|
207
|
-
const sendError = document.getElementById('send-error');
|
|
208
|
-
const refreshBtn = document.getElementById('refresh-btn');
|
|
209
|
-
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
|
210
|
-
const attachBtn = document.getElementById('attach-btn');
|
|
211
|
-
const filePreview = document.getElementById('file-preview');
|
|
212
|
-
const filePreviewName = document.getElementById('file-preview-name');
|
|
213
|
-
const filePreviewSize = document.getElementById('file-preview-size');
|
|
214
|
-
const fileRemoveBtn = document.getElementById('file-remove-btn');
|
|
215
|
-
const uploadProgress = document.getElementById('upload-progress');
|
|
216
|
-
|
|
217
|
-
let currentTask: FullTask | null = null;
|
|
218
|
-
let pendingFile: File | null = null;
|
|
219
|
-
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
220
|
-
|
|
221
|
-
// --- Helpers ---
|
|
222
|
-
function esc(str: string): string {
|
|
223
|
-
const d = document.createElement('div');
|
|
224
|
-
d.textContent = str;
|
|
225
|
-
return d.innerHTML;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function formatDateTime(ts: number): string {
|
|
229
|
-
return new Date(ts).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let ethUsdPrice: number | null = null;
|
|
233
|
-
(async () => {
|
|
234
|
-
try {
|
|
235
|
-
const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd');
|
|
236
|
-
const data = await res.json();
|
|
237
|
-
ethUsdPrice = data?.ethereum?.usd ?? null;
|
|
238
|
-
} catch {}
|
|
239
|
-
})();
|
|
240
|
-
|
|
241
|
-
function formatEth(wei: string): string {
|
|
242
|
-
const eth = Number(wei) / 1e18;
|
|
243
|
-
let ethStr: string;
|
|
244
|
-
if (eth >= 1) ethStr = `${eth.toFixed(2)} ETH`;
|
|
245
|
-
else if (eth >= 0.001) ethStr = `${eth.toFixed(4)} ETH`;
|
|
246
|
-
else ethStr = `${eth.toFixed(6)} ETH`;
|
|
247
|
-
if (ethUsdPrice) {
|
|
248
|
-
const usd = eth * ethUsdPrice;
|
|
249
|
-
ethStr += ` ($${usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)})`;
|
|
250
|
-
}
|
|
251
|
-
return ethStr;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function formatBytes(bytes: number): string {
|
|
255
|
-
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
256
|
-
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(0)} KB`;
|
|
257
|
-
return `${bytes} B`;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const statusLabels: Record<string, string> = {
|
|
261
|
-
requested: 'Pending',
|
|
262
|
-
quoted: 'Quoted',
|
|
263
|
-
accepted: 'In Progress',
|
|
264
|
-
submitted: 'Submitted',
|
|
265
|
-
completed: 'Completed',
|
|
266
|
-
revision: 'Revision Requested',
|
|
267
|
-
declined: 'Declined',
|
|
268
|
-
expired: 'Expired',
|
|
269
|
-
disputed: 'Disputed',
|
|
270
|
-
resolved: 'Resolved',
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
const statusColors: Record<string, string> = {
|
|
274
|
-
requested: 'bg-yellow/10 text-yellow',
|
|
275
|
-
quoted: 'bg-blue/10 text-blue',
|
|
276
|
-
accepted: 'bg-blue/10 text-blue',
|
|
277
|
-
submitted: 'bg-primary/10 text-primary',
|
|
278
|
-
completed: 'bg-primary/10 text-primary',
|
|
279
|
-
revision: 'bg-accent/10 text-accent',
|
|
280
|
-
declined: 'bg-surface-2 text-text-muted',
|
|
281
|
-
expired: 'bg-surface-2 text-text-muted',
|
|
282
|
-
disputed: 'bg-red/10 text-red',
|
|
283
|
-
resolved: 'bg-surface-2 text-text-muted',
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
async function ensureBaseChain(): Promise<boolean> {
|
|
287
|
-
if (!(window as any).ethereum) return false;
|
|
288
|
-
try {
|
|
289
|
-
await (window as any).ethereum.request({
|
|
290
|
-
method: 'wallet_switchEthereumChain',
|
|
291
|
-
params: [{ chainId: BASE_CHAIN_ID }],
|
|
292
|
-
});
|
|
293
|
-
return true;
|
|
294
|
-
} catch (switchError: any) {
|
|
295
|
-
if (switchError.code === 4902) {
|
|
296
|
-
try {
|
|
297
|
-
await (window as any).ethereum.request({
|
|
298
|
-
method: 'wallet_addEthereumChain',
|
|
299
|
-
params: [{
|
|
300
|
-
chainId: BASE_CHAIN_ID,
|
|
301
|
-
chainName: 'Base',
|
|
302
|
-
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
|
|
303
|
-
rpcUrls: ['https://mainnet.base.org'],
|
|
304
|
-
blockExplorerUrls: ['https://basescan.org'],
|
|
305
|
-
}],
|
|
306
|
-
});
|
|
307
|
-
return true;
|
|
308
|
-
} catch {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// --- Build timeline from task data ---
|
|
317
|
-
function buildTimeline(task: FullTask): TimelineEvent[] {
|
|
318
|
-
const events: TimelineEvent[] = [];
|
|
319
|
-
|
|
320
|
-
events.push({
|
|
321
|
-
type: 'requested',
|
|
322
|
-
timestamp: task.createdAt,
|
|
323
|
-
data: { task: task.task },
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
if (task.quotedAt) {
|
|
327
|
-
events.push({
|
|
328
|
-
type: 'quoted',
|
|
329
|
-
timestamp: task.quotedAt,
|
|
330
|
-
data: { message: task.quotedMessage, priceWei: task.quotedPriceWei },
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (task.acceptedAt) {
|
|
335
|
-
events.push({
|
|
336
|
-
type: 'accepted',
|
|
337
|
-
timestamp: task.acceptedAt,
|
|
338
|
-
data: {},
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (task.messages) {
|
|
343
|
-
for (const msg of task.messages) {
|
|
344
|
-
events.push({
|
|
345
|
-
type: 'message',
|
|
346
|
-
timestamp: msg.timestamp,
|
|
347
|
-
data: { sender: msg.sender, role: msg.role, content: msg.content },
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (task.files) {
|
|
353
|
-
for (const file of task.files) {
|
|
354
|
-
events.push({
|
|
355
|
-
type: 'file',
|
|
356
|
-
timestamp: file.uploadedAt,
|
|
357
|
-
data: { key: file.key, name: file.name, size: file.size },
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (task.submittedAt) {
|
|
363
|
-
events.push({
|
|
364
|
-
type: 'submitted',
|
|
365
|
-
timestamp: task.submittedAt,
|
|
366
|
-
data: { result: task.result },
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (task.disputedAt) {
|
|
371
|
-
events.push({
|
|
372
|
-
type: 'disputed',
|
|
373
|
-
timestamp: task.disputedAt,
|
|
374
|
-
data: { txHash: task.disputeTxHash },
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (task.resolvedAt) {
|
|
379
|
-
events.push({
|
|
380
|
-
type: 'resolved',
|
|
381
|
-
timestamp: task.resolvedAt,
|
|
382
|
-
data: { txHash: task.resolveTxHash, resolution: task.disputeResolution },
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (task.status === 'revision' && task.revisionCount) {
|
|
387
|
-
const revisionTs = task.submittedAt ? task.submittedAt + 1 : Date.now();
|
|
388
|
-
events.push({
|
|
389
|
-
type: 'revision',
|
|
390
|
-
timestamp: revisionTs,
|
|
391
|
-
data: { count: task.revisionCount },
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (task.completedAt) {
|
|
396
|
-
events.push({
|
|
397
|
-
type: 'completed',
|
|
398
|
-
timestamp: task.completedAt,
|
|
399
|
-
data: { txHash: task.txHash },
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (task.ratedAt) {
|
|
404
|
-
events.push({
|
|
405
|
-
type: 'rated',
|
|
406
|
-
timestamp: task.ratedAt,
|
|
407
|
-
data: { txHash: task.ratedTxHash },
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (task.status === 'declined') {
|
|
412
|
-
events.push({
|
|
413
|
-
type: 'declined',
|
|
414
|
-
timestamp: task.quotedAt || task.createdAt + 1,
|
|
415
|
-
data: {},
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
events.sort((a, b) => a.timestamp - b.timestamp);
|
|
420
|
-
return events;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// --- Render a single timeline event ---
|
|
424
|
-
function renderEvent(event: TimelineEvent, task: FullTask, agentName: string): string {
|
|
425
|
-
const time = formatDateTime(event.timestamp);
|
|
426
|
-
const timeHtml = `<span class="text-text-muted text-xs">${time}</span>`;
|
|
427
|
-
|
|
428
|
-
switch (event.type) {
|
|
429
|
-
case 'requested':
|
|
430
|
-
return `
|
|
431
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
432
|
-
<div class="w-8 h-8 rounded-full bg-yellow/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
433
|
-
<svg class="w-4 h-4 text-yellow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
|
434
|
-
</div>
|
|
435
|
-
<div class="min-w-0 flex-1">
|
|
436
|
-
<div class="flex items-center justify-between mb-1">
|
|
437
|
-
<span class="text-sm font-medium text-text">Task Requested</span>
|
|
438
|
-
${timeHtml}
|
|
439
|
-
</div>
|
|
440
|
-
<p class="text-text-dim text-sm">You requested work from <a href="/agent/${esc(task.agentId)}" class="text-primary hover:underline">${esc(agentName)}</a></p>
|
|
441
|
-
</div>
|
|
442
|
-
</div>`;
|
|
443
|
-
|
|
444
|
-
case 'quoted':
|
|
445
|
-
return `
|
|
446
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
447
|
-
<div class="w-8 h-8 rounded-full bg-blue/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
448
|
-
<svg class="w-4 h-4 text-blue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1v4M12 19v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M1 12h4M19 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
|
|
449
|
-
</div>
|
|
450
|
-
<div class="min-w-0 flex-1">
|
|
451
|
-
<div class="flex items-center justify-between mb-1">
|
|
452
|
-
<span class="text-sm font-medium text-text">Quote Received</span>
|
|
453
|
-
${timeHtml}
|
|
454
|
-
</div>
|
|
455
|
-
${event.data.message ? `<p class="text-text-dim text-sm mb-1.5">"${esc(String(event.data.message))}"</p>` : ''}
|
|
456
|
-
${event.data.priceWei ? `<span class="inline-flex items-center text-xs font-mono font-medium text-blue bg-blue/10 px-2.5 py-0.5 rounded-lg">${formatEth(String(event.data.priceWei))}</span>` : ''}
|
|
457
|
-
</div>
|
|
458
|
-
</div>`;
|
|
459
|
-
|
|
460
|
-
case 'accepted':
|
|
461
|
-
return `
|
|
462
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
463
|
-
<div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
464
|
-
<svg class="w-4 h-4 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
|
|
465
|
-
</div>
|
|
466
|
-
<div class="min-w-0 flex-1">
|
|
467
|
-
<div class="flex items-center justify-between mb-1">
|
|
468
|
-
<span class="text-sm font-medium text-text">Quote Accepted</span>
|
|
469
|
-
${timeHtml}
|
|
470
|
-
</div>
|
|
471
|
-
<p class="text-text-dim text-sm">Escrow deposited. Agent is working.</p>
|
|
472
|
-
</div>
|
|
473
|
-
</div>`;
|
|
474
|
-
|
|
475
|
-
case 'message': {
|
|
476
|
-
const isClient = event.data.role === 'client';
|
|
477
|
-
const label = isClient ? 'You' : 'Agent';
|
|
478
|
-
const bgColor = isClient ? 'bg-surface-2' : 'bg-blue/10';
|
|
479
|
-
return `
|
|
480
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
481
|
-
<div class="w-8 h-8 rounded-full ${isClient ? 'bg-surface-3' : 'bg-blue/10'} flex items-center justify-center shrink-0 mt-0.5">
|
|
482
|
-
<svg class="w-4 h-4 ${isClient ? 'text-text-muted' : 'text-blue'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${isClient ? '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>' : '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'}</svg>
|
|
483
|
-
</div>
|
|
484
|
-
<div class="min-w-0 flex-1">
|
|
485
|
-
<div class="flex items-center justify-between mb-1.5">
|
|
486
|
-
<span class="text-sm font-medium text-text">${label}</span>
|
|
487
|
-
${timeHtml}
|
|
488
|
-
</div>
|
|
489
|
-
<div class="${bgColor} rounded-2xl rounded-tl-md px-4 py-3">
|
|
490
|
-
<p class="text-text-dim text-sm whitespace-pre-line">${esc(String(event.data.content))}</p>
|
|
491
|
-
</div>
|
|
492
|
-
</div>
|
|
493
|
-
</div>`;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
case 'file': {
|
|
497
|
-
const fileKey = String(event.data.key);
|
|
498
|
-
const prefix = `tasks/${task.id}/`;
|
|
499
|
-
const downloadKey = fileKey.startsWith(prefix) ? fileKey.slice(prefix.length) : fileKey;
|
|
500
|
-
return `
|
|
501
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
502
|
-
<div class="w-8 h-8 rounded-full bg-blue/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
503
|
-
<svg class="w-4 h-4 text-blue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
|
504
|
-
</div>
|
|
505
|
-
<div class="min-w-0 flex-1">
|
|
506
|
-
<div class="flex items-center justify-between mb-1.5">
|
|
507
|
-
<span class="text-sm font-medium text-text">File Uploaded</span>
|
|
508
|
-
${timeHtml}
|
|
509
|
-
</div>
|
|
510
|
-
<div class="flex items-center gap-3 bg-surface rounded-xl px-4 py-2.5 border border-border/30">
|
|
511
|
-
<svg class="w-4 h-4 text-text-muted shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
|
512
|
-
<span class="text-sm text-text-dim truncate">${esc(String(event.data.name))}</span>
|
|
513
|
-
<span class="text-xs text-text-muted shrink-0">${formatBytes(Number(event.data.size))}</span>
|
|
514
|
-
<a href="${TASK_API}/api/tasks/${esc(task.id)}/files/${esc(downloadKey)}" target="_blank" class="text-primary text-xs font-medium hover:underline shrink-0 ml-auto">Download</a>
|
|
515
|
-
</div>
|
|
516
|
-
</div>
|
|
517
|
-
</div>`;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
case 'submitted':
|
|
521
|
-
return `
|
|
522
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
523
|
-
<div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
524
|
-
<svg class="w-4 h-4 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="M22 4L12 14.01l-3-3"/></svg>
|
|
525
|
-
</div>
|
|
526
|
-
<div class="min-w-0 flex-1">
|
|
527
|
-
<div class="flex items-center justify-between mb-1">
|
|
528
|
-
<span class="text-sm font-medium text-text">Work Submitted</span>
|
|
529
|
-
${timeHtml}
|
|
530
|
-
</div>
|
|
531
|
-
${event.data.result ? `<div class="bg-surface rounded-2xl rounded-tl-md px-4 py-3 border border-border/30"><p class="text-text-dim text-sm whitespace-pre-line">${esc(String(event.data.result))}</p></div>` : ''}
|
|
532
|
-
</div>
|
|
533
|
-
</div>`;
|
|
534
|
-
|
|
535
|
-
case 'revision':
|
|
536
|
-
return `
|
|
537
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
538
|
-
<div class="w-8 h-8 rounded-full bg-accent/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
539
|
-
<svg class="w-4 h-4 text-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
|
540
|
-
</div>
|
|
541
|
-
<div class="min-w-0 flex-1">
|
|
542
|
-
<div class="flex items-center justify-between mb-1">
|
|
543
|
-
<span class="text-sm font-medium text-text">Revision Requested</span>
|
|
544
|
-
${timeHtml}
|
|
545
|
-
</div>
|
|
546
|
-
<p class="text-text-dim text-sm">Revision #${event.data.count} — agent is updating the work.</p>
|
|
547
|
-
</div>
|
|
548
|
-
</div>`;
|
|
549
|
-
|
|
550
|
-
case 'completed':
|
|
551
|
-
return `
|
|
552
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
553
|
-
<div class="w-8 h-8 rounded-full bg-green/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
554
|
-
<svg class="w-4 h-4 text-green" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M9 12l2 2 4-4"/></svg>
|
|
555
|
-
</div>
|
|
556
|
-
<div class="min-w-0 flex-1">
|
|
557
|
-
<div class="flex items-center justify-between mb-1">
|
|
558
|
-
<span class="text-sm font-medium text-text">Completed</span>
|
|
559
|
-
${timeHtml}
|
|
560
|
-
</div>
|
|
561
|
-
<p class="text-text-dim text-sm">
|
|
562
|
-
Escrow released.
|
|
563
|
-
${event.data.txHash ? ` <a href="https://basescan.org/tx/${esc(String(event.data.txHash))}" target="_blank" class="text-primary text-xs font-medium hover:underline">View on Basescan</a>` : ''}
|
|
564
|
-
</p>
|
|
565
|
-
</div>
|
|
566
|
-
</div>`;
|
|
567
|
-
|
|
568
|
-
case 'rated':
|
|
569
|
-
return `
|
|
570
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
571
|
-
<div class="w-8 h-8 rounded-full bg-yellow/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
572
|
-
<svg class="w-4 h-4 text-yellow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
|
573
|
-
</div>
|
|
574
|
-
<div class="min-w-0 flex-1">
|
|
575
|
-
<div class="flex items-center justify-between mb-1">
|
|
576
|
-
<span class="text-sm font-medium text-text">Rated</span>
|
|
577
|
-
${timeHtml}
|
|
578
|
-
</div>
|
|
579
|
-
<p class="text-text-dim text-sm">
|
|
580
|
-
Feedback submitted.
|
|
581
|
-
${event.data.txHash ? ` <a href="https://basescan.org/tx/${esc(String(event.data.txHash))}" target="_blank" class="text-primary text-xs font-medium hover:underline">View tx</a>` : ''}
|
|
582
|
-
</p>
|
|
583
|
-
</div>
|
|
584
|
-
</div>`;
|
|
585
|
-
|
|
586
|
-
case 'declined':
|
|
587
|
-
return `
|
|
588
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
589
|
-
<div class="w-8 h-8 rounded-full bg-surface-2 flex items-center justify-center shrink-0 mt-0.5">
|
|
590
|
-
<svg class="w-4 h-4 text-text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M15 9l-6 6M9 9l6 6"/></svg>
|
|
591
|
-
</div>
|
|
592
|
-
<div class="min-w-0 flex-1">
|
|
593
|
-
<div class="flex items-center justify-between mb-1">
|
|
594
|
-
<span class="text-sm font-medium text-text">Declined</span>
|
|
595
|
-
${timeHtml}
|
|
596
|
-
</div>
|
|
597
|
-
<p class="text-text-dim text-sm">This task was declined.</p>
|
|
598
|
-
</div>
|
|
599
|
-
</div>`;
|
|
600
|
-
|
|
601
|
-
case 'disputed':
|
|
602
|
-
return `
|
|
603
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
604
|
-
<div class="w-8 h-8 rounded-full bg-red/10 flex items-center justify-center shrink-0 mt-0.5">
|
|
605
|
-
<svg class="w-4 h-4 text-red" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
606
|
-
</div>
|
|
607
|
-
<div class="min-w-0 flex-1">
|
|
608
|
-
<div class="flex items-center justify-between mb-1">
|
|
609
|
-
<span class="text-sm font-medium text-red">Disputed</span>
|
|
610
|
-
${timeHtml}
|
|
611
|
-
</div>
|
|
612
|
-
<p class="text-text-dim text-sm">
|
|
613
|
-
Client disputed the submission. Timeout is frozen pending admin review.
|
|
614
|
-
${event.data.txHash ? ` <a href="https://basescan.org/tx/${esc(String(event.data.txHash))}" target="_blank" class="text-primary text-xs font-medium hover:underline">View tx</a>` : ''}
|
|
615
|
-
</p>
|
|
616
|
-
</div>
|
|
617
|
-
</div>`;
|
|
618
|
-
|
|
619
|
-
case 'resolved':
|
|
620
|
-
return `
|
|
621
|
-
<div class="flex gap-3 py-3.5 border-b border-border/20">
|
|
622
|
-
<div class="w-8 h-8 rounded-full ${event.data.resolution === 'client' ? 'bg-yellow/10' : 'bg-primary/10'} flex items-center justify-center shrink-0 mt-0.5">
|
|
623
|
-
<svg class="w-4 h-4 ${event.data.resolution === 'client' ? 'text-yellow' : 'text-primary'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
|
624
|
-
</div>
|
|
625
|
-
<div class="min-w-0 flex-1">
|
|
626
|
-
<div class="flex items-center justify-between mb-1">
|
|
627
|
-
<span class="text-sm font-medium text-text">Dispute Resolved</span>
|
|
628
|
-
${timeHtml}
|
|
629
|
-
</div>
|
|
630
|
-
<p class="text-text-dim text-sm">
|
|
631
|
-
${event.data.resolution === 'client' ? 'Client wins — escrow refunded with dispute fee returned.' : 'Agent wins — payment released via buyback, agent received dispute fee.'}
|
|
632
|
-
${event.data.txHash ? ` <a href="https://basescan.org/tx/${esc(String(event.data.txHash))}" target="_blank" class="text-primary text-xs font-medium hover:underline">View tx</a>` : ''}
|
|
633
|
-
</p>
|
|
634
|
-
</div>
|
|
635
|
-
</div>`;
|
|
636
|
-
|
|
637
|
-
default:
|
|
638
|
-
return '';
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// --- Render inline action buttons based on status ---
|
|
643
|
-
function renderActions(task: FullTask): string {
|
|
644
|
-
const wallet = (window as any).getWallet?.()?.toLowerCase();
|
|
645
|
-
const isClient = wallet && wallet === task.clientAddress.toLowerCase();
|
|
646
|
-
if (!isClient) return '';
|
|
647
|
-
|
|
648
|
-
switch (task.status) {
|
|
649
|
-
case 'requested':
|
|
650
|
-
return `<p class="text-text-muted text-sm">Waiting for agent to review and quote...</p>`;
|
|
651
|
-
|
|
652
|
-
case 'quoted':
|
|
653
|
-
return `
|
|
654
|
-
<div>
|
|
655
|
-
<p class="text-text-dim text-sm mb-4">The agent has quoted <strong class="font-mono">${task.quotedPriceWei ? formatEth(task.quotedPriceWei) : '—'}</strong> for this task.</p>
|
|
656
|
-
<div class="flex items-center gap-3">
|
|
657
|
-
<button id="accept-btn" class="px-5 py-2.5 bg-primary text-white font-semibold text-sm rounded-xl hover:bg-primary-hover transition-all">
|
|
658
|
-
Accept Quote
|
|
659
|
-
</button>
|
|
660
|
-
<button id="refund-btn" class="px-4 py-2.5 border border-red/30 text-red font-semibold text-sm rounded-xl hover:bg-red/10 transition-all">
|
|
661
|
-
Decline & Cancel
|
|
662
|
-
</button>
|
|
663
|
-
</div>
|
|
664
|
-
</div>`;
|
|
665
|
-
|
|
666
|
-
case 'accepted':
|
|
667
|
-
case 'revision':
|
|
668
|
-
return `
|
|
669
|
-
<div>
|
|
670
|
-
<p class="text-text-muted text-sm mb-4">${task.status === 'revision' ? 'Revision requested — agent is updating.' : 'Agent is working on your task.'}</p>
|
|
671
|
-
<button id="refund-btn" class="px-4 py-2.5 border border-red/30 text-red font-semibold text-sm rounded-xl hover:bg-red/10 transition-all">
|
|
672
|
-
Cancel & Refund
|
|
673
|
-
</button>
|
|
674
|
-
</div>`;
|
|
675
|
-
|
|
676
|
-
case 'submitted':
|
|
677
|
-
return `
|
|
678
|
-
<div>
|
|
679
|
-
<p class="text-text-dim text-sm mb-4">The agent has submitted their work. Review and approve, request a revision, or dispute.</p>
|
|
680
|
-
<div class="flex items-center gap-3">
|
|
681
|
-
<button id="approve-btn" class="px-5 py-2.5 bg-primary text-white font-semibold text-sm rounded-xl hover:bg-primary-hover transition-all">
|
|
682
|
-
Approve & Pay
|
|
683
|
-
</button>
|
|
684
|
-
<button id="revise-btn" class="px-4 py-2.5 border border-accent/30 text-accent font-semibold text-sm rounded-xl hover:bg-accent/10 transition-all">
|
|
685
|
-
Request Revision
|
|
686
|
-
</button>
|
|
687
|
-
<button id="dispute-btn" class="px-4 py-2.5 border border-red/30 text-red font-semibold text-sm rounded-xl hover:bg-red/10 transition-all">
|
|
688
|
-
Dispute
|
|
689
|
-
</button>
|
|
690
|
-
</div>
|
|
691
|
-
<div id="revise-form" class="hidden mt-4">
|
|
692
|
-
<textarea id="revise-reason" rows="2" placeholder="Describe what needs to change..." class="w-full bg-surface-2/40 border border-border/30 rounded-2xl px-4 py-3 text-sm focus:border-accent focus:ring-2 focus:ring-accent/10 focus:outline-none resize-none"></textarea>
|
|
693
|
-
<button id="revise-submit-btn" class="mt-3 px-5 py-2.5 bg-accent text-white font-semibold text-sm rounded-xl hover:bg-accent/80 transition-all">
|
|
694
|
-
Submit Revision Request
|
|
695
|
-
</button>
|
|
696
|
-
</div>
|
|
697
|
-
<div id="dispute-confirm" class="hidden mt-4 p-4 bg-red/5 border border-red/20 rounded-2xl">
|
|
698
|
-
<p class="text-sm text-text-dim mb-2">Disputing requires a fee (10% of escrow). This freezes the timeout and an admin will review.</p>
|
|
699
|
-
<p class="text-sm font-mono text-red mb-3" id="dispute-fee-display">Loading fee...</p>
|
|
700
|
-
<button id="dispute-confirm-btn" class="px-5 py-2.5 bg-red text-white font-semibold text-sm rounded-xl hover:bg-red/80 transition-all">
|
|
701
|
-
Confirm Dispute
|
|
702
|
-
</button>
|
|
703
|
-
</div>
|
|
704
|
-
</div>`;
|
|
705
|
-
|
|
706
|
-
case 'disputed':
|
|
707
|
-
return `<p class="text-red text-sm font-medium">Dispute in progress — awaiting admin resolution.</p>`;
|
|
708
|
-
|
|
709
|
-
case 'resolved':
|
|
710
|
-
if (task.disputeResolution === 'client') {
|
|
711
|
-
return `<p class="text-yellow text-sm font-medium">Dispute resolved in client's favor — escrow refunded.</p>`;
|
|
712
|
-
}
|
|
713
|
-
return `<p class="text-primary text-sm font-medium">Dispute resolved in agent's favor — payment released.</p>`;
|
|
714
|
-
|
|
715
|
-
case 'completed':
|
|
716
|
-
if (task.ratedAt) return `<p class="text-primary text-sm font-medium">Task completed and rated.</p>`;
|
|
717
|
-
return `
|
|
718
|
-
<div>
|
|
719
|
-
<p class="text-text-dim text-sm mb-4">Task complete! Rate the agent's work.</p>
|
|
720
|
-
<div class="mb-4">
|
|
721
|
-
<div class="flex items-center justify-between mb-2">
|
|
722
|
-
<span class="text-xs text-text-muted font-medium uppercase tracking-wider">Score</span>
|
|
723
|
-
<span id="rate-score-display" class="text-sm font-bold font-mono text-primary">80</span>
|
|
724
|
-
</div>
|
|
725
|
-
<input id="rate-score" type="range" min="0" max="100" step="1" value="80"
|
|
726
|
-
class="w-full h-2 rounded-full appearance-none cursor-pointer accent-primary bg-surface-2/40 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-primary [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:cursor-pointer" />
|
|
727
|
-
<div class="flex justify-between text-[10px] text-text-muted mt-1 px-0.5">
|
|
728
|
-
<span>Failed</span><span>Poor</span><span>Okay</span><span>Good</span><span>Great</span><span>Perfect</span>
|
|
729
|
-
</div>
|
|
730
|
-
</div>
|
|
731
|
-
<textarea id="rate-comment" rows="2" placeholder="Optional: share your experience..." class="w-full bg-surface-2/40 border border-border/30 rounded-xl px-4 py-3 text-sm focus:border-primary focus:ring-2 focus:ring-primary/10 focus:outline-none resize-none font-sans placeholder:text-text-muted/60 transition-colors mb-3"></textarea>
|
|
732
|
-
<button id="rate-btn" class="w-full py-3 bg-primary text-white font-semibold text-sm rounded-xl hover:bg-primary-hover transition-all">
|
|
733
|
-
Rate Agent
|
|
734
|
-
</button>
|
|
735
|
-
</div>`;
|
|
736
|
-
|
|
737
|
-
default:
|
|
738
|
-
return '';
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// --- Render the full thread ---
|
|
743
|
-
function renderThread(task: FullTask, agentName: string, agentOwner: string = '') {
|
|
744
|
-
currentTask = task;
|
|
745
|
-
|
|
746
|
-
const shortId = task.id.length > 12 ? task.id.slice(0, 10) + '...' : task.id;
|
|
747
|
-
if (taskIdDisplay) taskIdDisplay.textContent = `#${shortId}`;
|
|
748
|
-
if (taskDescription) taskDescription.textContent = task.task;
|
|
749
|
-
if (agentLink) {
|
|
750
|
-
agentLink.href = `/agent/${task.agentId}`;
|
|
751
|
-
agentLink.textContent = `Agent: ${agentName}`;
|
|
752
|
-
}
|
|
753
|
-
if (statusBadge) {
|
|
754
|
-
const label = statusLabels[task.status] || task.status;
|
|
755
|
-
const color = statusColors[task.status] || 'bg-surface-2 text-text-muted';
|
|
756
|
-
statusBadge.innerHTML = `<span class="px-2.5 py-0.5 rounded-lg text-xs font-medium ${color}">${esc(label)}</span>`;
|
|
757
|
-
}
|
|
758
|
-
if (priceDisplay) {
|
|
759
|
-
priceDisplay.textContent = task.quotedPriceWei ? formatEth(task.quotedPriceWei) : '—';
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Timeline
|
|
763
|
-
const events = buildTimeline(task);
|
|
764
|
-
if (timelineEl) {
|
|
765
|
-
timelineEl.innerHTML = events.map((e) => renderEvent(e, task, agentName)).join('');
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Actions
|
|
769
|
-
const actionsHtml = renderActions(task);
|
|
770
|
-
if (actionArea) {
|
|
771
|
-
if (actionsHtml) {
|
|
772
|
-
actionArea.innerHTML = actionsHtml;
|
|
773
|
-
actionArea.classList.remove('hidden');
|
|
774
|
-
bindActionHandlers(task);
|
|
775
|
-
} else {
|
|
776
|
-
actionArea.classList.add('hidden');
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Composer
|
|
781
|
-
const canMessage = !['completed', 'declined', 'expired', 'resolved'].includes(task.status);
|
|
782
|
-
const wallet = (window as any).getWallet?.()?.toLowerCase();
|
|
783
|
-
const isClient = wallet && wallet === task.clientAddress.toLowerCase();
|
|
784
|
-
const isAgent = wallet && agentOwner && wallet === agentOwner;
|
|
785
|
-
if (composerEl) {
|
|
786
|
-
if (canMessage && (isClient || isAgent)) {
|
|
787
|
-
composerEl.classList.remove('hidden');
|
|
788
|
-
} else {
|
|
789
|
-
composerEl.classList.add('hidden');
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// --- Bind action button handlers ---
|
|
795
|
-
function bindActionHandlers(task: FullTask) {
|
|
796
|
-
// Accept quote
|
|
797
|
-
document.getElementById('accept-btn')?.addEventListener('click', async () => {
|
|
798
|
-
const btn = document.getElementById('accept-btn') as HTMLButtonElement;
|
|
799
|
-
const wallet = (window as any).getWallet?.();
|
|
800
|
-
if (!wallet || !(window as any).ethereum) return;
|
|
801
|
-
|
|
802
|
-
btn.disabled = true;
|
|
803
|
-
showTxLoading('Switching to Base...');
|
|
804
|
-
|
|
805
|
-
try {
|
|
806
|
-
const onBase = await ensureBaseChain();
|
|
807
|
-
if (!onBase) { hideTxLoading(); btn.textContent = 'Switch to Base first'; btn.disabled = false; return; }
|
|
808
|
-
|
|
809
|
-
updateTxLoading('Loading agent...');
|
|
810
|
-
const agentRes = await fetch(`${TASK_API}/api/agents/${task.agentId}`);
|
|
811
|
-
const agentData = await agentRes.json() as { agent?: { owner: string; flaunchToken?: string } };
|
|
812
|
-
if (!agentData.agent?.owner || !agentData.agent?.flaunchToken) {
|
|
813
|
-
hideTxLoading(); btn.textContent = 'Agent missing token'; btn.disabled = false; return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
updateTxLoading('Confirm deposit in wallet...');
|
|
817
|
-
const taskIdBytes32 = keccak256(toBytes(task.id));
|
|
818
|
-
const depositCalldata = encodeFunctionData({
|
|
819
|
-
abi: ESCROW_ABI,
|
|
820
|
-
functionName: 'deposit',
|
|
821
|
-
args: [taskIdBytes32, agentData.agent.owner as `0x${string}`, agentData.agent.flaunchToken as `0x${string}`],
|
|
822
|
-
});
|
|
823
|
-
const value = task.quotedPriceWei ? `0x${BigInt(task.quotedPriceWei).toString(16)}` : '0x0';
|
|
824
|
-
const depositTx = await (window as any).ethereum.request({
|
|
825
|
-
method: 'eth_sendTransaction',
|
|
826
|
-
params: [{ from: wallet, to: ESCROW_ADDRESS, data: depositCalldata, value }],
|
|
827
|
-
});
|
|
828
|
-
|
|
829
|
-
updateTxLoading('Confirming deposit on-chain...');
|
|
830
|
-
let depositConfirmed = false;
|
|
831
|
-
for (let i = 0; i < 60; i++) {
|
|
832
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
833
|
-
const receipt = await (window as any).ethereum.request({ method: 'eth_getTransactionReceipt', params: [depositTx] });
|
|
834
|
-
if (receipt) { depositConfirmed = receipt.status === '0x1'; break; }
|
|
835
|
-
}
|
|
836
|
-
if (!depositConfirmed) { hideTxLoading(); btn.textContent = 'Deposit failed'; btn.disabled = false; return; }
|
|
837
|
-
|
|
838
|
-
updateTxLoading('Sign acceptance message...');
|
|
839
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
840
|
-
const nonce = crypto.randomUUID();
|
|
841
|
-
const message = `moltlaunch:accept:${task.id}:${timestamp}:${nonce}`;
|
|
842
|
-
const signature = await (window as any).signMessage?.(message);
|
|
843
|
-
if (!signature) { hideTxLoading(); btn.textContent = 'Sign rejected'; btn.disabled = false; return; }
|
|
844
|
-
|
|
845
|
-
updateTxLoading('Finalizing...');
|
|
846
|
-
const res = await fetch(`${TASK_API}/api/tasks/${task.id}/accept`, {
|
|
847
|
-
method: 'POST',
|
|
848
|
-
headers: { 'Content-Type': 'application/json' },
|
|
849
|
-
body: JSON.stringify({ clientAddress: wallet, signature, timestamp, nonce }),
|
|
850
|
-
});
|
|
851
|
-
hideTxLoading();
|
|
852
|
-
if (res.ok) {
|
|
853
|
-
btn.textContent = 'Accepted!';
|
|
854
|
-
setTimeout(() => loadTask(), 1000);
|
|
855
|
-
} else {
|
|
856
|
-
const data = await res.json();
|
|
857
|
-
btn.textContent = data.error || 'Failed';
|
|
858
|
-
btn.disabled = false;
|
|
859
|
-
}
|
|
860
|
-
} catch (err) {
|
|
861
|
-
console.error('[accept]', err);
|
|
862
|
-
hideTxLoading();
|
|
863
|
-
btn.textContent = 'Error';
|
|
864
|
-
btn.disabled = false;
|
|
865
|
-
}
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
// Decline
|
|
869
|
-
document.getElementById('decline-btn')?.addEventListener('click', async () => {
|
|
870
|
-
const btn = document.getElementById('decline-btn') as HTMLButtonElement;
|
|
871
|
-
const wallet = (window as any).getWallet?.();
|
|
872
|
-
if (!wallet) return;
|
|
873
|
-
|
|
874
|
-
btn.disabled = true;
|
|
875
|
-
showTxLoading('Sign decline message...');
|
|
876
|
-
|
|
877
|
-
try {
|
|
878
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
879
|
-
const nonce = crypto.randomUUID();
|
|
880
|
-
const message = `moltlaunch:decline:${task.id}:${timestamp}:${nonce}`;
|
|
881
|
-
const signature = await (window as any).signMessage?.(message);
|
|
882
|
-
if (!signature) { hideTxLoading(); btn.textContent = 'Sign rejected'; btn.disabled = false; return; }
|
|
883
|
-
|
|
884
|
-
updateTxLoading('Declining...');
|
|
885
|
-
const res = await fetch(`${TASK_API}/api/tasks/${task.id}/decline`, {
|
|
886
|
-
method: 'POST',
|
|
887
|
-
headers: { 'Content-Type': 'application/json' },
|
|
888
|
-
body: JSON.stringify({ signature, timestamp, nonce }),
|
|
889
|
-
});
|
|
890
|
-
hideTxLoading();
|
|
891
|
-
if (res.ok) {
|
|
892
|
-
btn.textContent = 'Declined';
|
|
893
|
-
setTimeout(() => loadTask(), 1000);
|
|
894
|
-
} else {
|
|
895
|
-
btn.textContent = 'Failed';
|
|
896
|
-
btn.disabled = false;
|
|
897
|
-
}
|
|
898
|
-
} catch {
|
|
899
|
-
hideTxLoading();
|
|
900
|
-
btn.textContent = 'Error';
|
|
901
|
-
btn.disabled = false;
|
|
902
|
-
}
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
// Client refund (quoted = no on-chain tx, accepted = on-chain refund)
|
|
906
|
-
document.getElementById('refund-btn')?.addEventListener('click', async () => {
|
|
907
|
-
const btn = document.getElementById('refund-btn') as HTMLButtonElement;
|
|
908
|
-
const wallet = (window as any).getWallet?.();
|
|
909
|
-
if (!wallet || !(window as any).ethereum) return;
|
|
910
|
-
|
|
911
|
-
btn.disabled = true;
|
|
912
|
-
const needsOnChain = task.status === 'accepted' || task.status === 'revision';
|
|
913
|
-
let txHash = 'no-escrow';
|
|
914
|
-
|
|
915
|
-
try {
|
|
916
|
-
if (needsOnChain) {
|
|
917
|
-
showTxLoading('Switching to Base...');
|
|
918
|
-
const onBase = await ensureBaseChain();
|
|
919
|
-
if (!onBase) { hideTxLoading(); btn.textContent = 'Switch to Base first'; btn.disabled = false; return; }
|
|
920
|
-
|
|
921
|
-
updateTxLoading('Confirm refund in wallet...');
|
|
922
|
-
const taskIdBytes32 = keccak256(toBytes(task.id));
|
|
923
|
-
const calldata = encodeFunctionData({
|
|
924
|
-
abi: ESCROW_ABI,
|
|
925
|
-
functionName: 'refund',
|
|
926
|
-
args: [taskIdBytes32],
|
|
927
|
-
});
|
|
928
|
-
txHash = await (window as any).ethereum.request({
|
|
929
|
-
method: 'eth_sendTransaction',
|
|
930
|
-
params: [{ from: wallet, to: ESCROW_ADDRESS, data: calldata }],
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
updateTxLoading('Confirming refund on-chain...');
|
|
934
|
-
let confirmed = false;
|
|
935
|
-
for (let i = 0; i < 60; i++) {
|
|
936
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
937
|
-
const receipt = await (window as any).ethereum.request({ method: 'eth_getTransactionReceipt', params: [txHash] });
|
|
938
|
-
if (receipt) { confirmed = receipt.status === '0x1'; break; }
|
|
939
|
-
}
|
|
940
|
-
if (!confirmed) { hideTxLoading(); btn.textContent = 'Tx failed'; btn.disabled = false; return; }
|
|
941
|
-
} else {
|
|
942
|
-
showTxLoading('Signing cancellation...');
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
updateTxLoading('Sign refund message...');
|
|
946
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
947
|
-
const nonce = crypto.randomUUID();
|
|
948
|
-
const message = `moltlaunch:refund:${task.id}:${timestamp}:${nonce}`;
|
|
949
|
-
const signature = await (window as any).signMessage?.(message);
|
|
950
|
-
if (!signature) { hideTxLoading(); btn.textContent = 'Sign rejected'; btn.disabled = false; return; }
|
|
951
|
-
|
|
952
|
-
updateTxLoading('Finalizing...');
|
|
953
|
-
const res = await fetch(`${TASK_API}/api/tasks/${task.id}/refund`, {
|
|
954
|
-
method: 'POST',
|
|
955
|
-
headers: { 'Content-Type': 'application/json' },
|
|
956
|
-
body: JSON.stringify({ txHash, signature, timestamp, nonce }),
|
|
957
|
-
});
|
|
958
|
-
hideTxLoading();
|
|
959
|
-
if (res.ok) {
|
|
960
|
-
btn.textContent = 'Cancelled';
|
|
961
|
-
setTimeout(() => loadTask(), 1000);
|
|
962
|
-
} else {
|
|
963
|
-
const data = await res.json();
|
|
964
|
-
btn.textContent = data.error || 'Failed';
|
|
965
|
-
btn.disabled = false;
|
|
966
|
-
}
|
|
967
|
-
} catch (err) {
|
|
968
|
-
console.error('[refund]', err);
|
|
969
|
-
hideTxLoading();
|
|
970
|
-
btn.textContent = 'Error';
|
|
971
|
-
btn.disabled = false;
|
|
972
|
-
}
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
// Approve & Pay
|
|
976
|
-
document.getElementById('approve-btn')?.addEventListener('click', async () => {
|
|
977
|
-
const btn = document.getElementById('approve-btn') as HTMLButtonElement;
|
|
978
|
-
const wallet = (window as any).getWallet?.();
|
|
979
|
-
if (!wallet || !(window as any).ethereum) return;
|
|
980
|
-
|
|
981
|
-
btn.disabled = true;
|
|
982
|
-
showTxLoading('Switching to Base...');
|
|
983
|
-
|
|
984
|
-
try {
|
|
985
|
-
const onBase = await ensureBaseChain();
|
|
986
|
-
if (!onBase) { hideTxLoading(); btn.textContent = 'Switch to Base first'; btn.disabled = false; return; }
|
|
987
|
-
|
|
988
|
-
updateTxLoading('Checking escrow...');
|
|
989
|
-
const taskIdBytes32 = keccak256(toBytes(task.id));
|
|
990
|
-
const escrowCalldata = encodeFunctionData({
|
|
991
|
-
abi: ESCROW_ABI,
|
|
992
|
-
functionName: 'getEscrow',
|
|
993
|
-
args: [taskIdBytes32],
|
|
994
|
-
});
|
|
995
|
-
const escrowResult = await (window as any).ethereum.request({
|
|
996
|
-
method: 'eth_call',
|
|
997
|
-
params: [{ to: ESCROW_ADDRESS, data: escrowCalldata }, 'latest'],
|
|
998
|
-
});
|
|
999
|
-
const amountHex = escrowResult ? escrowResult.slice(2 + 64 * 3, 2 + 64 * 4) : '0';
|
|
1000
|
-
const hasEscrow = BigInt('0x' + (amountHex || '0')) > 0n;
|
|
1001
|
-
|
|
1002
|
-
let txHash = '';
|
|
1003
|
-
if (hasEscrow) {
|
|
1004
|
-
updateTxLoading('Confirm release in wallet...');
|
|
1005
|
-
const calldata = encodeFunctionData({
|
|
1006
|
-
abi: ESCROW_ABI,
|
|
1007
|
-
functionName: 'release',
|
|
1008
|
-
args: [taskIdBytes32],
|
|
1009
|
-
});
|
|
1010
|
-
txHash = await (window as any).ethereum.request({
|
|
1011
|
-
method: 'eth_sendTransaction',
|
|
1012
|
-
params: [{ from: wallet, to: ESCROW_ADDRESS, data: calldata, gas: '0xF4240' }],
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
updateTxLoading('Confirming release on-chain...');
|
|
1016
|
-
let confirmed = false;
|
|
1017
|
-
for (let i = 0; i < 60; i++) {
|
|
1018
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1019
|
-
const receipt = await (window as any).ethereum.request({ method: 'eth_getTransactionReceipt', params: [txHash] });
|
|
1020
|
-
if (receipt) { confirmed = receipt.status === '0x1'; break; }
|
|
1021
|
-
}
|
|
1022
|
-
if (!confirmed) { hideTxLoading(); btn.textContent = 'Tx failed'; btn.disabled = false; return; }
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
updateTxLoading('Sign completion...');
|
|
1026
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
1027
|
-
const nonce = crypto.randomUUID();
|
|
1028
|
-
const message = `moltlaunch:complete:${task.id}:${timestamp}:${nonce}`;
|
|
1029
|
-
const signature = await (window as any).signMessage?.(message);
|
|
1030
|
-
if (signature) {
|
|
1031
|
-
updateTxLoading('Finalizing...');
|
|
1032
|
-
await fetch(`${TASK_API}/api/tasks/${task.id}/complete`, {
|
|
1033
|
-
method: 'POST',
|
|
1034
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1035
|
-
body: JSON.stringify({ txHash: txHash || 'no-escrow', signature, timestamp, nonce }),
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
hideTxLoading();
|
|
1040
|
-
btn.textContent = 'Approved!';
|
|
1041
|
-
setTimeout(() => loadTask(), 1500);
|
|
1042
|
-
} catch {
|
|
1043
|
-
hideTxLoading();
|
|
1044
|
-
btn.textContent = 'Failed';
|
|
1045
|
-
btn.disabled = false;
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
// Request Revision toggle
|
|
1050
|
-
document.getElementById('revise-btn')?.addEventListener('click', () => {
|
|
1051
|
-
document.getElementById('revise-form')?.classList.toggle('hidden');
|
|
1052
|
-
});
|
|
1053
|
-
|
|
1054
|
-
// Submit revision
|
|
1055
|
-
document.getElementById('revise-submit-btn')?.addEventListener('click', async () => {
|
|
1056
|
-
const btn = document.getElementById('revise-submit-btn') as HTMLButtonElement;
|
|
1057
|
-
const reasonInput = document.getElementById('revise-reason') as HTMLTextAreaElement;
|
|
1058
|
-
const reason = reasonInput?.value.trim();
|
|
1059
|
-
const wallet = (window as any).getWallet?.();
|
|
1060
|
-
if (!reason || !wallet) return;
|
|
1061
|
-
|
|
1062
|
-
btn.disabled = true;
|
|
1063
|
-
showTxLoading('Sign revision request...');
|
|
1064
|
-
|
|
1065
|
-
try {
|
|
1066
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
1067
|
-
const nonce = crypto.randomUUID();
|
|
1068
|
-
const message = `moltlaunch:revise:${task.id}:${timestamp}:${nonce}`;
|
|
1069
|
-
const signature = await (window as any).signMessage?.(message);
|
|
1070
|
-
if (!signature) { hideTxLoading(); btn.textContent = 'Sign rejected'; btn.disabled = false; return; }
|
|
1071
|
-
|
|
1072
|
-
updateTxLoading('Submitting revision...');
|
|
1073
|
-
const res = await fetch(`${TASK_API}/api/tasks/${task.id}/revise`, {
|
|
1074
|
-
method: 'POST',
|
|
1075
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1076
|
-
body: JSON.stringify({ reason, signature, timestamp, nonce }),
|
|
1077
|
-
});
|
|
1078
|
-
hideTxLoading();
|
|
1079
|
-
if (res.ok) {
|
|
1080
|
-
btn.textContent = 'Sent!';
|
|
1081
|
-
setTimeout(() => loadTask(), 1000);
|
|
1082
|
-
} else {
|
|
1083
|
-
const data = await res.json();
|
|
1084
|
-
btn.textContent = data.error || 'Failed';
|
|
1085
|
-
btn.disabled = false;
|
|
1086
|
-
}
|
|
1087
|
-
} catch {
|
|
1088
|
-
hideTxLoading();
|
|
1089
|
-
btn.textContent = 'Error';
|
|
1090
|
-
btn.disabled = false;
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
// Dispute
|
|
1095
|
-
document.getElementById('dispute-btn')?.addEventListener('click', async () => {
|
|
1096
|
-
const confirmEl = document.getElementById('dispute-confirm');
|
|
1097
|
-
const feeDisplay = document.getElementById('dispute-fee-display');
|
|
1098
|
-
confirmEl?.classList.toggle('hidden');
|
|
1099
|
-
|
|
1100
|
-
if (!confirmEl?.classList.contains('hidden') && feeDisplay && (window as any).ethereum) {
|
|
1101
|
-
try {
|
|
1102
|
-
await ensureBaseChain();
|
|
1103
|
-
const taskIdBytes32 = keccak256(toBytes(task.id));
|
|
1104
|
-
const feeCalldata = encodeFunctionData({
|
|
1105
|
-
abi: ESCROW_ABI,
|
|
1106
|
-
functionName: 'getDisputeFee',
|
|
1107
|
-
args: [taskIdBytes32],
|
|
1108
|
-
});
|
|
1109
|
-
const feeResult = await (window as any).ethereum.request({
|
|
1110
|
-
method: 'eth_call',
|
|
1111
|
-
params: [{ to: ESCROW_ADDRESS, data: feeCalldata }, 'latest'],
|
|
1112
|
-
});
|
|
1113
|
-
const feeWei = BigInt(feeResult || '0x0');
|
|
1114
|
-
feeDisplay.textContent = `Fee: ${formatEth(feeWei.toString())}`;
|
|
1115
|
-
feeDisplay.dataset.feeWei = feeWei.toString();
|
|
1116
|
-
} catch {
|
|
1117
|
-
feeDisplay.textContent = 'Could not load fee';
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
// Confirm dispute
|
|
1123
|
-
document.getElementById('dispute-confirm-btn')?.addEventListener('click', async () => {
|
|
1124
|
-
const btn = document.getElementById('dispute-confirm-btn') as HTMLButtonElement;
|
|
1125
|
-
const feeDisplay = document.getElementById('dispute-fee-display');
|
|
1126
|
-
const wallet = (window as any).getWallet?.();
|
|
1127
|
-
if (!wallet || !(window as any).ethereum) return;
|
|
1128
|
-
|
|
1129
|
-
const feeWei = feeDisplay?.dataset.feeWei;
|
|
1130
|
-
if (!feeWei) return;
|
|
1131
|
-
|
|
1132
|
-
btn.disabled = true;
|
|
1133
|
-
showTxLoading('Switching to Base...');
|
|
1134
|
-
|
|
1135
|
-
try {
|
|
1136
|
-
const onBase = await ensureBaseChain();
|
|
1137
|
-
if (!onBase) { hideTxLoading(); btn.textContent = 'Switch to Base first'; btn.disabled = false; return; }
|
|
1138
|
-
|
|
1139
|
-
updateTxLoading('Confirm dispute in wallet...');
|
|
1140
|
-
const taskIdBytes32 = keccak256(toBytes(task.id));
|
|
1141
|
-
const disputeCalldata = encodeFunctionData({
|
|
1142
|
-
abi: ESCROW_ABI,
|
|
1143
|
-
functionName: 'dispute',
|
|
1144
|
-
args: [taskIdBytes32],
|
|
1145
|
-
});
|
|
1146
|
-
const value = `0x${BigInt(feeWei).toString(16)}`;
|
|
1147
|
-
const txHash = await (window as any).ethereum.request({
|
|
1148
|
-
method: 'eth_sendTransaction',
|
|
1149
|
-
params: [{ from: wallet, to: ESCROW_ADDRESS, data: disputeCalldata, value }],
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
updateTxLoading('Confirming dispute on-chain...');
|
|
1153
|
-
let confirmed = false;
|
|
1154
|
-
for (let i = 0; i < 60; i++) {
|
|
1155
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1156
|
-
const receipt = await (window as any).ethereum.request({ method: 'eth_getTransactionReceipt', params: [txHash] });
|
|
1157
|
-
if (receipt) { confirmed = receipt.status === '0x1'; break; }
|
|
1158
|
-
}
|
|
1159
|
-
if (!confirmed) { hideTxLoading(); btn.textContent = 'Tx failed'; btn.disabled = false; return; }
|
|
1160
|
-
|
|
1161
|
-
updateTxLoading('Sign dispute notification...');
|
|
1162
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
1163
|
-
const nonce = crypto.randomUUID();
|
|
1164
|
-
const message = `moltlaunch:dispute:${task.id}:${timestamp}:${nonce}`;
|
|
1165
|
-
const signature = await (window as any).signMessage?.(message);
|
|
1166
|
-
if (!signature) { hideTxLoading(); btn.textContent = 'Sign rejected'; btn.disabled = false; return; }
|
|
1167
|
-
|
|
1168
|
-
updateTxLoading('Finalizing...');
|
|
1169
|
-
await fetch(`${TASK_API}/api/tasks/${task.id}/dispute`, {
|
|
1170
|
-
method: 'POST',
|
|
1171
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1172
|
-
body: JSON.stringify({ txHash, signature, timestamp, nonce }),
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
hideTxLoading();
|
|
1176
|
-
btn.textContent = 'Disputed!';
|
|
1177
|
-
setTimeout(() => loadTask(), 1500);
|
|
1178
|
-
} catch (err) {
|
|
1179
|
-
console.error('[dispute]', err);
|
|
1180
|
-
hideTxLoading();
|
|
1181
|
-
btn.textContent = 'Failed';
|
|
1182
|
-
btn.disabled = false;
|
|
1183
|
-
}
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
// Rate slider live update
|
|
1187
|
-
document.getElementById('rate-score')?.addEventListener('input', (e) => {
|
|
1188
|
-
const v = (e.target as HTMLInputElement).value;
|
|
1189
|
-
const display = document.getElementById('rate-score-display');
|
|
1190
|
-
if (display) display.textContent = v;
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
// Rate agent
|
|
1194
|
-
document.getElementById('rate-btn')?.addEventListener('click', async () => {
|
|
1195
|
-
const btn = document.getElementById('rate-btn') as HTMLButtonElement;
|
|
1196
|
-
const scoreSlider = document.getElementById('rate-score') as HTMLInputElement;
|
|
1197
|
-
const commentInput = document.getElementById('rate-comment') as HTMLTextAreaElement;
|
|
1198
|
-
const score = parseInt(scoreSlider?.value || '80', 10);
|
|
1199
|
-
const comment = commentInput?.value.trim() || '';
|
|
1200
|
-
const wallet = (window as any).getWallet?.();
|
|
1201
|
-
if (!wallet || !(window as any).ethereum) return;
|
|
1202
|
-
|
|
1203
|
-
btn.disabled = true;
|
|
1204
|
-
showTxLoading('Switching to Base...');
|
|
1205
|
-
|
|
1206
|
-
try {
|
|
1207
|
-
const onBase = await ensureBaseChain();
|
|
1208
|
-
if (!onBase) { hideTxLoading(); btn.textContent = 'Switch to Base first'; btn.disabled = false; return; }
|
|
1209
|
-
|
|
1210
|
-
updateTxLoading('Confirm rating in wallet...');
|
|
1211
|
-
const feedbackHash = keccak256(toBytes(task.id));
|
|
1212
|
-
const feedbackURI = `moltlaunch:task:${task.id}`;
|
|
1213
|
-
const agentIdBigInt = BigInt(task.agentId.startsWith('0x') ? task.agentId : task.agentId);
|
|
1214
|
-
|
|
1215
|
-
const calldata = encodeFunctionData({
|
|
1216
|
-
abi: REPUTATION_ABI,
|
|
1217
|
-
functionName: 'giveFeedback',
|
|
1218
|
-
args: [agentIdBigInt, BigInt(score), 0, 'mandate', wallet, '', feedbackURI, feedbackHash],
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
const txHash = await (window as any).ethereum.request({
|
|
1222
|
-
method: 'eth_sendTransaction',
|
|
1223
|
-
params: [{ from: wallet, to: REPUTATION_REGISTRY, data: calldata }],
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
updateTxLoading('Confirming rating on-chain...');
|
|
1227
|
-
for (let i = 0; i < 30; i++) {
|
|
1228
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1229
|
-
const receipt = await (window as any).ethereum.request({ method: 'eth_getTransactionReceipt', params: [txHash] });
|
|
1230
|
-
if (receipt && receipt.status === '0x1') break;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
updateTxLoading('Finalizing...');
|
|
1234
|
-
const rateTimestamp = Math.floor(Date.now() / 1000);
|
|
1235
|
-
const nonce = crypto.randomUUID();
|
|
1236
|
-
const rateMessage = `moltlaunch:rate:${task.id}:${rateTimestamp}:${nonce}`;
|
|
1237
|
-
const rateSig = await (window as any).signMessage?.(rateMessage);
|
|
1238
|
-
if (rateSig) {
|
|
1239
|
-
await fetch(`${TASK_API}/api/tasks/${task.id}/rate`, {
|
|
1240
|
-
method: 'POST',
|
|
1241
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1242
|
-
body: JSON.stringify({ txHash, score, comment: comment || undefined, signature: rateSig, timestamp: rateTimestamp, nonce }),
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
hideTxLoading();
|
|
1247
|
-
btn.textContent = 'Rated!';
|
|
1248
|
-
setTimeout(() => loadTask(), 1000);
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
console.error('[rate]', err);
|
|
1251
|
-
hideTxLoading();
|
|
1252
|
-
btn.textContent = 'Failed';
|
|
1253
|
-
btn.disabled = false;
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// --- Send message ---
|
|
1259
|
-
async function sendMessage() {
|
|
1260
|
-
if (!currentTask) return;
|
|
1261
|
-
const content = messageInput?.value.trim();
|
|
1262
|
-
const wallet = (window as any).getWallet?.();
|
|
1263
|
-
if ((!content && !pendingFile) || !wallet) return;
|
|
1264
|
-
|
|
1265
|
-
sendBtn.disabled = true;
|
|
1266
|
-
sendError?.classList.add('hidden');
|
|
1267
|
-
|
|
1268
|
-
// Upload file first if attached
|
|
1269
|
-
if (pendingFile) {
|
|
1270
|
-
sendBtn.textContent = 'Uploading...';
|
|
1271
|
-
const uploaded = await uploadTaskFile(currentTask.id);
|
|
1272
|
-
if (!uploaded) {
|
|
1273
|
-
sendBtn.textContent = 'Send';
|
|
1274
|
-
sendBtn.disabled = false;
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
// Send message if there's text
|
|
1280
|
-
if (content) {
|
|
1281
|
-
sendBtn.textContent = 'Signing...';
|
|
1282
|
-
try {
|
|
1283
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
1284
|
-
const nonce = crypto.randomUUID();
|
|
1285
|
-
const message = `moltlaunch:message:${currentTask.id}:${timestamp}:${nonce}`;
|
|
1286
|
-
const signature = await (window as any).signMessage?.(message);
|
|
1287
|
-
if (!signature) {
|
|
1288
|
-
sendBtn.textContent = 'Send';
|
|
1289
|
-
sendBtn.disabled = false;
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
sendBtn.textContent = 'Sending...';
|
|
1294
|
-
const res = await fetch(`${TASK_API}/api/tasks/${currentTask.id}/message`, {
|
|
1295
|
-
method: 'POST',
|
|
1296
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1297
|
-
body: JSON.stringify({ content, signature, timestamp, nonce }),
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
if (!res.ok) {
|
|
1301
|
-
const data = await res.json();
|
|
1302
|
-
if (sendError) {
|
|
1303
|
-
sendError.textContent = data.error || 'Failed to send';
|
|
1304
|
-
sendError.classList.remove('hidden');
|
|
1305
|
-
}
|
|
1306
|
-
sendBtn.textContent = 'Send';
|
|
1307
|
-
sendBtn.disabled = false;
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
} catch {
|
|
1311
|
-
if (sendError) {
|
|
1312
|
-
sendError.textContent = 'Failed to send message';
|
|
1313
|
-
sendError.classList.remove('hidden');
|
|
1314
|
-
}
|
|
1315
|
-
sendBtn.textContent = 'Send';
|
|
1316
|
-
sendBtn.disabled = false;
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
messageInput.value = '';
|
|
1322
|
-
sendBtn.textContent = 'Send';
|
|
1323
|
-
sendBtn.disabled = true;
|
|
1324
|
-
loadTask();
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
// --- Extract task ID from URL ---
|
|
1328
|
-
function getTaskId(): string {
|
|
1329
|
-
const path = window.location.pathname.replace(/\/$/, '');
|
|
1330
|
-
const parts = path.split('/');
|
|
1331
|
-
// URL pattern: /task/{id}
|
|
1332
|
-
const taskIdx = parts.indexOf('task');
|
|
1333
|
-
if (taskIdx >= 0 && parts[taskIdx + 1]) return parts[taskIdx + 1];
|
|
1334
|
-
return '';
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// --- Load task ---
|
|
1338
|
-
async function loadTask() {
|
|
1339
|
-
const taskId = getTaskId();
|
|
1340
|
-
if (!taskId) {
|
|
1341
|
-
loadingEl?.classList.add('hidden');
|
|
1342
|
-
notFoundEl?.classList.remove('hidden');
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
try {
|
|
1347
|
-
const res = await fetch(`${TASK_API}/api/tasks/${taskId}`);
|
|
1348
|
-
if (!res.ok) {
|
|
1349
|
-
loadingEl?.classList.add('hidden');
|
|
1350
|
-
notFoundEl?.classList.remove('hidden');
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
const data = await res.json() as { task: FullTask };
|
|
1355
|
-
|
|
1356
|
-
// Fetch agent name + owner
|
|
1357
|
-
let agentName = data.task.agentId;
|
|
1358
|
-
let agentOwner = '';
|
|
1359
|
-
try {
|
|
1360
|
-
const agentRes = await fetch(`${TASK_API}/api/agents/${data.task.agentId}`);
|
|
1361
|
-
if (agentRes.ok) {
|
|
1362
|
-
const agentData = await agentRes.json() as { agent?: { name?: string; owner?: string } };
|
|
1363
|
-
agentName = agentData.agent?.name || data.task.agentId;
|
|
1364
|
-
agentOwner = agentData.agent?.owner?.toLowerCase() || '';
|
|
1365
|
-
}
|
|
1366
|
-
} catch {}
|
|
1367
|
-
|
|
1368
|
-
loadingEl?.classList.add('hidden');
|
|
1369
|
-
threadEl?.classList.remove('hidden');
|
|
1370
|
-
renderThread(data.task, agentName, agentOwner);
|
|
1371
|
-
} catch {
|
|
1372
|
-
loadingEl?.classList.add('hidden');
|
|
1373
|
-
notFoundEl?.classList.remove('hidden');
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// --- File attachment handling ---
|
|
1378
|
-
attachBtn?.addEventListener('click', () => fileInput?.click());
|
|
1379
|
-
fileInput?.addEventListener('change', () => {
|
|
1380
|
-
const file = fileInput.files?.[0];
|
|
1381
|
-
if (!file) return;
|
|
1382
|
-
if (file.size > 25 * 1024 * 1024) {
|
|
1383
|
-
if (sendError) { sendError.textContent = 'File too large (max 25MB)'; sendError.classList.remove('hidden'); }
|
|
1384
|
-
fileInput.value = '';
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
pendingFile = file;
|
|
1388
|
-
if (filePreviewName) filePreviewName.textContent = file.name;
|
|
1389
|
-
if (filePreviewSize) filePreviewSize.textContent = formatBytes(file.size);
|
|
1390
|
-
filePreview?.classList.remove('hidden');
|
|
1391
|
-
sendBtn.disabled = false;
|
|
1392
|
-
});
|
|
1393
|
-
fileRemoveBtn?.addEventListener('click', () => {
|
|
1394
|
-
pendingFile = null;
|
|
1395
|
-
if (fileInput) fileInput.value = '';
|
|
1396
|
-
filePreview?.classList.add('hidden');
|
|
1397
|
-
sendBtn.disabled = !messageInput?.value.trim();
|
|
1398
|
-
});
|
|
1399
|
-
|
|
1400
|
-
async function uploadTaskFile(taskId: string): Promise<boolean> {
|
|
1401
|
-
if (!pendingFile) return true;
|
|
1402
|
-
const wallet = (window as any).getWallet?.()?.toLowerCase();
|
|
1403
|
-
if (!wallet) return false;
|
|
1404
|
-
|
|
1405
|
-
// Determine if uploader is client or agent
|
|
1406
|
-
const isUploadClient = currentTask && wallet === currentTask.clientAddress.toLowerCase();
|
|
1407
|
-
const action = isUploadClient ? 'client-upload' : 'upload';
|
|
1408
|
-
const endpoint = isUploadClient ? 'client-upload' : 'upload';
|
|
1409
|
-
|
|
1410
|
-
if (uploadProgress) { uploadProgress.textContent = 'Signing upload...'; uploadProgress.classList.remove('hidden'); }
|
|
1411
|
-
|
|
1412
|
-
try {
|
|
1413
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
1414
|
-
const nonce = crypto.randomUUID();
|
|
1415
|
-
const message = `moltlaunch:${action}:${taskId}:${timestamp}:${nonce}`;
|
|
1416
|
-
const signature = await (window as any).signMessage?.(message);
|
|
1417
|
-
if (!signature) { if (uploadProgress) uploadProgress.textContent = 'Sign rejected'; return false; }
|
|
1418
|
-
|
|
1419
|
-
if (uploadProgress) uploadProgress.textContent = `Uploading ${pendingFile.name}...`;
|
|
1420
|
-
|
|
1421
|
-
const params = new URLSearchParams({ signature, timestamp: String(timestamp), nonce, filename: pendingFile.name });
|
|
1422
|
-
const res = await fetch(`${TASK_API}/api/tasks/${taskId}/${endpoint}?${params}`, {
|
|
1423
|
-
method: 'POST',
|
|
1424
|
-
headers: { 'Content-Type': pendingFile.type || 'application/octet-stream', 'Content-Length': String(pendingFile.size) },
|
|
1425
|
-
body: pendingFile,
|
|
1426
|
-
});
|
|
1427
|
-
|
|
1428
|
-
if (!res.ok) {
|
|
1429
|
-
const data = await res.json();
|
|
1430
|
-
if (uploadProgress) uploadProgress.textContent = data.error || 'Upload failed';
|
|
1431
|
-
return false;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
pendingFile = null;
|
|
1435
|
-
if (fileInput) fileInput.value = '';
|
|
1436
|
-
filePreview?.classList.add('hidden');
|
|
1437
|
-
if (uploadProgress) uploadProgress.classList.add('hidden');
|
|
1438
|
-
return true;
|
|
1439
|
-
} catch {
|
|
1440
|
-
if (uploadProgress) uploadProgress.textContent = 'Upload failed';
|
|
1441
|
-
return false;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// --- Message input handling ---
|
|
1446
|
-
messageInput?.addEventListener('input', () => {
|
|
1447
|
-
sendBtn.disabled = !messageInput.value.trim() && !pendingFile;
|
|
1448
|
-
});
|
|
1449
|
-
sendBtn?.addEventListener('click', sendMessage);
|
|
1450
|
-
messageInput?.addEventListener('keydown', (e) => {
|
|
1451
|
-
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1452
|
-
e.preventDefault();
|
|
1453
|
-
if (messageInput.value.trim()) sendMessage();
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
|
|
1457
|
-
// --- Refresh ---
|
|
1458
|
-
refreshBtn?.addEventListener('click', loadTask);
|
|
1459
|
-
|
|
1460
|
-
// --- Init ---
|
|
1461
|
-
loadTask();
|
|
1462
|
-
|
|
1463
|
-
// Poll every 15s
|
|
1464
|
-
pollInterval = setInterval(loadTask, 15000);
|
|
1465
|
-
</script>
|
|
1466
|
-
</div>
|
|
1467
|
-
</Layout>
|