717 lines
29 KiB
HTML
717 lines
29 KiB
HTML
<!-- 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>Brainstorm the intervention — 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; }
|
|
html { scroll-behavior: smooth; }
|
|
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 160px; }
|
|
|
|
/* ── header ─────────────────────────── */
|
|
header.page-head {
|
|
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;
|
|
}
|
|
a.back {
|
|
position: absolute;
|
|
top: 58px; right: 0;
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
color: var(--g500);
|
|
text-decoration: none;
|
|
}
|
|
a.back:hover { color: var(--clay); }
|
|
h1 {
|
|
font-family: var(--serif);
|
|
font-weight: 500;
|
|
font-size: clamp(32px, 4.6vw, 46px);
|
|
line-height: 1.08;
|
|
letter-spacing: -0.018em;
|
|
margin: 0 0 10px;
|
|
}
|
|
.lede {
|
|
font-size: 16px;
|
|
color: var(--g700);
|
|
max-width: 640px;
|
|
margin: 0 0 30px;
|
|
}
|
|
|
|
/* ── prompt box ─────────────────────── */
|
|
.prompt-card {
|
|
background: var(--paper);
|
|
border: 1.5px solid var(--g300);
|
|
border-radius: 14px;
|
|
padding: 22px 24px 24px;
|
|
position: relative;
|
|
}
|
|
.prompt-label {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: var(--clay);
|
|
margin-bottom: 12px;
|
|
}
|
|
.prompt-card blockquote {
|
|
margin: 0;
|
|
font-family: var(--serif);
|
|
font-style: italic;
|
|
font-size: 17.5px;
|
|
line-height: 1.5;
|
|
color: var(--slate);
|
|
max-width: 60ch;
|
|
}
|
|
button.copy-prompt {
|
|
position: absolute;
|
|
top: 16px; right: 16px;
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.04em;
|
|
color: var(--g700);
|
|
background: var(--g100);
|
|
border: 1.5px solid var(--g300);
|
|
border-radius: 8px;
|
|
padding: 6px 12px;
|
|
cursor: pointer;
|
|
transition: border-color 120ms, color 120ms;
|
|
}
|
|
button.copy-prompt:hover { border-color: var(--clay); color: var(--clay-d); }
|
|
button.copy-prompt.copied { color: var(--olive); border-color: var(--olive); }
|
|
@media (max-width: 560px) {
|
|
button.copy-prompt { position: static; margin-top: 14px; }
|
|
}
|
|
|
|
/* ── divider ────────────────────────── */
|
|
.divider {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
margin: 46px 0 34px;
|
|
font-family: var(--mono);
|
|
font-size: 11.5px;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: var(--g500);
|
|
white-space: nowrap;
|
|
}
|
|
.divider::before, .divider::after {
|
|
content: "";
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--g300);
|
|
}
|
|
|
|
/* ── artifact frame ─────────────────── */
|
|
.artifact {
|
|
background: var(--paper);
|
|
border: 1.5px solid var(--g300);
|
|
border-radius: 14px;
|
|
padding: 30px clamp(16px, 3.5vw, 34px) 34px;
|
|
}
|
|
.artifact h2 {
|
|
font-family: var(--serif);
|
|
font-weight: 500;
|
|
font-size: clamp(23px, 3vw, 29px);
|
|
letter-spacing: -0.015em;
|
|
line-height: 1.15;
|
|
margin: 6px 0 6px;
|
|
}
|
|
.art-eyebrow {
|
|
font-family: var(--mono);
|
|
font-size: 11px;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: var(--g500);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.art-eyebrow::before {
|
|
content: "";
|
|
width: 24px; height: 1.5px;
|
|
background: var(--clay);
|
|
flex: none;
|
|
}
|
|
.art-sub {
|
|
font-size: 14.5px;
|
|
color: var(--g700);
|
|
max-width: 66ch;
|
|
margin: 0 0 26px;
|
|
}
|
|
.art-sub code {
|
|
font-family: var(--mono);
|
|
font-size: 12.5px;
|
|
background: var(--g100);
|
|
border-radius: 5px;
|
|
padding: 1px 5px;
|
|
}
|
|
|
|
/* ── spectrum strip ─────────────────── */
|
|
.spectrum {
|
|
margin: 8px 0 34px;
|
|
}
|
|
.spec-labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-family: var(--mono);
|
|
font-size: 10.5px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--g500);
|
|
margin-bottom: 10px;
|
|
}
|
|
.spec-labels .lo { color: var(--olive); }
|
|
.spec-labels .hi { color: var(--clay-d); }
|
|
.spec-track {
|
|
position: relative;
|
|
height: 56px;
|
|
}
|
|
.spec-line {
|
|
position: absolute;
|
|
left: 0; right: 0; top: 27px;
|
|
height: 2px;
|
|
background: linear-gradient(90deg, var(--olive), var(--oat) 45%, var(--clay));
|
|
border-radius: 1px;
|
|
}
|
|
.spec-dot {
|
|
position: absolute;
|
|
top: 27px;
|
|
transform: translate(-50%, -50%);
|
|
width: 30px; height: 30px;
|
|
border-radius: 50%;
|
|
background: var(--paper);
|
|
border: 2px solid var(--g500);
|
|
color: var(--g700);
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
transition: border-color 120ms, background 120ms, color 120ms, transform 120ms;
|
|
}
|
|
.spec-dot:hover { border-color: var(--slate); transform: translate(-50%, -50%) scale(1.12); }
|
|
.spec-dot.on {
|
|
background: var(--clay);
|
|
border-color: var(--clay-d);
|
|
color: #fff;
|
|
}
|
|
.spec-ticks {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 11px;
|
|
color: var(--g500);
|
|
margin-top: 2px;
|
|
}
|
|
@media (max-width: 560px) {
|
|
.spec-dot { width: 24px; height: 24px; font-size: 10.5px; }
|
|
.spec-labels { font-size: 9px; }
|
|
}
|
|
|
|
/* ── intervention cards ─────────────── */
|
|
.cards { display: flex; flex-direction: column; gap: 14px; }
|
|
.iv {
|
|
border: 1.5px solid var(--g300);
|
|
border-radius: 12px;
|
|
background: var(--ivory);
|
|
padding: 18px 20px 16px;
|
|
scroll-margin-top: 20px;
|
|
transition: border-color 140ms, background 140ms;
|
|
}
|
|
.iv.checked { border-color: var(--clay); background: #FDF6F2; }
|
|
.iv-top {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.iv-num {
|
|
font-family: var(--mono);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--clay-d);
|
|
flex: none;
|
|
}
|
|
.iv h3 {
|
|
font-family: var(--serif);
|
|
font-weight: 500;
|
|
font-size: 19px;
|
|
letter-spacing: -0.01em;
|
|
margin: 0;
|
|
flex: 1 1 auto;
|
|
min-width: 200px;
|
|
}
|
|
.chips { display: flex; gap: 8px; align-items: center; flex: none; }
|
|
.effort {
|
|
font-family: var(--mono);
|
|
font-size: 10.5px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.06em;
|
|
border-radius: 999px;
|
|
padding: 3px 10px;
|
|
border: 1.5px solid transparent;
|
|
}
|
|
.effort.s { background: #EEF2E6; color: var(--olive); border-color: #C9D4B6; }
|
|
.effort.m { background: var(--oat); color: #7A6A4F; border-color: var(--g300); }
|
|
.effort.l { background: #F8E7DE; color: var(--clay-d); border-color: #EBC5B4; }
|
|
.effort.xl { background: var(--clay); color: #fff; border-color: var(--clay-d); }
|
|
.path {
|
|
display: inline-block;
|
|
font-family: var(--mono);
|
|
font-size: 11.5px;
|
|
color: var(--g700);
|
|
background: var(--g100);
|
|
border: 1px solid var(--g200);
|
|
border-radius: 6px;
|
|
padding: 2px 8px;
|
|
margin: 8px 0 2px;
|
|
max-width: 100%;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.iv-body { font-size: 14px; color: var(--g700); margin: 8px 0 0; }
|
|
.iv-body strong { color: var(--slate); font-weight: 600; }
|
|
.iv-found {
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--g500);
|
|
}
|
|
.iv-impact {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: baseline;
|
|
margin-top: 10px;
|
|
font-size: 13.5px;
|
|
color: var(--g700);
|
|
}
|
|
.iv-impact .tag {
|
|
font-family: var(--mono);
|
|
font-size: 10px;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--olive);
|
|
flex: none;
|
|
}
|
|
.iv-foot {
|
|
margin-top: 14px;
|
|
padding-top: 12px;
|
|
border-top: 1px dashed var(--g300);
|
|
}
|
|
label.resonate {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
font-size: 13.5px;
|
|
font-weight: 500;
|
|
color: var(--g700);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
label.resonate input {
|
|
appearance: none;
|
|
-webkit-appearance: none;
|
|
width: 18px; height: 18px;
|
|
border: 1.5px solid var(--g500);
|
|
border-radius: 5px;
|
|
background: var(--paper);
|
|
cursor: pointer;
|
|
margin: 0;
|
|
display: grid;
|
|
place-content: center;
|
|
transition: border-color 120ms, background 120ms;
|
|
}
|
|
label.resonate input::before {
|
|
content: "";
|
|
width: 9px; height: 9px;
|
|
transform: scale(0);
|
|
transition: transform 100ms;
|
|
clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 16%, 84% 4%, 38% 71%);
|
|
background: #fff;
|
|
}
|
|
label.resonate input:checked { background: var(--clay); border-color: var(--clay-d); }
|
|
label.resonate input:checked::before { transform: scale(1.15); }
|
|
.iv.checked label.resonate { color: var(--clay-d); }
|
|
|
|
/* ── sticky reply bar ───────────────── */
|
|
.replybar {
|
|
position: fixed;
|
|
left: 0; right: 0; bottom: 0;
|
|
background: var(--slate);
|
|
color: var(--ivory);
|
|
z-index: 50;
|
|
box-shadow: 0 -8px 24px rgba(20,20,19,.15);
|
|
}
|
|
.replybar-inner {
|
|
max-width: 980px;
|
|
margin: 0 auto;
|
|
padding: 12px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
.rb-count {
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
color: var(--oat);
|
|
flex: none;
|
|
white-space: nowrap;
|
|
}
|
|
.rb-count b { color: var(--clay); font-size: 15px; }
|
|
.rb-reply {
|
|
flex: 1;
|
|
font-family: var(--mono);
|
|
font-size: 12.5px;
|
|
color: var(--g300);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
min-width: 0;
|
|
}
|
|
.rb-reply.live { color: var(--ivory); }
|
|
button.rb-copy {
|
|
flex: none;
|
|
font-family: var(--mono);
|
|
font-size: 12px;
|
|
letter-spacing: 0.03em;
|
|
background: var(--clay);
|
|
color: #fff;
|
|
border: 1.5px solid var(--clay-d);
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
transition: background 120ms, opacity 120ms;
|
|
}
|
|
button.rb-copy:hover:not(:disabled) { background: var(--clay-d); }
|
|
button.rb-copy:disabled { opacity: .35; cursor: default; }
|
|
button.rb-copy.copied { background: var(--olive); border-color: var(--olive); }
|
|
@media (max-width: 640px) {
|
|
.replybar-inner { flex-wrap: wrap; gap: 8px; padding: 10px 16px; }
|
|
.rb-reply { order: 3; flex-basis: 100%; }
|
|
}
|
|
|
|
/* ── footer ─────────────────────────── */
|
|
footer {
|
|
margin-top: 56px;
|
|
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 class="page-head">
|
|
<a class="back" href="index.html">← All examples</a>
|
|
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
|
|
<h1>Brainstorm the intervention</h1>
|
|
<p class="lede">Before committing to a fix, ask for the whole option space — grounded in what actually exists in the code — so the choice you make is informed by alternatives you didn't know you had.</p>
|
|
|
|
<div class="prompt-card">
|
|
<div class="prompt-label">The prompt</div>
|
|
<blockquote>“Here's my rough problem: Acme users churn after onboarding. Search the codebase and brainstorm 10 places we could intervene, from cheapest to most ambitious. I'll tell you which ones resonate.”</blockquote>
|
|
<button class="copy-prompt" id="copyPrompt" type="button">copy prompt</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="divider">What Claude produced</div>
|
|
|
|
<div class="artifact">
|
|
<div class="art-eyebrow">Acme · post-onboarding churn</div>
|
|
<h2>10 places to intervene, cheapest to most ambitious</h2>
|
|
<p class="art-sub">I searched <code>apps/web</code>, <code>services/</code>, and <code>packages/</code> for onboarding, invite, and first-session code paths. A pattern showed up: <strong>a lot of retention machinery already exists but is disconnected</strong> — the cheapest options below are mostly wiring, not building. Check the ones that resonate and copy the reply at the bottom.</p>
|
|
|
|
<div class="spectrum">
|
|
<div class="spec-labels">
|
|
<span class="lo">Ship this afternoon</span>
|
|
<span class="hi">Quarter-long bet</span>
|
|
</div>
|
|
<div class="spec-track" id="specTrack">
|
|
<div class="spec-line"></div>
|
|
<button class="spec-dot" style="left:3%" data-n="1" type="button" title="Fix the dead-end empty state">1</button>
|
|
<button class="spec-dot" style="left:10%" data-n="2" type="button" title="Turn on the sample project">2</button>
|
|
<button class="spec-dot" style="left:19%" data-n="3" type="button" title="Show pending invites in-app">3</button>
|
|
<button class="spec-dot" style="left:29%" data-n="4" type="button" title="Wire up the milestones checklist">4</button>
|
|
<button class="spec-dot" style="left:39%" data-n="5" type="button" title="Engineer the first-comment moment">5</button>
|
|
<button class="spec-dot" style="left:49%" data-n="6" type="button" title="Rescue silently failed first uploads">6</button>
|
|
<button class="spec-dot" style="left:60%" data-n="7" type="button" title="Watch-together first session">7</button>
|
|
<button class="spec-dot" style="left:71%" data-n="8" type="button" title="Review workflow templates">8</button>
|
|
<button class="spec-dot" style="left:84%" data-n="9" type="button" title="Built-in quick screen recording">9</button>
|
|
<button class="spec-dot" style="left:96%" data-n="10" type="button" title="First-class client review portals">10</button>
|
|
</div>
|
|
<div class="spec-ticks"><span>wiring</span><span>new UI</span><span>new lifecycle</span><span>new surface</span></div>
|
|
</div>
|
|
|
|
<div class="cards">
|
|
|
|
<div class="iv" id="iv1">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#1</span>
|
|
<h3>Fix the dead-end empty state</h3>
|
|
<div class="chips"><span class="effort s">S</span></div>
|
|
</div>
|
|
<span class="path">apps/web/src/onboarding/EmptyState.tsx</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> The zero-projects dashboard renders the illustration and the copy <em>“No projects yet”</em> — and nothing else. <code>NewProjectButton</code> exists two directories over but was never imported here; the only way forward is the tiny “+” in the top nav. Every churned user's last screen was probably this one.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>100% of new users hit this screen; a real CTA plus a “drop a video anywhere” hint removes the most literal dead end in the product.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="1"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv2">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#2</span>
|
|
<h3>Turn on the sample project that already exists</h3>
|
|
<div class="chips"><span class="effort s">S</span></div>
|
|
</div>
|
|
<span class="path">apps/web/src/projects/sampleProject.ts</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Someone built a complete seeded demo project — “Product teaser v3” with a 45s clip, four timestamped comments, and one resolved thread — behind the flag <code>sample_project_v1</code>. The flag is still <strong>false for everyone</strong> and the file hasn't been touched in 8 months. The feature is done; it was just never shipped.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>New users see what a live review looks like before they've uploaded anything. Cheapest possible “aha” — flip a flag, verify the seed script.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="2"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv3">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#3</span>
|
|
<h3>Show pending invites inside the app</h3>
|
|
<div class="chips"><span class="effort s">S</span></div>
|
|
</div>
|
|
<span class="path">services/mailer/templates/invite.mjml · apps/web/src/team/TeamPanel.tsx</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Invites go out as email only. <code>TeamPanel</code> queries <code>team_members</code> but ignores the <code>invites</code> table entirely — an inviter can't see that their teammate never accepted, and there's no resend. For a <strong>collaborative</strong> review tool, a solo user is a churn-in-waiting.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>Surfacing “2 invites pending · resend” turns invisible invite decay into a visible, fixable state.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="3"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv4">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#4</span>
|
|
<h3>Wire the milestones table to a dashboard checklist</h3>
|
|
<div class="chips"><span class="effort m">M</span></div>
|
|
</div>
|
|
<span class="path">services/api/src/models/userMilestones.ts · apps/web/src/dashboard/Dashboard.tsx</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> The backend already records <code>first_upload</code>, <code>first_comment</code>, <code>first_invite_accepted</code>, and <code>first_approval</code> in <code>user_milestones</code> — but <strong>no client code reads the table</strong>. It exists purely for an analytics dashboard. All the ingredients for a “3 of 4 steps done” checklist are sitting there.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>Checklists reliably lift activation; this one needs one endpoint and one component because tracking already works.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="4"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv5">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#5</span>
|
|
<h3>Engineer the first-comment moment</h3>
|
|
<div class="chips"><span class="effort m">M</span></div>
|
|
</div>
|
|
<span class="path">apps/web/src/review/CommentComposer.tsx · apps/web/src/review/Timeline.tsx</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Timestamped comments are Acme's core loop, but the affordance is hidden: clicking the timeline seeds the timestamp into the composer, and nothing teaches this. <code>Timeline.tsx</code> has a <code>TODO: first-run hint?</code> from the original author. Add a ghost pin on first play — “click anywhere to comment at this moment”.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>If first-session comment rate is the activation metric (it should be), this is the most direct lever on it.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="5"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv6">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#6</span>
|
|
<h3>Rescue silently failed first uploads</h3>
|
|
<div class="chips"><span class="effort m">M</span></div>
|
|
</div>
|
|
<span class="path">services/ingest/src/transcode/queue.ts</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> When transcode fails (unsupported codec, oversized ProRes, corrupt moov atom), the job moves to <code>failed</code>, logs to Sentry, and <strong>tells the user nothing</strong> — the project card just shows a spinner forever. There's a retry-with-fallback-preset code path that's only reachable from an internal admin script.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>A user whose first upload hangs forever doesn't file a ticket — they leave. Auto-retry plus an honest error email converts a silent churn path into a support path.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="6"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv7">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#7</span>
|
|
<h3>Watch-together first session for teams</h3>
|
|
<div class="chips"><span class="effort l">L</span></div>
|
|
</div>
|
|
<span class="path">apps/web/src/review/PresenceProvider.tsx · services/realtime/</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Live presence is fully built — shared playhead, live cursors, “Maya is watching” badges — but it only lights up if two people <em>happen</em> to open the same review. Nothing engineers that coincidence. Add a “review together” scheduling nudge when a teammate accepts an invite, deep-linking both into the same session.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>Teams that experience one live session likely retain at a different rate than solo users; this makes the multiplayer moment deliberate instead of lucky.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="7"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv8">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#8</span>
|
|
<h3>Ship five review-workflow templates</h3>
|
|
<div class="chips"><span class="effort l">L</span></div>
|
|
</div>
|
|
<span class="path">packages/workflows/src/definitions/</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Approval workflows (rough cut → client review → final sign-off) are fully configurable — and start <strong>completely blank</strong>. The definitions directory contains only <code>__fixtures__</code> for tests. Judging by a comment in <code>WorkflowEditor.tsx</code>, power users copy a shared Notion doc to set theirs up. Ship 5 opinionated templates: agency delivery, YouTube pipeline, client sign-off, internal QA, localization pass.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>Templates convert “configure this abstraction” into “pick your job” — the classic fix for blank-canvas churn in workflow tools.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="8"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv9">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#9</span>
|
|
<h3>Built-in quick screen & camera recording</h3>
|
|
<div class="chips"><span class="effort xl">XL</span></div>
|
|
</div>
|
|
<span class="path">new surface · services/ingest already accepts chunked webm</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Today Acme's onboarding has a hard external dependency: <strong>you need a finished video before the product is useful</strong>. But <code>services/ingest</code> already accepts chunked webm uploads (built for the mobile app), so a MediaRecorder-based “record your screen, get feedback” flow needs no pipeline work — “just” the capture UI. Removes the biggest reason a trial goes idle: nothing to review yet.</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>Changes who can activate — designers and PMs without an editor can create the first reviewable artifact themselves. Big build, big surface area.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="9"> This resonates</label></div>
|
|
</div>
|
|
|
|
<div class="iv" id="iv10">
|
|
<div class="iv-top">
|
|
<span class="iv-num">#10</span>
|
|
<h3>First-class client review portals</h3>
|
|
<div class="chips"><span class="effort xl">XL</span></div>
|
|
</div>
|
|
<span class="path">services/auth/src/guestTokens.ts · apps/web/src/share/</span>
|
|
<p class="iv-body"><span class="iv-found">Found in code —</span> Guest share links exist but are deliberately crippled: tokens expire after 24h, guests can't comment, and there's a <code>// TODO(sasha): revisit guest perms after launch</code> from two years ago. The users who churn are often the ones whose <em>clients</em> wouldn't sign up. Make guests first-class — persistent portal, comment-with-name, no account — and every external review becomes a retention loop (and an acquisition one).</p>
|
|
<div class="iv-impact"><span class="tag">Impact</span>Attacks the structural cause: Acme's value depends on people who refuse to create accounts. Quarter-long bet with auth, billing, and abuse-prevention implications.</div>
|
|
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="10"> This resonates</label></div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
|
|
</div>
|
|
|
|
<div class="replybar">
|
|
<div class="replybar-inner">
|
|
<span class="rb-count"><b id="rbNum">0</b> resonate</span>
|
|
<span class="rb-reply" id="rbReply">Check the interventions that resonate — your reply builds here.</span>
|
|
<button class="rb-copy" id="rbCopy" type="button" disabled>copy reply</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
var PROMPT = "Here's my rough problem: Acme users churn after onboarding. Search the codebase and brainstorm 10 places we could intervene, from cheapest to most ambitious. I'll tell you which ones resonate.";
|
|
|
|
function copyText(text, btn, label, doneLabel) {
|
|
function done() {
|
|
btn.classList.add('copied');
|
|
btn.textContent = doneLabel;
|
|
setTimeout(function () {
|
|
btn.classList.remove('copied');
|
|
btn.textContent = label;
|
|
}, 1600);
|
|
}
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(text).then(done, function () { fallback(); });
|
|
} else { fallback(); }
|
|
function fallback() {
|
|
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);
|
|
done();
|
|
}
|
|
}
|
|
|
|
var copyPromptBtn = document.getElementById('copyPrompt');
|
|
copyPromptBtn.addEventListener('click', function () {
|
|
copyText(PROMPT, copyPromptBtn, 'copy prompt', 'copied');
|
|
});
|
|
|
|
var boxes = Array.prototype.slice.call(document.querySelectorAll('label.resonate input'));
|
|
var dots = {};
|
|
Array.prototype.forEach.call(document.querySelectorAll('.spec-dot'), function (d) {
|
|
dots[d.getAttribute('data-n')] = d;
|
|
d.addEventListener('click', function () {
|
|
var card = document.getElementById('iv' + d.getAttribute('data-n'));
|
|
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
});
|
|
});
|
|
|
|
var rbNum = document.getElementById('rbNum');
|
|
var rbReply = document.getElementById('rbReply');
|
|
var rbCopy = document.getElementById('rbCopy');
|
|
var currentReply = '';
|
|
|
|
function update() {
|
|
var picked = boxes
|
|
.filter(function (b) { return b.checked; })
|
|
.map(function (b) { return parseInt(b.getAttribute('data-n'), 10); })
|
|
.sort(function (a, b) { return a - b; });
|
|
|
|
boxes.forEach(function (b) {
|
|
var n = b.getAttribute('data-n');
|
|
var card = document.getElementById('iv' + n);
|
|
if (card) card.classList.toggle('checked', b.checked);
|
|
if (dots[n]) dots[n].classList.toggle('on', b.checked);
|
|
});
|
|
|
|
rbNum.textContent = picked.length;
|
|
if (picked.length === 0) {
|
|
currentReply = '';
|
|
rbReply.textContent = 'Check the interventions that resonate — your reply builds here.';
|
|
rbReply.classList.remove('live');
|
|
rbCopy.disabled = true;
|
|
} else {
|
|
var list = picked.map(function (n) { return '#' + n; }).join(', ');
|
|
currentReply = 'These resonate: ' + list + ' — start with #' + picked[0];
|
|
rbReply.textContent = '“' + currentReply + '”';
|
|
rbReply.classList.add('live');
|
|
rbCopy.disabled = false;
|
|
}
|
|
}
|
|
|
|
boxes.forEach(function (b) { b.addEventListener('change', update); });
|
|
rbCopy.addEventListener('click', function () {
|
|
if (currentReply) copyText(currentReply, rbCopy, 'copy reply', 'copied');
|
|
});
|
|
|
|
update();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|