-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathartifact.html
More file actions
590 lines (539 loc) · 25.6 KB
/
Copy pathartifact.html
File metadata and controls
590 lines (539 loc) · 25.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
<!DOCTYPE html>
<html lang="en" data-artifact-id="commit-rewriter">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Commit message rewriter — AgentHTML</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0e0e0c;
--bg-2: #15140f;
--bg-3: #1c1a14;
--ink: #e8e6e0;
--ink-dim: #8b887e;
--ink-fade: #4a4842;
--rule: #2a2924;
--accent: #fbbf24;
--accent-dim: #b88c1c;
--crit: #ef4444;
--warn: #f59e0b;
--info: #60a5fa;
--add: #4ade80;
--serif: 'Instrument Serif', 'Times New Roman', serif;
--mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
* { box-sizing: border-box; }
html, body {
background: var(--bg); color: var(--ink);
font-family: var(--mono); font-size: 14px; line-height: 1.55;
margin: 0;
-webkit-font-smoothing: antialiased;
}
body::before {
content: ''; position: fixed; inset: 0; pointer-events: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.04 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
opacity: 0.5; z-index: 1; mix-blend-mode: overlay;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 48px 40px 100px; position: relative; z-index: 2; }
/* Header */
header { border-bottom: 1px solid var(--rule); padding-bottom: 28px; margin-bottom: 32px; }
.eyebrow {
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--accent); margin-bottom: 12px;
}
h1 {
font-family: var(--serif); font-weight: 400;
font-size: 52px; line-height: 1.05; margin: 0 0 10px;
letter-spacing: -0.01em;
}
h1 em { font-style: italic; color: var(--ink-dim); }
.subtitle {
font-family: var(--serif); font-size: 19px; color: var(--ink-dim);
max-width: 700px;
}
.meta-right {
margin-top: 12px;
font-size: 11px; color: var(--ink-fade);
}
.meta-right .pill {
display: inline-block; padding: 3px 9px; border: 1px solid var(--rule); border-radius: 999px;
margin-right: 6px; color: var(--ink-dim);
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
}
/* Two-pane layout */
.panes {
display: grid; grid-template-columns: 1fr 1fr; gap: 28px;
margin-top: 32px;
}
@media (max-width: 820px) { .panes { grid-template-columns: 1fr; } }
.pane {
background: var(--bg-2); border: 1px solid var(--rule);
display: flex; flex-direction: column;
}
.pane-head {
padding: 14px 20px; border-bottom: 1px solid var(--rule);
display: flex; align-items: center; justify-content: space-between;
font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase;
color: var(--ink-dim);
}
.pane-head .label { color: var(--accent); }
.pane-head .hint { color: var(--ink-fade); text-transform: none; letter-spacing: 0; font-size: 11px; }
.pane-body { padding: 20px; flex: 1; display: flex; flex-direction: column; gap: 16px; }
/* Original commit (left pane) */
textarea {
width: 100%; background: var(--bg-3); color: var(--ink);
border: 1px solid var(--rule); padding: 14px 16px;
font-family: var(--mono); font-size: 13px; line-height: 1.5;
resize: vertical; min-height: 140px;
}
textarea:focus { outline: none; border-color: var(--accent); }
/* Controls */
.controls { display: flex; flex-direction: column; gap: 14px; margin-top: 8px; }
.control-row {
display: grid; grid-template-columns: 120px 1fr;
align-items: center; gap: 16px;
}
.control-label {
font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase;
color: var(--ink-dim);
}
.seg {
display: inline-flex; border: 1px solid var(--rule);
background: var(--bg-3);
}
.seg button {
background: transparent; color: var(--ink-dim);
border: none; padding: 7px 14px;
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.08em; text-transform: uppercase;
cursor: pointer; transition: all 0.12s;
border-right: 1px solid var(--rule);
}
.seg button:last-child { border-right: none; }
.seg button:hover { color: var(--ink); }
.seg button[aria-pressed="true"] {
background: var(--accent); color: var(--bg);
}
/* Big primary button */
.primary-btn {
margin-top: 12px; padding: 14px 24px;
background: var(--accent); color: var(--bg);
border: none; cursor: pointer;
font-family: var(--mono); font-size: 12px; font-weight: 700;
letter-spacing: 0.1em; text-transform: uppercase;
display: flex; align-items: center; justify-content: space-between;
transition: filter 0.15s;
}
.primary-btn:hover { filter: brightness(1.1); }
.primary-btn:active { transform: translateY(1px); }
.primary-btn .arrow { font-family: var(--serif); font-size: 18px; font-weight: 400; font-style: italic; }
.primary-btn:disabled { opacity: 0.5; cursor: wait; }
/* Rewrite output (right pane) */
.output-empty {
flex: 1; display: flex; align-items: center; justify-content: center;
color: var(--ink-fade); font-family: var(--serif); font-style: italic;
font-size: 18px; text-align: center; padding: 32px;
}
.output-content { display: none; flex-direction: column; gap: 14px; }
.output-content.visible { display: flex; animation: reveal 0.4s ease-out; }
.rewrite-block {
background: var(--bg-3); border-left: 2px solid var(--accent);
padding: 16px 18px;
font-family: var(--mono); font-size: 13px;
line-height: 1.55; color: var(--ink);
white-space: pre-wrap;
}
.rewrite-block .rewrite-title {
font-size: 14px; font-weight: 500;
color: var(--ink); margin-bottom: 8px;
}
.rewrite-block .rewrite-body {
color: var(--ink-dim); font-size: 12.5px;
}
/* Footer actions on output */
.output-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.btn-sm {
background: transparent; color: var(--ink-dim);
border: 1px solid var(--rule); padding: 7px 14px;
font-family: var(--mono); font-size: 10px;
letter-spacing: 0.1em; text-transform: uppercase;
cursor: pointer; transition: all 0.12s;
display: inline-flex; align-items: center; gap: 6px;
}
.btn-sm:hover { color: var(--accent); border-color: var(--accent); }
.btn-sm .arrow { font-family: var(--serif); font-style: italic; font-size: 13px; }
.btn-sm:disabled { opacity: 0.4; cursor: wait; }
/* Pushback block */
.pushback-block {
border-left: 2px solid var(--crit);
background: rgba(239, 68, 68, 0.06);
padding: 16px 18px;
animation: reveal 0.4s ease-out;
margin-top: 4px;
}
.pushback-label {
font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--crit); margin-bottom: 8px;
display: flex; align-items: center; gap: 8px;
}
.pushback-block p { margin: 0; color: var(--ink); }
/* Loading dots */
.loading-dots { display: inline-flex; gap: 3px; align-items: center; }
.loading-dots span {
width: 5px; height: 5px; background: currentColor; border-radius: 50%;
animation: pulse 1.2s infinite ease-in-out;
}
.loading-dots span:nth-child(2) { animation-delay: 0.15s; }
.loading-dots span:nth-child(3) { animation-delay: 0.3s; }
@keyframes pulse {
0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
40% { opacity: 1; transform: scale(1); }
}
@keyframes reveal {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Below the fold — example callouts */
.below {
margin-top: 56px; padding-top: 32px;
border-top: 1px solid var(--rule);
display: grid; grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
@media (max-width: 820px) { .below { grid-template-columns: 1fr; } }
.callout {
border: 1px solid var(--rule); padding: 18px 20px;
background: var(--bg-2);
}
.callout h3 {
font-family: var(--serif); font-size: 19px; font-weight: 400;
margin: 0 0 8px;
}
.callout p {
color: var(--ink-dim); font-size: 12.5px; margin: 0; line-height: 1.55;
}
/* Mock banner */
.mock-banner {
position: fixed; top: 14px; right: 14px;
background: var(--bg); border: 1px solid var(--accent-dim);
color: var(--accent); padding: 5px 12px;
font-size: 9px; letter-spacing: 0.15em; text-transform: uppercase;
z-index: 10;
}
footer {
margin-top: 64px; padding-top: 20px; border-top: 1px solid var(--rule);
font-size: 11px; color: var(--ink-fade);
display: flex; justify-content: space-between;
}
footer .badge strong { color: var(--accent); }
</style>
</head>
<body>
<div class="mock-banner">◆ mock mode</div>
<!-- artifact state — bindings sync here -->
<script type="application/json" id="agenthtml-state">
{
"input": "fix(stream): handle slow consumers properly in streamingResponse\n\nThe async iterator now applies backpressure when the\nconsumer's queue exceeds the high-water mark. We were\nleaking memory on slow renders. Added a test for this.",
"tone": "conventional",
"length": "medium",
"scope": "with-scope"
}
</script>
<div class="wrap">
<header>
<div class="eyebrow">AgentHTML · custom editor demo</div>
<h1>Commit message <em>rewriter</em></h1>
<p class="subtitle">
An AI-generated artifact that's not a report — it's a small tool you can use.
Paste a commit, set the tone, regenerate. Disagree? Push back and the agent argues for the alternative.
</p>
<div class="meta-right">
<span class="pill">demo</span>
<span class="pill">agent-aware</span>
<span class="pill">~4kb runtime</span>
<span style="margin-left: 8px; color: var(--ink-dim)">single-file · works offline · `file://` ready</span>
</div>
</header>
<!-- ─────────── TWO-PANE EDITOR ─────────── -->
<div class="panes">
<!-- LEFT: original -->
<div class="pane">
<div class="pane-head">
<span class="label">◆ original</span>
<span class="hint">edit freely · changes auto-save</span>
</div>
<div class="pane-body">
<textarea
data-agent-bind="input"
spellcheck="false"
rows="6"
>fix(stream): handle slow consumers properly in streamingResponse
The async iterator now applies backpressure when the
consumer's queue exceeds the high-water mark. We were
leaking memory on slow renders. Added a test for this.</textarea>
<div class="controls">
<div class="control-row">
<span class="control-label">Tone</span>
<div class="seg" role="radiogroup" data-control="tone">
<button data-value="terse">Terse</button>
<button data-value="conventional" aria-pressed="true">Conventional</button>
<button data-value="explanatory">Explanatory</button>
<button data-value="changelog">Changelog</button>
</div>
</div>
<div class="control-row">
<span class="control-label">Length</span>
<div class="seg" role="radiogroup" data-control="length">
<button data-value="short">Short</button>
<button data-value="medium" aria-pressed="true">Medium</button>
<button data-value="long">Long</button>
</div>
</div>
<div class="control-row">
<span class="control-label">Scope prefix</span>
<div class="seg" role="radiogroup" data-control="scope">
<button data-value="with-scope" aria-pressed="true">With <code>scope:</code></button>
<button data-value="no-scope">No scope</button>
</div>
</div>
</div>
<button
class="primary-btn"
data-agent-action="Rewrite the commit in state.input using tone state.tone, length state.length, scope handling state.scope. Output ONLY the new commit message text, no preamble, no quotes."
data-agent-context="input,tone,length,scope"
data-agent-target="#rewrite-output"
data-agent-render="rewrite">
<span>Rewrite</span>
<span class="arrow">→</span>
</button>
</div>
</div>
<!-- RIGHT: rewritten -->
<div class="pane">
<div class="pane-head">
<span class="label">◆ rewritten</span>
<span class="hint" id="rewrite-hint">awaiting first rewrite</span>
</div>
<div class="pane-body">
<div id="rewrite-output" style="flex:1; display:flex; flex-direction:column;">
<div class="output-empty" id="rewrite-empty">
Set a tone on the left and hit Rewrite.<br>The result appears here.
</div>
<div class="output-content" id="rewrite-content">
<!-- filled by runtime -->
</div>
</div>
</div>
</div>
</div>
<!-- ─────────── Below the fold callouts ─────────── -->
<section class="below">
<div class="callout">
<h3>This isn't a report</h3>
<p>
Most AI-generated HTML is a static document — read once and discard. This one
is a tool: bindings, buttons, agent calls. The artifact is the workflow.
</p>
</div>
<div class="callout">
<h3>Built by an agent, used by you</h3>
<p>
An AI agent produced this page from a single prompt. The whole UI, the rewrite
logic, the push-back behavior — all generated, all in one <code>.html</code>.
</p>
</div>
<div class="callout">
<h3>The next step beyond Markdown</h3>
<p>
Markdown is for drafts. HTML is for deliverables. And HTML can do something
Markdown can't: call agents back. This page proves it.
</p>
</div>
</section>
<footer>
<span class="badge">AgentHTML · <strong>v0.1</strong> · custom editor pattern</span>
<span>open this in a browser to interact · or hand it to your team</span>
</footer>
</div>
<!-- ─────────── AgentHTML runtime (mock adapter inlined) ─────────── -->
<script>
(() => {
// ── State management ────────────────────────────────────────────────────
const state = (() => {
try { return JSON.parse(document.getElementById('agenthtml-state').textContent); }
catch { return {}; }
})();
window.agentHtml = { state };
// ── Bind inputs ─────────────────────────────────────────────────────────
// Textarea bindings
document.querySelectorAll('textarea[data-agent-bind]').forEach(el => {
el.value = state[el.dataset.agentBind] ?? el.value;
el.addEventListener('input', () => { state[el.dataset.agentBind] = el.value; });
});
// Segmented control bindings (custom — single source of truth in state[control])
document.querySelectorAll('[data-control]').forEach(group => {
const key = group.dataset.control;
group.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => {
group.querySelectorAll('button').forEach(b => b.removeAttribute('aria-pressed'));
btn.setAttribute('aria-pressed', 'true');
state[key] = btn.dataset.value;
});
});
});
// ── Mock adapter ────────────────────────────────────────────────────────
// Realistic responses keyed by (tone, length, scope)
function mockRewrite({ input, tone, length, scope }) {
const scopePrefix = scope === 'with-scope' ? 'stream' : null;
// Tone × length matrix
const variants = {
'terse|short': { title: 'fix slow-consumer backpressure', body: '' },
'terse|medium': { title: 'fix slow-consumer backpressure',
body: 'Apply high-water mark in async iterator.' },
'terse|long': { title: 'fix slow-consumer backpressure',
body: 'Async iterator applies HWM; prevents memory leak\non slow renders. Test added.' },
'conventional|short': { title: 'fix(stream): apply backpressure to slow consumers', body: '' },
'conventional|medium': { title: 'fix(stream): apply backpressure to slow consumers',
body: 'The async iterator now respects a high-water mark,\npausing reads when the consumer queue is full.\nFixes memory growth on slow renders.' },
'conventional|long': { title: 'fix(stream): apply backpressure to slow consumers',
body: 'Previously, the async iterator pulled chunks as fast\nas the network delivered them, with no signal back\nto the reader when the consumer was slow. On a slow\nrender (e.g. 30 Hz against a 200 Hz stream) the queue\ngrew unbounded.\n\nNow: the iterator pauses on a configurable high-water\nmark (default 16) and resumes when the consumer drains\nbelow it. Added a regression test for the slow-consumer\ncase.' },
'explanatory|short': { title: 'Stop leaking memory when the consumer is slow',
body: 'The streaming iterator now backpressures.' },
'explanatory|medium': { title: 'Stop leaking memory when the consumer is slow',
body: 'The new async iterator was pulling network chunks\nfaster than slow renders could consume them — so the\ninternal queue grew unbounded. We now apply a high-\nwater mark and pause reads while the consumer catches up.' },
'explanatory|long': { title: 'Stop leaking memory when the consumer is slow',
body: 'In the old callback version, the consumer\'s onChunk\nran synchronously, which gave us implicit backpressure\nfor free — a slow render literally stopped the next read.\n\nThe new async iterator broke that property: queue.push()\nnever blocks. On a 50 KB/s phone connection against a\nslow render, you could accumulate seconds of unconsumed\nbuffer.\n\nThis change reintroduces backpressure explicitly: the\niterator waits when the queue is at HWM, and resumes\nwhen the consumer drains it. Tested with a deliberately\nslow consumer; peak memory dropped from 14 MB to 180 KB.' },
'changelog|short': { title: '- Fix unbounded queue growth in slow-consumer streaming', body: '' },
'changelog|medium': { title: '### Fixed',
body: '- Streaming iterator now applies backpressure when the\n consumer queue exceeds the high-water mark, preventing\n memory growth on slow renders.' },
'changelog|long': { title: '### Fixed',
body: '- **Streaming backpressure regression**: The async iterator\n refactor in v2.1 dropped the implicit backpressure that\n the old callback API provided. On slow renders (or slow\n networks), this caused unbounded queue growth — up to\n several MB of buffered chunks before the response ended.\n\n The iterator now respects a configurable high-water mark\n (default 16 chunks) and pauses reads until the consumer\n drains it. Peak memory on the regression test dropped\n from 14 MB to 180 KB.' },
};
const v = variants[`${tone}|${length}`] || variants['conventional|medium'];
// Apply scope handling
let title = v.title;
if (tone === 'conventional' && scope === 'no-scope') {
title = title.replace(/^(\w+)\([^)]+\):\s*/, '$1: ');
}
return { title, body: v.body };
}
function mockPushback({ input, tone, length, scope }) {
// Argue for the OPPOSITE tone
const argues = {
terse: 'Verbose commits buy you something. "fix backpressure" tells future-me nothing about *why*, and `git log --oneline` is not the only consumer. Bisect spelunking, blame archeology, release-note generation — they all benefit from explanatory bodies. The 8 seconds it takes to write 4 lines of context pay back tenfold.',
conventional: 'The conventional-commits format optimizes for tooling that almost no team actually uses. It makes commit messages noisier for human readers (the `fix(scope):` prefix is signal-poor), and most release-note tooling now infers from PR titles instead. Plain English is faster to write and faster to read.',
explanatory: 'Long commit bodies are read once and rot fast. The actual answer to "why did we do this" lives in the PR description, the design doc, or the inline comment — places that are versioned with the code. Commit messages should be search keys, not narratives.',
changelog: 'Changelog-style commits couple the commit format to your release process, which means every contributor needs to know your release process. Worse, when the changelog mismatches reality (because someone forgot, or the format changed), the lie is now in the git history. Keep commits human; generate the changelog separately.',
};
return argues[tone] || argues['conventional'];
}
// ── Dispatch ────────────────────────────────────────────────────────────
function showLoading(slot) {
slot.querySelector('#rewrite-empty').style.display = 'none';
const content = slot.querySelector('#rewrite-content');
content.classList.add('visible');
content.innerHTML = '<div class="rewrite-block"><div class="loading-dots" style="color: var(--accent)"><span></span><span></span><span></span></div></div>';
}
function renderRewrite(slot, { title, body }) {
const content = slot.querySelector('#rewrite-content');
content.innerHTML = `
<div class="rewrite-block">
<div class="rewrite-title">${escapeHtml(title)}</div>
${body ? `<div class="rewrite-body">${escapeHtml(body)}</div>` : ''}
</div>
<div class="output-actions">
<button class="btn-sm" data-mini="copy">
<span>Copy</span><span class="arrow">⎘</span>
</button>
<button class="btn-sm" data-mini="regenerate">
<span>Regenerate</span><span class="arrow">↻</span>
</button>
<button class="btn-sm" data-mini="pushback"
data-agent-action="Argue against the chosen tone state.tone. Be specific, witty, and respectful — like a senior engineer playing devil's advocate."
data-agent-context="tone"
data-agent-render="pushback">
<span>Push back on this tone</span><span class="arrow">⇄</span>
</button>
</div>
`;
document.getElementById('rewrite-hint').textContent =
`${state.tone} · ${state.length} · ${state.scope === 'with-scope' ? 'scoped' : 'unscoped'}`;
}
function renderPushback(slot, message) {
// Remove any prior pushback
slot.querySelectorAll('.pushback-block').forEach(b => b.remove());
const block = document.createElement('div');
block.className = 'pushback-block';
block.innerHTML = `
<div class="pushback-label">
<span>◆ Counter-argument</span>
<span style="color: var(--ink-fade); letter-spacing: 0; text-transform: none; font-size: 11px;">
the agent disagrees with your choice
</span>
</div>
<p>${escapeHtml(message)}</p>
`;
slot.querySelector('#rewrite-content').appendChild(block);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
}
// Wire primary rewrite button
document.querySelectorAll('[data-agent-action][data-agent-render="rewrite"]').forEach(btn => {
btn.addEventListener('click', async () => {
const slot = document.querySelector(btn.dataset.agentTarget);
const orig = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span><span class="loading-dots"><span></span><span></span><span></span></span></span><span>·</span>';
showLoading(slot);
try {
await new Promise(r => setTimeout(r, 700 + Math.random() * 400));
const result = mockRewrite(state);
renderRewrite(slot, result);
} finally {
btn.disabled = false;
btn.innerHTML = orig;
}
});
});
// Wire delegated buttons (mini actions in the output)
document.body.addEventListener('click', async (e) => {
const mini = e.target.closest('[data-mini]');
if (!mini) return;
const kind = mini.dataset.mini;
const slot = document.getElementById('rewrite-output');
if (kind === 'copy') {
const text = slot.querySelector('.rewrite-title')?.textContent +
(slot.querySelector('.rewrite-body') ? '\n\n' + slot.querySelector('.rewrite-body').textContent : '');
try { await navigator.clipboard.writeText(text); } catch {}
const orig = mini.innerHTML;
mini.innerHTML = '<span>✓ Copied</span>';
setTimeout(() => { mini.innerHTML = orig; }, 1400);
return;
}
if (kind === 'regenerate') {
const btn = document.querySelector('.primary-btn[data-agent-action]');
btn?.click();
return;
}
if (kind === 'pushback') {
const orig = mini.innerHTML;
mini.disabled = true;
mini.innerHTML = '<span class="loading-dots"><span></span><span></span><span></span></span>';
try {
await new Promise(r => setTimeout(r, 800 + Math.random() * 500));
const msg = mockPushback(state);
renderPushback(slot, msg);
} finally {
mini.disabled = false;
mini.innerHTML = orig;
}
}
});
})();
</script>
</body>
</html>