import { useEffect, useState, type FormEvent } from "react"; import { createPortal } from "react-dom"; import { checkGrammar, rewriteText, STRENGTH_MAP, STRENGTHS, TONES, type AccountInfo, type ApiError, type GrammarIssue, type StrengthLabel, type Tone, } from "./api"; import { useAuth } from "./auth"; import type { PlanCard } from "./supabase"; const LOGO_SRC = "/zuzu-logo.png"; const LOGO_FALLBACK = "/favicon.svg"; const MAX_CHARS = 50000; const GRAMMAR_MAX_CHARS = 12000; type ProductId = "writer" | "grammar"; const PRODUCTS: { id: ProductId; name: string; short: string; tagline: string; blurb: string; status: "live" | "preview"; }[] = [ { 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: "Full sentence grammar, spelling, and punctuation via self-hosted LanguageTool — apply fixes with one click.", status: "preview", }, ]; function BrandLogo({ size = 44, className = "" }: { size?: number; className?: string }) { return ( ZuZu Writer { const img = e.currentTarget; if (img.src.endsWith("favicon.svg")) return; img.src = LOGO_FALLBACK; }} /> ); } /** Official multicolor Google “G” mark for the OAuth button. */ function GoogleGMark({ size = 18 }: { size?: number }) { return ( ); } const SAMPLE_TEXT = "Artificial intelligence has significantly transformed the way individuals create written content in recent years. Many professionals now rely on advanced language models to generate initial drafts quickly and efficiently. However, the resulting text can sometimes appear repetitive, overly formal, or lacking a natural human voice. Therefore, it is essential to carefully refine AI-generated material so that it communicates clearly, remains original in wording, and feels authentic to the intended audience."; const TONE_HINT: Record = { Neutral: "Clear and balanced — light elevate only", Casual: "Conversational — contractions + downshift lexicon", Formal: "Professional — elevated wording, no contractions", Academic: "Scholarly — denser lexicon + discourse markers", }; function wordCount(text: string): number { return text.trim() ? text.trim().split(/\s+/).length : 0; } function ProductSwitcher({ product, onChange, }: { product: ProductId; onChange: (id: ProductId) => void; }) { return (
{PRODUCTS.map((p) => ( ))}
); } function SiteNav({ product, onSelectProduct, }: { product: ProductId; onSelectProduct: (id: ProductId) => void; }) { return ( ); } function applyGrammarFix(text: string, issue: GrammarIssue): string { if (issue.suggestion == null) return text; return text.slice(0, issue.start) + issue.suggestion + text.slice(issue.end); } /** Keep remaining issues after one fix, shifting offsets past the edit. */ function remainIssuesAfterFix(issues: GrammarIssue[], applied: GrammarIssue): GrammarIssue[] { if (applied.suggestion == null) { return issues.filter((i) => i.id !== applied.id); } const oldLen = Math.max(0, applied.end - applied.start); const delta = applied.suggestion.length - oldLen; return issues .filter((i) => i.id !== applied.id) .filter((i) => i.end <= applied.start || i.start >= applied.end) // drop overlaps .map((i) => { if (i.start >= applied.end) { return { ...i, start: i.start + delta, end: i.end + delta, id: `${i.id}-s${delta}`, }; } return i; }); } function applyAllGrammarFixes(text: string, issues: GrammarIssue[]): string { 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; } 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."; const GRAMMAR_LANGUAGES: { id: string; label: string }[] = [ { id: "en-US", label: "English (US)" }, { id: "en-GB", label: "English (UK)" }, { id: "en-AU", label: "English (AU)" }, { id: "en-CA", label: "English (CA)" }, { id: "en-ZA", label: "English (ZA)" }, { id: "en-NZ", label: "English (NZ)" }, ]; function AuthModal({ open, onClose, initialMode = "signup", title, }: { open: boolean; onClose: () => void; initialMode?: "signin" | "signup"; title?: string; }) { const { signInWithPassword, signUp, signInWithGoogle } = useAuth(); const [mode, setMode] = useState<"signin" | "signup">(initialMode); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [error, setError] = useState(""); useEffect(() => { if (open) { setMode(initialMode); setError(""); setMessage(""); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; } return undefined; }, [open, initialMode]); if (!open) return null; async function onSubmit(e: FormEvent) { e.preventDefault(); setBusy(true); setError(""); setMessage(""); try { if (mode === "signin") { await signInWithPassword(email.trim(), password); onClose(); } else { const result = await signUp(email.trim(), password); if (result === "check_email") { setMessage("Check your email to confirm your account, then sign in."); } else { onClose(); } } } catch (err) { setError(err instanceof Error ? err.message : "Authentication failed."); } finally { setBusy(false); } } return createPortal(
e.stopPropagation()} >

