chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:33:58 +08:00
commit 6a2f6934bb
38 changed files with 20049 additions and 0 deletions
+644
View File
@@ -0,0 +1,644 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The interview — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── header ─────────────────────────── */
header { padding: 56px 0 8px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.backlink {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px;
color: var(--g500); text-decoration: none;
letter-spacing: 0.04em;
}
.backlink:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(34px, 5vw, 50px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 0 0 10px;
}
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 30px; }
/* ── prompt box ─────────────────────── */
.promptbox {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 24px 20px;
position: relative;
}
.promptbox .label {
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--clay); margin-bottom: 10px;
display: flex; align-items: center; gap: 12px;
}
.promptbox .label::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.promptbox blockquote {
margin: 0; padding: 0;
font-family: var(--serif); font-style: italic;
font-size: 17.5px; line-height: 1.5; color: var(--slate);
max-width: 46em;
}
.copybtn {
position: absolute; top: 18px; right: 18px;
font-family: var(--mono); font-size: 11.5px;
letter-spacing: 0.04em;
background: var(--g100); color: var(--g700);
border: 1.5px solid var(--g300); border-radius: 7px;
padding: 5px 11px; cursor: pointer;
}
.copybtn:hover { border-color: var(--clay); color: var(--clay-d); }
.copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
/* ── divider ────────────────────────── */
.divider {
display: flex; align-items: center; gap: 16px;
margin: 44px 0 30px;
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--g500); white-space: nowrap;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--g300); }
/* ══ THE ARTIFACT ═════════════════════ */
.artifact {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
overflow: hidden;
}
.art-head {
padding: 16px 22px;
border-bottom: 1.5px solid var(--g200);
display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
background: var(--g100);
}
.art-head .t { font-family: var(--serif); font-weight: 500; font-size: 17px; letter-spacing: -0.01em; }
.art-head .s { font-family: var(--mono); font-size: 11.5px; color: var(--g500); }
.art-body { display: grid; grid-template-columns: 216px 1fr; min-height: 480px; }
@media (max-width: 720px) { .art-body { grid-template-columns: 1fr; min-height: 0; } }
/* rail */
.rail {
border-right: 1.5px solid var(--g200);
padding: 20px 16px;
background: var(--ivory);
}
@media (max-width: 720px) {
.rail { border-right: none; border-bottom: 1.5px solid var(--g200); padding: 14px 16px; }
}
.rail .rail-label {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--g500); margin-bottom: 12px;
}
.rail-item {
display: flex; align-items: center; gap: 10px;
padding: 6px 8px; margin: 0 -8px;
border-radius: 8px;
font-size: 12.5px; color: var(--g500);
cursor: default;
border: none; background: none; width: calc(100% + 16px);
text-align: left; font-family: var(--sans);
}
.rail-item.done { color: var(--g700); cursor: pointer; }
.rail-item.done:hover { background: var(--g100); }
.rail-item.current { color: var(--slate); font-weight: 600; background: var(--g100); }
.rail-dot {
width: 11px; height: 11px; border-radius: 50%; flex: none;
border: 2px solid var(--dotc, var(--g300));
background: transparent;
}
.rail-item.done .rail-dot { background: var(--dotc); }
.rail-item.current .rail-dot { box-shadow: 0 0 0 3px var(--g200); }
.rail-item .rail-q { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-item .tick { margin-left: auto; font-size: 11px; color: var(--olive); flex: none; }
.rail-meta {
margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--g200);
font-family: var(--mono); font-size: 11px; color: var(--g500); line-height: 1.8;
}
.rail-meta b { color: var(--g700); font-weight: 600; }
@media (max-width: 720px) {
.rail .rail-q, .rail .tick, .rail-meta, .rail .rail-label { display: none; }
.rail { display: flex; align-items: center; gap: 10px; }
.rail-item { width: auto; padding: 4px; margin: 0; }
}
/* stage */
.stage { padding: 28px 30px 30px; position: relative; }
@media (max-width: 720px) { .stage { padding: 22px 18px 24px; } }
.qcard { animation: qin .28s ease both; }
.qcard.leaving { animation: qout .18s ease both; }
@keyframes qin { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes qout { from { opacity: 1; } to { opacity: 0; transform: translateY(-8px); } }
.badge {
display: inline-block;
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.1em; text-transform: uppercase;
padding: 3px 9px; border-radius: 5px;
color: #fff; background: var(--g500);
margin-bottom: 4px;
}
.qnum { font-family: var(--mono); font-size: 11.5px; color: var(--g500); margin-left: 10px; }
.qtext {
font-family: var(--serif); font-weight: 500;
font-size: clamp(20px, 3vw, 25px); line-height: 1.25;
letter-spacing: -0.012em;
margin: 10px 0 6px; max-width: 34em;
}
.why {
font-size: 13.5px; color: var(--g500); margin: 0 0 22px;
display: flex; gap: 8px; align-items: baseline;
}
.why::before { content: "↳"; color: var(--clay); flex: none; }
.opts { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
@media (max-width: 620px) { .opts { grid-template-columns: 1fr; } }
.opt {
text-align: left;
background: var(--ivory);
border: 1.5px solid var(--g300);
border-radius: 11px;
padding: 14px 16px;
cursor: pointer;
font-family: var(--sans);
color: var(--slate);
transition: border-color .12s, background .12s, transform .12s;
}
.opt:hover { border-color: var(--clay); background: var(--paper); transform: translateY(-1px); }
.opt.picked { border-color: var(--clay); background: var(--paper); box-shadow: 0 0 0 3px rgba(217,119,87,.15); }
.opt .ot { font-weight: 600; font-size: 14.5px; display: block; margin-bottom: 3px; }
.opt .od { font-size: 12.5px; color: var(--g500); line-height: 1.45; display: block; }
.otherrow { display: flex; gap: 8px; margin-top: 12px; }
.otherrow input {
flex: 1; min-width: 0;
font-family: var(--sans); font-size: 14px;
padding: 10px 13px;
border: 1.5px solid var(--g300); border-radius: 9px;
background: var(--ivory); color: var(--slate);
}
.otherrow input::placeholder { color: var(--g500); }
.otherrow input:focus { outline: none; border-color: var(--clay); background: var(--paper); }
.otherrow button {
font-family: var(--mono); font-size: 12px;
padding: 0 16px; border-radius: 9px; cursor: pointer;
border: 1.5px solid var(--g300); background: var(--g100); color: var(--g700);
}
.otherrow button:hover { border-color: var(--clay); color: var(--clay-d); }
.navrow { display: flex; justify-content: space-between; align-items: center; margin-top: 24px; }
.backbtn {
font-family: var(--mono); font-size: 12px;
background: none; border: none; cursor: pointer;
color: var(--g500); padding: 6px 0;
}
.backbtn:hover { color: var(--clay-d); }
.backbtn[disabled] { visibility: hidden; }
.navhint { font-family: var(--mono); font-size: 11px; color: var(--g500); }
/* summary */
.sum-h { font-family: var(--serif); font-weight: 500; font-size: 24px; letter-spacing: -0.012em; margin: 4px 0 4px; }
.sum-sub { font-size: 14px; color: var(--g500); margin: 0 0 20px; }
.dtable { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.dtable-scroll { overflow-x: auto; }
.dtable th {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: 0.1em; text-transform: uppercase;
color: var(--g500); font-weight: 500;
text-align: left; padding: 8px 12px 8px 0;
border-bottom: 1.5px solid var(--g300);
white-space: nowrap;
}
.dtable td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--g200); vertical-align: top; }
.dtable tr:last-child td { border-bottom: none; }
.dtable .pill {
font-family: var(--mono); font-size: 10px;
letter-spacing: 0.08em; text-transform: uppercase;
color: #fff; padding: 2px 7px; border-radius: 4px; white-space: nowrap;
}
.dtable .topic { color: var(--g500); white-space: nowrap; }
.dtable .dec { font-weight: 600; }
.dtable .redo {
font-family: var(--mono); font-size: 11px; color: var(--g500);
background: none; border: none; cursor: pointer; padding: 0;
}
.dtable .redo:hover { color: var(--clay-d); }
.genblock { margin-top: 26px; }
.genblock .gb-label {
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--clay); margin-bottom: 8px;
display: flex; align-items: center; gap: 10px;
}
.genblock .gb-label::before { content: ""; width: 18px; height: 1.5px; background: var(--clay); flex: none; }
.genwrap { position: relative; }
.genwrap pre {
margin: 0;
background: var(--slate);
color: var(--g200);
border-radius: 11px;
padding: 18px 20px;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
}
.genwrap .copybtn { top: 12px; right: 12px; background: var(--g700); border-color: var(--g700); color: var(--g300); }
.genwrap .copybtn:hover { color: var(--paper); border-color: var(--g500); }
.genwrap .copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
.restart {
margin-top: 18px;
font-family: var(--mono); font-size: 12px;
background: none; border: none; cursor: pointer;
color: var(--g500); padding: 0;
}
.restart:hover { color: var(--clay-d); }
footer {
margin-top: 56px; padding-top: 22px;
border-top: 1px solid var(--g300);
font-size: 13px; color: var(--g500);
}
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
footer a:hover { text-decoration-color: var(--clay); }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<a class="backlink" href="index.html">← All examples</a>
<h1>The interview</h1>
<p class="lede">Instead of guessing at ambiguous requirements, ask Claude to interview you — one question at a time, ordered by how much each answer would change the architecture.</p>
<div class="promptbox">
<div class="label">The prompt</div>
<button class="copybtn" id="copyPrompt">copy prompt</button>
<blockquote id="promptText">Interview me one question at a time about anything still ambiguous in the annotation-export feature. Prioritize questions where my answer would change the architecture.</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<div class="artifact">
<div class="art-head">
<span class="t">Annotation export — spec interview</span>
<span class="s">acme/web · 7 open questions · ordered by blast radius</span>
</div>
<div class="art-body">
<nav class="rail" id="rail" aria-label="Interview progress"></nav>
<div class="stage" id="stage"></div>
</div>
</div>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<script>
(function () {
"use strict";
var RADII = {
arch: { label: "Architecture", color: "var(--clay)" },
data: { label: "Data model", color: "var(--clay-d)" },
ux: { label: "UX", color: "var(--olive)" },
pol: { label: "Polish", color: "var(--g500)" }
};
var QUESTIONS = [
{
id: "runtime",
radius: "arch",
short: "Where exports run",
topic: "Execution model",
q: "Where should export generation actually run?",
why: "This decides whether we need a job queue, a notification path, and an artifacts bucket — or none of those. Everything else layers on top.",
opts: [
{ t: "Synchronously in the API request", d: "Simple, but a 400-comment review on a 2-hour cut will blow past the 30s gateway timeout." },
{ t: "Background job via the existing sidekiq queue", d: "Reuses jobs/export_worker patterns from proxy transcodes; needs a “ready” notification." },
{ t: "Entirely client-side from the annotations API", d: "Zero new backend, but caps us at formats the browser can assemble, and leaks nothing to support later." }
]
},
{
id: "scope",
radius: "arch",
short: "Export scope",
topic: "Scope of one export",
q: "What is the unit of a single export?",
why: "Project-level export means fan-out across videos, pagination, and a manifest format — roughly 3× the surface of per-video.",
opts: [
{ t: "One video at a time", d: "Matches the review-page mental model; a project export is just N clicks for now." },
{ t: "A whole project (all videos)", d: "What the Meridian Post account asked for; implies zip bundling and per-video manifests." },
{ t: "Arbitrary user-picked selection", d: "Most flexible, most UI; needs a selection model that doesnt exist anywhere yet." }
]
},
{
id: "drawings",
radius: "data",
short: "Drawings included?",
topic: "Drawing annotations",
q: "Do frame drawings export, or only text comments?",
why: "Drawings live as canvas stroke JSON in annotation_shapes; including them means rasterizing server-side or shipping raw vectors.",
opts: [
{ t: "Text comments only (v1)", d: "Ships fastest; drawings show as “[drawing on frame 4211]” placeholders." },
{ t: "Rasterize drawings to PNG stills", d: "Best for PDF reports; needs a headless render step — the thumbnail worker is close but not identical." },
{ t: "Include raw stroke JSON", d: "Lossless and cheap, but only useful to someone re-importing into Acme." }
]
},
{
id: "timecode",
radius: "data",
short: "Timestamp format",
topic: "Timestamp representation",
q: "How should annotation timestamps be represented in the export?",
why: "We store milliseconds; editors live in SMPTE timecode. Converting needs the source fps, which is null for 3% of legacy uploads.",
opts: [
{ t: "SMPTE timecode (HH:MM:SS:FF)", d: "What Resolve and Premiere expect; requires an fps backfill for legacy videos." },
{ t: "Plain seconds / milliseconds", d: "No conversion risk, but every editor will hand-convert and some will do it wrong." },
{ t: "Both columns, always", d: "Slightly wider files; nobody ever has to ask which one is authoritative." }
]
},
{
id: "formats",
radius: "ux",
short: "v1 formats",
topic: "Export formats",
q: "Which formats does v1 ship with?",
why: "Each format is mostly independent work, but the choice signals who the feature is for — producers, editors, or clients.",
opts: [
{ t: "CSV only", d: "One afternoon of work once the pipeline exists; producers can pivot it however they like." },
{ t: "PDF review report", d: "Client-facing artifact with thumbnails; only meaningful if drawings rasterize." },
{ t: "NLE marker file (EDL / Resolve CSV)", d: "The power-user ask from the forum thread; timecode question becomes load-bearing." },
{ t: "CSV + NLE markers, PDF later", d: "Covers both internal and editor workflows without blocking on rasterization." }
]
},
{
id: "access",
radius: "ux",
short: "Who can export",
topic: "Export permissions",
q: "Who is allowed to export?",
why: "Guest reviewers use unauthenticated share links — if they can export, annotations silently leave the permission boundary.",
opts: [
{ t: "Anyone who can view the video", d: "Includes share-link guests; simplest rule, biggest data-egress surface." },
{ t: "Project members only", d: "Guests keep commenting but cant bulk-extract; needs a disabled-state explanation in the UI." },
{ t: "Owner and admins only", d: "Most conservative; likely generates “can you export this for me” busywork." }
]
},
{
id: "naming",
radius: "pol",
short: "Files & naming",
topic: "File naming & bundling",
q: "How are export files named and bundled?",
why: "Pure convention, but the first support ticket after launch is always “which file is which cut.”",
opts: [
{ t: "{project}_{video}_{date}.csv, flat", d: "Predictable and grep-able; long titles get truncated at 80 chars." },
{ t: "Zip per export with a manifest.json", d: "One artifact per export regardless of scope; slight friction for single-file cases." },
{ t: "Let the user pick at export time", d: "A settings surface for something almost nobody will change." }
]
}
];
var answers = new Array(QUESTIONS.length).fill(null); // {text, custom}
var current = 0; // index, or QUESTIONS.length => summary
var rail = document.getElementById("rail");
var stage = document.getElementById("stage");
function answeredCount() {
return answers.filter(function (a) { return a !== null; }).length;
}
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function renderRail() {
var html = '<div class="rail-label">Interview progress</div>';
QUESTIONS.forEach(function (q, i) {
var r = RADII[q.radius];
var cls = "rail-item" + (i === current ? " current" : "") + (answers[i] ? " done" : "");
html += '<button type="button" class="' + cls + '" data-i="' + i + '" style="--dotc:' + r.color + '"' +
(answers[i] ? '' : ' tabindex="-1"') + '>' +
'<span class="rail-dot"></span><span class="rail-q">' + esc(q.short) + '</span>' +
(answers[i] ? '<span class="tick">✓</span>' : '') + '</button>';
});
html += '<div class="rail-meta"><b>' + answeredCount() + '</b> of 7 answered<br>' +
'<span style="color:var(--clay)">●</span> architecture &nbsp;' +
'<span style="color:var(--clay-d)">●</span> data<br>' +
'<span style="color:var(--olive)">●</span> ux &nbsp;' +
'<span style="color:var(--g500)">●</span> polish</div>';
rail.innerHTML = html;
rail.querySelectorAll(".rail-item.done").forEach(function (el) {
el.addEventListener("click", function () { go(parseInt(el.dataset.i, 10)); });
});
}
function renderQuestion() {
var q = QUESTIONS[current];
var r = RADII[q.radius];
var picked = answers[current];
var html = '<div class="qcard">' +
'<span class="badge" style="background:' + r.color + '">' + esc(r.label) + '</span>' +
'<span class="qnum">Question ' + (current + 1) + ' of ' + QUESTIONS.length + '</span>' +
'<h2 class="qtext">' + esc(q.q) + '</h2>' +
'<p class="why">' + esc(q.why) + '</p>' +
'<div class="opts">';
q.opts.forEach(function (o, oi) {
var isPicked = picked && !picked.custom && picked.text === o.t;
html += '<button type="button" class="opt' + (isPicked ? ' picked' : '') + '" data-oi="' + oi + '">' +
'<span class="ot">' + esc(o.t) + '</span><span class="od">' + esc(o.d) + '</span></button>';
});
html += '</div>' +
'<div class="otherrow">' +
'<input type="text" id="otherInput" placeholder="Other — type your own answer…" value="' +
(picked && picked.custom ? esc(picked.text) : '') + '">' +
'<button type="button" id="otherGo">answer ↵</button></div>' +
'<div class="navrow">' +
'<button type="button" class="backbtn" id="backBtn"' + (current === 0 ? ' disabled' : '') + '>← previous question</button>' +
'<span class="navhint">pick a card to continue</span>' +
'</div></div>';
stage.innerHTML = html;
stage.querySelectorAll(".opt").forEach(function (el) {
el.addEventListener("click", function () {
var o = q.opts[parseInt(el.dataset.oi, 10)];
answers[current] = { text: o.t, custom: false };
el.classList.add("picked");
renderRail();
setTimeout(function () { go(current + 1); }, 240);
});
});
var input = stage.querySelector("#otherInput");
function submitOther() {
var v = input.value.trim();
if (!v) { input.focus(); return; }
answers[current] = { text: v, custom: true };
renderRail();
go(current + 1);
}
stage.querySelector("#otherGo").addEventListener("click", submitOther);
input.addEventListener("keydown", function (e) { if (e.key === "Enter") submitOther(); });
stage.querySelector("#backBtn").addEventListener("click", function () { go(current - 1); });
}
function buildFollowupPrompt() {
var lines = [
"Here's what we decided in the interview — implement the",
"annotation-export feature with these constraints:",
""
];
QUESTIONS.forEach(function (q, i) {
var a = answers[i];
lines.push((i + 1) + ". " + q.topic + ": " + (a ? a.text : "(unanswered — use your judgment)") +
(a && a.custom ? " [my own wording — ask if unclear]" : ""));
});
lines.push("");
lines.push("Treat the architecture and data-model decisions (14) as fixed.");
lines.push("If anything in 57 conflicts with them, flag it before writing code.");
lines.push("Start with a short plan, then implement.");
return lines.join("\n");
}
function renderSummary() {
var rows = "";
QUESTIONS.forEach(function (q, i) {
var r = RADII[q.radius];
var a = answers[i];
rows += "<tr>" +
'<td><span class="pill" style="background:' + r.color + '">' + esc(r.label) + '</span></td>' +
'<td class="topic">' + esc(q.topic) + '</td>' +
'<td class="dec">' + (a ? esc(a.text) + (a.custom ? ' <span style="font-weight:400;color:var(--g500)">(your wording)</span>' : '') : '<span style="color:var(--g500)">—</span>') + '</td>' +
'<td><button type="button" class="redo" data-i="' + i + '">edit</button></td>' +
"</tr>";
});
stage.innerHTML = '<div class="qcard">' +
'<span class="badge" style="background:var(--olive)">Interview complete</span>' +
'<h2 class="sum-h">Seven decisions, zero assumptions.</h2>' +
'<p class="sum-sub">Every answer below would otherwise have been guessed silently during implementation.</p>' +
'<div class="dtable-scroll"><table class="dtable"><thead><tr>' +
'<th>Radius</th><th>Question</th><th>Decision</th><th></th>' +
'</tr></thead><tbody>' + rows + '</tbody></table></div>' +
'<div class="genblock">' +
'<div class="gb-label">Generated follow-up prompt</div>' +
'<div class="genwrap"><button type="button" class="copybtn" id="copyGen">copy</button>' +
'<pre id="genPre">' + esc(buildFollowupPrompt()) + '</pre></div>' +
'</div>' +
'<button type="button" class="restart" id="restartBtn">↺ start over</button>' +
'</div>';
stage.querySelectorAll(".redo").forEach(function (el) {
el.addEventListener("click", function () { go(parseInt(el.dataset.i, 10)); });
});
wireCopy(stage.querySelector("#copyGen"), function () { return buildFollowupPrompt(); });
stage.querySelector("#restartBtn").addEventListener("click", function () {
answers = new Array(QUESTIONS.length).fill(null);
go(0);
});
}
function go(idx) {
var card = stage.querySelector(".qcard");
var doRender = function () {
current = idx;
renderRail();
if (current >= QUESTIONS.length) renderSummary();
else renderQuestion();
};
if (card) {
card.classList.add("leaving");
setTimeout(doRender, 160);
} else {
doRender();
}
}
function wireCopy(btn, getText) {
btn.addEventListener("click", function () {
var text = getText();
var done = function () {
var orig = btn.textContent;
btn.textContent = "copied ✓";
btn.classList.add("done");
setTimeout(function () { btn.textContent = orig; btn.classList.remove("done"); }, 1600);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });
} else {
fallbackCopy(text); done();
}
});
}
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) {}
document.body.removeChild(ta);
}
wireCopy(document.getElementById("copyPrompt"), function () {
return document.getElementById("promptText").textContent;
});
renderRail();
renderQuestion();
})();
</script>
</body>
</html>