// ResultScreen — shown when the adaptive flow ends. Sections:
// 0. Header row (title + small itinerary mandala + email box)
// 1. Headline recommendation
// 2. Hypothesis templates the researcher can adopt
// 3. Conditions to defend
// 4. Top candidate techniques
// 5. Assumptions
// 6. Suggested software
// 7. Recommended authors with clickable links + foundational references
// 8. Replicable methods recipe (existing component)
// 9. Comparison to alternatives
// 10. Reasoning trail
const { useState } = React;
// Build the list of "transformation conditions". For the recommended sector,
// we walk every answered question and flag those whose chosen option
// contributes a NON-POSITIVE weight (0, absent or negative) to that sector,
// while OTHER options of the same question contribute positively. Those are
// the design choices the researcher would have to revisit to actually be able
// to apply the recommended technique.
function buildTransformationConditions(history, winner) {
if (!winner || !history || history.length === 0) return [];
const bank = window.QUESTIONS || [];
const conditions = [];
for (const h of history) {
const q = bank.find(x => x.id === h.questionId);
if (!q || !Array.isArray(q.options)) continue;
const chosenWeight = (h.weights && h.weights[winner.id]) || 0;
if (chosenWeight > 0) continue; // chosen answer already supports the winner
// Any option of the same question that is STRICTLY MORE COMPATIBLE
// with the winner than the chosen one. This guarantees we usually
// surface at least two alternatives even when only one option carries
// a positive weight (the neighbouring 0-weight option is still
// "less incompatible" than a -2 chosen option).
const allBetter = q.options
.map((o, i) => ({
idx: i,
label: o.label,
weight: (o.weights && o.weights[winner.id]) || 0,
}))
.filter(o => o.idx !== h.optionIdx && o.weight > chosenWeight)
.sort((a, b) => b.weight - a.weight);
if (allBetter.length === 0) continue;
// Keep up to three alternatives. If positive-weight options exist,
// prefer those; otherwise fall back to the best non-positive ones so
// the section always offers at least two alternatives when possible.
const positives = allBetter.filter(o => o.weight > 0);
const better = (positives.length >= 2 ? positives : allBetter).slice(0, 3);
conditions.push({
questionId: h.questionId,
questionText: h.questionText,
chosen: h.answerLabel,
better,
});
}
return conditions;
}
function buildEmailHtml({ winner, history, scores, transformations, top3, total }) {
const esc = (s) => String(s == null ? '' : s)
.replace(/&/g, '&').replace(//g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
const css = `
body { font-family: Georgia, 'EB Garamond', serif; color: #1a1a1a; background: #fafaf6; margin: 0; padding: 0; }
.wrap { max-width: 640px; margin: 0 auto; padding: 28px 24px; }
h1 { font-size: 26px; margin: 0 0 4px; color: #0e1530; font-weight: 500; }
h2 { font-size: 16px; letter-spacing: 0.18em; text-transform: uppercase;
color: #5e6680; margin: 28px 0 10px; font-weight: 500; font-style: italic; }
.sub { font-style: italic; color: #5e6680; font-size: 16px; margin: 0 0 18px; }
p { line-height: 1.55; }
ul { padding-left: 20px; margin: 0; }
li { margin: 4px 0; line-height: 1.5; }
.card { background: #fff; border: 1px solid rgba(26,37,64,0.15); border-radius: 6px;
padding: 16px 18px; margin: 10px 0; }
.trail { border-left: 2px solid rgba(26,37,64,0.2); padding-left: 14px; margin: 6px 0 14px; }
.trail-q { font-style: italic; color: #5e6680; font-size: 13px; }
.trail-a { font-size: 14px; color: #0e1530; margin-top: 2px; }
.author { display:block; padding: 8px 0; border-top: 1px solid rgba(26,37,64,0.1); }
.author:first-child { border-top:none; }
.author-name { font-weight: 500; color: #0e1530; }
.author-work { font-style: italic; color: #5e6680; font-size: 13px; }
a { color: #1a2540; }
.footer { margin-top: 28px; font-size: 12px; color: #5e6680; font-style: italic;
border-top: 1px solid rgba(26,37,64,0.15); padding-top: 14px; }
`;
const hyps = (winner.hypotheses || []).map(h => `
${esc(h)} `).join('');
const conds = (winner.conditions || []).map(c => `${esc(c)} `).join('');
const techs = (winner.techniques || []).map(t => `${esc(t)} `).join('');
const ass = (winner.assumes || []).map(a => `${esc(a)} `).join('');
const sw = (winner.software || []).map(s => `${esc(s)} `).join('');
const proc = (winner.procedure || []).map(p => `${esc(p)} `).join('');
const auths = (winner.authors || []).map(a => `
${esc(a.name)}${a.url ? `
↗ ` : ''}
${esc(a.work || '')}
${a.why ? `
${esc(a.why)}
` : ''}
`).join('');
const refs = (winner.keyRefs || []).map(r => `${esc(r)} `).join('');
const trail = (history || []).map(h => `
${esc(h.questionId)} · ${esc(h.questionText)}
→ ${esc(h.answerLabel)}
`).join('');
const trans = (transformations || []).length ? `
Conditions for switching to ${esc(winner.label)}
Some of your answers carry a 0 weight for ${esc(winner.label)}. To apply this
technique you would need to revisit the design points below.
${transformations.map(t => `
${esc(t.questionId)} · ${esc(t.questionText)}
You chose: ${esc(t.chosen)}
The recommendation would be:
${t.better.map(b => `${esc(b.label)} `).join('')}
`).join('')}
` : '';
const cmp = (top3 || []).map((c, i) => `
0${i + 1}
${esc(c.label)}
${esc(c.sub)} — ${(c.score / total * 100).toFixed(0)}% match
`).join('');
return `${esc(winner.label)}
Method Atlas — Recommendation
${esc(winner.label)}
${esc(winner.goal || winner.sub || '')}
${winner.description ? `
${esc(winner.description)}
` : ''}
${hyps ? `
Hypothesis templates you can adopt ` : ''}
${conds ? `
When this technique is the right call ` : ''}
${techs ? `
Top candidate techniques ` : ''}
${ass ? `
Assumptions to defend ` : ''}
${sw ? `
Suggested software ` : ''}
${proc ? `
Procedural checklist ${proc} ` : ''}
${auths ? `
Recommended authors ${auths}
` : ''}
${refs ? `
Foundational references ` : ''}
${trans}
${cmp ? `
Alternative candidates ${cmp}` : ''}
${trail ? `
Your reasoning trail ${trail}` : ''}
`;
}
function buildEmailText({ winner, history, top3, total }) {
const lines = [];
lines.push(`METHOD ATLAS — RECOMMENDATION`);
lines.push('='.repeat(40));
lines.push(winner.label);
if (winner.goal) lines.push(winner.goal);
if (winner.description) lines.push('\n' + winner.description);
if ((winner.hypotheses || []).length) {
lines.push('\nHypothesis templates:');
winner.hypotheses.forEach((h, i) => lines.push(` H${i + 1}. ${h}`));
}
if ((winner.techniques || []).length) {
lines.push('\nTop candidate techniques:');
winner.techniques.forEach(t => lines.push(` - ${t}`));
}
if ((winner.assumes || []).length) {
lines.push('\nAssumptions to defend:');
winner.assumes.forEach(a => lines.push(` - ${a}`));
}
if ((winner.authors || []).length) {
lines.push('\nRecommended authors:');
winner.authors.forEach(a => lines.push(` - ${a.name} — ${a.work || ''} ${a.url ? '(' + a.url + ')' : ''}`));
}
if ((history || []).length) {
lines.push('\nYour reasoning trail:');
history.forEach(h => lines.push(` ${h.questionId}: ${h.questionText}\n → ${h.answerLabel}`));
}
if ((top3 || []).length) {
lines.push('\nAlternatives:');
top3.forEach((c, i) => lines.push(` 0${i + 1}. ${c.label} — ${(c.score / total * 100).toFixed(0)}% match`));
}
lines.push('\n— Method Atlas · Entrepreneur Research · upo.digital');
return lines.join('\n');
}
window.ResultScreen = function ResultScreen({ scores, history, onRestart, onAlternativeClick, recHistory = [], onRevertRecommendation, onTrack, sessionId, tone = 'serif-dark' }) {
const sectors = window.TAXONOMY.sectors;
const ranked = [...sectors]
.map(s => ({ ...s, score: scores[s.id] || 0 }))
.sort((a, b) => b.score - a.score);
const winner = ranked[0];
const top3 = ranked.slice(0, 3);
const total = top3.reduce((a, b) => a + b.score, 0) || 1;
const transformations = buildTransformationConditions(history, winner);
const [email, setEmail] = useState('');
const [emailBusy, setEmailBusy] = useState(false);
const [emailMsg, setEmailMsg] = useState(null);
const sendEmail = async () => {
if (emailBusy) return;
const value = (email || '').trim();
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) {
setEmailMsg({ ok: false, text: 'Please enter a valid email address.' });
return;
}
setEmailBusy(true);
setEmailMsg(null);
try {
const html = buildEmailHtml({ winner, history, scores, transformations, top3, total });
const text = buildEmailText({ winner, history, top3, total });
const subject = `Method Atlas — Your recommendation: ${winner.label}`;
const res = await fetch('./api.php?action=send_email', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId || null,
email: value,
winner_id: winner.id,
winner_label: winner.label,
subject,
html_body: html,
text_body: text,
payload: {
history: (history || []).map(h => ({
questionId: h.questionId, answerLabel: h.answerLabel, optionIdx: h.optionIdx,
})),
top3: top3.map(c => ({ id: c.id, label: c.label, score: c.score })),
scores,
},
}),
});
const j = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`);
setEmailMsg({ ok: true, text: 'Email sent. Check your inbox (and spam folder if you do not see it).' });
if (onTrack) onTrack('email_send', { email: value, winner_id: winner.id, status: 'sent' });
} catch (e) {
setEmailMsg({ ok: false, text: 'Could not send: ' + (e.message || String(e)) });
if (onTrack) onTrack('email_send_error', { email: value, error: String(e.message || e) });
} finally {
setEmailBusy(false);
}
};
return (
{/* === Top header: title block on the left, mandala + email on the right === */}
Recommendation
↻ Begin new inquiry
{winner.label}
{winner.goal}
{winner.description &&
{winner.description}
}
{recHistory.length > 0 && (
Previous recommendation{recHistory.length === 1 ? '' : 's'} —
click to recover or pick another:
{recHistory.map((snap, i) => (
onRevertRecommendation && onRevertRecommendation(i)}
title={`Restore the recommendation: ${snap.winnerLabel}`}>
↩
{snap.winnerLabel}
))}
)}
Email this recommendation
We can send you the full recommendation (with hypotheses, authors,
trail and alternatives) to your inbox.
setEmail(e.target.value)}
disabled={emailBusy}
onKeyDown={e => { if (e.key === 'Enter') sendEmail(); }}
/>
{emailBusy ? 'Sending…' : 'Send'}
{emailMsg && (
{emailMsg.text}
)}
{/* === Hypotheses & conditions === */}
Hypothesis templates you can adopt
{(winner.hypotheses || []).map((h, i) => (
H{i + 1} {h}
))}
When this technique is the right call
{(winner.conditions || []).map((c, i) => (
● {c}
))}
{/* === Two-column grid: techniques + assumptions + software === */}
Top candidate techniques
{winner.techniques.map(t => · {t} )}
Assumptions to defend
{winner.assumes.map(a => · {a} )}
Suggested software
{(winner.software || []).map(s => · {s} )}
{winner.procedure && winner.procedure.length > 0 && (
Procedural checklist
{winner.procedure.map((p, i) => {p} )}
)}
{/* === Recommended authors with links === */}
Recommended authors to consult
Drawn from the bibliography you supplied. Each link opens the most
useful entry point — DOI, repository or canonical source — for the
recommended technique.
{/* === Foundational references === */}
{winner.keyRefs && winner.keyRefs.length > 0 && (
Foundational references
{winner.keyRefs.map((r, i) => {r} )}
)}
{/* === Transformation conditions: design changes the researcher
would need to make in order to actually apply the recommended
technique. Clicking an alternative re-runs the recommendation
as if the user had picked that option. === */}
{transformations.length > 0 && (
Conditions for switching to {winner.label}
The recommendation above reflects the design choices you made.
Some of those choices carry a 0 weight for{' '}
{winner.label} — meaning, in their current form, they are not
compatible with this technique. Click any alternative
below and we will recompute the recommendation as if
you had chosen that option instead.
)}
{/* === Alternatives === */}
Alternative candidates
{top3.map((c, i) => (
0{i + 1}
{c.label}
{(c.score / total * 100).toFixed(0)}% match
{c.sub}
))}
{/* === Reasoning trail === */}
Your reasoning trail
{history.map((h, i) => (
{h.questionId} {h.questionText}
→ {h.answerLabel}
{h.note &&
"{h.note}"
}
))}
);
};