"""Patch frontend/dist/assets/index.js with multi-product UI.""" from pathlib import Path path = Path(__file__).resolve().parent.parent / "frontend" / "dist" / "assets" / "index.js" text = path.read_text(encoding="utf-8") marker = "function UpgradeCard(" if "const PRODUCTS =" not in text: insert = r''' const PRODUCTS = [ { id: "writer", name: "ZuZu Writer", short: "Writer", tagline: "Turn AI drafts into natural wording.", blurb: "Rewrite ChatGPT, Gemini, and Claude text in Neutral, Casual, Formal, or Academic tone — classical NLP, no LLM.", status: "live", }, { id: "grammar", name: "ZuZu Grammar", short: "Grammar", tagline: "Catch grammar and spelling before you publish.", blurb: "Scan drafts for grammar, punctuation, and common spelling issues, then apply fixes with one click.", status: "preview", }, ]; const GRAMMAR_SAMPLE = "teh quick brown fox jump over the lazy dog. i think this sentance is seperate from the other one and it it needs fixing."; async function checkGrammar(text, accessToken) { const headers = { "Content-Type": "application/json" }; if (accessToken) headers.Authorization = `Bearer ${accessToken}`; const res = await fetch("/v1/grammar", { method: "POST", headers, body: JSON.stringify({ text }) }); if (!res.ok) { let detail = "Grammar check failed."; try { const data = await res.json(); detail = data.detail || detail; } catch (_) {} throw new Error(typeof detail === "string" ? detail : "Grammar check failed."); } return res.json(); } function applyGrammarFix(text, issue) { if (issue.suggestion == null) return text; return text.slice(0, issue.start) + issue.suggestion + text.slice(issue.end); } function applyAllGrammarFixes(text, issues) { let next = text; const ordered = [...issues].sort((a, b) => b.start - a.start); for (const issue of ordered) { if (issue.suggestion == null) continue; next = applyGrammarFix(next, issue); } return next; } function ProductSwitcher({ product, onChange }) { return h("div", { className: "product-switcher", role: "tablist", "aria-label": "Products" }, PRODUCTS.map((p) => h("button", { key: p.id, type: "button", role: "tab", "aria-selected": product === p.id, className: product === p.id ? "active" : "", onClick: () => onChange(p.id), }, p.short, p.status === "preview" ? h("span", { className: "product-pill" }, "Preview") : null)), ); } function SiteNav({ product, onSelectProduct }) { return h("nav", { className: "site-nav", "aria-label": "Site" }, h("a", { href: "#products" }, "Products"), h("a", { href: "#plans" }, "Plans"), h("button", { type: "button", className: "nav-product", onClick: () => onSelectProduct("writer") }, product === "writer" ? "Open Writer" : "Writer"), h("button", { type: "button", className: "nav-product", onClick: () => onSelectProduct("grammar") }, product === "grammar" ? "Open Grammar" : "Grammar"), ); } ''' text = text.replace(marker, insert + marker, 1) start = text.index("function LandingSections(") end = text.index("function App()") landing = r'''function LandingSections({ plans, onSignUp, authEnabled, onSelectProduct }) { const free = plans.find((p) => p.id === "free"); const pro = plans.find((p) => p.id === "pro"); const plus = plans.find((p) => p.id === "plus"); return h("div", { className: "landing" }, h("section", { className: "land-block", id: "products" }, h("h2", null, "Our products"), h("p", { className: "land-lead" }, "Two focused tools under ZuZu — pick the job you need today."), h("div", { className: "product-grid" }, PRODUCTS.map((p) => h("article", { key: p.id, className: `product-card product-card-${p.id}` }, h("div", { className: "product-card-top" }, h("h3", null, p.name), p.status === "preview" ? h("span", { className: "product-pill" }, "Preview") : null, ), h("p", { className: "product-tagline" }, p.tagline), h("p", null, p.blurb), h("ul", null, ...(p.id === "writer" ? [ h("li", { key: "w1" }, "Tone: Neutral, Casual, Formal, Academic"), h("li", { key: "w2" }, "Offline classical NLP rewrite engine"), h("li", { key: "w3" }, "Free preview, then Free / Pro / Plus plans"), ] : [ h("li", { key: "g1" }, "Grammar, punctuation & common spelling"), h("li", { key: "g2" }, "Click to apply suggested fixes"), h("li", { key: "g3" }, "Works alongside Writer in the same account"), ]), ), h("button", { type: "button", className: "btn btn-primary", onClick: () => { onSelectProduct(p.id); window.scrollTo({ top: 0, behavior: "smooth" }); }, }, `Open ${p.short}`), )), ), ), h("section", { className: "land-block" }, h("h2", null, "How ZuZu works"), h("p", { className: "land-lead" }, "Use Writer to humanize AI drafts, then Grammar to polish before you publish."), h("ol", { className: "steps" }, h("li", null, h("span", { className: "step-num" }, "1"), h("div", null, h("strong", null, "Choose a product"), h("p", null, "Switch between Writer and Grammar from the top of the page."))), h("li", null, h("span", { className: "step-num" }, "2"), h("div", null, h("strong", null, "Paste your draft"), h("p", null, "Drop in AI or human text and run Rewrite or Check grammar."))), h("li", null, h("span", { className: "step-num" }, "3"), h("div", null, h("strong", null, "Review, then unlock more"), h("p", null, "Try a short preview, sign up for Free, go Pro or Plus when you write every day."))), ), ), h("section", { className: "land-block" }, h("h2", null, "Who it's for"), h("p", { className: "land-lead" }, "Built for people who draft with AI and publish as themselves."), h("div", { className: "audience-grid" }, h("article", null, h("h3", null, "Students & academic writers"), h("p", null, "Refine AI-assisted notes into clearer Academic or Formal wording. Follow your institution's rules.")), h("article", null, h("h3", null, "Freelancers & professionals"), h("p", null, "Turn stiff AI emails and reports into confident, natural communication.")), h("article", null, h("h3", null, "Bloggers & SEO writers"), h("p", null, "Refresh repetitive AI drafts into readable posts that still keep your meaning.")), h("article", null, h("h3", null, "Social & content teams"), h("p", null, "Humanize captions and scripts so they sound like your brand, not a model.")), ), ), h("section", { className: "land-block", id: "plans" }, h("h2", null, "Simple plans"), h("p", { className: "land-lead" }, "One account for Writer today — Grammar preview is included. Checkout for Pro/Plus coming soon."), h("div", { className: "pricing-grid" }, h("article", { className: "price-card" }, h("h3", null, "Free"), h("p", { className: "price-amount" }, "₹0"), h("ul", null, h("li", null, `${(free && free.max_words_per_request) || 400} words / rewrite`), h("li", null, `${(free && free.daily_rewrites) || 5} rewrites / day`), h("li", null, "Grammar preview included"), ), authEnabled ? h("button", { type: "button", className: "btn btn-quiet", onClick: onSignUp }, "Create free account") : null, ), h("article", { className: "price-card price-card-pro" }, h("h3", null, "Pro"), h("p", { className: "price-amount" }, `₹${(pro && pro.price_inr_monthly) || 199}`, h("span", null, "/mo")), h("ul", null, h("li", null, `${((pro && pro.max_words_per_request) || 2000).toLocaleString()} words / rewrite`), h("li", null, `${(pro && pro.daily_rewrites) || 50} rewrites / day`), h("li", null, "Best for daily AI drafts"), ), h("p", { className: "price-soon" }, "Checkout coming soon"), ), h("article", { className: "price-card price-card-plus" }, h("h3", null, "Plus"), h("p", { className: "price-amount" }, `₹${(plus && plus.price_inr_monthly) || 499}`, h("span", null, "/mo")), h("ul", null, h("li", null, `${((plus && plus.max_words_per_request) || 5000).toLocaleString()} words / rewrite`), h("li", null, `${(plus && plus.daily_rewrites) || 200} rewrites / day`), h("li", null, "Heavy use & longer documents"), ), h("p", { className: "price-soon" }, "Checkout coming soon"), ), ), h("p", { className: "plans-note" }, "Visitors can try a short free Writer preview on the homepage before signing up."), ), h("footer", { className: "site-footer" }, h("p", null, "Review every rewrite and grammar suggestion before you share or publish."), h("p", { className: "footer-brand" }, h(BrandLogo, { size: 28 }), h("span", null, "ZuZu")), ), ); } ''' text = text[:start] + landing + text[end:] needle = " const [authTitle, setAuthTitle] = useState(undefined);\n\n const isGuest" if "const [product, setProduct]" not in text: text = text.replace( needle, """ const [authTitle, setAuthTitle] = useState(undefined); const [product, setProduct] = useState("writer"); const [grammarText, setGrammarText] = useState(""); const [grammarIssues, setGrammarIssues] = useState([]); const [grammarNote, setGrammarNote] = useState(""); const [grammarLoading, setGrammarLoading] = useState(false); const [grammarError, setGrammarError] = useState(""); const [grammarMeta, setGrammarMeta] = useState(""); const activeProduct = PRODUCTS.find((p) => p.id === product) || PRODUCTS[0]; const isGuest""", 1, ) if "async function onGrammarCheck" not in text: text = text.replace( " onRewrite();\n", ' if (product === "writer") onRewrite();\n else onGrammarCheck();\n', 1, ) text = text.replace( """ function openAuth(mode, title) { setAuthMode(mode); setAuthTitle(title); setAuthOpen(true); } async function onRewrite()""", """ function openAuth(mode, title) { setAuthMode(mode); setAuthTitle(title); setAuthOpen(true); } function selectProduct(id) { setProduct(id); setError(""); setGrammarError(""); } async function onGrammarCheck() { const text = grammarText.trim(); if (!text) { setGrammarError("Paste some text first — or try the sample."); return; } if (text.length > MAX_CHARS) { setGrammarError(`Text is too long (${text.length.toLocaleString()} chars).`); return; } setGrammarLoading(true); setGrammarError(""); setGrammarMeta("Checking…"); try { const result = await checkGrammar(text, session && session.access_token); setGrammarIssues(result.issues || []); setGrammarNote(result.note || ""); const n = (result.issues || []).length; setGrammarMeta(n ? `${n} issue${n === 1 ? "" : "s"} · ${result.input_words} words` : `No issues found · ${result.input_words} words`); } catch (err) { setGrammarError(err instanceof Error ? err.message : "Grammar check failed."); setGrammarMeta(""); setGrammarIssues([]); } finally { setGrammarLoading(false); } } function onApplyGrammarIssue(issue) { setGrammarText((prev) => applyGrammarFix(prev, issue)); setGrammarIssues([]); setGrammarMeta("Fix applied — run Check grammar again to refresh."); setGrammarNote(""); } function onApplyAllGrammar() { if (!grammarIssues.length) return; setGrammarText((prev) => applyAllGrammarFixes(prev, grammarIssues)); setGrammarIssues([]); setGrammarMeta("All suggested fixes applied — run Check grammar again to refresh."); setGrammarNote(""); } async function onRewrite()""", 1, ) old_header = """ h(\"p\", { className: \"brand-tag\" }, \"From AI-generated to plagiarism-safe — rewrite in a voice that feels real.\"), ),""" new_header = """ h(\"p\", { className: \"brand-tag\" }, activeProduct.tagline), h(SiteNav, { product, onSelectProduct: selectProduct }), ),""" if "activeProduct.tagline" not in text: text = text.replace(old_header, new_header, 1) text = text.replace('h("h1", null, "ZuZu Writer"),', 'h("h1", null, activeProduct.name),', 1) if "h(ProductSwitcher" not in text: text = text.replace( " isGuest && !idleSignedOut\n", ' h(ProductSwitcher, { product, onChange: selectProduct }),\n isGuest && !idleSignedOut && product === "writer"\n', 1, ) text = text.replace( """ h(LandingSections, { plans, authEnabled, onSignUp: () => openAuth("signup", "Create free account"), }),""", """ h(LandingSections, { plans, authEnabled, onSignUp: () => openAuth("signup", "Create free account"), onSelectProduct: selectProduct, }),""", 1, ) stage_start = text.find(' h("div", { className: "stage" },') stage_end = text.find(" showUpgrade && authEnabled") if stage_start != -1 and stage_end != -1 and "grammar-stage" not in text: writer_stage = text[stage_start:stage_end] grammar_stage = r''' product === "grammar" ? h("div", { className: "stage grammar-stage" }, h("div", { className: "toolbar" }, h("div", { className: "toolbar-controls" }, h("p", { className: "grammar-lead" }, "Preview rule engine — grammar, punctuation, and common spelling. Fuller checks coming next."), ), h("div", { className: "toolbar-actions" }, h("button", { type: "button", className: "btn btn-quiet", disabled: grammarLoading, onClick: () => { setGrammarText(""); setGrammarIssues([]); setGrammarMeta(""); setGrammarError(""); setGrammarNote(""); } }, "Clear"), h("button", { type: "button", className: "btn btn-quiet", disabled: grammarLoading, onClick: () => { setGrammarText(GRAMMAR_SAMPLE); setGrammarIssues([]); setGrammarError(""); setGrammarMeta("Sample loaded — hit Check grammar."); setGrammarNote(""); } }, "Try sample"), grammarIssues.length ? h("button", { type: "button", className: "btn btn-quiet", disabled: grammarLoading, onClick: onApplyAllGrammar }, "Apply all") : null, h("button", { type: "button", className: "btn btn-primary btn-rewrite", disabled: grammarLoading, onClick: () => onGrammarCheck() }, grammarLoading ? "Checking…" : "Check grammar"), ), ), h("section", { className: "editors grammar-editors" }, h("div", { className: "pane" }, h("div", { className: "pane-head" }, h("h2", null, "Your text")), h("textarea", { value: grammarText, onChange: (e) => { setGrammarText(e.target.value); setGrammarIssues([]); }, placeholder: "Paste a draft to check grammar and spelling…", spellCheck: true, }), ), h("div", { className: "pane pane-out grammar-issues-pane" }, h("div", { className: "pane-head" }, h("h2", null, "Issues"), h("span", { className: "issue-count" }, grammarIssues.length ? `${grammarIssues.length} found` : (grammarMeta ? "Clean" : "—")), ), grammarIssues.length ? h("ul", { className: "issue-list" }, grammarIssues.map((issue) => h("li", { key: issue.id, className: `issue-item cat-${issue.category}` }, h("div", { className: "issue-main" }, h("span", { className: "issue-cat" }, issue.category), h("p", null, issue.message), h("code", { className: "issue-snippet" }, (grammarText.slice(issue.start, issue.end) || "…") + (issue.suggestion != null ? ` → ${issue.suggestion}` : ""), ), ), issue.suggestion != null ? h("button", { type: "button", className: "btn btn-quiet btn-tiny", onClick: () => onApplyGrammarIssue(issue) }, "Apply") : null, )), ) : h("p", { className: "grammar-empty" }, grammarNote || "Run Check grammar to see suggestions here."), ), ), h("div", { className: "statusbar" }, h("div", { className: grammarError ? "error" : grammarLoading ? "loading" : undefined }, grammarError || grammarMeta || "Paste text → Check grammar"), h("div", { className: "counts" }, h("span", null, `${wordCount(grammarText)} words`)), ), ) : ''' text = text[:stage_start] + grammar_stage + writer_stage + text[stage_end:] text = text.replace( " showUpgrade && authEnabled\n", ' showUpgrade && authEnabled && product === "writer"\n', 1, ) text = text.replace( ' h("p", { className: "hint" }, "Review the rewrite before you share or publish it."),', ' h("p", { className: "hint" }, product === "grammar" ? "Review every suggestion before you publish." : "Review the rewrite before you share or publish it."),', 1, ) path.write_text(text, encoding="utf-8") print("patched", path) print("PRODUCTS", "const PRODUCTS =" in text) print("grammar-stage", "grammar-stage" in text) print("ProductSwitcher", "h(ProductSwitcher" in text)