export type Tone = "Neutral" | "Casual" | "Formal" | "Academic"; export type StrengthLabel = "Light" | "Normal" | "Heavy"; export const TONES: Tone[] = ["Neutral", "Casual", "Formal", "Academic"]; export const STRENGTHS: StrengthLabel[] = ["Light", "Normal", "Heavy"]; export const STRENGTH_MAP: Record = { Light: 0, Normal: 1, Heavy: 2, }; export type RewriteMeta = { input_words: number; output_words: number; seconds: number; lexical_refined: number; ml_polish_requested: boolean; tone: string; strength: number; }; export type AccountInfo = { email: string | null; display_name: string | null; role: string; plan: { id: string; name: string; daily_rewrites: number; max_words_per_request: number; daily_word_cap: number; price_inr_monthly: number; }; usage: { date: string; rewrite_count: number; word_count: number; remaining_rewrites: number; remaining_words: number; }; }; export type RewriteResponse = { rewrite: string; meta: RewriteMeta; account?: AccountInfo | null; }; export type ApiError = Error & { status?: number; code?: string }; function detailMessage(detail: unknown, fallback: string): string { if (typeof detail === "string") return detail; if (Array.isArray(detail)) { return detail .map((d) => (typeof d === "object" && d && "msg" in d ? String(d.msg) : String(d))) .join(" "); } return fallback; } export async function rewriteText( payload: { text: string; tone: Tone; strength: number; preserve_length: boolean; ml_polish?: boolean; }, accessToken?: string | null, ): Promise { const headers: Record = { "Content-Type": "application/json" }; if (accessToken) headers.Authorization = `Bearer ${accessToken}`; const res = await fetch("/v1/rewrite", { method: "POST", headers, body: JSON.stringify(payload), }); if (!res.ok) { let detail: unknown = "Rewrite failed."; try { const data = await res.json(); detail = data.detail ?? detail; } catch { /* ignore */ } const err = new Error(detailMessage(detail, "Rewrite failed.")) as ApiError; err.status = res.status; if (res.status === 401 || res.status === 429 || res.status === 413) { err.code = "limit"; } throw err; } return res.json(); } export type GrammarIssue = { id: string; start: number; end: number; message: string; suggestion: string | null; category: string; }; export type GrammarResponse = { issues: GrammarIssue[]; input_words: number; engine: string; language?: string; note?: string; }; export async function checkGrammar( text: string, options?: { language?: string; accessToken?: string | null }, ): Promise { const headers: Record = { "Content-Type": "application/json" }; const accessToken = options?.accessToken; if (accessToken) headers.Authorization = `Bearer ${accessToken}`; let res: Response; try { res = await fetch("/v1/grammar", { method: "POST", headers, body: JSON.stringify({ text, language: options?.language || "en-US", }), }); } catch { const err = new Error( "Could not reach the grammar API. Is the ZuZu server running?", ) as ApiError; err.code = "network"; throw err; } if (!res.ok) { let detail: unknown = `Grammar check failed (HTTP ${res.status}).`; try { const data = await res.json(); detail = data.detail ?? detail; } catch { /* non-JSON body — often means the Space/app build is missing /v1/grammar */ if (res.status === 404) { detail = "Grammar API not found (404). Restart/redeploy the app so /v1/grammar is available."; } } const err = new Error(detailMessage(detail, `Grammar check failed (HTTP ${res.status}).`)) as ApiError; err.status = res.status; throw err; } return res.json(); } export async function fetchMe(accessToken?: string | null): Promise<{ auth_enabled: boolean; account: AccountInfo | null; }> { const headers: Record = {}; if (accessToken) headers.Authorization = `Bearer ${accessToken}`; const res = await fetch("/v1/me", { headers }); if (!res.ok) { throw new Error("Could not load account."); } return res.json(); }