feat: integrate Gemini Pro 1.5 with KAM persona for data analysis

This commit is contained in:
Christian Vidal Wolf
2026-02-10 15:15:44 +01:00
parent 96e9583fdb
commit 3f6fdc8d72
2 changed files with 69 additions and 61 deletions
+24 -24
View File
@@ -24,13 +24,13 @@ const ModelMessage: React.FC<{ text: string }> = ({ text }) => {
}; };
const parseBold = (content: string, keyPrefix: string) => { const parseBold = (content: string, keyPrefix: string) => {
const parts = content.split(/(\*\*.*?\*\*)/g); const parts = content.split(/(\*\*.*?\*\*)/g);
return parts.map((part, i) => { return parts.map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) { if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={`${keyPrefix}-${i}`}>{part.slice(2, -2)}</strong>; return <strong key={`${keyPrefix}-${i}`}>{part.slice(2, -2)}</strong>;
} }
return part; return part;
}); });
} }
text.split('\n').forEach((line, index) => { text.split('\n').forEach((line, index) => {
@@ -41,11 +41,11 @@ const ModelMessage: React.FC<{ text: string }> = ({ text }) => {
} else { } else {
flushList(); flushList();
if (line.trim() !== '') { if (line.trim() !== '') {
elements.push( elements.push(
<p key={`p-${index}`} className="my-1"> <p key={`p-${index}`} className="my-1">
{parseBold(line, `p-${index}`)} {parseBold(line, `p-${index}`)}
</p> </p>
); );
} }
} }
}); });
@@ -58,7 +58,7 @@ const ModelMessage: React.FC<{ text: string }> = ({ text }) => {
const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) => { const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) => {
const [messages, setMessages] = useState<ChatMessage[]>([ const [messages, setMessages] = useState<ChatMessage[]>([
{ role: 'model', text: 'Hello! I am your Sales Data Assistant. Ask me anything about the loaded data.', timestamp: new Date() } { role: 'model', text: '¡Hola! Soy tu Estratega y KAM (Key Account Manager) experto. He analizado tus datos actuales de ventas y publicidad. ¿En qué puedo ayudarte a optimizar tu negocio hoy?', timestamp: new Date() }
]); ]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false); const [isTyping, setIsTyping] = useState(false);
@@ -106,14 +106,14 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
<div <div
className={`fixed z-50 bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden className={`fixed z-50 bg-slate-900 border border-border shadow-2xl transition-all duration-300 flex flex-col overflow-hidden
${isOpen ${isOpen
? 'bottom-6 right-6 w-96 h-[600px] rounded-2xl opacity-100 translate-y-0' ? 'bottom-6 right-6 w-96 h-[600px] rounded-2xl opacity-100 translate-y-0'
: 'bottom-6 right-6 w-96 h-0 opacity-0 translate-y-10 pointer-events-none'}`} : 'bottom-6 right-6 w-96 h-0 opacity-0 translate-y-10 pointer-events-none'}`}
> >
{/* Header */} {/* Header */}
<div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur"> <div className="bg-primary/10 p-4 border-b border-border flex justify-between items-center backdrop-blur">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div> <div className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></div>
<h3 className="font-bold text-slate-100">Data Assistant</h3> <h3 className="font-bold text-white tracking-wide">KAM Strategy Expert</h3>
</div> </div>
<button onClick={() => setIsOpen(false)} className="text-slate-400 hover:text-white transition-colors"> <button onClick={() => setIsOpen(false)} className="text-slate-400 hover:text-white transition-colors">
<CloseIcon /> <CloseIcon />
@@ -135,13 +135,13 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
</div> </div>
))} ))}
{isTyping && ( {isTyping && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-slate-800 border border-border rounded-2xl rounded-bl-none p-4 flex gap-1 items-center"> <div className="bg-slate-800 border border-border rounded-2xl rounded-bl-none p-4 flex gap-1 items-center">
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce"></div> <div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce"></div>
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-100"></div> <div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-100"></div>
<div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-200"></div> <div className="w-2 h-2 bg-slate-500 rounded-full animate-bounce delay-200"></div>
</div> </div>
</div> </div>
)} )}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>
@@ -154,7 +154,7 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Ask about trends, totals..." placeholder="Pregunta sobre tendencias, recomendaciones de publicidad..."
className="w-full bg-slate-950 border border-slate-700 text-slate-200 rounded-full py-3 pl-4 pr-12 focus:outline-none focus:border-primary transition-colors placeholder-slate-500 text-sm" className="w-full bg-slate-950 border border-slate-700 text-slate-200 rounded-full py-3 pl-4 pr-12 focus:outline-none focus:border-primary transition-colors placeholder-slate-500 text-sm"
/> />
<button <button
+28 -20
View File
@@ -6,17 +6,25 @@ import { AggregatedData } from "./types";
declare const process: any; declare const process: any;
const SYSTEM_INSTRUCTION = ` const SYSTEM_INSTRUCTION = `
You are a senior data analyst assistant for a retail dashboard called "Craze Analytix". You are an expert Key Account Manager (KAM) and Senior Sales Strategist for "Craze Analytix".
You have access to a detailed report of the currently filtered sales data. Your specialty is analyzing retail data, advertising performance, and market trends to provide high-level strategic recommendations.
The data includes Sell Out (Revenue in €), Units Sold, Product Lines, Customers/Markets, and Seasonality trends.
Your goal is to answer user questions specific to the provided data. 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 "Trends" or "Seasonality", look at the Monthly Seasonality section.
- If asked about "Growth" or "Decline", look at the Top/Bottom Movers sections. - If asked about "Growth" or "Decline", look at the Top/Bottom Movers sections.
- If asked about specific Product Lines, look at the Product Line Breakdown. - When comparing metrics, calculate variances or ratios if relevant (e.g. Sales vs Ads growth).
- Always format numbers clearly (e.g., "€1.2M", "€5,200", "15k units"). - Format your response with clear headers and bullet points for readability.
- When comparing years, calculate the percentage difference if not explicitly provided. - Respond in the same language as the user's question (e.g., if they ask in Spanish, answer in Spanish).
- Keep answers professional, concise, and business-focused.
`; `;
// Helper to get API key safely // Helper to get API key safely
@@ -28,7 +36,7 @@ const getApiKey = (): string | undefined => {
} }
}; };
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`;
export const queryGemini = async ( export const queryGemini = async (
@@ -51,36 +59,36 @@ export const queryGemini = async (
// 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])) // Descending years
.map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.units)}`) .map(([year, data]) => ` - ${year}: ${formatCurrency(data.sellOut)} | ${formatUnits(data.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 // 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 // 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');
// 3. Top Movers (Growth Table) - Limit to Top 10 // 3. Top Movers (Growth Table) - Limit to Top 10
const growthSummary = context.topMovers.slice(0, 10).map(m => const growthSummary = context.topMovers.slice(0, 10).map(m =>
` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)` ` - ${m.line}: +€${m.sellOutGrowthValue.toLocaleString('de-DE')} (${m.sellOutGrowthPercentage.toFixed(1)}%)`
).join('\n'); ).join('\n');
// 4. Declining Movers (Decline Table) - Limit to Top 10 // 4. Declining Movers (Decline Table) - Limit to Top 10
const declineSummary = context.bottomMovers.slice(0, 10).map(m => const declineSummary = context.bottomMovers.slice(0, 10).map(m =>
` - ${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 to save tokens but give depth
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');
// 6. Customer Distribution (Customer Chart) // 6. Customer Distribution (Customer Chart)
const customerSummary = context.byCustomer.map(c => const customerSummary = context.byCustomer.map(c =>
` - ${c.name}: ${formatCurrency(c.value)}` ` - ${c.name}: ${formatCurrency(c.value)}`
).join('\n'); ).join('\n');
const fullReport = ` const fullReport = `
@@ -124,7 +132,7 @@ ${customerSummary}
console.error("Gemini API Error:", error); console.error("Gemini API Error:", error);
if (error.message && error.message.includes("Not implemented on this platform")) { if (error.message && error.message.includes("Not implemented on this platform")) {
return "System Error: The AI SDK detected a platform mismatch."; return "System Error: The AI SDK detected a platform mismatch.";
} }
return `Error: ${error.message || "An unexpected error occurred while analyzing the data."}`; return `Error: ${error.message || "An unexpected error occurred while analyzing the data."}`;