ZuZu Writer

{title || (mode === "signin" ? "Sign in" : "Create free account")}

Unlock longer rewrites and daily limits. Google or email — takes a minute.

void onSubmit(e)}>
or
{error ?

{error}

: null} {message ?

{message}

: null}
, document.body, ); } function UpgradeCard({ account, plans, guestMaxWords, onSignUp, onSignIn, }: { account: AccountInfo | null; plans: PlanCard[]; guestMaxWords: number; onSignUp: () => void; onSignIn: () => void; }) { const planId = account?.plan.id ?? "guest"; if (planId === "pro" || planId === "plus") return null; const free = plans.find((p) => p.id === "free"); const pro = plans.find((p) => p.id === "pro"); const isGuest = planId === "guest" || account?.role === "guest" || !account?.email; const previewWords = isGuest ? guestMaxWords : account?.plan.max_words_per_request ?? 100; const previewRewrites = account?.plan.daily_rewrites ?? 1; return (

{isGuest ? "Liked the rewrite? Unlock more with a free account" : "Need longer drafts every day? Go Pro"}

{isGuest ? `Preview is capped at ${previewWords} words and ${previewRewrites} rewrite/day. Sign up free for higher limits — or Pro for serious daily use.` : `You're on ${account?.plan.name ?? "Free"}. Pro gives up to ${pro?.max_words_per_request?.toLocaleString() ?? "2,000"} words per rewrite and ${pro?.daily_rewrites ?? 50} rewrites/day.`}

{isGuest && free ? (
Free {free.daily_rewrites}/day · {free.max_words_per_request} words ₹0
) : null} {pro ? (
Pro {pro.daily_rewrites}/day · {pro.max_words_per_request.toLocaleString()} words ₹{pro.price_inr_monthly}/mo
) : null}
{isGuest ? ( <> ) : ( )}
{!isGuest ? (

Until checkout is live, an admin can set plan_id = pro on your profile in Supabase.

) : null}
); } function LandingSections({ plans, onSignUp, authEnabled, onSelectProduct, }: { plans: PlanCard[]; onSignUp: () => void; authEnabled: boolean; onSelectProduct: (id: ProductId) => void; }) { 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 (

Our products

Two focused tools under ZuZu — pick the job you need today.

{PRODUCTS.map((p) => (

{p.name}

{p.status === "preview" ? Preview : null}

{p.tagline}

{p.blurb}

    {p.id === "writer" ? ( <>
  • Tone: Neutral, Casual, Formal, Academic
  • Offline classical NLP rewrite engine
  • Free preview, then Free / Pro / Plus plans
  • ) : ( <>
  • Full sentence grammar via LanguageTool
  • Spelling, punctuation & style suggestions
  • Click to apply fixes · works with your ZuZu account
  • )}
))}

How ZuZu works

Use Writer to humanize AI drafts, then Grammar to polish before you publish.

  1. 1
    Choose a product

    Switch between Writer and Grammar from the top of the page.

  2. 2
    Paste your draft

    Drop in AI or human text and run Rewrite or Check grammar.

  3. 3
    Review, then unlock more

    Try a short preview, sign up for Free, go Pro or Plus when you write every day.

Who it’s for

Built for people who draft with AI and publish as themselves.

Students & academic writers

Refine AI-assisted notes into clearer Academic or Formal wording. Follow your institution’s rules.

Freelancers & professionals

Turn stiff AI emails and reports into confident, natural communication.

Bloggers & SEO writers

Refresh repetitive AI drafts into readable posts that still keep your meaning.

Social & content teams

Humanize captions and scripts so they sound like your brand, not a model.

Simple plans

One account for Writer today — Grammar preview is included. Checkout for Pro/Plus coming soon.

Free

₹0

  • {free?.max_words_per_request ?? 400} words / rewrite
  • {free?.daily_rewrites ?? 5} rewrites / day
  • Grammar preview included
{authEnabled ? ( ) : null}

Pro

₹{pro?.price_inr_monthly ?? 199} /mo

  • {(pro?.max_words_per_request ?? 2000).toLocaleString()} words / rewrite
  • {pro?.daily_rewrites ?? 50} rewrites / day
  • Best for daily AI drafts

Checkout coming soon

Plus

₹{plus?.price_inr_monthly ?? 499} /mo

  • {(plus?.max_words_per_request ?? 5000).toLocaleString()} words / rewrite
  • {plus?.daily_rewrites ?? 200} rewrites / day
  • Heavy use & longer documents

Checkout coming soon

Visitors can try a short free Writer preview on the homepage before signing up.

Review every rewrite and grammar suggestion before you share or publish.

ZuZu

); } export default function App() { const { ready, authEnabled, session, account, setAccount, signOut, plans, guestMaxWords, refreshAccount, idleSignedOut, clearIdleNotice, sessionIdleMinutes, } = useAuth(); const [input, setInput] = useState(""); const [output, setOutput] = useState(""); const [tone, setTone] = useState("Neutral"); const [strength, setStrength] = useState("Normal"); const [preserveLength, setPreserveLength] = useState(true); const [mlPolish, setMlPolish] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [meta, setMeta] = useState(""); const [copied, setCopied] = useState(false); const [freshOut, setFreshOut] = useState(false); const [showUpgrade, setShowUpgrade] = useState(false); const [authOpen, setAuthOpen] = useState(false); const [authMode, setAuthMode] = useState<"signin" | "signup">("signup"); const [authTitle, setAuthTitle] = useState(); const [product, setProduct] = useState("writer"); const [grammarText, setGrammarText] = useState(""); const [grammarLanguage, setGrammarLanguage] = useState("en-US"); const [grammarIssues, setGrammarIssues] = useState([]); const [grammarNote, setGrammarNote] = useState(""); const [grammarEngine, setGrammarEngine] = 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 = Boolean(authEnabled && !session); // Guests always use config teaser limit (ignore inflated account payloads / env mistakes) const maxWords = isGuest ? guestMaxWords : account?.plan.max_words_per_request ?? 50000; useEffect(() => { if (!copied) return; const t = window.setTimeout(() => setCopied(false), 1600); return () => window.clearTimeout(t); }, [copied]); useEffect(() => { if (session) { setAuthOpen(false); void refreshAccount(); } }, [session, refreshAccount]); useEffect(() => { if (isGuest) setShowUpgrade(true); }, [isGuest]); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); if (product === "writer") void onRewrite(); else void onGrammarCheck(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }, [input, tone, strength, preserveLength, mlPolish, loading, session, product, grammarText, grammarLoading, grammarLanguage]); function openAuth(mode: "signin" | "signup", title?: string) { setAuthMode(mode); setAuthTitle(title); setAuthOpen(true); } function selectProduct(id: ProductId) { setProduct(id); setError(""); setGrammarError(""); } function loadSample() { setInput(SAMPLE_TEXT); setError(""); setMeta("Sample loaded — hit Rewrite to try it."); } function loadGrammarSample() { setGrammarText(GRAMMAR_SAMPLE); setGrammarIssues([]); setGrammarError(""); setGrammarMeta("Sample loaded — hit Check grammar."); setGrammarNote(""); } async function onGrammarCheck() { const text = grammarText.trim(); if (!text) { setGrammarError("Paste some text first — or try the sample."); return; } if (text.length > GRAMMAR_MAX_CHARS) { setGrammarError( `Text is too long for grammar check (${text.length.toLocaleString()} chars). Max is ${GRAMMAR_MAX_CHARS.toLocaleString()} — shorten or split into sections.`, ); return; } setGrammarLoading(true); setGrammarError(""); setGrammarMeta("Checking…"); setGrammarEngine(""); try { const result = await checkGrammar(text, { language: grammarLanguage, accessToken: session?.access_token, }); setGrammarIssues(result.issues); setGrammarNote(result.note ?? ""); setGrammarEngine(result.engine || ""); setGrammarMeta( result.issues.length ? `${result.issues.length} issue${result.issues.length === 1 ? "" : "s"} · ${result.input_words} words · ${result.language ?? grammarLanguage}` : `No issues found · ${result.input_words} words · ${result.language ?? grammarLanguage}`, ); } catch (err) { const apiErr = err as ApiError; setGrammarError(apiErr.message || "Grammar check failed."); setGrammarMeta(""); setGrammarEngine(""); setGrammarIssues([]); } finally { setGrammarLoading(false); } } function onApplyGrammarIssue(issue: GrammarIssue) { setGrammarText((prev) => applyGrammarFix(prev, issue)); const remaining = remainIssuesAfterFix(grammarIssues, issue); setGrammarIssues(remaining); setGrammarMeta( remaining.length ? `Fix applied · ${remaining.length} issue${remaining.length === 1 ? "" : "s"} left` : "Fix applied · no issues left — run Check grammar again to confirm.", ); } function onApplyAllGrammar() { if (!grammarIssues.length) return; setGrammarText((prev) => applyAllGrammarFixes(prev, grammarIssues)); setGrammarIssues([]); setGrammarMeta("All suggested fixes applied — run Check grammar again to confirm."); } async function onRewrite() { const text = input.trim(); if (!text) { setError("Paste some text first — or try the sample."); return; } if (text.length > MAX_CHARS) { setError(`Text is too long (${text.length.toLocaleString()} chars).`); return; } const words = wordCount(text); if (authEnabled && words > maxWords) { setError( `This text has ${words} words. ${isGuest ? "Preview" : account?.plan.name ?? "Your plan"} allows ${maxWords} words per rewrite.`, ); setShowUpgrade(true); if (isGuest) openAuth("signup", "Sign up to rewrite longer text"); return; } setLoading(true); setError(""); setMeta("Rewriting…"); setFreshOut(false); try { const result = await rewriteText( { text, tone, strength: STRENGTH_MAP[strength], preserve_length: preserveLength, ml_polish: mlPolish, }, session?.access_token, ); setOutput(result.rewrite); setFreshOut(true); if (result.account) setAccount(result.account); const left = result.account?.usage?.remaining_rewrites; const quota = left != null ? ` · ${left} rewrite${left === 1 ? "" : "s"} left today` : ""; const lexCount = result.meta.lexical_refined ?? 0; const polishNote = mlPolish ? ` · polish on · ${lexCount} synonym-touched sentence${lexCount === 1 ? "" : "s"}` : ` · polish off · ${lexCount} synonym-touched sentence${lexCount === 1 ? "" : "s"}`; setMeta( `${result.meta.input_words.toLocaleString()} → ${result.meta.output_words.toLocaleString()} words · ${result.meta.seconds}s${polishNote}${quota}`, ); if ( authEnabled && result.account && (result.account.plan.id === "guest" || result.account.plan.id === "free") ) { setShowUpgrade(true); } } catch (err) { const apiErr = err as ApiError; setError(apiErr.message || "Rewrite failed."); setMeta(""); if (apiErr.code === "limit") { setShowUpgrade(true); if (isGuest) openAuth("signup", "Free preview used — sign up for more"); } } finally { setLoading(false); } } async function onCopy() { if (!output.trim()) return; try { await navigator.clipboard.writeText(output); setCopied(true); } catch { setError("Could not copy to clipboard."); } } function onDownload() { if (!output.trim()) return; const blob = new Blob([output], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "zuzu-writer.txt"; a.click(); URL.revokeObjectURL(url); } const inWords = wordCount(input); const outWords = wordCount(output); const overCap = authEnabled && inWords > maxWords; const meterPct = authEnabled ? Math.min(100, Math.round((inWords / Math.max(maxWords, 1)) * 100)) : 0; if (!ready) { return (

Loading…

); } return (

ZuZu

{activeProduct.name}

{activeProduct.tagline}

{session && account ? (
{account.plan.name} {account.usage.remaining_rewrites}/{account.plan.daily_rewrites} left today {account.email}
) : authEnabled ? (
Preview {account ? `${account.usage.remaining_rewrites} of ${account.plan.daily_rewrites} free today` : `Up to ${guestMaxWords} words`}
) : (
{product === "writer" ? `${tone} · ${strength}` : "Grammar preview"} ⌘/Ctrl + Enter
)}
{idleSignedOut ? (
Signed out after {sessionIdleMinutes} minutes of inactivity.
) : null} {isGuest && !idleSignedOut && product === "writer" ? (
Free Preview

Try up to {maxWords} words, {account?.plan.daily_rewrites ?? 1}{" "} rewrite/day — then unlock Free for longer drafts.

) : null} {product === "grammar" ? (
Language
{GRAMMAR_LANGUAGES.map((lang) => ( ))}

Self-hosted LanguageTool checks full sentence grammar for{" "} {GRAMMAR_LANGUAGES.find((l) => l.id === grammarLanguage)?.label ?? grammarLanguage} . Long text is checked in small chunks. If LanguageTool is offline, basic local rules are used instead.

{grammarEngine ? ( Engine: {grammarEngine === "languagetool" ? "LanguageTool" : grammarEngine === "rules" ? "Local rules" : grammarEngine} ) : null}
{grammarIssues.length ? ( ) : null}

Your text