import { useState } from "react"; // ─── THEMES ─────────────────────────────────────────────────────────────────── const THEMES = { cosmic: { label: "🌌 Cosmic / Space", coverGradient: "linear-gradient(160deg, #1a0a3a 0%, #2d1260 40%, #0d0820 100%)", coverSubtitle: "#a78bfa", coverAuthor: "#e8d5ff", coverPublisher: "#7c6fa0", coverRule: "linear-gradient(to right, transparent, #f0d080, transparent)", coverBorder: "rgba(167,139,250,0.2)", accent: "#6b3fa0", gold: "#c9993a", pageBg: "#fdf8ef", section: "#6b3fa0", border: "#d4b896", rule: "linear-gradient(to right, #c9993a, transparent)", dropCap: "#6b3fa0", emblem: "🌌", stars: true, }, nature: { label: "🌿 Nature / Adventure", coverGradient: "linear-gradient(160deg, #0d2b1a 0%, #1a4a2e 45%, #0a1f10 100%)", coverSubtitle: "#6ee7b7", coverAuthor: "#d1fae5", coverPublisher: "#4a7c59", coverRule: "linear-gradient(to right, transparent, #fbbf24, transparent)", coverBorder: "rgba(110,231,183,0.2)", accent: "#166534", gold: "#b45309", pageBg: "#f7fdf4", section: "#166534", border: "#bbf7d0", rule: "linear-gradient(to right, #b45309, transparent)", dropCap: "#166534", emblem: "🌿", stars: false, }, ocean: { label: "🌊 Ocean / Magic", coverGradient: "linear-gradient(160deg, #0c2340 0%, #0e4a6e 45%, #051523 100%)", coverSubtitle: "#38bdf8", coverAuthor: "#e0f2fe", coverPublisher: "#3b6b8a", coverRule: "linear-gradient(to right, transparent, #f0a050, transparent)", coverBorder: "rgba(125,211,252,0.2)", accent: "#0369a1", gold: "#ea7c2b", pageBg: "#f0f9ff", section: "#0369a1", border: "#bae6fd", rule: "linear-gradient(to right, #ea7c2b, transparent)", dropCap: "#0369a1", emblem: "🌊", stars: false, }, fairytale: { label: "✨ Fairy Tale", coverGradient: "linear-gradient(160deg, #3b0764 0%, #6b21a8 45%, #1e0535 100%)", coverSubtitle: "#f472b6", coverAuthor: "#fce7f3", coverPublisher: "#9b5080", coverRule: "linear-gradient(to right, transparent, #fde68a, transparent)", coverBorder: "rgba(249,168,212,0.2)", accent: "#86198f", gold: "#d97706", pageBg: "#fdf4ff", section: "#86198f", border: "#f5d0fe", rule: "linear-gradient(to right, #d97706, transparent)", dropCap: "#86198f", emblem: "✨", stars: false, }, adventure: { label: "⚔️ Adventure / Heroes", coverGradient: "linear-gradient(160deg, #1c0a00 0%, #431407 45%, #0f0500 100%)", coverSubtitle: "#fb923c", coverAuthor: "#fff7ed", coverPublisher: "#7c3a1a", coverRule: "linear-gradient(to right, transparent, #fbbf24, transparent)", coverBorder: "rgba(253,186,116,0.2)", accent: "#9a3412", gold: "#d97706", pageBg: "#fffbf5", section: "#9a3412", border: "#fed7aa", rule: "linear-gradient(to right, #d97706, transparent)", dropCap: "#9a3412", emblem: "⚔️", stars: false, }, mystery: { label: "🔮 Mystery / Thriller", coverGradient: "linear-gradient(160deg, #0a0a14 0%, #1e1b4b 45%, #050508 100%)", coverSubtitle: "#818cf8", coverAuthor: "#e0e7ff", coverPublisher: "#4b4a7a", coverRule: "linear-gradient(to right, transparent, #c0c0c0, transparent)", coverBorder: "rgba(199,210,254,0.2)", accent: "#3730a3", gold: "#9ca3af", pageBg: "#f8f8fc", section: "#3730a3", border: "#c7d2fe", rule: "linear-gradient(to right, #9ca3af, transparent)", dropCap: "#3730a3", emblem: "🔮", stars: false, }, }; // ─── STRUCTURED PARSER ──────────────────────────────────────────────────────── function parseStructured(raw) { const lines = raw.split("\n").map(l => l.trim()).filter(Boolean); const book = { title:"", subtitle:"", author:"", publisher:"", characters:[], chapters:[], findIt:[], discussion:[], vocabulary:[], parentNote:"" }; let mode = null, currentChapter = null, parentLines = [], headerDone = false; for (const line of lines) { const isSec = /^[✨📖🔍💬📝🌿]/.test(line) || /^(story|worksheet)$/i.test(line); if (isSec) headerDone = true; if (!headerDone) { if (!book.title) { book.title = line; continue; } if (!book.subtitle && !line.toLowerCase().startsWith("by ")) { book.subtitle = line; continue; } if (line.toLowerCase().startsWith("by ")) { const p = line.replace(/^by\s+/i,"").split("•").map(s=>s.trim()); book.author = p[0]||""; book.publisher = p.slice(1).join(" • ")||""; continue; } } if (line.includes("MEET THE CHARACTERS")||line.startsWith("✨")) { mode="chars"; continue; } if (line.includes("THE STORY")||line.startsWith("📖")||/^story$/i.test(line)) { mode="story"; continue; } if (line.includes("FIND IT")||line.startsWith("🔍")) { mode="find"; continue; } if (line.includes("LET'S TALK")||line.startsWith("💬")) { mode="disc"; continue; } if (line.includes("VOCABULARY")||line.startsWith("📝")) { mode="vocab"; continue; } if (line.includes("PARENTS")||line.includes("EDUCATORS")||line.startsWith("🌿")) { mode="parent"; continue; } if (mode==="chars") { const m=line.match(/^([A-Za-z ]+)\s*[—–-]\s*(.+)/); if(m) book.characters.push({name:m[1].trim(),desc:m[2].trim()}); } if (mode==="story") { const c=line.match(/^Chapter\s+(\d+):\s*(.+)/i); if(c){currentChapter={number:c[1],title:c[2],body:""}; book.chapters.push(currentChapter);}else if(currentChapter){currentChapter.body+=(currentChapter.body?" ":"")+line;} } if (mode==="find") { const c=line.replace(/^[☐✓✔□]\s*/,"").trim(); if(c) book.findIt.push(c); } if (mode==="disc") { const m=line.match(/^\d+\.\s+(.+)/); if(m) book.discussion.push(m[1]); } if (mode==="vocab") { const m=line.match(/^([a-zA-Z]+)\s*[—–-]\s*(.+)/); if(m) book.vocabulary.push({word:m[1],def:m[2]}); } if (mode==="parent") parentLines.push(line); } book.parentNote = parentLines.join(" "); return book; } function isWellStructured(b) { return b.title && (b.chapters.length > 0 || b.characters.length > 0); } // ─── AI PARSER ──────────────────────────────────────────────────────────────── async function parseWithAI(raw) { const res = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: "claude-sonnet-4-6", max_tokens: 1000, messages: [{ role: "user", content: `Extract structured data from this children's story. Return ONLY valid JSON, no markdown. Shape: {"title":"","subtitle":"","author":"","publisher":"","characters":[{"name":"","desc":""}],"chapters":[{"number":"1","title":"","body":""}],"findIt":[],"discussion":[],"vocabulary":[{"word":"","def":""}],"parentNote":""} Rules: if no chapters, wrap full narrative as Chapter 1. Keep body text complete. Empty arrays for missing sections. Story: ${raw.slice(0,8000)}` }], }), }); const data = await res.json(); const text = data.content?.find(b=>b.type==="text")?.text||"{}"; return JSON.parse(text.replace(/```json|```/g,"").trim()); } // ─── HTML BUILDER ───────────────────────────────────────────────────────────── function buildHTML(book, theme, mode) { const t = THEMES[theme]; const isPrint = mode === "print"; const esc = s => String(s||"").replace(/&/g,"&").replace(//g,">"); const stars = t.stars ? `
` : ""; const charCards = book.characters.map(c=>`
${esc(c.name)}
${esc(c.desc)}
`).join(""); const chapters = book.chapters.map(ch=>`
Chapter ${esc(ch.number)}

