| """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) | |