Skip to content

Commit eaafee6

Browse files
authored
Merge pull request #92 from vilpessoa/claude/friendly-dijkstra-sxYN5
feat(ia): chamada direta ao Gemini sem proxy serverless
2 parents f72f75a + d145bc6 commit eaafee6

2 files changed

Lines changed: 35 additions & 39 deletions

File tree

.env.example

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# URL pública do proxy serverless que encapsula a Gemini API.
2-
# A GEMINI_API_KEY NUNCA deve ser colocada no frontend — ela vive apenas
3-
# nas variáveis de ambiente do provedor serverless (Vercel/Netlify/Cloudflare).
4-
VITE_GEMINI_PROXY_URL=https://your-proxy.vercel.app/api/gemini
1+
# Chave da API do Google Gemini (obtenha em https://aistudio.google.com/app/apikey)
2+
# Atenção: esta chave fica exposta no bundle do frontend — use apenas para testes
3+
# ou proteja com restrições de domínio no Google AI Studio.
4+
VITE_GEMINI_API_KEY=sua_chave_aqui

src/services/geminiService.ts

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,82 +23,78 @@ const ACTION_CONTEXT: Record<AiAction, string> = {
2323
'Verifique possíveis erros de sintaxe, parênteses órfãos ou vírgulas ausentes e conserte-os.',
2424
};
2525

26+
const MODEL = 'gemini-2.0-flash';
2627
const TIMEOUT_MS = 15_000;
2728

28-
export interface RunAiActionOptions {
29-
action: AiAction;
30-
code: string;
31-
history?: { role: 'user' | 'model'; text: string }[];
32-
signal?: AbortSignal;
33-
}
34-
35-
export async function runAiAction(opts: RunAiActionOptions): Promise<string> {
36-
const { action, code, history, signal: externalSignal } = opts;
29+
export async function runAiAction(opts: { action: AiAction; code: string }): Promise<string> {
30+
const { action, code } = opts;
3731

3832
if (!code.trim()) {
3933
throw { kind: 'empty' } satisfies AiError;
4034
}
4135

42-
const proxyUrl = (import.meta.env.VITE_GEMINI_PROXY_URL as string | undefined) ?? '/api/gemini';
36+
const apiKey = import.meta.env.VITE_GEMINI_API_KEY as string | undefined;
37+
if (!apiKey) {
38+
throw { kind: 'config' } satisfies AiError;
39+
}
40+
41+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${apiKey}`;
4342

4443
const controller = new AbortController();
4544
const timeoutId = window.setTimeout(() => controller.abort(), TIMEOUT_MS);
46-
if (externalSignal) {
47-
if (externalSignal.aborted) controller.abort();
48-
else externalSignal.addEventListener('abort', () => controller.abort(), { once: true });
49-
}
5045

5146
let response: Response;
5247
try {
53-
response = await fetch(proxyUrl, {
48+
response = await fetch(url, {
5449
method: 'POST',
5550
headers: { 'Content-Type': 'application/json' },
5651
body: JSON.stringify({
57-
action,
58-
code,
59-
history: history ?? [],
60-
systemInstruction: promptText,
61-
actionContext: ACTION_CONTEXT[action],
62-
model: 'gemini-2.5-flash',
52+
systemInstruction: { parts: [{ text: promptText }] },
53+
contents: [
54+
{
55+
role: 'user',
56+
parts: [{ text: `${ACTION_CONTEXT[action]}\n\nCódigo:\n${code}` }],
57+
},
58+
],
6359
}),
6460
signal: controller.signal,
6561
});
6662
} catch (err) {
6763
window.clearTimeout(timeoutId);
68-
if ((err as Error)?.name === 'AbortError') {
69-
throw { kind: 'network' } satisfies AiError;
70-
}
7164
throw { kind: 'network' } satisfies AiError;
7265
}
7366
window.clearTimeout(timeoutId);
7467

7568
if (!response.ok) {
7669
const status = response.status;
7770
if (status === 429) {
78-
const scope = response.headers.get('x-quota-scope');
79-
if (scope === 'daily') throw { kind: 'rate_daily' } satisfies AiError;
80-
throw { kind: 'rate_minute' } satisfies AiError;
71+
let errorText = '';
72+
try { errorText = await response.text(); } catch { /* ignore */ }
73+
const scope = /per day|daily/i.test(errorText) ? 'rate_daily' : 'rate_minute';
74+
throw { kind: scope } satisfies AiError;
8175
}
8276
if (status === 401 || status === 403) {
8377
throw { kind: 'auth' } satisfies AiError;
8478
}
8579
let message = `HTTP ${status}`;
8680
try {
8781
const body = await response.json();
88-
if (body?.error) message = String(body.error);
89-
} catch {
90-
/* ignore */
91-
}
82+
if (body?.error?.message) message = String(body.error.message);
83+
} catch { /* ignore */ }
9284
throw { kind: 'unknown', message } satisfies AiError;
9385
}
9486

95-
let payload: { text?: string };
87+
let data: { candidates?: { content?: { parts?: { text?: string }[] } }[] };
9688
try {
97-
payload = await response.json();
89+
data = await response.json();
9890
} catch {
99-
throw { kind: 'unknown', message: 'Resposta inválida do servidor.' } satisfies AiError;
91+
throw { kind: 'unknown', message: 'Resposta inválida da API.' } satisfies AiError;
10092
}
10193

102-
const raw = payload.text ?? '';
94+
const raw =
95+
data?.candidates?.[0]?.content?.parts
96+
?.map((p) => p.text ?? '')
97+
.join('') ?? '';
98+
10399
return stripCodeFences(raw);
104100
}

0 commit comments

Comments
 (0)