513 lines
27 KiB
JavaScript
513 lines
27 KiB
JavaScript
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 parsePastedJsonPayload(value) {
|
|
const text = String(value || '').trim()
|
|
.replace(/^```(?:json)?\s*/i, '')
|
|
.replace(/```$/i, '')
|
|
.trim();
|
|
const jsonText = text.startsWith('{') && text.endsWith('}') ? text : extractFirstJsonObject(text);
|
|
if (!jsonText) return null;
|
|
try {
|
|
const parsed = JSON.parse(jsonText);
|
|
const looksLikeBridgePayload = parsed && typeof parsed === 'object' && !Array.isArray(parsed) && (
|
|
parsed.schema || parsed.featureSet || parsed.feature_set || parsed.candidateSet || parsed.candidate_set || parsed.candidateFeatureSet || parsed.candidate_feature_set || parsed.rankReadyFeatureSet || parsed.rank_ready_feature_set
|
|
|| parsed.snapshot || parsed.conceptMap || parsed.concept_map || parsed.buildOrder || parsed.build_order || parsed.firstWeekBuildOrder || parsed.first_week_build_order || parsed.rankedBuildOrder || parsed.ranked_build_order || parsed.rankReadyBuildOrder || parsed.rank_ready_build_order || parsed.lenses
|
|
|| parsed.payload || parsed.rankPayload || parsed.rank_payload || parsed.scattermindPayload || parsed.scattermind_payload || parsed.conceptMapJson || parsed.rankerInput || parsed.ranker_input || parsed.rankerHandoff || parsed.ranker_handoff || parsed.rankerBridge || parsed.ranker_bridge || parsed.rankReady || parsed.rank_ready || parsed.bridge || parsed.bridgePayload || parsed.bridge_payload || parsed.continuation || parsed.continuationPlan || parsed.continuation_plan
|
|
|| parsed.concept_map_json || parsed.fullReadingJson || parsed.full_reading_json || parsed.fullReading || parsed.full_reading
|
|
|| parsed.glimpseJson || parsed.glimpse_json || parsed.snapshotJson || parsed.snapshot_json || parsed.buildOrderPreview || parsed.build_order_preview || parsed.firstWeekBuildOrder || parsed.first_week_build_order || parsed.rankedBuildOrder || parsed.ranked_build_order || parsed.rankReadyBuildOrder || parsed.rank_ready_build_order
|
|
|| parsed.reference_code || parsed.referenceCode || parsed.artifactId || parsed.sourceArtifactId || parsed.source_artifact_id
|
|
|| parsed.ideaText || parsed.idea_text || parsed.originalPrompt || parsed.original_prompt || parsed.sourceSummary || parsed.source_summary || parsed.opening_reflection || parsed.restated_idea
|
|
|| Array.isArray(parsed.features) || Array.isArray(parsed.actions) || Array.isArray(parsed.candidates)
|
|
|| Array.isArray(parsed.candidateActions) || Array.isArray(parsed.candidate_actions) || Array.isArray(parsed.candidateMoves) || Array.isArray(parsed.candidate_moves)
|
|
|| Array.isArray(parsed.rankReadyActions) || Array.isArray(parsed.rank_ready_actions)
|
|
|| Array.isArray(parsed.recommendedActions) || Array.isArray(parsed.recommended_actions) || Array.isArray(parsed.suggestedActions) || Array.isArray(parsed.suggested_actions)
|
|
|| Array.isArray(parsed.nextActions) || Array.isArray(parsed.next_actions) || Array.isArray(parsed.nextMoves) || Array.isArray(parsed.next_moves)
|
|
|| Array.isArray(parsed.possibleNextMoves) || Array.isArray(parsed.possible_next_moves) || Array.isArray(parsed.suggestedNextMoves) || Array.isArray(parsed.suggested_next_moves)
|
|
|| Array.isArray(parsed.recommendations) || Array.isArray(parsed.opportunities)
|
|
|| Array.isArray(parsed.doFirst) || Array.isArray(parsed.do_first) || Array.isArray(parsed.continueFirst) || Array.isArray(parsed.continue_first) || Array.isArray(parsed.makeTangible) || Array.isArray(parsed.make_tangible)
|
|
|| Array.isArray(parsed.validateNext) || Array.isArray(parsed.validate_next) || Array.isArray(parsed.evidenceNext) || Array.isArray(parsed.evidence_next) || Array.isArray(parsed.tryNext) || Array.isArray(parsed.try_next)
|
|
|| Array.isArray(parsed.next48Hours) || Array.isArray(parsed.next_48_hours) || Array.isArray(parsed.first48Hours) || Array.isArray(parsed.first_48_hours) || Array.isArray(parsed.nextTwoDays) || Array.isArray(parsed.next_two_days)
|
|
|| Array.isArray(parsed.deferred) || Array.isArray(parsed.holdForLater) || Array.isArray(parsed.hold_for_later)
|
|
|| Array.isArray(parsed.parkingLot) || Array.isArray(parsed.parking_lot) || Array.isArray(parsed.setAside) || Array.isArray(parsed.set_aside)
|
|
|| Array.isArray(parsed.threads_to_hold) || Array.isArray(parsed.threadsToHold) || Array.isArray(parsed.actionThreads) || Array.isArray(parsed.action_threads)
|
|
|| Array.isArray(parsed.questions_to_sit_with) || Array.isArray(parsed.questionsToSitWith) || Array.isArray(parsed.evidenceQuestions) || Array.isArray(parsed.evidence_questions) || Array.isArray(parsed.proofQuestions) || Array.isArray(parsed.proof_questions) || Array.isArray(parsed.proofPlan) || Array.isArray(parsed.proof_plan) || Array.isArray(parsed.validationQuestions) || Array.isArray(parsed.validation_questions) || Array.isArray(parsed.validationPlan) || Array.isArray(parsed.validation_plan) || Array.isArray(parsed.decisionQuestions) || Array.isArray(parsed.decision_questions) || Array.isArray(parsed.questionsToAnswer) || Array.isArray(parsed.questions_to_answer) || Array.isArray(parsed.followupQuestions) || Array.isArray(parsed.followup_questions) || Array.isArray(parsed.openQuestions) || Array.isArray(parsed.open_questions)
|
|
|| Array.isArray(parsed.assumptionTests) || Array.isArray(parsed.assumption_tests) || Array.isArray(parsed.riskiestAssumptions) || Array.isArray(parsed.riskiest_assumptions) || Array.isArray(parsed.risksToTest) || Array.isArray(parsed.risks_to_test)
|
|
);
|
|
return looksLikeBridgePayload ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function extractFirstJsonObject(text = '') {
|
|
const start = text.indexOf('{');
|
|
if (start < 0) return '';
|
|
let depth = 0;
|
|
let inString = false;
|
|
let escaped = false;
|
|
for (let index = start; index < text.length; index += 1) {
|
|
const char = text[index];
|
|
if (escaped) {
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === '\\' && inString) {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (char === '"') {
|
|
inString = !inString;
|
|
continue;
|
|
}
|
|
if (inString) continue;
|
|
if (char === '{') depth += 1;
|
|
if (char === '}') {
|
|
depth -= 1;
|
|
if (depth === 0) return text.slice(start, index + 1);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function payloadFromForm(formPayload) {
|
|
const ideaJson = parsePastedJsonPayload(formPayload.idea);
|
|
const optionsJson = parsePastedJsonPayload(formPayload.optionsText);
|
|
const embedded = ideaJson || optionsJson;
|
|
if (!embedded) return formPayload;
|
|
const unwrapCandidate = embedded.payload || embedded.rankPayload || embedded.rank_payload || embedded.scattermindPayload || embedded.scattermind_payload || embedded.rankerBridge || embedded.ranker_bridge || embedded.continuation || embedded.continuationPlan || embedded.continuation_plan || embedded;
|
|
const unwrapped = unwrapCandidate && typeof unwrapCandidate === 'object' && !Array.isArray(unwrapCandidate) ? unwrapCandidate : embedded;
|
|
const merged = { ...unwrapped };
|
|
if (!merged.mode && formPayload.mode) merged.mode = formPayload.mode;
|
|
if (String(formPayload.context || '').trim() && !merged.context) merged.context = formPayload.context;
|
|
return merged;
|
|
}
|
|
|
|
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 renderPills(items = []) {
|
|
if (!items.length) return '';
|
|
return `<div class="signal-pills">${items.map((item) => `<span>${escapeHtml(item)}</span>`).join('')}</div>`;
|
|
}
|
|
|
|
function renderSourceTrace(sourceTrace = {}) {
|
|
const hasTrace = sourceTrace.sourceSection || sourceTrace.sourceId || sourceTrace.sourceQuote;
|
|
if (!hasTrace) return '';
|
|
const label = [sourceTrace.sourceTitle, sourceTrace.sourceId || sourceTrace.sourceSection].filter(Boolean).join(' · ');
|
|
return `
|
|
<div class="source-trace">
|
|
<span>Source trace</span>
|
|
${label ? `<b>${escapeHtml(label)}</b>` : ''}
|
|
${sourceTrace.sourceQuote ? `<p>${escapeHtml(sourceTrace.sourceQuote)}</p>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderItemSourceTrace(item = {}) {
|
|
const provenance = item.provenance || {};
|
|
const trace = {
|
|
sourceSection: provenance.sourceSection || '',
|
|
sourceId: provenance.sourceId || '',
|
|
sourceTitle: provenance.sourceTitle || '',
|
|
sourceQuote: provenance.sourceQuote || '',
|
|
};
|
|
const hasTrace = trace.sourceSection || trace.sourceId || trace.sourceQuote;
|
|
if (!hasTrace) return '';
|
|
const label = [trace.sourceTitle, trace.sourceId || trace.sourceSection].filter(Boolean).join(' · ');
|
|
return `
|
|
<details class="item-source-trace">
|
|
<summary>Source trace${label ? ` · ${escapeHtml(label)}` : ''}</summary>
|
|
${trace.sourceQuote ? `<p>${escapeHtml(trace.sourceQuote)}</p>` : ''}
|
|
${trace.sourceSection ? `<small>Section: ${escapeHtml(trace.sourceSection)}</small>` : ''}
|
|
</details>
|
|
`;
|
|
}
|
|
|
|
function readinessTone(status = '') {
|
|
if (status === 'ready') return 'ready';
|
|
if (status === 'usable-with-warnings') return 'warn';
|
|
return status ? 'blocked' : '';
|
|
}
|
|
|
|
function renderHandoffStatus(handoff = {}) {
|
|
const readiness = handoff.readiness || {};
|
|
if (!readiness.status) return '';
|
|
const warnings = handoff.warnings || [];
|
|
return `
|
|
<article class="brief-card handoff-card status-${escapeHtml(readiness.status)}">
|
|
<span>Bridge handoff readiness</span>
|
|
<p><b>${escapeHtml(readiness.label || readiness.status)}</b> — ${escapeHtml(readiness.summary || '')}</p>
|
|
${(readiness.nextChecks || []).length ? `<ol>${readiness.nextChecks.map((step) => `<li>${escapeHtml(step)}</li>`).join('')}</ol>` : ''}
|
|
${warnings.length ? `<div class="handoff-warnings">${warnings.map((warning) => `<code>${escapeHtml(warning)}</code>`).join('')}</div>` : ''}
|
|
</article>
|
|
`;
|
|
}
|
|
|
|
function renderBridgeHandoffStrip(data = {}) {
|
|
const handoff = data.handoff || {};
|
|
const readiness = handoff.readiness || {};
|
|
if (!readiness.status) return '';
|
|
const source = handoff.source || data.brief?.source || data.input?.provenance || {};
|
|
const activeSlice = handoff.activeSlice || {};
|
|
const activeItem = activeSlice.item || {};
|
|
const sourceBits = [source.snapshotTitle, source.artifactId].filter(Boolean).join(' · ');
|
|
const tone = readinessTone(readiness.status);
|
|
return `
|
|
<section class="handoff-strip status-${escapeHtml(tone)}" aria-label="Bridge handoff readiness">
|
|
<div>
|
|
<span>Bridge readiness</span>
|
|
<strong>${escapeHtml(readiness.label || readiness.status)}</strong>
|
|
<p>${escapeHtml(readiness.summary || 'Read the warnings before handing this back to Scattermind.')}</p>
|
|
</div>
|
|
<div>
|
|
<span>Active slice</span>
|
|
<strong>${escapeHtml(activeItem.title || data.brief?.decisionReceipt?.activeMove || 'No active move')}</strong>
|
|
<p>${escapeHtml(activeSlice.rule || data.brief?.decisionReceipt?.handoffRule || 'Only the Do first item is active.')}</p>
|
|
</div>
|
|
<div>
|
|
<span>Source</span>
|
|
<strong>${escapeHtml(sourceBits || 'No artifact attached')}</strong>
|
|
<p>${escapeHtml(source.originalPromptExcerpt || source.sourceSummaryExcerpt || 'Add source context before treating this as a durable bridge handoff.')}</p>
|
|
</div>
|
|
${(readiness.nextChecks || []).length ? `<small>Next check: ${escapeHtml(readiness.nextChecks[0])}</small>` : ''}
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
function renderFirstScreen(firstScreen = {}) {
|
|
if (!firstScreen.headline) return '';
|
|
const held = firstScreen.holdBack || [];
|
|
const guardrails = firstScreen.guardrails || [];
|
|
return `
|
|
<section class="active-slice-strip" aria-label="Active build slice">
|
|
<div class="active-slice-main">
|
|
<span>One thing to do now</span>
|
|
<h3>${escapeHtml(firstScreen.headline)}</h3>
|
|
<p>${escapeHtml(firstScreen.primaryAction || 'Run the smallest manual proof before product machinery.')}</p>
|
|
</div>
|
|
<div>
|
|
<span>Proof question</span>
|
|
<p>${escapeHtml(firstScreen.proofQuestion || 'What evidence would change this order?')}</p>
|
|
</div>
|
|
<div>
|
|
<span>Why this wins</span>
|
|
<p>${escapeHtml(firstScreen.why || 'It is the best first proof slice.')}</p>
|
|
</div>
|
|
<div class="proof-gate">
|
|
<span>Pass / stop signals</span>
|
|
<p><b>Pass:</b> ${escapeHtml(firstScreen.passSignal || 'A real user can name why this should be first.')}</p>
|
|
<p><b>Stop:</b> ${escapeHtml(firstScreen.stopSignal || 'The proof creates no clear action, request, or value signal.')}</p>
|
|
<small>${escapeHtml(firstScreen.proofCadence || 'Run one tiny proof cycle, then rerank.')}</small>
|
|
</div>
|
|
${held.length ? `<div><span>Hold back</span><ul>${held.map((item) => `<li><b>${escapeHtml(item.title)}</b>${item.lane ? ` — ${escapeHtml(item.lane)}` : ''}</li>`).join('')}</ul></div>` : ''}
|
|
${firstScreen.sourceQuote ? `<blockquote class="active-source-quote"><span>${escapeHtml(firstScreen.sourceTitle || 'Source quote')}</span>${escapeHtml(firstScreen.sourceQuote)}</blockquote>` : ''}
|
|
${guardrails.length ? `<small>Guardrails: ${guardrails.map(escapeHtml).join(' · ')}</small>` : ''}
|
|
${firstScreen.sourceAnchor ? `<small>Source anchor: ${escapeHtml(firstScreen.sourceAnchor)}</small>` : ''}
|
|
<small>${escapeHtml(firstScreen.rule || 'One active move. Everything else waits.')}</small>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
function renderDecisionReceipt(receipt = {}) {
|
|
if (!receipt.activeMove) return '';
|
|
const held = receipt.doNotStartYet || [];
|
|
return `
|
|
<section class="decision-receipt" aria-label="Decision receipt">
|
|
<div>
|
|
<span>Active move</span>
|
|
<strong>${escapeHtml(receipt.activeMove)}</strong>
|
|
</div>
|
|
<div>
|
|
<span>First proof step</span>
|
|
<p>${escapeHtml(receipt.firstProofStep || 'Run the smallest manual proof before product machinery.')}</p>
|
|
</div>
|
|
<div>
|
|
<span>Evidence question</span>
|
|
<p>${escapeHtml(receipt.evidenceQuestion || 'What would make this ranking obviously right or wrong?')}</p>
|
|
</div>
|
|
<div>
|
|
<span>Proof gate</span>
|
|
<p><b>Pass:</b> ${escapeHtml(receipt.passSignal || 'Evidence makes the active move obviously worth keeping first.')}</p>
|
|
<p><b>Stop:</b> ${escapeHtml(receipt.stopSignal || 'Evidence does not create a clear next action.')}</p>
|
|
</div>
|
|
${held.length ? `<div><span>Do not start yet</span><ul>${held.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul></div>` : ''}
|
|
${receipt.sourceAnchor ? `<small>Source anchor: ${escapeHtml(receipt.sourceAnchor)}</small>` : ''}
|
|
<small>${escapeHtml(receipt.handoffRule || 'Only the Do first item is active.')}</small>
|
|
</section>
|
|
`;
|
|
}
|
|
|
|
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>
|
|
${renderItemSourceTrace(item)}
|
|
${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 sourceCitation(data) {
|
|
const brief = data.brief || {};
|
|
const source = brief.source || data.handoff?.source || data.input?.provenance || {};
|
|
const trace = brief.quickGlance?.sourceTrace || {};
|
|
const parts = [
|
|
source.artifactId ? `Artifact: ${source.artifactId}` : '',
|
|
source.conceptMapId ? `Concept Map: ${source.conceptMapId}` : '',
|
|
source.snapshotTitle ? `Title: ${source.snapshotTitle}` : '',
|
|
trace.sourceSection ? `Source section: ${trace.sourceSection}` : '',
|
|
trace.sourceId ? `Source item: ${trace.sourceId}` : '',
|
|
trace.sourceTitle ? `Source title: ${trace.sourceTitle}` : '',
|
|
trace.sourceQuote ? `Source quote: ${trace.sourceQuote}` : '',
|
|
source.originalPromptExcerpt ? `Original prompt: ${source.originalPromptExcerpt}` : '',
|
|
source.sourceSummaryExcerpt ? `Source summary: ${source.sourceSummaryExcerpt}` : '',
|
|
].filter(Boolean);
|
|
return parts.length ? parts.join('\n') : 'No source citation was carried into this result.';
|
|
}
|
|
|
|
function markdownBrief(data) {
|
|
if (data.handoff?.copyableText) return data.handoff.copyableText;
|
|
const brief = data.brief || {};
|
|
const glance = brief.quickGlance || {};
|
|
const ranked = data.ranked || [];
|
|
const citation = sourceCitation(data);
|
|
const hasCitation = !citation.startsWith('No source citation');
|
|
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 || ''}`,
|
|
...(hasCitation ? ['', '## Source citation', citation] : []),
|
|
'',
|
|
...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'));
|
|
document.querySelector('#copySource')?.addEventListener('click', () => copyText(sourceCitation(data), 'Source citation'));
|
|
}
|
|
|
|
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 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><span>${escapeHtml(data.handoff?.readiness?.label || data.handoff?.readiness?.status || 'handoff unchecked')}</span></div>
|
|
</div>
|
|
|
|
${renderBridgeHandoffStrip(data)}
|
|
<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>
|
|
${renderFirstScreen(brief.firstScreen || {})}
|
|
${renderDecisionReceipt(brief.decisionReceipt || {})}
|
|
${renderSourceTrace(glance.sourceTrace || {})}
|
|
|
|
<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>
|
|
<button class="button ghost" type="button" id="copySource">Copy source citation</button>
|
|
</div>
|
|
|
|
<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>
|
|
${renderHandoffStatus(data.handoff || {})}
|
|
${(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>
|
|
<ul>${brief.assumptions.map((assumption) => `<li>${escapeHtml(assumption)}</li>`).join('')}</ul>
|
|
</article>
|
|
` : ''}
|
|
${(brief.expertReflections || []).map((reflection) => `
|
|
<article class="brief-card expert-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>
|
|
`;
|
|
attachResultActions(data);
|
|
results.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
async function createFeedbackMap(event) {
|
|
event.preventDefault();
|
|
const submit = form.querySelector('button[type="submit"]');
|
|
const payload = payloadFromForm(Object.fromEntries(new FormData(form).entries()));
|
|
if (!String(payload.optionsText || '').trim() && !payload.conceptMap && !payload.featureSet && !payload.lenses) payload.optionsText = payload.idea;
|
|
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);
|