1const state = { 2 status: null, 3 user: null, 4 project: null, 5 tools: [], 6 currentTool: null, 7 currentOutput: null 8}; 9 10const $ = selector => document.querySelector(selector); 11const $$ = selector => [...document.querySelectorAll(selector)]; 12 13async function api(url, options = {}) { 14 const response = await fetch(url, { 15 ...options, 16 headers: { "Content-Type": "application/json", ...(options.headers || {}) } 17 }); 18 const data = await response.json().catch(() => ({})); 19 if (!response.ok) throw new Error(data.error || "Something went wrong."); 20 return data; 21} 22 23function toast(message, type = "success") { 24 const el = $("#toast"); 25 el.textContent = message; 26 el.className = `toast show ${type}`; 27 clearTimeout(toast.timer); 28 toast.timer = setTimeout(() => el.className = "toast", 3200); 29} 30 31function escapeHtml(value) { 32 return String(value || "").replace(/[&<>'"]/g, char => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ })[char]); 33} 34 35function formObject(form) { 36 return Object.fromEntries(new FormData(form).entries()); 37} 38 39function openAuth(mode = "signup") { 40 $("#authModal").classList.remove("hidden"); 41 $("#authModal").setAttribute("aria-hidden", "false"); 42 switchAuth(mode); 43} 44 45function closeAuth() { 46 $("#authModal").classList.add("hidden"); 47 $("#authModal").setAttribute("aria-hidden", "true"); 48} 49 50function switchAuth(mode) { 51 $("#signupPanel").classList.toggle("hidden", mode !== "signup"); 52 $("#loginPanel").classList.toggle("hidden", mode !== "login"); 53} 54 55async function initialize() { 56 state.status = await api("/api/status"); 57 $$("[data-checkout-link]").forEach(link => { 58 link.href = state.status.checkoutUrl || "#"; 59 }); 60 if (state.status.demoMode) $("#demoUpgrade").classList.remove("hidden"); 61 try { 62 const data = await api("/api/me"); 63 state.user = data.user; 64 state.project = data.project; 65 await enterApp(); 66 } catch { 67 $("#marketing").classList.remove("hidden"); 68 } 69} 70 71async function enterApp() { 72 const data = await api("/api/tools"); 73 state.tools = data.tools; 74 $("#marketing").classList.add("hidden"); 75 $("#authModal").classList.add("hidden"); 76 $("#appShell").classList.remove("hidden"); 77 $("#welcomeName").textContent = `Welcome, ${state.user.name.split(/\s+/)[0]}`; 78 $("#modeBadge").textContent = state.user.access === "ai_email_money" ? "AI Email Money" : "Free Builder"; 79 updateUpgradeOffers(); 80 renderNav(); 81 updateProgress(); 82 const firstIncomplete = state.tools.find(tool => tool.day && !state.project.completedDays.includes(tool.day)) || state.tools[0]; 83 selectTool(firstIncomplete.id); 84} 85 86function updateUpgradeOffers() { 87 const hasPaidAccess = state.user?.access === "ai_email_money"; 88 const hasCompletedDaySeven = state.project?.completedDays.includes(7); 89 $("#upgradeCard").classList.toggle("hidden", hasPaidAccess); 90 $("#completionUpgrade").classList.toggle("hidden", hasPaidAccess || !hasCompletedDaySeven); 91} 92 93function renderNav() { 94 const render = tier => state.tools.filter(tool => tool.tier === tier).map(tool => { 95 const complete = tool.day && state.project.completedDays.includes(tool.day); 96 const locked = tool.tier === "paid" && state.user.access !== "ai_email_money"; 97 return `<button class="nav-tool ${complete ? "complete" : ""} ${locked ? "locked" : ""}" data-tool="${tool.id}"> 98 <b>${escapeHtml(tool.icon)}</b><span>${escapeHtml(tool.title)}</span><i>${complete ? "â" : locked ? "â" : ""}</i> 99 </button>`; 100 }).join(""); 101 $("#freeNav").innerHTML = render("free"); 102 $("#paidNav").innerHTML = render("paid"); 103 $$("[data-tool]").forEach(button => button.addEventListener("click", () => selectTool(button.dataset.tool))); 104} 105 106function updateProgress() { 107 const complete = state.project.completedDays.length; 108 $("#progressText").textContent = `${complete} of 7 complete`; 109 $("#progressBar").style.width = `${complete / 7 * 100}%`; 110} 111 112function selectTool(id) { 113 const tool = state.tools.find(item => item.id === id); 114 if (!tool) return; 115 if (tool.tier === "paid" && state.user.access !== "ai_email_money") { 116 toast("This workspace unlocks with AI Email Money.", "error"); 117 $("#upgradeCard").scrollIntoView({ behavior: "smooth" }); 118 return; 119 } 120 state.currentTool = tool; 121 state.currentOutput = state.project.sections[tool.id] || null; 122 $$(".nav-tool").forEach(button => button.classList.toggle("active", button.dataset.tool === id)); 123 $("#toolEyebrow").textContent = tool.day ? `DAY ${tool.day} · ${tool.eyebrow}` : tool.eyebrow; 124 $("#toolTitle").textContent = tool.title; 125 $("#toolDescription").textContent = tool.description; 126 $("#toolOutcome").textContent = tool.outcome;
127 $("#inputCount").textContent = `${tool.inputs.length} QUESTIONS`; 128 renderFields(tool); 129 renderResult(state.currentOutput); 130 $(".sidebar").classList.remove("open"); 131 window.scrollTo({ top: 0, behavior: "smooth" }); 132} 133 134function renderFields(tool) { 135 const savedContext = state.project.context?.[tool.id] || {}; 136 $("#dynamicFields").innerHTML = tool.inputs.map(field => { 137 const saved = escapeHtml(savedContext[field.id] || ""); 138 if (field.type === "textarea") return `<label>${escapeHtml(field.label)}<textarea name="${field.id}" placeholder="${escapeHtml(field.placeholder)}">${saved}</textarea></label>`; 139 if (field.type === "select") return `<label>${escapeHtml(field.label)}<select name="${field.id}">${field.options.map(option => `<option ${saved === escapeHtml(option) ? "selected" : ""}>${escapeHtml(option)}</option>`).join("")}</select></label>`; 140 return `<label>${escapeHtml(field.label)}<input name="${field.id}" value="${saved}" placeholder="${escapeHtml(field.placeholder || "")}"></label>`; 141 }).join(""); 142} 143 144function renderResult(output) { 145 const panel = $("#resultPanel"); 146 panel.classList.toggle("empty", !output); 147 $("#emptyResult").classList.toggle("hidden", Boolean(output)); 148 $("#resultContent").classList.toggle("hidden", !output); 149 if (!output) return; 150 $("#resultTitle").textContent = output.title; 151 $("#resultSummary").textContent = output.summary; 152 $("#primaryOutput").value = output.primary_output; 153 $("#resultFields").innerHTML = output.fields.map(item => `<div class="result-field"><span>${escapeHtml(item.label)}</span><strong>${escapeHtml(item.value)}</strong></div>`).join(""); 154 $("#alternatives").innerHTML = output.alternatives.map(item => `<li>${escapeHtml(item)}</li>`).join(""); 155 $("#actionSteps").innerHTML = output.action_steps.map(item => `<li>${escapeHtml(item)}</li>`).join(""); 156 const done = state.currentTool.day && state.project.completedDays.includes(state.currentTool.day); 157 $("#completeButton").classList.toggle("hidden", !state.currentTool.day); 158 $("#completeButton").innerHTML = done ? "Completed <span>â</span>" : "Approve and Complete This Day <span>â</span>"; 159 $("#completeButton").disabled = Boolean(done); 160} 161 162async function handleGenerate(event) { 163 event.preventDefault(); 164 const button = $("#generateButton"); 165 button.disabled = true; 166 button.querySelector(".button-label").textContent = "Building your resultâ¦"; 167 button.querySelector(".loader").classList.remove("hidden"); 168 try { 169 const data = await api(`/api/generate/${state.currentTool.id}`, { method: "POST", body: JSON.stringify({ input: formObject(event.currentTarget) }) }); 170 state.currentOutput = data.output; 171 state.project = data.project; 172 renderResult(data.output); 173 toast(`Result saved · ${data.usage.used} of ${data.usage.limit} generations used`); 174 } catch (error) { 175 toast(error.message, "error"); 176 } finally { 177 button.disabled = false; 178 button.querySelector(".button-label").textContent = state.currentOutput ? "Generate a New Version" : "Generate My Result"; 179 button.querySelector(".loader").classList.add("hidden"); 180 } 181} 182 183async function completeDay() { 184 if (!state.currentTool.day) return; 185 try { 186 await saveEdits(false); 187 const data = await api(`/api/complete/${state.currentTool.day}`, { method: "POST" }); 188 state.project = data.project; 189 updateProgress(); 190 renderNav(); 191 renderResult(state.currentOutput); 192 updateUpgradeOffers(); 193 toast(state.currentTool.day === 7 ? "Your Newsletter Launch Package is ready." : `Day ${state.currentTool.day} complete.`); 194 if (state.currentTool.day === 7 && state.user.access !== "ai_email_money") { 195 $("#completionUpgrade").scrollIntoView({ behavior: "smooth", block: "start" }); 196 } 197 } catch (error) { 198 toast(error.message, "error"); 199 } 200} 201 202async function saveEdits(showConfirmation = true) { 203 if (!state.currentOutput) return; 204 const primaryOutput = $("#primaryOutput").value.trim(); 205 if (primaryOutput === state.currentOutput.primary_output) return; 206 const data = await api(`/api/result/${state.currentTool.id}`, { 207 method: "PATCH", 208 body: JSON.stringify({ primary_output: primaryOutput }) 209 }); 210 state.currentOutput = data.output; 211 state.project = data.project; 212 if (showConfirmation) toast("Your changes were saved."); 213} 214 215async function submitAuth(event, mode) { 216 event.preventDefault(); 217 const button = event.currentTarget.querySelector("button[type=submit]"); 218 button.disabled = true; 219 try { 220 const data = await api(`/api/auth/${mode}`, { method: "POST", body: JSON.stringify(formObject(event.currentTarget)) }); 221 state.user = data.user; 222 state.project = data.project; 223 await enterApp(); 224 } catch (error) { 225 toast(error.message, "error"); 226 } finally { 227 button.disabled = false;
228 } 229} 230 231$$('[data-open-auth]').forEach(button => button.addEventListener("click", () => openAuth(button.dataset.openAuth))); 232$$('[data-close-auth]').forEach(button => button.addEventListener("click", closeAuth)); 233$$('[data-switch-auth]').forEach(button => button.addEventListener("click", () => switchAuth(button.dataset.switchAuth))); 234$("#signupForm").addEventListener("submit", event => submitAuth(event, "signup")); 235$("#loginForm").addEventListener("submit", event => submitAuth(event, "login")); 236$("#builderForm").addEventListener("submit", handleGenerate); 237$("#completeButton").addEventListener("click", completeDay); 238$("#copyResult").addEventListener("click", async () => { 239 await navigator.clipboard.writeText($("#primaryOutput").value || ""); 240 toast("Copied to clipboard."); 241}); 242$("#saveResult").addEventListener("click", () => saveEdits().catch(error => toast(error.message, "error"))); 243$("#menuButton").addEventListener("click", () => $(".sidebar").classList.toggle("open")); 244$("#logoutButton").addEventListener("click", async () => { await api("/api/auth/logout", { method: "POST" }); location.reload(); }); 245async function refreshAccess() { 246 try { 247 const data = await api("/api/access/refresh", { method: "POST" }); 248 state.user = data.user; 249 if (data.accessFound) { 250 await enterApp(); 251 toast(data.message); 252 } else { 253 updateUpgradeOffers(); 254 toast(data.message, "error"); 255 } 256 } catch (error) { toast(error.message, "error"); } 257} 258$$("[data-refresh-access]").forEach(button => button.addEventListener("click", refreshAccess)); 259$("#demoUpgrade").addEventListener("click", async () => { 260 try { 261 const data = await api("/api/demo/upgrade", { method: "POST" }); 262 state.user = data.user; 263 await enterApp(); 264 toast("Paid workspace unlocked in demo mode."); 265 } catch (error) { toast(error.message, "error"); } 266}); 267 268initialize().catch(error => toast(error.message, "error"));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.