mirror of
https://github.com/christianvidalwolf-prog/CrazeAnalytix.git
synced 2026-08-03 18:05:23 +02:00
feat: integrate Gemini Pro 1.5 with KAM persona for data analysis
This commit is contained in:
+32
-32
@@ -22,15 +22,15 @@ const ModelMessage: React.FC<{ text: string }> = ({ text }) => {
|
|||||||
listItems = [];
|
listItems = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
||||||
@@ -79,7 +79,7 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
|
|||||||
setIsTyping(true);
|
setIsTyping(true);
|
||||||
|
|
||||||
const responseText = await onSendMessage(userMsg.text);
|
const responseText = await onSendMessage(userMsg.text);
|
||||||
|
|
||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
setMessages(prev => [...prev, { role: 'model', text: responseText, timestamp: new Date() }]);
|
setMessages(prev => [...prev, { role: 'model', text: responseText, timestamp: new Date() }]);
|
||||||
};
|
};
|
||||||
@@ -103,17 +103,17 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Chat Window */}
|
{/* Chat Window */}
|
||||||
<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 />
|
||||||
@@ -124,10 +124,10 @@ const AIChat: React.FC<AIChatProps> = ({ onSendMessage, isOpen, setIsOpen }) =>
|
|||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-950/50">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-950/50">
|
||||||
{messages.map((msg, idx) => (
|
{messages.map((msg, idx) => (
|
||||||
<div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
<div key={idx} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||||
<div
|
<div
|
||||||
className={`max-w-[85%] rounded-2xl p-3 text-sm leading-relaxed shadow-sm
|
className={`max-w-[85%] rounded-2xl p-3 text-sm leading-relaxed shadow-sm
|
||||||
${msg.role === 'user'
|
${msg.role === 'user'
|
||||||
? 'bg-primary text-white rounded-br-none'
|
? 'bg-primary text-white rounded-br-none'
|
||||||
: 'bg-slate-800 text-slate-200 border border-border rounded-bl-none'}`}
|
: 'bg-slate-800 text-slate-200 border border-border rounded-bl-none'}`}
|
||||||
>
|
>
|
||||||
{msg.role === 'model' ? <ModelMessage text={msg.text} /> : msg.text}
|
{msg.role === 'model' ? <ModelMessage text={msg.text} /> : msg.text}
|
||||||
@@ -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,10 +154,10 @@ 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
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={!input.trim() || isTyping}
|
disabled={!input.trim() || isTyping}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-primary text-white rounded-full hover:bg-indigo-400 disabled:opacity-50 transition-colors"
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-2 bg-primary text-white rounded-full hover:bg-indigo-400 disabled:opacity-50 transition-colors"
|
||||||
|
|||||||
+37
-29
@@ -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,15 +36,15 @@ 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 (
|
||||||
question: string,
|
question: string,
|
||||||
context: AggregatedData,
|
context: AggregatedData,
|
||||||
filteredRecordCount: number
|
filteredRecordCount: number
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
|
|
||||||
const apiKey = getApiKey();
|
const apiKey = getApiKey();
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
@@ -48,39 +56,39 @@ export const queryGemini = async (
|
|||||||
|
|
||||||
// --- 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])) // 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 = `
|
||||||
@@ -122,9 +130,9 @@ ${customerSummary}
|
|||||||
return response.text || "I couldn't generate a response based on the data provided.";
|
return response.text || "I couldn't generate a response based on the data provided.";
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
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."}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user