const sample = { 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 - Accounts and saved workspaces - Team voting on roadmap priority - Subscription billing layer - Polished export for sharing the defended order`, context: 'Snapshot / Concept Map handoff, solo builder, tired non-AI-native user, avoid auth/workspaces/billing before proof.', 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])); } function metricPct(value) { return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 10))); } function toast(message) { toastEl.textContent = message; toastEl.hidden = false; clearTimeout(toastEl._timer); toastEl._timer = setTimeout(() => { toastEl.hidden = true; }, 2600); } function fillSample() { form.idea.value = sample.idea; form.optionsText.value = sample.optionsText; form.context.value = sample.context; form.mode.value = sample.mode; document.querySelector('#try')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); toast('Sample loaded. Run it or replace it with your own mess.'); } function laneClass(lane) { return `lane-${lane?.id || 'defer'}`; } function renderMetrics(metrics = {}) { const items = [ ['Value', metrics.value], ['Feasible', metrics.feasibility], ['Confidence', metrics.confidence], ['Revenue', metrics.revenue], ['Risk', metrics.risk], ]; return `
${items.map(([label, value]) => `
${label}
`).join('')}
`; } function renderPills(items = []) { if (!items.length) return ''; return `
${items.map((item) => `${escapeHtml(item)}`).join('')}
`; } function renderRankCard(item) { return `
#${item.rank} ${escapeHtml(item.lane?.label || 'Ranked')} ${item.metrics.score}

${escapeHtml(item.title)}

${item.description && item.description !== item.title ? `

${escapeHtml(item.description)}

` : ''} ${renderPills(item.scoreDrivers || [])}

Why: ${escapeHtml(item.reason)}.

Concern: ${escapeHtml(item.concern)}

Next step

${escapeHtml(item.nextStep || 'Run the smallest proof step.')}

Evidence question

${escapeHtml(item.evidenceQuestion || 'What proof would change this ranking?')}

Success / kill signal

${escapeHtml(item.successSignal || '')} ${escapeHtml(item.killSignal ? `Kill if: ${item.killSignal}` : '')}

${renderMetrics(item.metrics)} ${renderPills(item.scoringNotes || [])}
`; } function renderLane(ranked, laneId) { const items = ranked.filter((item) => item.lane?.id === laneId); const meta = laneMeta[laneId]; return `
${escapeHtml(meta.title)} ${items.length}

${escapeHtml(meta.note)}

${items.length ? items.map(renderRankCard).join('') : 'No items landed here.'}
`; } 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 = `

${escapeHtml(data.mode?.label || 'Ranked feedback')} · first-pass judgement memo

${escapeHtml(brief.headline || 'Ranked feedback map')}

${escapeHtml(brief.summary || '')}

Not an oracle${escapeHtml(data.rankConfidence?.level || 'first pass')} confidence${ranked.length} items judged
Do this first${escapeHtml(glance.topPick || 'Add options')}
Why it wins

${escapeHtml(glance.whyThisWins || 'The list needs more comparable options.')}

Proof to collect

${escapeHtml(glance.evidenceQuestion || 'Name the evidence that would change the ranking.')}

Trap to avoid

${escapeHtml(glance.biggestTrap || brief.caution || 'Do not treat first-pass judgement as final truth.')}

${['do', 'test', 'defer', 'park'].map((lane) => renderLane(ranked, lane)).join('')}
Next 48 hours
    ${(brief.next48Hours || []).map((step) => `
  1. ${escapeHtml(step)}
  2. `).join('')}
Rank confidence

${escapeHtml(data.rankConfidence?.level || 'First pass')} — ${escapeHtml(data.rankConfidence?.reason || brief.caution || '')}

${(brief.whatWouldChangeRanking || []).length ? `
What would change the order
    ${brief.whatWouldChangeRanking.map((change) => `
  1. ${escapeHtml(change)}
  2. `).join('')}
` : ''} ${(data.closeCalls || []).length ? `
Close calls
    ${data.closeCalls.map((call) => `
  1. ${escapeHtml(call.note)}
  2. `).join('')}
` : ''} ${(brief.assumptions || []).length ? `
Context carried forward
` : ''} ${(brief.expertReflections || []).map((reflection) => `
${escapeHtml(reflection.lens)}

${escapeHtml(reflection.text)}

`).join('')}

${escapeHtml(brief.caution || 'First-pass judgement, not an oracle.')}

`; attachResultActions(data); results.scrollIntoView({ behavior: 'smooth', block: 'start' }); } async function createFeedbackMap(event) { event.preventDefault(); const submit = form.querySelector('button[type="submit"]'); const payload = Object.fromEntries(new FormData(form).entries()); submit.disabled = true; submit.textContent = 'Judging…'; try { const response = await fetch('/api/rank-feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) throw new Error(data.error || 'Could not rank this list.'); renderResults(data); } catch (error) { toast(error.message); } finally { submit.disabled = false; submit.textContent = 'Create ranked feedback map'; } } form.addEventListener('submit', createFeedbackMap); document.querySelector('#loadSample')?.addEventListener('click', fillSample); document.querySelector('#loadSampleTop')?.addEventListener('click', fillSample);