mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 13:15:24 +02:00
fix: move Gemini API calls server-side to fix API key issue on Vercel
This commit is contained in:
@@ -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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-68
@@ -1,47 +1,5 @@
|
|||||||
|
|
||||||
import { GoogleGenAI } from "@google/genai";
|
import { AggregatedData } from '../types';
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCurrency = (val: number) => `€${val.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
|
const formatCurrency = (val: number) => `€${val.toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
|
||||||
const formatUnits = (val: number) => `${val.toLocaleString('de-DE')} units`;
|
const formatUnits = (val: number) => `${val.toLocaleString('de-DE')} units`;
|
||||||
@@ -52,28 +10,18 @@ export const queryGemini = async (
|
|||||||
filteredRecordCount: number
|
filteredRecordCount: number
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
|
|
||||||
const apiKey = getApiKey();
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return "API Key is missing. Please configure your environment variables (API_KEY) or check your .env file.";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ai = new GoogleGenAI({ apiKey });
|
|
||||||
|
|
||||||
// --- CONTEXT GENERATION ---
|
// --- CONTEXT GENERATION ---
|
||||||
// We construct a structured report mirroring the dashboard charts
|
// We construct a structured report mirroring the dashboard charts
|
||||||
|
|
||||||
// 1. Totals by Year (KPI Cards)
|
// 1. Totals by Year (KPI Cards)
|
||||||
const yearlySummary = Object.entries(context.totalsByYear)
|
const yearlySummary = Object.entries(context.totalsByYear)
|
||||||
.sort((a, b) => parseInt(b[0]) - parseInt(a[0])) // Descending years
|
.sort((a, b) => parseInt(b[0]) - parseInt(a[0]))
|
||||||
.map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.units)}`)
|
.map(([year, data]) => ` - ${year}: ${formatCurrency((data as any).sellOut)} | ${formatUnits((data as any).units)}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
// 2. Seasonality (Line Chart Data)
|
// 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 => {
|
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(', ');
|
const yearValues = context.availableYears.map(y => `${y}: ${formatCurrency(p[y] as number || 0)}`).join(', ');
|
||||||
return ` - ${p.name}: [${yearValues}]`;
|
return ` - ${p.name}: [${yearValues}]`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
@@ -88,7 +36,7 @@ export const queryGemini = async (
|
|||||||
` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
|
` - ${m.line}: -€${Math.abs(m.sellOutGrowthValue).toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
|
||||||
).join('\n');
|
).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) =>
|
const topLinesSummary = context.byLine.slice(0, 50).map((l, i) =>
|
||||||
` ${i + 1}. ${l.name}: ${formatCurrency(l.value)} | ${formatUnits(l.units)}`
|
` ${i + 1}. ${l.name}: ${formatCurrency(l.value)} | ${formatUnits(l.units)}`
|
||||||
).join('\n');
|
).join('\n');
|
||||||
@@ -126,22 +74,22 @@ PERFORMANCE BY CUSTOMER:
|
|||||||
${customerSummary}
|
${customerSummary}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const response = await ai.models.generateContent({
|
// Call our serverless API endpoint (key is safe server-side)
|
||||||
model: 'gemini-2.5-flash',
|
const response = await fetch('/api/ask-gemini', {
|
||||||
contents: `Context Data:\n${fullReport}\n\nUser Question: ${question}`,
|
method: 'POST',
|
||||||
config: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
systemInstruction: SYSTEM_INSTRUCTION,
|
body: JSON.stringify({ question, context: fullReport })
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.text || "I couldn't generate a response based on the data provided.";
|
if (!response.ok) {
|
||||||
} catch (error: any) {
|
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||||
console.error("Gemini API Error:", error);
|
throw new Error(errorData.error || `Server error: ${response.status}`);
|
||||||
|
|
||||||
if (error.message && error.message.includes("Not implemented on this platform")) {
|
|
||||||
return "System Error: The AI SDK detected a platform mismatch.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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."}`;
|
return `Error: ${error.message || "An unexpected error occurred while analyzing the data."}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user