From 1bea36aa7421432c1dc887ed30e6712bd9e7d781 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Tue, 10 Feb 2026 15:35:10 +0100 Subject: [PATCH] fix: move Gemini API calls server-side to fix API key issue on Vercel --- api/ask-gemini.ts | 83 ++++++++++++++++++++++++++++++++++++++ services/geminiService.ts | 84 ++++++++------------------------------- 2 files changed, 99 insertions(+), 68 deletions(-) create mode 100644 api/ask-gemini.ts diff --git a/api/ask-gemini.ts b/api/ask-gemini.ts new file mode 100644 index 0000000..d6dae16 --- /dev/null +++ b/api/ask-gemini.ts @@ -0,0 +1,83 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +const SYSTEM_INSTRUCTION = ` +You are an expert Key Account Manager (KAM) and Senior Sales Strategist for "Craze Analytix". +Your specialty is analyzing retail data, advertising performance, and market trends to provide high-level strategic recommendations. + +You have access to a detailed report of the currently filtered sales and advertising data. +The data includes Sell Out (€), Units Sold, Product Lines, Customer Markets, Seasonality (Monthly Trends), and Growth/Decline metrics. + +Your objective: +1. **Analyze**: Deeply study the provided data according to the user's specific query. +2. **Opinion**: Provide expert opinions on the health of the business, product performance, or market position. +3. **Recommendations**: Give actionable, professional advice to improve sales, ROAS, stock health, or market share. + +Guidelines: +- Maintain a professional, consultative, and business-driven tone (KAM style). +- Always justify your recommendations with specific numbers from the data. +- If asked about "Trends" or "Seasonality", look at the Monthly Seasonality section. +- If asked about "Growth" or "Decline", look at the Top/Bottom Movers sections. +- When comparing metrics, calculate variances or ratios if relevant (e.g. Sales vs Ads growth). +- Format your response with clear headers and bullet points for readability. +- Respond in the same language as the user's question (e.g., if they ask in Spanish, answer in Spanish). +`; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + // CORS headers + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + return res.status(200).end(); + } + + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const apiKey = process.env.GEMINI_API_KEY || process.env.API_KEY; + + if (!apiKey) { + console.error('[ask-gemini] GEMINI_API_KEY is not set in environment variables'); + return res.status(500).json({ error: 'API Key is not configured on the server. Please add GEMINI_API_KEY to Vercel Environment Variables.' }); + } + + try { + const { question, context } = req.body; + + if (!question || !context) { + return res.status(400).json({ error: 'Missing question or context in request body' }); + } + + // Call Gemini API directly via REST (no SDK needed server-side) + const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`; + + const response = await fetch(geminiUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + system_instruction: { + parts: [{ text: SYSTEM_INSTRUCTION }] + }, + contents: [{ + parts: [{ text: `Context Data:\n${context}\n\nUser Question: ${question}` }] + }] + }) + }); + + if (!response.ok) { + const errorData = await response.text(); + console.error('[ask-gemini] Gemini API error:', response.status, errorData); + throw new Error(`Gemini API error: ${response.status}`); + } + + const data = await response.json(); + const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || "I couldn't generate a response."; + + res.status(200).json({ response: text }); + } catch (error: any) { + console.error('[ask-gemini] Error:', error); + res.status(500).json({ error: error.message || 'An unexpected error occurred' }); + } +} diff --git a/services/geminiService.ts b/services/geminiService.ts index 5456e8e..e6eb3cf 100644 --- a/services/geminiService.ts +++ b/services/geminiService.ts @@ -1,47 +1,5 @@ -import { GoogleGenAI } from "@google/genai"; -import { AggregatedData } from "./types"; - -// Declare process to avoid TypeScript errors without causing aggressive bundler shims -declare const process: any; - -const SYSTEM_INSTRUCTION = ` -You are an expert Key Account Manager (KAM) and Senior Sales Strategist for "Craze Analytix". -Your specialty is analyzing retail data, advertising performance, and market trends to provide high-level strategic recommendations. - -You have access to a detailed report of the currently filtered sales and advertising data. -The data includes Sell Out (€), Units Sold, Product Lines, Customer Markets, Seasonality (Monthly Trends), and Growth/Decline metrics. - -Your objective: -1. **Analyze**: Deeply study the provided data according to the user's specific query. -2. **Opinion**: Provide expert opinions on the health of the business, product performance, or market position. -3. **Recommendations**: Give actionable, professional advice to improve sales, ROAS, stock health, or market share. - -Guidelines: -- Maintain a professional, consultative, and business-driven tone (KAM style). -- Always justify your recommendations with specific numbers from the data. -- If asked about "Trends" or "Seasonality", look at the Monthly Seasonality section. -- If asked about "Growth" or "Decline", look at the Top/Bottom Movers sections. -- When comparing metrics, calculate variances or ratios if relevant (e.g. Sales vs Ads growth). -- Format your response with clear headers and bullet points for readability. -- Respond in the same language as the user's question (e.g., if they ask in Spanish, answer in Spanish). -`; - -// Helper to get API key safely -const getApiKey = (): string | undefined => { - // Try different sources (Vite defines these at build time) - const key = - (typeof process !== 'undefined' && process.env ? process.env.API_KEY || process.env.GEMINI_API_KEY : undefined) || - (import.meta as any).env?.VITE_GEMINI_API_KEY || - (import.meta as any).env?.API_KEY || - (import.meta as any).env?.GEMINI_API_KEY; - - // Ensure it's not a literal "undefined" string from a failed build injection - if (key === "undefined" || key === "null" || !key) { - return undefined; - } - return key; -}; +import { AggregatedData } from '../types'; const formatCurrency = (val: number) => `€${val.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; const formatUnits = (val: number) => `${val.toLocaleString('de-DE')} units`; @@ -52,28 +10,18 @@ export const queryGemini = async ( filteredRecordCount: number ): Promise => { - const apiKey = getApiKey(); - - if (!apiKey) { - return "API Key is missing. Please configure your environment variables (API_KEY) or check your .env file."; - } - try { - const ai = new GoogleGenAI({ apiKey }); - // --- CONTEXT GENERATION --- // We construct a structured report mirroring the dashboard charts // 1. Totals by Year (KPI Cards) const yearlySummary = Object.entries(context.totalsByYear) - .sort((a, b) => parseInt(b[0]) - parseInt(a[0])) // Descending years - .map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.units)}`) + .sort((a, b) => parseInt(b[0]) - parseInt(a[0])) + .map(([year, data]) => ` - ${year}: ${formatCurrency((data as any).sellOut)} | ${formatUnits((data as any).units)}`) .join('\n'); // 2. Seasonality (Line Chart Data) - // We simplify this to a CSV-like list for the AI to parse trends const seasonalitySummary = context.seasonality.map(p => { - // Extract values for each year in the point const yearValues = context.availableYears.map(y => `${y}: ${formatCurrency(p[y] as number || 0)}`).join(', '); return ` - ${p.name}: [${yearValues}]`; }).join('\n'); @@ -88,7 +36,7 @@ export const queryGemini = async ( ` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)` ).join('\n'); - // 5. Product Lines Overview (Bar Charts) - Limit to Top 50 to save tokens but give depth + // 5. Product Lines Overview (Bar Charts) - Limit to Top 50 const topLinesSummary = context.byLine.slice(0, 50).map((l, i) => ` ${i + 1}. ${l.name}: ${formatCurrency(l.value)} | ${formatUnits(l.units)}` ).join('\n'); @@ -126,22 +74,22 @@ PERFORMANCE BY CUSTOMER: ${customerSummary} `; - const response = await ai.models.generateContent({ - model: 'gemini-2.5-flash', - contents: `Context Data:\n${fullReport}\n\nUser Question: ${question}`, - config: { - systemInstruction: SYSTEM_INSTRUCTION, - } + // Call our serverless API endpoint (key is safe server-side) + const response = await fetch('/api/ask-gemini', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ question, context: fullReport }) }); - return response.text || "I couldn't generate a response based on the data provided."; - } catch (error: any) { - console.error("Gemini API Error:", error); - - if (error.message && error.message.includes("Not implemented on this platform")) { - return "System Error: The AI SDK detected a platform mismatch."; + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unknown error' })); + throw new Error(errorData.error || `Server error: ${response.status}`); } + const data = await response.json(); + return data.response || "I couldn't generate a response based on the data provided."; + } catch (error: any) { + console.error("Gemini API Error:", error); return `Error: ${error.message || "An unexpected error occurred while analyzing the data."}`; } };