(() => {
function qs(sel, root = document) { return root.querySelector(sel); }

function escapeHtml(s) {
return String(s ?? ”)
.replaceAll(‘&’, ‘&’)
.replaceAll(‘<', '<') .replaceAll('>‘, ‘>’)
.replaceAll(‘”‘, ‘"’)
.replaceAll(“‘”, ‘'’);
}
function getSelectedTone() {
const params = new URLSearchParams(window.location.search);
const t = (sessionStorage.getItem(‘tone_override’) || params.get(‘tone’) || ‘direct’)
.toLowerCase()
.trim();

return (t === ‘warm’ || t === ‘light’ || t === ‘direct’) ? t : ‘direct’;
}

function renderError(root, msg) {
root.innerHTML = `

${escapeHtml(msg)}

`;
}

function renderLoading(root, msg) {
root.innerHTML = `

${escapeHtml(msg)}

`;
}

async function fetchJson(url, opts) {
const res = await fetch(url, opts);
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch (_) { json = null; }

if (!res.ok) {
const detail = json ? JSON.stringify(json) : text;
throw new Error(`${res.status} ${res.statusText}: ${detail}`);
}
return json ?? {};
}

function tryParseJson(s) {
try { return JSON.parse(s); } catch (_) { return null; }
}

function normaliseContext(ctx) {
// We don’t assume exact field names; we keep it resilient.
// Your handler is described as: chapter defs + chapter connections + latest coach feedback. :contentReference[oaicite:3]{index=3}
return {
raw: ctx,
chapter: ctx.chapter ?? ctx.chapter_key ?? ctx.chapterId ?? null,
chapterDef: ctx.chapter_def ?? ctx.chapterDef ?? null,
connections: ctx.connections ?? ctx.chapter_connections ?? ctx.chapterConnections ?? [],
coachFeedback: ctx.coach_feedback ?? ctx.coachFeedback ?? ctx.feedback ?? null,
};
}

function buildPrompt(ctx, userpass, chapter) {
const tone = getSelectedTone();

const meta = ctx.raw?.chapterMeta || ctx.chapterMeta;

const chTitle =
meta && meta.ok && meta.title
? meta.title
: ”;

const sectionTitle =
ctx.chapterDef?.section_title ||
ctx.chapterDef?.section ||
ctx.raw?.section_title ||
”;

const connections = Array.isArray(ctx.connections) ? ctx.connections.slice(0, 12) : [];
const coachFb = ctx.coachFeedback || null;

const compact = {
userpass,
chapter,
chapterTitle: chTitle,
sectionTitle,
chapterDef: ctx.chapterDef,
connections,
coachFeedback: coachFb,
};

// IMPORTANT: return a plain prompt string (no nested template literals)
return [
`You are the Change Pathway AI Coach. Write a chapter introduction page for the user.`,
“,
`TONE (mandatory):`,
`- The user selected tone is: ${tone}.`,
`- direct = succinct, candid, no cheerleading, no “softeners”.`,
`- warm = calm, respectful, supportive, not sentimental.`,
`- light = warm + slightly more upbeat/energetic, still not jokey.`,
“,
`Goal:`,
`- Introduce the chapter clearly and motivatingly.`,
`- Weave in any salient user-specific context from:`,
` (a) chapter connections (matched terms / rationale / quote)`,
` (b) the latest user feedback where aspect = “Coach” (if present)`,
`- Keep it concise and practical.`,
“,
`Output format:`,
`Return ONLY valid JSON with these keys:`,
`- “heading” (string)`,
`- “body” (array of 2-4 short paragraphs as strings)`,
`- “personalised” (array of 1-3 bullet strings, only if genuinely relevant; otherwise empty array)`,
`- “quote” (string, optional)`,
`- “quoteAuthor” (string, optional)`,
`- “cta” (string)`,
“,
`Context JSON:`,
JSON.stringify(compact),
].join(‘\n’);
}

function renderPage(root, data, insight1Url) {
const meta = window.__cpChapterMeta;
const heading =
meta && meta.ok && meta.chapter_number && meta.title
? escapeHtml(`Chapter ${meta.chapter_number} Overview: ${meta.title}`)
: escapeHtml(data.heading || ‘Chapter’);
const sub = data.subheading ? `

${escapeHtml(data.subheading)}

` : ”;

const bodyArr = Array.isArray(data.body) ? data.body : [];
const bodyHtml = bodyArr.map(p => `

${escapeHtml(p)}

`).join(”);

const persArr = Array.isArray(data.personalised) ? data.personalised : [];
const persHtml = persArr.length
? `

A quick note for you
    ${persArr.map(b => `

  • ${escapeHtml(b)}
  • `).join(”)}

`
: ”;

const quote = data.quote ? escapeHtml(data.quote) : ”;
const quoteAuthor = data.quoteAuthor ? escapeHtml(data.quoteAuthor) : ”;
const quoteHtml = quote
? `

“${quote}”

${quoteAuthor ? `

${quoteAuthor}

` : ”}

`
: ”;

const ctaLabel = escapeHtml(data.cta || ‘Start Insight 1’);

root.innerHTML = `

${heading}

${sub}

${bodyHtml}

${persHtml}
${quoteHtml}

`;
}

async function main() {
const root = qs(‘#cp-chapter-root’);
if (!root) return;

const cfg = window.cpVars && window.cpVars.chapterPage ? window.cpVars.chapterPage : null;
if (!cfg) {
renderError(root, ‘Chapter AI failed: missing window.cpVars.chapterPage’);
return;
}

const params = new URLSearchParams(window.location.search);
const userpass = params.get(‘userpass’);
if (!userpass) {
renderError(root, ‘Chapter AI failed: Missing userpass in URL.’);
return;
}

const token = cfg.token || ”;
const chapter = cfg.chapter || ‘1.1’;
const contextUrl = cfg.contextUrl || (cfg.restBase ? `${cfg.restBase.replace(/\/$/, ”)}/chapter-page-context` : ”);
const aiUrl = cfg.aiUrl || (cfg.restBase ? `${cfg.restBase.replace(/\/$/, ”)}/ai` : ”);
const insight1Url = cfg.insight1Url || ‘#’;

if (!contextUrl) {
renderError(root, ‘Chapter AI failed: Missing contextUrl.’);
return;
}
if (!aiUrl) {
renderError(root, ‘Chapter AI failed: Missing aiUrl.’);
return;
}

renderLoading(root, ‘Loading chapter…’);

// 1) Fetch context (GET)
let ctxJson;
try {
const url = `${contextUrl}?userpass=${encodeURIComponent(userpass)}&chapter=${encodeURIComponent(chapter)}`;
ctxJson = await fetchJson(url, {
method: ‘GET’,
headers: {
‘x-cp-token’: token,
},
});
} catch (e) {
renderError(root, `Chapter AI failed: Context error: ${e.message}`);
return;
}

const ctx = normaliseContext(ctxJson);
window.__cpChapterMeta = ctx.raw?.chapterMeta || null;

// 2) Ask AI to produce the intro page (POST)
renderLoading(root, ‘Writing your chapter introduction…’);

let aiText = ”;
try {
const prompt = buildPrompt(ctx, userpass, chapter);
const body = {
model: ‘gpt-4o-mini’,
temperature: 0.6,
messages: [
{ role: ‘system’, content: ‘You are a helpful coaching assistant.’ },
{ role: ‘user’, content: prompt },
],
};

const aiJson = await fetchJson(aiUrl, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
‘x-cp-token’: token,
},
body: JSON.stringify(body),
});

aiText = aiJson.text || ”;
} catch (e) {
renderError(root, `Chapter AI failed: AI error: ${e.message}`);
return;
}

// 3) Render
const parsed = tryParseJson(aiText);
if (!parsed || typeof parsed !== ‘object’) {
// fallback: show raw text, but still don’t break the page
root.innerHTML = `

${escapeHtml(aiText)}

`;
return;
}

renderPage(root, parsed, insight1Url);
}

if (document.readyState === ‘loading’) {
document.addEventListener(‘DOMContentLoaded’, main);
} else {
main();
}
})();