slapify 0.0.13 → 0.0.16
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 +342 -258
- package/dist/ai/interpreter.d.ts +13 -0
- package/dist/ai/interpreter.d.ts.map +1 -1
- package/dist/ai/interpreter.js +43 -5
- package/dist/ai/interpreter.js.map +1 -1
- package/dist/cli.js +500 -152
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/perf/audit.d.ts +215 -0
- package/dist/perf/audit.d.ts.map +1 -0
- package/dist/perf/audit.js +635 -0
- package/dist/perf/audit.js.map +1 -0
- package/dist/report/generator.d.ts +1 -0
- package/dist/report/generator.d.ts.map +1 -1
- package/dist/report/generator.js +92 -0
- package/dist/report/generator.js.map +1 -1
- package/dist/runner/index.d.ts +14 -1
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/index.js +195 -13
- package/dist/runner/index.js.map +1 -1
- package/dist/task/index.d.ts +5 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +4 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/report.d.ts +9 -0
- package/dist/task/report.d.ts.map +1 -0
- package/dist/task/report.js +740 -0
- package/dist/task/report.js.map +1 -0
- package/dist/task/runner.d.ts +3 -0
- package/dist/task/runner.d.ts.map +1 -0
- package/dist/task/runner.js +1362 -0
- package/dist/task/runner.js.map +1 -0
- package/dist/task/session.d.ts +18 -0
- package/dist/task/session.d.ts.map +1 -0
- package/dist/task/session.js +153 -0
- package/dist/task/session.js.map +1 -0
- package/dist/task/tools.d.ts +253 -0
- package/dist/task/tools.d.ts.map +1 -0
- package/dist/task/tools.js +258 -0
- package/dist/task/tools.js.map +1 -0
- package/dist/task/types.d.ts +153 -0
- package/dist/task/types.d.ts.map +1 -0
- package/dist/task/types.js +2 -0
- package/dist/task/types.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -13
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
function statusColor(status) {
|
|
4
|
+
switch (status) {
|
|
5
|
+
case "completed":
|
|
6
|
+
return "#22c55e";
|
|
7
|
+
case "failed":
|
|
8
|
+
return "#ef4444";
|
|
9
|
+
case "scheduled":
|
|
10
|
+
return "#3b82f6";
|
|
11
|
+
case "sleeping":
|
|
12
|
+
return "#a78bfa";
|
|
13
|
+
default:
|
|
14
|
+
return "#f59e0b";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function statusIcon(status) {
|
|
18
|
+
switch (status) {
|
|
19
|
+
case "completed":
|
|
20
|
+
return "✅";
|
|
21
|
+
case "failed":
|
|
22
|
+
return "❌";
|
|
23
|
+
case "scheduled":
|
|
24
|
+
return "⏰";
|
|
25
|
+
case "sleeping":
|
|
26
|
+
return "😴";
|
|
27
|
+
default:
|
|
28
|
+
return "⟳";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function toolIcon(toolName) {
|
|
32
|
+
const icons = {
|
|
33
|
+
navigate: "🌐",
|
|
34
|
+
get_page_state: "📄",
|
|
35
|
+
click: "🖱️",
|
|
36
|
+
type: "⌨️",
|
|
37
|
+
press: "⌨️",
|
|
38
|
+
scroll: "↕️",
|
|
39
|
+
wait: "⏳",
|
|
40
|
+
screenshot: "📸",
|
|
41
|
+
reload: "🔄",
|
|
42
|
+
go_back: "⬅️",
|
|
43
|
+
list_credential_profiles: "🔑",
|
|
44
|
+
inject_credentials: "💉",
|
|
45
|
+
fill_login_form: "🔐",
|
|
46
|
+
save_credentials: "💾",
|
|
47
|
+
remember: "🧠",
|
|
48
|
+
recall: "🔍",
|
|
49
|
+
list_memories: "📚",
|
|
50
|
+
schedule: "⏰",
|
|
51
|
+
sleep_until: "😴",
|
|
52
|
+
done: "✅",
|
|
53
|
+
fetch_url: "⚡",
|
|
54
|
+
status_update: "📢",
|
|
55
|
+
ask_user: "🙋",
|
|
56
|
+
};
|
|
57
|
+
return icons[toolName] || "🔧";
|
|
58
|
+
}
|
|
59
|
+
function perfScoreColor(score) {
|
|
60
|
+
if (score >= 90)
|
|
61
|
+
return "#22c55e";
|
|
62
|
+
if (score >= 50)
|
|
63
|
+
return "#f59e0b";
|
|
64
|
+
return "#ef4444";
|
|
65
|
+
}
|
|
66
|
+
function perfScoreLabel(score) {
|
|
67
|
+
if (score >= 90)
|
|
68
|
+
return "Good";
|
|
69
|
+
if (score >= 50)
|
|
70
|
+
return "Needs Improvement";
|
|
71
|
+
return "Poor";
|
|
72
|
+
}
|
|
73
|
+
function vitalRating(name, value) {
|
|
74
|
+
const thresholds = {
|
|
75
|
+
fcp: [1800, 3000],
|
|
76
|
+
lcp: [2500, 4000],
|
|
77
|
+
cls: [0.1, 0.25],
|
|
78
|
+
ttfb: [800, 1800],
|
|
79
|
+
tbt: [200, 600],
|
|
80
|
+
};
|
|
81
|
+
const t = thresholds[name];
|
|
82
|
+
if (!t)
|
|
83
|
+
return "#94a3b8";
|
|
84
|
+
if (value <= t[0])
|
|
85
|
+
return "#22c55e";
|
|
86
|
+
if (value <= t[1])
|
|
87
|
+
return "#f59e0b";
|
|
88
|
+
return "#ef4444";
|
|
89
|
+
}
|
|
90
|
+
function fmt(bytes) {
|
|
91
|
+
if (bytes >= 1_048_576)
|
|
92
|
+
return `${(bytes / 1_048_576).toFixed(1)} MB`;
|
|
93
|
+
if (bytes >= 1024)
|
|
94
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
95
|
+
return `${bytes} B`;
|
|
96
|
+
}
|
|
97
|
+
function renderNetworkSection(net) {
|
|
98
|
+
if (!net)
|
|
99
|
+
return "";
|
|
100
|
+
const th = `style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:5px 10px"`;
|
|
101
|
+
const td = (color = "#e2e8f0") => `style="color:${color};padding:5px 10px;font-size:0.8rem"`;
|
|
102
|
+
// ── Summary pills ─────────────────────────────────────────────────────────
|
|
103
|
+
const pill = (label, value, color = "#94a3b8") => `<div style="background:#1e293b;border-radius:8px;padding:10px 14px;min-width:100px">
|
|
104
|
+
<div style="font-size:0.68rem;color:#64748b">${label}</div>
|
|
105
|
+
<div style="font-size:1rem;font-weight:700;color:${color};margin-top:2px">${value}</div>
|
|
106
|
+
</div>`;
|
|
107
|
+
const pills = [
|
|
108
|
+
pill("Requests", String(net.totalRequests)),
|
|
109
|
+
pill("Total Size", fmt(net.totalBytes), net.totalBytes > 2_000_000 ? "#f87171" : "#e2e8f0"),
|
|
110
|
+
pill("JavaScript", fmt(net.jsBytes), net.jsBytes > 500_000 ? "#f59e0b" : "#e2e8f0"),
|
|
111
|
+
pill("CSS", fmt(net.cssBytes)),
|
|
112
|
+
pill("Images", fmt(net.imageBytes)),
|
|
113
|
+
pill("Long Tasks", String(net.longTasks.length), net.longTasks.length > 3 ? "#f59e0b" : "#e2e8f0"),
|
|
114
|
+
pill("Blocking JS", `${net.totalBlockingMs}ms`, net.totalBlockingMs > 300
|
|
115
|
+
? "#f87171"
|
|
116
|
+
: net.totalBlockingMs > 100
|
|
117
|
+
? "#f59e0b"
|
|
118
|
+
: "#22c55e"),
|
|
119
|
+
...(net.memoryMB != null ? [pill("JS Heap", `${net.memoryMB} MB`)] : []),
|
|
120
|
+
].join("");
|
|
121
|
+
// ── Heaviest resources ────────────────────────────────────────────────────
|
|
122
|
+
const heavyRows = net.heaviestResources
|
|
123
|
+
.filter((r) => r.size > 0)
|
|
124
|
+
.map((r) => `<tr>
|
|
125
|
+
<td ${td()}>${escHtml(r.url.split("/").slice(-3).join("/"))}</td>
|
|
126
|
+
<td ${td("#94a3b8")}>${r.type}</td>
|
|
127
|
+
<td ${td(r.size > 500_000
|
|
128
|
+
? "#f87171"
|
|
129
|
+
: r.size > 100_000
|
|
130
|
+
? "#f59e0b"
|
|
131
|
+
: "#86efac")}>${fmt(r.size)}</td>
|
|
132
|
+
<td ${td("#64748b")}>${r.duration}ms</td>
|
|
133
|
+
${r.renderBlocking
|
|
134
|
+
? `<td style="color:#f87171;font-size:0.75rem;padding:5px 10px">⚠ blocking</td>`
|
|
135
|
+
: `<td></td>`}
|
|
136
|
+
</tr>`)
|
|
137
|
+
.join("");
|
|
138
|
+
const heavyTable = heavyRows
|
|
139
|
+
? `<div style="margin-top:16px">
|
|
140
|
+
<h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">Heaviest Resources</h4>
|
|
141
|
+
<table style="width:100%;border-collapse:collapse">
|
|
142
|
+
<thead><tr>
|
|
143
|
+
<th ${th}>URL</th><th ${th}>Type</th><th ${th}>Size</th><th ${th}>Load</th><th ${th}></th>
|
|
144
|
+
</tr></thead>
|
|
145
|
+
<tbody>${heavyRows}</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
</div>`
|
|
148
|
+
: "";
|
|
149
|
+
// ── API calls ─────────────────────────────────────────────────────────────
|
|
150
|
+
const apiRows = net.apiCalls
|
|
151
|
+
.slice(0, 20)
|
|
152
|
+
.map((r) => {
|
|
153
|
+
const isSlow = r.duration >= 500;
|
|
154
|
+
const statusColor = r.failed ? "#f87171" : "#86efac";
|
|
155
|
+
return `<tr>
|
|
156
|
+
<td ${td("#7dd3fc")}>${escHtml(r.method)}</td>
|
|
157
|
+
<td ${td()}>${escHtml(r.url.length > 80 ? "…" + r.url.slice(-80) : r.url)}</td>
|
|
158
|
+
<td ${td(statusColor)}>${r.status || "err"}</td>
|
|
159
|
+
<td ${td(isSlow ? "#f87171" : r.duration > 200 ? "#f59e0b" : "#86efac")}>${r.duration}ms${isSlow ? " ⚠" : ""}</td>
|
|
160
|
+
</tr>`;
|
|
161
|
+
})
|
|
162
|
+
.join("");
|
|
163
|
+
const apiTable = apiRows
|
|
164
|
+
? `<div style="margin-top:16px">
|
|
165
|
+
<h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">
|
|
166
|
+
API Calls <span style="color:#64748b;font-weight:400;font-size:0.72rem">${net.failedApiCalls.length > 0
|
|
167
|
+
? `· ${net.failedApiCalls.length} failed`
|
|
168
|
+
: ""}${net.slowApiCalls.length > 0 ? ` · ${net.slowApiCalls.length} slow` : ""}</span>
|
|
169
|
+
</h4>
|
|
170
|
+
<table style="width:100%;border-collapse:collapse">
|
|
171
|
+
<thead><tr>
|
|
172
|
+
<th ${th}>Method</th><th ${th}>URL</th><th ${th}>Status</th><th ${th}>Time</th>
|
|
173
|
+
</tr></thead>
|
|
174
|
+
<tbody>${apiRows}</tbody>
|
|
175
|
+
</table>
|
|
176
|
+
</div>`
|
|
177
|
+
: "";
|
|
178
|
+
// ── Long tasks ────────────────────────────────────────────────────────────
|
|
179
|
+
const longTaskRows = net.longTasks
|
|
180
|
+
.sort((a, b) => b.duration - a.duration)
|
|
181
|
+
.slice(0, 8)
|
|
182
|
+
.map((t) => `<tr>
|
|
183
|
+
<td ${td(t.duration > 200 ? "#f87171" : "#f59e0b")}>${t.duration}ms</td>
|
|
184
|
+
<td ${td("#64748b")}>at ${t.startTime}ms</td>
|
|
185
|
+
</tr>`)
|
|
186
|
+
.join("");
|
|
187
|
+
const longTaskTable = longTaskRows && net.longTasks.length > 0
|
|
188
|
+
? `<div style="margin-top:16px">
|
|
189
|
+
<h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">
|
|
190
|
+
Long Tasks <span style="color:#64748b;font-weight:400;font-size:0.72rem">(JS blocking main thread >50ms)</span>
|
|
191
|
+
</h4>
|
|
192
|
+
<table style="width:100%;border-collapse:collapse">
|
|
193
|
+
<thead><tr><th ${th}>Duration</th><th ${th}>When</th></tr></thead>
|
|
194
|
+
<tbody>${longTaskRows}</tbody>
|
|
195
|
+
</table>
|
|
196
|
+
</div>`
|
|
197
|
+
: "";
|
|
198
|
+
return `
|
|
199
|
+
<div style="border-top:1px solid #1e293b;margin-top:24px;padding-top:20px">
|
|
200
|
+
<h3 style="font-size:0.9rem;font-weight:600;color:#e2e8f0;margin-bottom:14px">🌐 Network & Runtime</h3>
|
|
201
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:4px">${pills}</div>
|
|
202
|
+
${heavyTable}
|
|
203
|
+
${apiTable}
|
|
204
|
+
${longTaskTable}
|
|
205
|
+
</div>`;
|
|
206
|
+
}
|
|
207
|
+
function renderPerfSection(perf) {
|
|
208
|
+
// Support both old .lighthouse and new .scores field names
|
|
209
|
+
const scores = perf.scores ?? perf.lighthouse ?? null;
|
|
210
|
+
const gauges = scores
|
|
211
|
+
? ["performance", "accessibility", "bestPractices", "seo"]
|
|
212
|
+
.map((key) => {
|
|
213
|
+
const score = scores[key] ?? 0;
|
|
214
|
+
const label = key === "bestPractices"
|
|
215
|
+
? "Best Practices"
|
|
216
|
+
: key.charAt(0).toUpperCase() + key.slice(1);
|
|
217
|
+
const color = perfScoreColor(score);
|
|
218
|
+
const circumference = 2 * Math.PI * 28;
|
|
219
|
+
const dash = (score / 100) * circumference;
|
|
220
|
+
return `<div style="text-align:center;min-width:100px">
|
|
221
|
+
<svg width="72" height="72" viewBox="0 0 72 72">
|
|
222
|
+
<circle cx="36" cy="36" r="28" fill="none" stroke="#1e293b" stroke-width="8"/>
|
|
223
|
+
<circle cx="36" cy="36" r="28" fill="none" stroke="${color}" stroke-width="8"
|
|
224
|
+
stroke-dasharray="${dash.toFixed(1)} ${circumference.toFixed(1)}"
|
|
225
|
+
stroke-linecap="round" transform="rotate(-90 36 36)"/>
|
|
226
|
+
<text x="36" y="41" text-anchor="middle" fill="${color}" font-size="16" font-weight="700">${score}</text>
|
|
227
|
+
</svg>
|
|
228
|
+
<div style="font-size:0.72rem;color:#94a3b8;margin-top:4px">${label}</div>
|
|
229
|
+
<div style="font-size:0.68rem;color:${color}">${perfScoreLabel(score)}</div>
|
|
230
|
+
</div>`;
|
|
231
|
+
})
|
|
232
|
+
.join("")
|
|
233
|
+
: "";
|
|
234
|
+
const vitalsRows = Object.entries(perf.vitals)
|
|
235
|
+
.filter(([, v]) => v != null)
|
|
236
|
+
.map(([key, value]) => {
|
|
237
|
+
const display = key === "cls" ? value.toFixed(4) : `${value}ms`;
|
|
238
|
+
const color = vitalRating(key, value);
|
|
239
|
+
const labels = {
|
|
240
|
+
fcp: "First Contentful Paint",
|
|
241
|
+
lcp: "Largest Contentful Paint",
|
|
242
|
+
cls: "Cumulative Layout Shift",
|
|
243
|
+
ttfb: "Time to First Byte",
|
|
244
|
+
domContentLoaded: "DOM Content Loaded",
|
|
245
|
+
loadComplete: "Load Complete",
|
|
246
|
+
};
|
|
247
|
+
return `<tr>
|
|
248
|
+
<td style="color:#94a3b8;padding:6px 12px;font-size:0.8rem">${labels[key] || key}</td>
|
|
249
|
+
<td style="color:${color};padding:6px 12px;font-size:0.8rem;font-weight:600">${display}</td>
|
|
250
|
+
</tr>`;
|
|
251
|
+
})
|
|
252
|
+
.join("");
|
|
253
|
+
const labMetrics = scores
|
|
254
|
+
? [
|
|
255
|
+
["FCP", scores.fcp, "ms"],
|
|
256
|
+
["LCP", scores.lcp, "ms"],
|
|
257
|
+
["CLS", scores.cls?.toFixed(4), ""],
|
|
258
|
+
["TBT", scores.tbt, "ms"],
|
|
259
|
+
["Speed Index", scores.speedIndex, "ms"],
|
|
260
|
+
["TTI", scores.tti, "ms"],
|
|
261
|
+
]
|
|
262
|
+
.filter(([, v]) => v != null)
|
|
263
|
+
.map(([label, value, unit]) => `<div style="background:#1e293b;border-radius:8px;padding:12px 16px;min-width:120px">
|
|
264
|
+
<div style="font-size:0.72rem;color:#64748b">${label}</div>
|
|
265
|
+
<div style="font-size:1.1rem;font-weight:700;color:#e2e8f0;margin-top:2px">${value}${unit}</div>
|
|
266
|
+
</div>`)
|
|
267
|
+
.join("")
|
|
268
|
+
: "";
|
|
269
|
+
// Framework badge (strip outer parens: "(Next.js)" → "Next.js")
|
|
270
|
+
const reactVer = perf.react?.version;
|
|
271
|
+
const frameworkName = reactVer?.startsWith("(")
|
|
272
|
+
? reactVer.slice(1, -1)
|
|
273
|
+
: reactVer;
|
|
274
|
+
const frameworkBadge = frameworkName
|
|
275
|
+
? ` <span style="color:#64748b;font-weight:400;font-size:0.8rem">${escHtml(frameworkName)}</span>`
|
|
276
|
+
: "";
|
|
277
|
+
// Passive render issues table
|
|
278
|
+
const renderIssuesHtml = perf.react?.detected && (perf.react.issues?.length ?? 0) > 0
|
|
279
|
+
? `<table style="width:100%;border-collapse:collapse;margin-top:8px">
|
|
280
|
+
<thead><tr>
|
|
281
|
+
<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Component</th>
|
|
282
|
+
<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Renders</th>
|
|
283
|
+
${perf.react.issues[0]?.avgMs != null
|
|
284
|
+
? '<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Avg Time</th>'
|
|
285
|
+
: ""}
|
|
286
|
+
</tr></thead>
|
|
287
|
+
<tbody>
|
|
288
|
+
${perf.react.issues
|
|
289
|
+
.map((i) => `<tr>
|
|
290
|
+
<td style="color:#fcd34d;padding:6px 12px;font-size:0.8rem;font-family:monospace">${escHtml(i.component)}</td>
|
|
291
|
+
<td style="color:#f87171;padding:6px 12px;font-size:0.8rem;font-weight:600">${i.renderCount}</td>
|
|
292
|
+
${i.avgMs != null
|
|
293
|
+
? `<td style="color:#94a3b8;padding:6px 12px;font-size:0.8rem">${i.avgMs}ms</td>`
|
|
294
|
+
: ""}
|
|
295
|
+
</tr>`)
|
|
296
|
+
.join("")}
|
|
297
|
+
</tbody>
|
|
298
|
+
</table>`
|
|
299
|
+
: "";
|
|
300
|
+
// Interaction test results table
|
|
301
|
+
const interactionTests = perf.react?.interactionTests ?? [];
|
|
302
|
+
const interactionHtml = interactionTests.length > 0
|
|
303
|
+
? `<div style="margin-top:16px">
|
|
304
|
+
<h4 style="font-size:0.8rem;font-weight:600;color:#94a3b8;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.05em">Interaction Tests</h4>
|
|
305
|
+
<table style="width:100%;border-collapse:collapse">
|
|
306
|
+
<thead><tr>
|
|
307
|
+
<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Action</th>
|
|
308
|
+
<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">DOM Changes</th>
|
|
309
|
+
<th style="text-align:left;color:#64748b;font-size:0.72rem;text-transform:uppercase;padding:6px 12px">Status</th>
|
|
310
|
+
</tr></thead>
|
|
311
|
+
<tbody>
|
|
312
|
+
${interactionTests
|
|
313
|
+
.map((t) => `<tr>
|
|
314
|
+
<td style="color:#e2e8f0;padding:6px 12px;font-size:0.8rem">${escHtml(t.action)}</td>
|
|
315
|
+
<td style="color:${t.flagged ? "#f87171" : "#86efac"};padding:6px 12px;font-size:0.8rem;font-weight:600">${t.mutations}</td>
|
|
316
|
+
<td style="padding:6px 12px;font-size:0.8rem">${t.flagged
|
|
317
|
+
? '<span style="color:#f87171">⚠ High activity</span>'
|
|
318
|
+
: '<span style="color:#86efac">✓ Normal</span>'}</td>
|
|
319
|
+
</tr>`)
|
|
320
|
+
.join("")}
|
|
321
|
+
</tbody>
|
|
322
|
+
</table>
|
|
323
|
+
</div>`
|
|
324
|
+
: "";
|
|
325
|
+
const frameworkSection = perf.react?.detected
|
|
326
|
+
? `<div style="margin-top:24px">
|
|
327
|
+
<h3 style="font-size:0.9rem;font-weight:600;color:#e2e8f0;margin-bottom:12px">⚛️ Framework Analysis${frameworkBadge}</h3>
|
|
328
|
+
${renderIssuesHtml
|
|
329
|
+
? renderIssuesHtml
|
|
330
|
+
: `<p style="color:#22c55e;font-size:0.85rem">✅ No passive re-render issues detected</p>`}
|
|
331
|
+
${interactionHtml}
|
|
332
|
+
</div>`
|
|
333
|
+
: perf.react?.detected === false
|
|
334
|
+
? `<p style="color:#64748b;font-size:0.85rem;margin-top:16px">ℹ️ No component framework detected on this page</p>`
|
|
335
|
+
: "";
|
|
336
|
+
return `
|
|
337
|
+
<div style="background:#0f172a;border:1px solid #1e293b;border-radius:12px;padding:24px;margin-bottom:32px">
|
|
338
|
+
<h2 style="font-size:1.1rem;font-weight:600;color:#e2e8f0;margin-bottom:20px">⚡ Performance
|
|
339
|
+
<span style="font-size:0.75rem;font-weight:400;color:#64748b;margin-left:8px">${escHtml(perf.url)}</span>
|
|
340
|
+
</h2>
|
|
341
|
+
|
|
342
|
+
${gauges
|
|
343
|
+
? `<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:24px">${gauges}</div>`
|
|
344
|
+
: ""}
|
|
345
|
+
|
|
346
|
+
${labMetrics
|
|
347
|
+
? `<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:24px">${labMetrics}</div>`
|
|
348
|
+
: ""}
|
|
349
|
+
|
|
350
|
+
${vitalsRows
|
|
351
|
+
? `<div style="margin-bottom:16px">
|
|
352
|
+
<h3 style="font-size:0.85rem;font-weight:600;color:#94a3b8;margin-bottom:8px">Real User Metrics</h3>
|
|
353
|
+
<table style="border-collapse:collapse"><tbody>${vitalsRows}</tbody></table>
|
|
354
|
+
</div>`
|
|
355
|
+
: ""}
|
|
356
|
+
|
|
357
|
+
${frameworkSection}
|
|
358
|
+
|
|
359
|
+
${renderNetworkSection(perf.network)}
|
|
360
|
+
|
|
361
|
+
${perf.lighthouseReportPath
|
|
362
|
+
? `<p style="margin-top:16px;font-size:0.78rem;color:#64748b">Full report: <a href="${escHtml(perf.lighthouseReportPath)}" style="color:#7dd3fc">${escHtml(perf.lighthouseReportPath)}</a></p>`
|
|
363
|
+
: ""}
|
|
364
|
+
</div>`;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Renders either a single audit section or, when multiple pages were audited,
|
|
368
|
+
* a URL tab bar + the selected page's full detail panel.
|
|
369
|
+
*/
|
|
370
|
+
function renderAllPerfSections(session) {
|
|
371
|
+
const audits = session.perfAudits ?? (session.perfAudit ? [session.perfAudit] : []);
|
|
372
|
+
if (audits.length === 0)
|
|
373
|
+
return "";
|
|
374
|
+
if (audits.length === 1)
|
|
375
|
+
return renderPerfSection(audits[0]);
|
|
376
|
+
const pageLabel = (a) => {
|
|
377
|
+
try {
|
|
378
|
+
return new URL(a.url).pathname || "/";
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return a.url;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const tabs = audits
|
|
385
|
+
.map((a, i) => `
|
|
386
|
+
<button
|
|
387
|
+
id="slp-tab-${i}"
|
|
388
|
+
onclick="slpSwitch(${i})"
|
|
389
|
+
title="${escHtml(a.url)}"
|
|
390
|
+
style="
|
|
391
|
+
cursor:pointer;background:none;border:none;outline:none;
|
|
392
|
+
padding:9px 18px;font-size:0.82rem;font-weight:500;white-space:nowrap;
|
|
393
|
+
border-bottom:2px solid ${i === 0 ? "#7c3aed" : "transparent"};
|
|
394
|
+
color:${i === 0 ? "#e2e8f0" : "#64748b"};
|
|
395
|
+
transition:color .15s,border-color .15s;
|
|
396
|
+
"
|
|
397
|
+
>${escHtml(pageLabel(a))}</button>`)
|
|
398
|
+
.join("");
|
|
399
|
+
const panels = audits
|
|
400
|
+
.map((a, i) => `<div id="slp-panel-${i}" style="display:${i === 0 ? "block" : "none"}">${renderPerfSection(a)}</div>`)
|
|
401
|
+
.join("");
|
|
402
|
+
const script = `
|
|
403
|
+
<script>
|
|
404
|
+
function slpSwitch(idx) {
|
|
405
|
+
var n = ${audits.length};
|
|
406
|
+
for (var i = 0; i < n; i++) {
|
|
407
|
+
var p = document.getElementById('slp-panel-' + i);
|
|
408
|
+
var t = document.getElementById('slp-tab-' + i);
|
|
409
|
+
var active = i === idx;
|
|
410
|
+
if (p) p.style.display = active ? 'block' : 'none';
|
|
411
|
+
if (t) {
|
|
412
|
+
t.style.borderBottomColor = active ? '#7c3aed' : 'transparent';
|
|
413
|
+
t.style.color = active ? '#e2e8f0' : '#64748b';
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
<\/script>`;
|
|
418
|
+
return `
|
|
419
|
+
<div>
|
|
420
|
+
<div style="display:flex;flex-wrap:wrap;border-bottom:1px solid #1e293b;margin-bottom:20px;overflow-x:auto">
|
|
421
|
+
${tabs}
|
|
422
|
+
</div>
|
|
423
|
+
${panels}
|
|
424
|
+
</div>
|
|
425
|
+
${script}`;
|
|
426
|
+
}
|
|
427
|
+
function escHtml(s) {
|
|
428
|
+
return s
|
|
429
|
+
.replace(/&/g, "&")
|
|
430
|
+
.replace(/</g, "<")
|
|
431
|
+
.replace(/>/g, ">")
|
|
432
|
+
.replace(/"/g, """);
|
|
433
|
+
}
|
|
434
|
+
function formatArgs(args) {
|
|
435
|
+
const entries = Object.entries(args);
|
|
436
|
+
if (entries.length === 0)
|
|
437
|
+
return "";
|
|
438
|
+
return entries
|
|
439
|
+
.map(([k, v]) => {
|
|
440
|
+
const val = typeof v === "string" ? v : JSON.stringify(v);
|
|
441
|
+
return `<span class="arg-key">${escHtml(k)}</span><span class="arg-eq">=</span><span class="arg-val">${escHtml(val.slice(0, 120))}${val.length > 120 ? "…" : ""}</span>`;
|
|
442
|
+
})
|
|
443
|
+
.join(" ");
|
|
444
|
+
}
|
|
445
|
+
function formatResult(result) {
|
|
446
|
+
const s = typeof result === "string" ? result : JSON.stringify(result);
|
|
447
|
+
return escHtml(s.slice(0, 300)) + (s.length > 300 ? "…" : "");
|
|
448
|
+
}
|
|
449
|
+
export function generateTaskReportHtml(report) {
|
|
450
|
+
const { session, events } = report;
|
|
451
|
+
const durationMs = new Date(session.updatedAt).getTime() -
|
|
452
|
+
new Date(session.createdAt).getTime();
|
|
453
|
+
const durationStr = durationMs < 60000
|
|
454
|
+
? `${Math.round(durationMs / 1000)}s`
|
|
455
|
+
: `${Math.floor(durationMs / 60000)}m ${Math.round((durationMs % 60000) / 1000)}s`;
|
|
456
|
+
const timelineRows = events
|
|
457
|
+
.filter((e) => [
|
|
458
|
+
"tool_call",
|
|
459
|
+
"tool_error",
|
|
460
|
+
"memory_update",
|
|
461
|
+
"scheduled",
|
|
462
|
+
"sleeping_until",
|
|
463
|
+
"session_end",
|
|
464
|
+
"llm_response",
|
|
465
|
+
].includes(e.type))
|
|
466
|
+
.map((event) => {
|
|
467
|
+
const ts = new Date(event.ts).toLocaleTimeString();
|
|
468
|
+
if (event.type === "tool_call") {
|
|
469
|
+
const icon = toolIcon(event.toolName);
|
|
470
|
+
const isDone = event.toolName === "done";
|
|
471
|
+
return `
|
|
472
|
+
<tr class="${isDone ? "row-done" : "row-tool"}">
|
|
473
|
+
<td class="cell-time">${ts}</td>
|
|
474
|
+
<td class="cell-icon">${icon}</td>
|
|
475
|
+
<td class="cell-tool"><span class="tag-tool">${escHtml(event.toolName)}</span></td>
|
|
476
|
+
<td class="cell-args">${formatArgs(event.args)}</td>
|
|
477
|
+
<td class="cell-result result-ok">${formatResult(event.result)}</td>
|
|
478
|
+
</tr>`;
|
|
479
|
+
}
|
|
480
|
+
if (event.type === "tool_error") {
|
|
481
|
+
const icon = toolIcon(event.toolName);
|
|
482
|
+
return `
|
|
483
|
+
<tr class="row-error">
|
|
484
|
+
<td class="cell-time">${ts}</td>
|
|
485
|
+
<td class="cell-icon">${icon}</td>
|
|
486
|
+
<td class="cell-tool"><span class="tag-tool tag-error">${escHtml(event.toolName)}</span></td>
|
|
487
|
+
<td class="cell-args">${formatArgs(event.args)}</td>
|
|
488
|
+
<td class="cell-result result-error">${escHtml(event.error)}</td>
|
|
489
|
+
</tr>`;
|
|
490
|
+
}
|
|
491
|
+
if (event.type === "memory_update") {
|
|
492
|
+
return `
|
|
493
|
+
<tr class="row-memory">
|
|
494
|
+
<td class="cell-time">${ts}</td>
|
|
495
|
+
<td class="cell-icon">🧠</td>
|
|
496
|
+
<td class="cell-tool"><span class="tag-memory">remember</span></td>
|
|
497
|
+
<td class="cell-args"><span class="arg-key">${escHtml(event.key)}</span> = <span class="arg-val">${escHtml(event.value.slice(0, 120))}</span></td>
|
|
498
|
+
<td class="cell-result result-ok">stored</td>
|
|
499
|
+
</tr>`;
|
|
500
|
+
}
|
|
501
|
+
if (event.type === "scheduled") {
|
|
502
|
+
return `
|
|
503
|
+
<tr class="row-scheduled">
|
|
504
|
+
<td class="cell-time">${ts}</td>
|
|
505
|
+
<td class="cell-icon">⏰</td>
|
|
506
|
+
<td class="cell-tool"><span class="tag-scheduled">schedule</span></td>
|
|
507
|
+
<td class="cell-args"><span class="arg-key">cron</span>=<span class="arg-val">${escHtml(event.cron)}</span></td>
|
|
508
|
+
<td class="cell-result result-ok">${escHtml(event.task)}</td>
|
|
509
|
+
</tr>`;
|
|
510
|
+
}
|
|
511
|
+
if (event.type === "sleeping_until") {
|
|
512
|
+
return `
|
|
513
|
+
<tr class="row-sleeping">
|
|
514
|
+
<td class="cell-time">${ts}</td>
|
|
515
|
+
<td class="cell-icon">😴</td>
|
|
516
|
+
<td class="cell-tool"><span class="tag-sleeping">sleep</span></td>
|
|
517
|
+
<td class="cell-args"></td>
|
|
518
|
+
<td class="cell-result result-ok">until ${escHtml(new Date(event.until).toLocaleString())}</td>
|
|
519
|
+
</tr>`;
|
|
520
|
+
}
|
|
521
|
+
if (event.type === "llm_response" && event.text) {
|
|
522
|
+
const preview = event.text.slice(0, 300);
|
|
523
|
+
return `
|
|
524
|
+
<tr class="row-think">
|
|
525
|
+
<td class="cell-time">${ts}</td>
|
|
526
|
+
<td class="cell-icon">💬</td>
|
|
527
|
+
<td class="cell-tool"><span class="tag-think">thought</span></td>
|
|
528
|
+
<td class="cell-args" colspan="2"><span class="markdown-inline" data-md="${escHtml(preview)}${event.text.length > 300 ? "…" : ""}"></span></td>
|
|
529
|
+
</tr>`;
|
|
530
|
+
}
|
|
531
|
+
return "";
|
|
532
|
+
})
|
|
533
|
+
.join("\n");
|
|
534
|
+
const memoryRows = Object.entries(session.memory)
|
|
535
|
+
.map(([k, v]) => `
|
|
536
|
+
<tr>
|
|
537
|
+
<td class="mem-key">${escHtml(k)}</td>
|
|
538
|
+
<td class="mem-val">${escHtml(v)}</td>
|
|
539
|
+
</tr>`)
|
|
540
|
+
.join("\n");
|
|
541
|
+
const scheduledRows = session.scheduledJobs
|
|
542
|
+
.map((j) => `
|
|
543
|
+
<tr>
|
|
544
|
+
<td>${escHtml(j.cron)}</td>
|
|
545
|
+
<td>${escHtml(j.taskDescription)}</td>
|
|
546
|
+
<td>${j.lastRun ? new Date(j.lastRun).toLocaleString() : "—"}</td>
|
|
547
|
+
</tr>`)
|
|
548
|
+
.join("\n");
|
|
549
|
+
// Summary is stored as raw markdown text — rendered by marked.js in the browser
|
|
550
|
+
const summaryHtml = session.finalSummary
|
|
551
|
+
? `<div class="summary-box markdown-body" data-md="${escHtml(session.finalSummary)}"></div>`
|
|
552
|
+
: "";
|
|
553
|
+
return `<!DOCTYPE html>
|
|
554
|
+
<html lang="en">
|
|
555
|
+
<head>
|
|
556
|
+
<meta charset="UTF-8"/>
|
|
557
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
558
|
+
<title>Task Report — ${escHtml(session.goal.slice(0, 60))}</title>
|
|
559
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
560
|
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
561
|
+
<style>
|
|
562
|
+
body { font-family: 'Inter', system-ui, sans-serif; background: #0f172a; color: #e2e8f0; }
|
|
563
|
+
.summary-box { background: #1e293b; border-left: 4px solid #22c55e; padding: 1rem 1.5rem; border-radius: 0.5rem; font-size: 0.95rem; line-height: 1.7; }
|
|
564
|
+
/* Markdown prose styles inside summary and thought cells */
|
|
565
|
+
.markdown-body h1,.markdown-body h2,.markdown-body h3 { font-weight: 700; margin: 0.75em 0 0.35em; color: #f1f5f9; }
|
|
566
|
+
.markdown-body h1 { font-size: 1.25rem; }
|
|
567
|
+
.markdown-body h2 { font-size: 1.05rem; }
|
|
568
|
+
.markdown-body h3 { font-size: 0.95rem; }
|
|
569
|
+
.markdown-body p { margin: 0.4em 0; }
|
|
570
|
+
.markdown-body ul,.markdown-body ol { padding-left: 1.4em; margin: 0.4em 0; }
|
|
571
|
+
.markdown-body li { margin: 0.15em 0; }
|
|
572
|
+
.markdown-body strong { color: #f8fafc; font-weight: 600; }
|
|
573
|
+
.markdown-body em { color: #cbd5e1; }
|
|
574
|
+
.markdown-body code { background: #0f172a; color: #7dd3fc; padding: 1px 5px; border-radius: 4px; font-size: 0.85em; font-family: monospace; }
|
|
575
|
+
.markdown-body pre { background: #0f172a; border-radius: 6px; padding: 0.75rem 1rem; overflow-x: auto; margin: 0.5em 0; }
|
|
576
|
+
.markdown-body pre code { background: none; padding: 0; }
|
|
577
|
+
.markdown-body hr { border-color: #334155; margin: 0.75em 0; }
|
|
578
|
+
.markdown-body blockquote { border-left: 3px solid #475569; padding-left: 0.75rem; color: #94a3b8; margin: 0.4em 0; }
|
|
579
|
+
.markdown-body table { border-collapse: collapse; width: 100%; margin: 0.5em 0; font-size: 0.85rem; }
|
|
580
|
+
.markdown-body th { background: #1e293b; color: #94a3b8; padding: 6px 12px; text-align: left; font-weight: 600; border: 1px solid #334155; }
|
|
581
|
+
.markdown-body td { padding: 5px 12px; border: 1px solid #1e293b; color: #e2e8f0; }
|
|
582
|
+
.markdown-body tr:nth-child(even) td { background: #0f1b2d; }
|
|
583
|
+
/* Inline thought text (smaller, muted) */
|
|
584
|
+
.markdown-inline p { margin: 0; display: inline; }
|
|
585
|
+
.markdown-inline { color: #94a3b8; font-size: 0.82rem; }
|
|
586
|
+
table { border-collapse: collapse; width: 100%; }
|
|
587
|
+
th { background: #1e293b; color: #94a3b8; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.6rem 0.75rem; text-align: left; }
|
|
588
|
+
td { padding: 0.45rem 0.75rem; font-size: 0.82rem; border-bottom: 1px solid #1e293b; vertical-align: top; }
|
|
589
|
+
tr:hover td { background: #1e293b55; }
|
|
590
|
+
.cell-time { color: #64748b; font-family: monospace; white-space: nowrap; width: 70px; }
|
|
591
|
+
.cell-icon { font-size: 1rem; text-align: center; width: 30px; }
|
|
592
|
+
.cell-tool { white-space: nowrap; }
|
|
593
|
+
.cell-args { color: #94a3b8; font-size: 0.78rem; }
|
|
594
|
+
.arg-key { color: #7dd3fc; }
|
|
595
|
+
.arg-eq { color: #64748b; }
|
|
596
|
+
.arg-val { color: #fcd34d; }
|
|
597
|
+
.tag-tool { background: #1e40af33; color: #93c5fd; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
|
|
598
|
+
.tag-error { background: #7f1d1d44; color: #fca5a5; }
|
|
599
|
+
.tag-memory { background: #4c1d9544; color: #c4b5fd; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
|
|
600
|
+
.tag-scheduled { background: #164e6344; color: #67e8f9; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
|
|
601
|
+
.tag-sleeping { background: #3b0764; color: #d8b4fe; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
|
|
602
|
+
.tag-think { background: #422006; color: #fde68a; padding: 1px 7px; border-radius: 999px; font-size: 0.73rem; }
|
|
603
|
+
.result-ok { color: #86efac; }
|
|
604
|
+
.result-error { color: #fca5a5; }
|
|
605
|
+
.row-done td { background: #14532d22; }
|
|
606
|
+
.row-error td { background: #7f1d1d22; }
|
|
607
|
+
.row-memory td { background: #1a0533; }
|
|
608
|
+
.row-scheduled td { background: #0c4a6e22; }
|
|
609
|
+
.row-sleeping td { background: #150a33; }
|
|
610
|
+
.row-think td { background: #1c1a00; }
|
|
611
|
+
.mem-key { color: #7dd3fc; font-family: monospace; width: 220px; }
|
|
612
|
+
.mem-val { color: #fde68a; font-family: monospace; white-space: pre-wrap; word-break: break-all; }
|
|
613
|
+
.stat-card { background: #1e293b; border-radius: 0.75rem; padding: 1.25rem 1.5rem; }
|
|
614
|
+
.stat-num { font-size: 2rem; font-weight: 700; line-height: 1; }
|
|
615
|
+
.stat-label { font-size: 0.75rem; color: #64748b; margin-top: 0.25rem; }
|
|
616
|
+
</style>
|
|
617
|
+
</head>
|
|
618
|
+
<body class="min-h-screen">
|
|
619
|
+
<div class="max-w-6xl mx-auto px-4 py-10">
|
|
620
|
+
<div class="mb-8">
|
|
621
|
+
<div class="flex items-center gap-3 mb-1">
|
|
622
|
+
<span class="text-2xl">🤖</span>
|
|
623
|
+
<h1 class="text-2xl font-bold text-white">Task Report</h1>
|
|
624
|
+
<span style="color:${statusColor(session.status)}" class="ml-2 text-lg">${statusIcon(session.status)} ${session.status}</span>
|
|
625
|
+
</div>
|
|
626
|
+
<p class="text-slate-400 text-sm mt-1 font-mono">${escHtml(session.id)}</p>
|
|
627
|
+
<p class="text-slate-200 text-base mt-3 font-medium">"${escHtml(session.goal)}"</p>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
631
|
+
<div class="stat-card">
|
|
632
|
+
<div class="stat-num text-blue-400">${session.iteration}</div>
|
|
633
|
+
<div class="stat-label">Iterations</div>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="stat-card">
|
|
636
|
+
<div class="stat-num text-green-400">${durationStr}</div>
|
|
637
|
+
<div class="stat-label">Duration</div>
|
|
638
|
+
</div>
|
|
639
|
+
<div class="stat-card">
|
|
640
|
+
<div class="stat-num text-purple-400">${Object.keys(session.memory).length}</div>
|
|
641
|
+
<div class="stat-label">Memory Items</div>
|
|
642
|
+
</div>
|
|
643
|
+
<div class="stat-card">
|
|
644
|
+
<div class="stat-num text-cyan-400">${session.scheduledJobs.length}</div>
|
|
645
|
+
<div class="stat-label">Scheduled Jobs</div>
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
|
|
649
|
+
${summaryHtml
|
|
650
|
+
? `
|
|
651
|
+
<div class="mb-8">
|
|
652
|
+
<h2 class="text-lg font-semibold text-white mb-3">📋 Summary</h2>
|
|
653
|
+
${summaryHtml}
|
|
654
|
+
</div>`
|
|
655
|
+
: ""}
|
|
656
|
+
|
|
657
|
+
${renderAllPerfSections(session)}
|
|
658
|
+
|
|
659
|
+
${memoryRows
|
|
660
|
+
? `
|
|
661
|
+
<div class="mb-8">
|
|
662
|
+
<h2 class="text-lg font-semibold text-white mb-3">🧠 Memory at Completion</h2>
|
|
663
|
+
<div class="rounded-lg overflow-hidden border border-slate-700">
|
|
664
|
+
<table>
|
|
665
|
+
<thead><tr><th>Key</th><th>Value</th></tr></thead>
|
|
666
|
+
<tbody>${memoryRows}</tbody>
|
|
667
|
+
</table>
|
|
668
|
+
</div>
|
|
669
|
+
</div>`
|
|
670
|
+
: ""}
|
|
671
|
+
|
|
672
|
+
${scheduledRows
|
|
673
|
+
? `
|
|
674
|
+
<div class="mb-8">
|
|
675
|
+
<h2 class="text-lg font-semibold text-white mb-3">⏰ Scheduled Jobs</h2>
|
|
676
|
+
<div class="rounded-lg overflow-hidden border border-slate-700">
|
|
677
|
+
<table>
|
|
678
|
+
<thead><tr><th>Cron</th><th>Task</th><th>Last Run</th></tr></thead>
|
|
679
|
+
<tbody>${scheduledRows}</tbody>
|
|
680
|
+
</table>
|
|
681
|
+
</div>
|
|
682
|
+
</div>`
|
|
683
|
+
: ""}
|
|
684
|
+
|
|
685
|
+
<div class="mb-8">
|
|
686
|
+
<h2 class="text-lg font-semibold text-white mb-3">📜 Action Timeline</h2>
|
|
687
|
+
<div class="rounded-lg overflow-hidden border border-slate-700">
|
|
688
|
+
<table>
|
|
689
|
+
<thead>
|
|
690
|
+
<tr><th>Time</th><th></th><th>Action</th><th>Arguments</th><th>Result</th></tr>
|
|
691
|
+
</thead>
|
|
692
|
+
<tbody>${timelineRows}</tbody>
|
|
693
|
+
</table>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
<p class="text-center text-slate-600 text-xs mt-8">
|
|
698
|
+
Generated by Slapify Task Agent • ${new Date(report.generatedAt).toLocaleString()}
|
|
699
|
+
• Session: ${escHtml(session.id)}
|
|
700
|
+
</p>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
<script>
|
|
704
|
+
// Render all markdown blocks once marked.js is loaded
|
|
705
|
+
(function renderMarkdown() {
|
|
706
|
+
if (typeof marked === 'undefined') {
|
|
707
|
+
// Retry after CDN script loads
|
|
708
|
+
window.addEventListener('load', renderMarkdown);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
marked.setOptions({ breaks: true, gfm: true });
|
|
712
|
+
|
|
713
|
+
// Summary box — full markdown prose
|
|
714
|
+
document.querySelectorAll('.markdown-body[data-md]').forEach(function(el) {
|
|
715
|
+
el.innerHTML = marked.parse(el.getAttribute('data-md') || '');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Inline thought previews — strip to plain text then render inline
|
|
719
|
+
document.querySelectorAll('.markdown-inline[data-md]').forEach(function(el) {
|
|
720
|
+
el.innerHTML = marked.parseInline(el.getAttribute('data-md') || '');
|
|
721
|
+
});
|
|
722
|
+
})();
|
|
723
|
+
</script>
|
|
724
|
+
</body>
|
|
725
|
+
</html>`;
|
|
726
|
+
}
|
|
727
|
+
export function saveTaskReport(session, events, outputDir) {
|
|
728
|
+
const dir = outputDir || path.join(process.cwd(), "slapify-task-reports");
|
|
729
|
+
if (!fs.existsSync(dir))
|
|
730
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
731
|
+
const reportPath = path.join(dir, `${session.id}.html`);
|
|
732
|
+
const html = generateTaskReportHtml({
|
|
733
|
+
session,
|
|
734
|
+
events,
|
|
735
|
+
generatedAt: new Date().toISOString(),
|
|
736
|
+
});
|
|
737
|
+
fs.writeFileSync(reportPath, html);
|
|
738
|
+
return reportPath;
|
|
739
|
+
}
|
|
740
|
+
//# sourceMappingURL=report.js.map
|