File size: 4,587 Bytes
1103f3b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c212805
 
1103f3b
 
 
 
b387e01
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1103f3b
 
 
b387e01
1103f3b
 
4c5fda9
 
b387e01
 
 
4c5fda9
 
 
b387e01
 
 
 
 
 
 
 
 
 
3b361f1
b387e01
 
 
 
 
 
1103f3b
 
b387e01
1103f3b
 
 
b387e01
1103f3b
 
b387e01
1103f3b
 
 
4c5fda9
 
 
 
 
 
b387e01
 
 
 
2d0fe75
 
 
 
 
 
 
 
 
 
 
 
 
7063659
2d0fe75
 
 
 
 
7063659
2d0fe75
 
7063659
2d0fe75
 
fe5d482
 
 
 
 
7063659
 
 
 
fe5d482
 
 
 
 
 
 
 
 
2d0fe75
fe5d482
2d0fe75
 
 
 
fe5d482
 
 
 
 
2d0fe75
fe5d482
2d0fe75
 
 
 
 
 
4c5fda9
b387e01
 
 
4c5fda9
 
 
b387e01
 
1103f3b
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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<StrengthLabel, number> = {
  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<RewriteResponse> {
  const headers: Record<string, string> = { "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<GrammarResponse> {
  const headers: Record<string, string> = { "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<string, string> = {};
  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();
}