/* ============================================================ AI BOOK BUILDER — standalone build for static hosting Staged long-form writing studio: idea -> title/concept -> outline -> chapter plan -> section-by-section drafting. Generation is routed through ./api.php (your Anthropic key). No build step: this file is compiled in the browser by Babel. ============================================================ */ const { useState, useEffect, useRef, useCallback } = React; const STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;0,9..144,700;1,9..144,500&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&display=swap'); * { box-sizing: border-box; } :root{ --paper:#f3ecdc; --paper-2:#ece2cd; --card:#fcf9f1; --ink:#231c14; --ink-soft:#5b4f3d; --ink-faint:#8a7c64; --line:#d9ccae; --line-soft:#e6dcc4; --accent:#8c3522; --accent-soft:#b25438; --gold:#a9802f; --ok:#4f6b3a; } .abb-root{ font-family:'Newsreader', Georgia, serif; color:var(--ink); background: radial-gradient(120% 90% at 100% 0%, rgba(169,128,47,0.07), transparent 55%), radial-gradient(120% 90% at 0% 100%, rgba(140,53,34,0.06), transparent 55%), var(--paper); min-height:100vh; -webkit-font-smoothing:antialiased; } .abb-root::before{ content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:0.5; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E"); } .abb-wrap{position:relative;z-index:1;} .serif-display{font-family:'Fraunces','Newsreader',serif;} .label{font-family:'Fraunces',serif;text-transform:uppercase;letter-spacing:0.18em;font-size:11px;font-weight:600;color:var(--ink-faint);} .topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between; padding:14px 26px;border-bottom:1px solid var(--line); background:rgba(243,236,220,0.85);backdrop-filter:blur(8px);} .brand{display:flex;align-items:center;gap:12px;} .brand-mark{width:34px;height:34px;border:1.5px solid var(--ink);border-radius:50%; display:grid;place-items:center;font-family:'Fraunces',serif;font-weight:600;font-size:17px; background:var(--card);} .brand-name{font-family:'Fraunces',serif;font-weight:600;font-size:18px;letter-spacing:0.01em;} .brand-sub{font-size:12px;color:var(--ink-faint);letter-spacing:0.04em;margin-top:-2px;} .steps{display:flex;gap:6px;align-items:center;flex-wrap:wrap;} .step-dot{font-family:'Fraunces',serif;font-size:11.5px;letter-spacing:0.04em;color:var(--ink-faint); padding:4px 10px;border-radius:999px;border:1px solid transparent;white-space:nowrap;cursor:default;} .step-dot.active{color:var(--accent);border-color:var(--line);background:var(--card);} .step-dot.done{color:var(--ink-soft);cursor:pointer;} .stage{max-width:760px;margin:0 auto;padding:46px 26px 90px;} .stage.wide{max-width:1180px;} .fade-up{animation:fadeUp .5s cubic-bezier(.2,.7,.2,1) both;} @keyframes fadeUp{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}} .stagger > *{animation:fadeUp .55s cubic-bezier(.2,.7,.2,1) both;} .stagger > *:nth-child(1){animation-delay:.04s} .stagger > *:nth-child(2){animation-delay:.12s} .stagger > *:nth-child(3){animation-delay:.2s} .stagger > *:nth-child(4){animation-delay:.28s} .stagger > *:nth-child(5){animation-delay:.36s} .eyebrow{font-family:'Fraunces',serif;text-transform:uppercase;letter-spacing:0.32em;font-size:12px;color:var(--accent);font-weight:600;} h1.title{font-family:'Fraunces',serif;font-weight:500;font-size:clamp(34px,6vw,58px);line-height:1.02;margin:14px 0 0;letter-spacing:-0.01em;} .lede{font-size:19px;line-height:1.5;color:var(--ink-soft);max-width:54ch;margin-top:18px;} h2.sec{font-family:'Fraunces',serif;font-weight:500;font-size:clamp(26px,4vw,38px);margin:0;letter-spacing:-0.01em;} .card{background:var(--card);border:1px solid var(--line);border-radius:4px;padding:22px 24px; box-shadow:0 1px 0 rgba(35,28,20,0.03), 0 18px 40px -34px rgba(35,28,20,0.5);} .choice{cursor:pointer;text-align:left;transition:transform .18s ease, box-shadow .25s ease, border-color .2s;} .choice:hover{transform:translateY(-3px);box-shadow:0 1px 0 rgba(35,28,20,0.03),0 26px 50px -32px rgba(35,28,20,0.55);border-color:var(--gold);} .choice.sel{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent),0 18px 40px -34px rgba(140,53,34,0.5);} .btn{font-family:'Fraunces',serif;font-weight:600;letter-spacing:0.02em;cursor:pointer;border-radius:3px; border:1px solid var(--ink);background:var(--ink);color:var(--paper);padding:12px 22px;font-size:15px; transition:transform .14s ease,opacity .2s,background .2s;} .btn:hover{transform:translateY(-1px);} .btn:disabled{opacity:.45;cursor:not-allowed;transform:none;} .btn.accent{background:var(--accent);border-color:var(--accent);} .btn.ghost{background:transparent;color:var(--ink);} .btn.ghost:hover{background:var(--paper-2);} .btn.sm{padding:7px 13px;font-size:13px;} .btn.tiny{padding:5px 10px;font-size:12px;border-radius:3px;} .ilabel{display:block;margin-bottom:7px;} .input,.area,.select{width:100%;font-family:'Newsreader',serif;font-size:16px;color:var(--ink); background:var(--card);border:1px solid var(--line);border-radius:3px;padding:11px 13px;outline:none; transition:border-color .2s, box-shadow .2s;} .input:focus,.area:focus,.select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(140,53,34,0.1);} .area{resize:vertical;line-height:1.55;} .hint{font-size:13px;color:var(--ink-faint);margin-top:6px;} .field{margin-bottom:18px;} .grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;} @media(max-width:640px){.grid2{grid-template-columns:1fr;}} .pill{display:inline-flex;align-items:center;gap:7px;font-family:'Fraunces',serif;font-size:12px;letter-spacing:0.04em; padding:6px 12px;border-radius:999px;border:1px solid var(--line);background:var(--card);color:var(--ink-soft);cursor:pointer;} .pill.on{background:var(--ink);color:var(--paper);border-color:var(--ink);} .divider{height:1px;background:var(--line);margin:30px 0;} .row{display:flex;align-items:center;gap:12px;flex-wrap:wrap;} .spread{justify-content:space-between;} .spin{display:inline-block;width:15px;height:15px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:sp .7s linear infinite;vertical-align:-2px;} @keyframes sp{to{transform:rotate(360deg)}} .dots::after{content:"";animation:dt 1.4s steps(4,end) infinite;} @keyframes dt{0%{content:""}25%{content:"."}50%{content:".."}75%{content:"..."}} .err{background:#fbeee9;border:1px solid var(--accent-soft);color:var(--accent);padding:11px 14px;border-radius:3px;font-size:14px;} .ws{display:grid;grid-template-columns:300px 1fr;gap:30px;align-items:start;} @media(max-width:880px){.ws{grid-template-columns:1fr;}} .rail{position:sticky;top:78px;} .chap-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:3px;cursor:pointer; border:1px solid transparent;transition:background .15s,border-color .15s;} .chap-item:hover{background:var(--paper-2);} .chap-item.sel{background:var(--card);border-color:var(--line);} .statusdot{width:9px;height:9px;border-radius:50%;flex:none;border:1.5px solid var(--ink-faint);} .statusdot.part{background:var(--gold);border-color:var(--gold);} .statusdot.done{background:var(--ok);border-color:var(--ok);} .chap-no{font-family:'Fraunces',serif;font-size:12px;color:var(--ink-faint);width:22px;flex:none;} .chap-t{font-size:14.5px;line-height:1.25;overflow:hidden;text-overflow:ellipsis;} .prog{height:7px;background:var(--paper-2);border-radius:999px;overflow:hidden;border:1px solid var(--line-soft);} .prog > i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--gold));border-radius:999px;transition:width .5s ease;} .section-block{position:relative;} .manuscript .area{font-size:17.5px;line-height:1.72;border-color:transparent;background:transparent;padding:6px 2px;} .manuscript .area:focus{background:var(--card);border-color:var(--line);padding:11px 13px;} .section-tools{opacity:0;transition:opacity .2s;display:flex;gap:6px;} .section-block:hover .section-tools{opacity:1;} .optcard{padding:16px 18px;} .optcard p{margin:0;font-size:16px;line-height:1.55;} .small{font-size:13px;color:var(--ink-faint);} .kbd{font-family:'Fraunces',serif;font-size:12px;color:var(--ink-faint);} a.link{color:var(--accent);text-decoration:none;border-bottom:1px solid var(--line);} `; /* ----------------------------- API ----------------------------- */ const API_URL = "./api.php"; // PHP proxy on your Hostinger domain const MODEL = "claude-sonnet-4-20250514"; async function callClaude({ system, user, maxTokens = 1200, maxRetries = 1 }) { let lastErr; for (let i = 0; i <= maxRetries; i++) { try { const res = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, max_tokens: maxTokens, system, messages: [{ role: "user", content: user }], }), }); const data = await res.json().catch(() => null); if (!res.ok) { const msg = data && data.error && data.error.message ? data.error.message : "Request failed (" + res.status + ")"; throw new Error(msg); } const text = (data.content || []) .filter((b) => b.type === "text") .map((b) => b.text) .join("\n") .trim(); if (!text) throw new Error("Empty response"); return text; } catch (e) { lastErr = e; } } throw lastErr || new Error("Generation failed"); } function stripFences(t) { return t.replace(/```(?:json)?/gi, "").trim(); } function safeParse(t) { const cleaned = stripFences(t); try { return JSON.parse(cleaned); } catch (_) {} const m = cleaned.match(/[\[{][\s\S]*[\]}]/); if (m) { try { return JSON.parse(m[0]); } catch (_) {} } return null; } function wordCount(s) { if (!s) return 0; const m = s.trim().match(/\S+/g); return m ? m.length : 0; } function chapterWords(ch) { return (ch.sections || []).reduce((a, s) => a + wordCount(s.text), 0); } function fmt(n) { return n.toLocaleString(); } function uid() { return Math.random().toString(36).slice(2, 9); } /* ----------------------------- storage (localStorage) ----------------------------- */ const KEY = "abb:project:v2"; function loadProject() { try { const v = localStorage.getItem(KEY); if (v) return JSON.parse(v); } catch (_) {} return null; } function saveProject(p) { try { localStorage.setItem(KEY, JSON.stringify(p)); } catch (_) {} } function blankProject() { return { id: uid(), stage: "welcome", mode: "guided", setup: { type: "fiction", titleChoice: "generate", titleInput: "", conceptChoice: "generate", conceptInput: "", chapterCount: 12, targetWords: 70000, genre: "", audience: "", tone: "", themes: "", style: "", outlineMode: "auto", }, titleOptions: [], title: "", conceptOptions: [], concept: "", outline: "", chapters: [], activeChapter: 0, createdAt: Date.now(), }; } function bookContext(p) { const s = p.setup; const lines = [ `Project type: ${s.type === "fiction" ? "Fiction" : "Nonfiction"}`, s.genre && `Genre/category: ${s.genre}`, s.audience && `Intended audience: ${s.audience}`, s.tone && `Tone: ${s.tone}`, s.themes && `Themes / subject focus: ${s.themes}`, s.style && `Writing style: ${s.style}`, p.title && `Working title: "${p.title}"`, p.concept && `Concept / premise: ${p.concept}`, `Planned length: about ${fmt(s.targetWords)} words across ${s.chapterCount} ${ s.type === "fiction" ? "chapters" : "chapters/sections" }.`, ].filter(Boolean); return lines.join("\n"); } const STEPS = [ ["mode", "Mode"], ["setup", "Setup"], ["titleconcept", "Title & Concept"], ["outline", "Outline"], ["chapterplan", "Chapters"], ["workspace", "Draft"], ]; /* ================================ APP ================================ */ function App() { const [p, setP] = useState(null); const [booting, setBooting] = useState(true); const saveTimer = useRef(null); useEffect(() => { const saved = loadProject(); setP(saved || blankProject()); setBooting(false); }, []); useEffect(() => { if (!p) return; clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => saveProject(p), 500); }, [p]); const update = useCallback((patch) => { setP((prev) => ({ ...prev, ...(typeof patch === "function" ? patch(prev) : patch) })); }, []); const setStage = useCallback((stage) => update({ stage }), [update]); if (booting || !p) { return (
 Opening your studio
); } const stepIndex = STEPS.findIndex((s) => s[0] === p.stage); return (
{ if (window.confirm("Start a brand-new book? Your current project will be cleared.")) { const np = blankProject(); setP(np); saveProject(np); } }} /> {p.stage === "welcome" && setStage("mode")} p={p} />} {p.stage === "mode" && setStage("setup")} />} {p.stage === "setup" && setStage("titleconcept")} />} {p.stage === "titleconcept" && setStage("outline")} onBack={() => setStage("setup")} />} {p.stage === "outline" && setStage("chapterplan")} onBack={() => setStage("titleconcept")} />} {p.stage === "chapterplan" && setStage("workspace")} onBack={() => setStage("outline")} />} {p.stage === "workspace" && setStage("chapterplan")} />}
); } /* ------------------------------ TopBar ------------------------------ */ function TopBar({ p, stepIndex, onStep, onReset }) { const total = p.setup.targetWords || 0; const written = (p.chapters || []).reduce((a, c) => a + chapterWords(c), 0); return (
B
AI Book Builder
idea · outline · manuscript
{p.stage !== "welcome" && (
{STEPS.map((s, i) => ( ))}
)}
{p.stage === "workspace" && (
Manuscript
{fmt(written)} / {fmt(total)}
)}
); } /* ------------------------------ Welcome ------------------------------ */ function Welcome({ onStart, p }) { const has = p.chapters.length > 0 || p.title || p.concept; return (
A studio for long-form writing

Write the whole book —
one deliberate stage at a time.

Plan and draft fiction or nonfiction up to 120,000 words. We move from idea to title, to a working outline, to chapter summaries, and finally into a drafting desk where the manuscript grows section by section — so nothing is dashed off in a single breathless block.

{has && A project is in progress — your place is saved in this browser.}
Staged, not rushed

Onboarding, then title & concept, then outline, then chapter summaries, then drafting. Each result is yours to edit before the next step.

Consistent to the end

Every chapter is drafted with the outline and the summaries of what came before it in view, keeping voice, plot, and argument coherent across the manuscript.

); } /* ------------------------------ Mode ------------------------------ */ function ModeSelect({ p, update, onNext }) { const set = (mode) => update({ mode }); return (
Step one

How would you like to work?

You can change nothing about your authorship — both modes let you edit every result.

); } /* ------------------------------ Setup ------------------------------ */ function Setup({ p, update, onNext }) { const s = p.setup; const setS = (patch) => update((prev) => ({ setup: { ...prev.setup, ...patch } })); const clampWords = (v) => Math.max(1000, Math.min(120000, v || 0)); const clampCh = (v) => Math.max(1, Math.min(60, v || 0)); const ready = s.genre.trim().length > 0; return (
Step two

Tell the studio about your book

Everything here shapes generation — and everything stays editable later.

Project type
{[["fiction", "Fiction"], ["nonfiction", "Nonfiction"]].map(([v, l]) => ( setS({ type: v })}>{l} ))}
Title
setS({ titleChoice: "generate" })}>Generate one for me setS({ titleChoice: "enter" })}>I'll enter my own
{s.titleChoice === "enter" && ( setS({ titleInput: e.target.value })} /> )}
Concept / premise
setS({ conceptChoice: "generate" })}>Generate one for me setS({ conceptChoice: "enter" })}>I'll write my own
{s.conceptChoice === "enter" && (