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

142 lines
5.3 KiB
JavaScript

const sample = {
idea: 'Scattermind clarified a messy course idea. Now I need the first build 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 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.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>
` : ''}
${(brief.assumptions || []).length ? `
<article class="brief-card">
<span>Context carried forward</span>
<ul>${brief.assumptions.map((assumption) => `<li>${escapeHtml(assumption)}</li>`).join('')}</ul>
</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);