Files
rank/public/app.js
T
2026-05-26 22:06:03 +02:00

130 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const sample = {
idea: 'Im building a lightweight product feedback tool for people with too many ideas and too many possible features. It should feel useful before becoming a workspace.',
optionsText: `- Ranked feedback map for pasted feature lists
- Expert reflections on the top options
- Accounts and saved workspaces
- Team voting on feature priority
- Exportable decision brief for Slack or Notion
- Custom criteria builder
- Paid deeper product strategy pass`,
context: 'MVP, solo builder, needs to feel valuable in under two minutes, avoid dashboard swamp.',
mode: 'progress',
};
const form = document.querySelector('#rankForm');
const results = document.querySelector('#results');
const toastEl = document.querySelector('#toast');
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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 `<div class="metrics">${items.map(([label, value]) => `
<div class="metric"><span>${label}</span><i><b style="width:${metricPct(value)}%"></b></i></div>
`).join('')}</div>`;
}
function renderResults(data) {
const ranked = data.ranked || [];
const brief = data.brief || {};
results.hidden = false;
results.innerHTML = `
<div class="results-head">
<p class="eyebrow">${escapeHtml(data.mode?.label || 'Ranked feedback')}</p>
<h2>${escapeHtml(brief.headline || 'Ranked feedback map')}</h2>
<p>${escapeHtml(brief.summary || '')}</p>
</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('')}
</div>
<div class="brief-grid">
<article class="brief-card next-card">
<span>Next 48 hours</span>
<ol>${(brief.next48Hours || []).map((step) => `<li>${escapeHtml(step)}</li>`).join('')}</ol>
</article>
${(brief.expertReflections || []).map((reflection) => `
<article class="brief-card">
<span>${escapeHtml(reflection.lens)}</span>
<p>${escapeHtml(reflection.text)}</p>
</article>
`).join('')}
</div>
<p class="caution">${escapeHtml(brief.caution || 'First-pass judgement, not an oracle.')}</p>
`;
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 = 'Ranking…';
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);