Strengthen rank feedback decision brief
This commit is contained in:
+133
-22
@@ -1,5 +1,5 @@
|
||||
const sample = {
|
||||
idea: 'Scattermind clarified a messy course idea. Now I need the first build order, not a dashboard.',
|
||||
idea: 'Scattermind clarified a messy course idea. Now I need feedback on the feature/functionality order, not a dashboard.',
|
||||
optionsText: `- Manual build-order preview from one Concept Map
|
||||
- Copyable decision brief with Do first / Validate next / Defer / Park
|
||||
- Evidence questions beside each next move
|
||||
@@ -11,9 +11,17 @@ const sample = {
|
||||
mode: 'mvp',
|
||||
};
|
||||
|
||||
const laneMeta = {
|
||||
do: { title: 'Do first', note: 'The active first move. Give it oxygen before the backlog starts breeding.' },
|
||||
test: { title: 'Validate next', note: 'Strong candidates, but they need evidence before build time.' },
|
||||
defer: { title: 'Defer', note: 'Useful later. Dangerous if it steals attention now.' },
|
||||
park: { title: 'Park / cut', note: 'Not in the active decision. Reopen only if new evidence earns it.' },
|
||||
};
|
||||
|
||||
const form = document.querySelector('#rankForm');
|
||||
const results = document.querySelector('#results');
|
||||
const toastEl = document.querySelector('#toast');
|
||||
let lastResult = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
||||
@@ -43,7 +51,7 @@ function laneClass(lane) {
|
||||
return `lane-${lane?.id || 'defer'}`;
|
||||
}
|
||||
|
||||
function renderMetrics(metrics) {
|
||||
function renderMetrics(metrics = {}) {
|
||||
const items = [
|
||||
['Value', metrics.value],
|
||||
['Feasible', metrics.feasibility],
|
||||
@@ -56,45 +64,147 @@ function renderMetrics(metrics) {
|
||||
`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderPills(items = []) {
|
||||
if (!items.length) return '';
|
||||
return `<div class="signal-pills">${items.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderRankCard(item) {
|
||||
return `
|
||||
<article class="rank-card ${laneClass(item.lane)}">
|
||||
<div class="rank-topline">
|
||||
<span class="rank-number">#${item.rank}</span>
|
||||
<span class="lane-pill">${escapeHtml(item.lane?.label || 'Ranked')}</span>
|
||||
<strong>${item.metrics.score}</strong>
|
||||
</div>
|
||||
<h3>${escapeHtml(item.title)}</h3>
|
||||
${item.description && item.description !== item.title ? `<p class="item-description">${escapeHtml(item.description)}</p>` : ''}
|
||||
${renderPills(item.scoreDrivers || [])}
|
||||
<p><b>Why:</b> ${escapeHtml(item.reason)}.</p>
|
||||
<p><b>Concern:</b> ${escapeHtml(item.concern)}</p>
|
||||
<div class="action-strip">
|
||||
<div><span>Next step</span><p>${escapeHtml(item.nextStep || 'Run the smallest proof step.')}</p></div>
|
||||
<div><span>Evidence question</span><p>${escapeHtml(item.evidenceQuestion || 'What proof would change this ranking?')}</p></div>
|
||||
<div><span>Success / kill signal</span><p>${escapeHtml(item.successSignal || '')} ${escapeHtml(item.killSignal ? `Kill if: ${item.killSignal}` : '')}</p></div>
|
||||
</div>
|
||||
${renderMetrics(item.metrics)}
|
||||
${renderPills(item.scoringNotes || [])}
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLane(ranked, laneId) {
|
||||
const items = ranked.filter((item) => item.lane?.id === laneId);
|
||||
const meta = laneMeta[laneId];
|
||||
return `
|
||||
<section class="lane-column ${items.length ? '' : 'empty-lane'}">
|
||||
<div class="lane-head">
|
||||
<span>${escapeHtml(meta.title)}</span>
|
||||
<b>${items.length}</b>
|
||||
</div>
|
||||
<p>${escapeHtml(meta.note)}</p>
|
||||
<div class="lane-stack">
|
||||
${items.length ? items.map(renderRankCard).join('') : '<em>No items landed here.</em>'}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function markdownBrief(data) {
|
||||
const brief = data.brief || {};
|
||||
const glance = brief.quickGlance || {};
|
||||
const ranked = data.ranked || [];
|
||||
const lanes = ['do', 'test', 'defer', 'park'];
|
||||
const laneTitles = { do: 'Do first', test: 'Validate next', defer: 'Defer', park: 'Park / cut' };
|
||||
return [
|
||||
`# ${brief.headline || 'Ranked feedback map'}`,
|
||||
'',
|
||||
brief.summary || '',
|
||||
'',
|
||||
'## Quick judgement',
|
||||
`- Top pick: ${glance.topPick || 'n/a'}`,
|
||||
`- Why it wins: ${glance.whyThisWins || 'n/a'}`,
|
||||
`- Next action: ${glance.nextAction || 'n/a'}`,
|
||||
`- Evidence question: ${glance.evidenceQuestion || 'n/a'}`,
|
||||
`- Biggest trap: ${glance.biggestTrap || 'n/a'}`,
|
||||
`- Confidence: ${data.rankConfidence?.level || 'n/a'} — ${data.rankConfidence?.reason || ''}`,
|
||||
'',
|
||||
...lanes.flatMap((lane) => {
|
||||
const items = ranked.filter((item) => item.lane?.id === lane);
|
||||
return [`## ${laneTitles[lane]}`, ...(items.length ? items.map((item) => `- #${item.rank} ${item.title}: ${item.reason}. Next: ${item.nextStep}`) : ['- None']) , ''];
|
||||
}),
|
||||
'## Next 48 hours',
|
||||
...(brief.next48Hours || []).map((step) => `- ${step}`),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function copyText(text, label) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast(`${label} copied.`);
|
||||
} catch {
|
||||
toast('Clipboard blocked. Browser being a tiny tyrant.');
|
||||
}
|
||||
}
|
||||
|
||||
function attachResultActions(data) {
|
||||
document.querySelector('#copyBrief')?.addEventListener('click', () => copyText(markdownBrief(data), 'Decision brief'));
|
||||
document.querySelector('#copyActions')?.addEventListener('click', () => copyText((data.brief?.next48Hours || []).map((step, index) => `${index + 1}. ${step}`).join('\n'), '48h actions'));
|
||||
document.querySelector('#copyJson')?.addEventListener('click', () => copyText(JSON.stringify({ brief: data.brief, ranked: data.ranked, buildOrder: data.buildOrderDetails, handoff: data.handoff }, null, 2), 'JSON handoff'));
|
||||
}
|
||||
|
||||
function renderResults(data) {
|
||||
lastResult = data;
|
||||
const ranked = data.ranked || [];
|
||||
const brief = data.brief || {};
|
||||
const glance = brief.quickGlance || {};
|
||||
results.hidden = false;
|
||||
results.innerHTML = `
|
||||
<div class="results-head">
|
||||
<p class="eyebrow">${escapeHtml(data.mode?.label || 'Ranked feedback')}</p>
|
||||
<div class="results-head memo-head">
|
||||
<p class="eyebrow">${escapeHtml(data.mode?.label || 'Ranked feedback')} · first-pass judgement memo</p>
|
||||
<h2>${escapeHtml(brief.headline || 'Ranked feedback map')}</h2>
|
||||
<p>${escapeHtml(brief.summary || '')}</p>
|
||||
<div class="memo-stamps"><span>Not an oracle</span><span>${escapeHtml(data.rankConfidence?.level || 'first pass')} confidence</span><span>${ranked.length} items judged</span></div>
|
||||
</div>
|
||||
|
||||
<div class="ranked-list">
|
||||
${ranked.map((item) => `
|
||||
<article class="rank-card ${laneClass(item.lane)}">
|
||||
<div class="rank-topline">
|
||||
<span class="rank-number">#${item.rank}</span>
|
||||
<span class="lane-pill">${escapeHtml(item.lane?.label || 'Ranked')}</span>
|
||||
<strong>${item.metrics.score}</strong>
|
||||
</div>
|
||||
<h3>${escapeHtml(item.title)}</h3>
|
||||
${item.description && item.description !== item.title ? `<p class="item-description">${escapeHtml(item.description)}</p>` : ''}
|
||||
<p><b>Why:</b> ${escapeHtml(item.reason)}.</p>
|
||||
<p><b>Concern:</b> ${escapeHtml(item.concern)}</p>
|
||||
${renderMetrics(item.metrics)}
|
||||
</article>
|
||||
`).join('')}
|
||||
<section class="quick-glance" aria-label="Quick judgement">
|
||||
<div><span>Do this first</span><strong>${escapeHtml(glance.topPick || 'Add options')}</strong></div>
|
||||
<div><span>Why it wins</span><p>${escapeHtml(glance.whyThisWins || 'The list needs more comparable options.')}</p></div>
|
||||
<div><span>Proof to collect</span><p>${escapeHtml(glance.evidenceQuestion || 'Name the evidence that would change the ranking.')}</p></div>
|
||||
<div><span>Trap to avoid</span><p>${escapeHtml(glance.biggestTrap || brief.caution || 'Do not treat first-pass judgement as final truth.')}</p></div>
|
||||
</section>
|
||||
|
||||
<div class="result-actions" aria-label="Copy result actions">
|
||||
<button class="button ghost" type="button" id="copyBrief">Copy decision brief</button>
|
||||
<button class="button ghost" type="button" id="copyActions">Copy 48h actions</button>
|
||||
<button class="button ghost" type="button" id="copyJson">Copy JSON handoff</button>
|
||||
</div>
|
||||
|
||||
<div class="brief-grid">
|
||||
<div class="lane-board">
|
||||
${['do', 'test', 'defer', 'park'].map((lane) => renderLane(ranked, lane)).join('')}
|
||||
</div>
|
||||
|
||||
<div class="brief-grid reflection-room">
|
||||
<article class="brief-card next-card">
|
||||
<span>Next 48 hours</span>
|
||||
<ol>${(brief.next48Hours || []).map((step) => `<li>${escapeHtml(step)}</li>`).join('')}</ol>
|
||||
</article>
|
||||
<article class="brief-card next-card">
|
||||
<span>Rank confidence</span>
|
||||
<p><b>${escapeHtml(data.rankConfidence?.level || 'First pass')}</b> — ${escapeHtml(data.rankConfidence?.reason || brief.caution || '')}</p>
|
||||
</article>
|
||||
${(brief.whatWouldChangeRanking || []).length ? `
|
||||
<article class="brief-card next-card">
|
||||
<span>What would change the order</span>
|
||||
<ol>${brief.whatWouldChangeRanking.map((change) => `<li>${escapeHtml(change)}</li>`).join('')}</ol>
|
||||
</article>
|
||||
` : ''}
|
||||
${(data.closeCalls || []).length ? `
|
||||
<article class="brief-card next-card">
|
||||
<span>Close calls</span>
|
||||
<ol>${data.closeCalls.map((call) => `<li>${escapeHtml(call.note)}</li>`).join('')}</ol>
|
||||
</article>
|
||||
` : ''}
|
||||
${(brief.assumptions || []).length ? `
|
||||
<article class="brief-card">
|
||||
<span>Context carried forward</span>
|
||||
@@ -102,7 +212,7 @@ function renderResults(data) {
|
||||
</article>
|
||||
` : ''}
|
||||
${(brief.expertReflections || []).map((reflection) => `
|
||||
<article class="brief-card">
|
||||
<article class="brief-card expert-card">
|
||||
<span>${escapeHtml(reflection.lens)}</span>
|
||||
<p>${escapeHtml(reflection.text)}</p>
|
||||
</article>
|
||||
@@ -110,6 +220,7 @@ function renderResults(data) {
|
||||
</div>
|
||||
<p class="caution">${escapeHtml(brief.caution || 'First-pass judgement, not an oracle.')}</p>
|
||||
`;
|
||||
attachResultActions(data);
|
||||
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
@@ -118,7 +229,7 @@ async function createFeedbackMap(event) {
|
||||
const submit = form.querySelector('button[type="submit"]');
|
||||
const payload = Object.fromEntries(new FormData(form).entries());
|
||||
submit.disabled = true;
|
||||
submit.textContent = 'Ranking…';
|
||||
submit.textContent = 'Judging…';
|
||||
try {
|
||||
const response = await fetch('/api/rank-feedback', {
|
||||
method: 'POST',
|
||||
|
||||
+18
-16
@@ -3,10 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#101626" />
|
||||
<meta name="rank-version" content="2.1.0-editorial-decision-room" />
|
||||
<title>Ranker — ranked feedback maps for messy decisions</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=2.1.0-editorial-decision-room" />
|
||||
<meta name="theme-color" content="#f3eee4" />
|
||||
<meta name="rank-version" content="2.2.0-feedback-front-door" />
|
||||
<title>Ranker — feedback front door for messy decisions</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=2.2.0-feedback-front-door" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page-shell">
|
||||
@@ -22,9 +22,9 @@
|
||||
|
||||
<div class="hero-grid" id="top">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Decision feedback for scatterminds, builders, and structured people</p>
|
||||
<h1 id="hero-title">Dump your options. See what deserves attention first.</h1>
|
||||
<p class="lede">Ranker turns a messy list of ideas, features, offers, roadmap items, or next moves into a ranked feedback map: what to test, build, defer, or drop — and why.</p>
|
||||
<p class="eyebrow">Feedback intake for ideas, features, offers, and next moves</p>
|
||||
<h1 id="hero-title">Submit the mess. Get a defended first move.</h1>
|
||||
<p class="lede">Ranker is the front door to useful feedback: paste an idea plus the features or possible moves around it, choose the judgement lens, and get a decision brief with reasons, risks, expert reflections, and next steps.</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button primary" href="#try">Rank a messy list</a>
|
||||
<button class="button ghost" type="button" id="loadSampleTop">Load sample</button>
|
||||
@@ -57,19 +57,19 @@
|
||||
|
||||
<section class="decision-tool" id="try" aria-labelledby="try-title">
|
||||
<div class="tool-intro">
|
||||
<p class="eyebrow">MVP · no account · no dashboard swamp</p>
|
||||
<h2 id="try-title">Rank the pile</h2>
|
||||
<p>Paste one idea plus the possible features, directions, offers, or next steps. Pick what the ranking should care about. Ranker gives a first-pass decision brief.</p>
|
||||
<p class="eyebrow">MVP · feedback front door · no account · no dashboard swamp</p>
|
||||
<h2 id="try-title">Send a decision brief to the room</h2>
|
||||
<p>Describe what you want feedback on, then list the possible features, directions, offers, or next steps. Ranker gives you the quick judgement first, then the deeper reflections.</p>
|
||||
</div>
|
||||
|
||||
<form class="rank-form" id="rankForm">
|
||||
<label>
|
||||
<span>Main idea or context</span>
|
||||
<span>What do you want feedback on?</span>
|
||||
<textarea name="idea" rows="4" placeholder="Example: I’m building a tool that helps freelancers package their services and decide what to sell first."></textarea>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Options to rank <b>required</b></span>
|
||||
<span>Possible moves / features / functionality <b>required</b></span>
|
||||
<textarea name="optionsText" rows="9" required placeholder="One per line. Bullets, rambling, half-thoughts are fine.
|
||||
- Offer critique
|
||||
- Pricing calculator
|
||||
@@ -80,16 +80,18 @@
|
||||
</label>
|
||||
|
||||
<fieldset class="mode-picker">
|
||||
<legend>What should the ranking care about?</legend>
|
||||
<legend>What should the judgement optimize for?</legend>
|
||||
<label><input type="radio" name="mode" value="progress" checked /> <span>Fastest useful progress</span></label>
|
||||
<label><input type="radio" name="mode" value="mvp" /> <span>Best MVP order</span></label>
|
||||
<label><input type="radio" name="mode" value="validation" /> <span>Fastest validation</span></label>
|
||||
<label><input type="radio" name="mode" value="revenue" /> <span>Revenue potential</span></label>
|
||||
<label><input type="radio" name="mode" value="risk" /> <span>Risk reduction</span></label>
|
||||
<label><input type="radio" name="mode" value="risk" /> <span>Safest proof order</span></label>
|
||||
<label><input type="radio" name="mode" value="learning" /> <span>Biggest assumption test</span></label>
|
||||
<label><input type="radio" name="mode" value="originality" /> <span>Most original</span></label>
|
||||
</fieldset>
|
||||
|
||||
<label>
|
||||
<span>Extra constraints <em>optional</em></span>
|
||||
<span>Constraints the feedback should respect <em>optional</em></span>
|
||||
<input name="context" placeholder="Example: one week, solo builder, no paid ads, must be useful on mobile" />
|
||||
</label>
|
||||
|
||||
@@ -123,6 +125,6 @@
|
||||
</main>
|
||||
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
<script src="/app.js?v=2.1.0-editorial-decision-room" type="module"></script>
|
||||
<script src="/app.js?v=2.2.0-feedback-front-door" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+14
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user