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 (
{
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(
{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.
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.