${esc(ch.title)}

${esc(ch.body)}

`).join(""); const findGrid = book.findIt.map(item=> `
${esc(item)}
` ).join(""); const discItems = book.discussion.map((q,i)=> `
${i+1}.${esc(q)}
` ).join(""); const vocabItems = book.vocabulary.map(v=> `
${esc(v.word)}${esc(v.def)}
` ).join(""); return ` ${esc(book.title)} ${isPrint?`
${stars}
${t.emblem}

${esc(book.title)}

${book.subtitle?`

${esc(book.subtitle)}

`:""}
by ${esc(book.author)}
${book.publisher?`
${esc(book.publisher)}
`:""}
${book.characters.length?`
Meet the Characters
${charCards}
`:""} ${isPrint ? `
${chapters}
` : book.chapters.map(ch=>`
Chapter ${esc(ch.number)}

${esc(ch.title)}

${esc(ch.body)}

`).join("") } ${book.findIt.length?`
${isPrint&&(book.characters.length||book.chapters.length)?`
`:""}
🔍Find It!
${findGrid}
`:""} ${book.discussion.length?`
${isPrint?`
`:""}
💬Let's Talk
${discItems}
`:""} ${book.vocabulary.length?`
${isPrint?`
`:""}
📝Vocabulary Builder
${vocabItems}
`:""} ${book.parentNote?`
${isPrint?`
`:""}
🌿For Parents & Educators

${esc(book.parentNote)}

`:""}
`; } // ─── UTILS ──────────────────────────────────────────────────────────────────── function download(html, filename) { const blob = new Blob([html], { type:"text/html;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href=url; a.download=filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function printWindow(html) { const w = window.open("","_blank"); if (!w) { alert("Allow popups to use Print / Save PDF."); return; } w.document.write(html); w.document.close(); } // ─── APP ────────────────────────────────────────────────────────────────────── export default function App() { const [raw, setRaw] = useState(""); const [theme, setTheme] = useState("cosmic"); const [status, setStatus] = useState(null); const [bookTitle, setBookTitle] = useState(""); const [cached, setCached] = useState(null); // parsed book cache const T = THEMES[theme]; async function resolveBook() { let book = parseStructured(raw); let usedAI = false; if (!isWellStructured(book)) { book = await parseWithAI(raw); usedAI = true; } setCached(book); return { book, usedAI }; } async function handleDownload() { if (!raw.trim()) return; setStatus("parsing"); try { const { book, usedAI } = await resolveBook(); const html = buildHTML(book, theme, "screen"); const slug = (book.title||"ebook").toLowerCase().replace(/[^a-z0-9]+/g,"-"); download(html, `${slug}.html`); setBookTitle(book.title); setStatus(usedAI?"ai":"ok"); } catch { setStatus("error"); } } async function handlePrint() { if (!raw.trim()) return; setStatus("parsing"); try { const book = cached || (await resolveBook()).book; const usedAI = !cached && !isWellStructured(parseStructured(raw)); const html = buildHTML(book, theme, "print"); printWindow(html); setBookTitle(book.title); setStatus(usedAI?"ai":"ok"); } catch { setStatus("error"); } } const loading = status === "parsing"; const ready = raw.trim().length > 0; const Btn = ({ onClick, label, variant }) => ( ); return (
{/* Header */}
📚

eBook Generator

Kaleidoscope Kids Books · Rooted In Hope Therapy

{/* Theme grid */}
Book Theme / Genre
{Object.entries(THEMES).map(([key,th])=>( ))}
{/* Live color strip */}
{/* Textarea */}