first commit

This commit is contained in:
2026-07-17 21:48:37 +05:30
commit f85599dac5
124 changed files with 10775 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Server-only Gemini client. Powers the AI assistant + translation features.
*
* SECURITY: GEMINI_API_KEY is read from the server environment and NEVER exposed
* to the client bundle (no NEXT_PUBLIC_ prefix). All calls go through BFF routes
* so the key stays on the server. This module must only be imported by route
* handlers / server code.
*/
const ENDPOINT = "https://generativelanguage.googleapis.com/v1beta/models";
export type GeminiResult =
| { ok: true; text: string }
| { ok: false; status: number; error: string; retryable: boolean };
/**
* Single-shot text generation. Returns a discriminated result so callers can
* surface a graceful message (e.g. on 429 quota-exhausted) instead of throwing.
*/
export async function geminiGenerate(prompt: string, opts?: { system?: string; temperature?: number }): Promise<GeminiResult> {
const key = process.env.GEMINI_API_KEY;
const model = process.env.GEMINI_MODEL || "gemini-2.0-flash";
if (!key) {
return { ok: false, status: 0, error: "GEMINI_API_KEY is not configured on the server.", retryable: false };
}
const body: Record<string, unknown> = {
contents: [{ role: "user", parts: [{ text: prompt }] }],
generationConfig: { temperature: opts?.temperature ?? 0.4 },
};
if (opts?.system) {
body.systemInstruction = { parts: [{ text: opts.system }] };
}
let res: Response;
try {
res = await fetch(`${ENDPOINT}/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(key)}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
cache: "no-store",
});
} catch (e) {
return { ok: false, status: 0, error: `Could not reach Gemini: ${(e as Error).message}`, retryable: true };
}
if (!res.ok) {
const detail = await res.text().catch(() => "");
const retryable = res.status === 429 || res.status >= 500;
let msg = `Gemini error ${res.status}`;
if (res.status === 429) msg = "The AI is rate-limited right now (quota exhausted). Please try again shortly.";
else if (res.status === 403) msg = "The AI request was rejected (check the API key / permissions).";
return { ok: false, status: res.status, error: msg, retryable };
}
const json = await res.json().catch(() => null);
const text: string | undefined = json?.candidates?.[0]?.content?.parts?.map((p: any) => p?.text).filter(Boolean).join("\n");
if (!text) {
// Blocked (safety) or empty candidate.
const reason = json?.candidates?.[0]?.finishReason || json?.promptFeedback?.blockReason;
return { ok: false, status: 200, error: reason ? `The AI returned no text (${reason}).` : "The AI returned an empty response.", retryable: false };
}
return { ok: true, text: text.trim() };
}