stock / src /components /transactions /AITransactionChat.tsx
Zelyanoth's picture
Upload 101 files
24d40b9 verified
import { useState, useRef, useEffect } from "react";
import { Send, Plus, ArrowRight, AlertTriangle } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Avatar } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { motion } from "framer-motion";
import { toast } from "sonner";
import { useNavigate } from "react-router-dom";
import database from "@/services/database";
type Message = {
id: number;
type: 'user' | 'bot';
text: string;
timestamp: string;
};
type TransactionData = {
amount?: number;
category?: string;
description?: string;
date?: string;
type?: 'income' | 'expense';
quantity?: number;
product?: string;
};
const AITransactionChat = () => {
const [messages, setMessages] = useState<Message[]>([
{
id: 1,
type: 'bot',
text: "Bienvenue! Je suis votre assistant de gestion. Vous pouvez me parler de vos transactions ou me demander des rapports sur vos stocks. Par exemple: 'Vendu 5 sacs de riz à 2000 FCFA chacun' ou 'Quels produits sont en rupture de stock?'",
timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
}
]);
const [input, setInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false);
const [extractedData, setExtractedData] = useState<TransactionData | null>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
// Auto-scroll to bottom when messages change
useEffect(() => {
if (scrollAreaRef.current) {
const scrollContainer = scrollAreaRef.current.querySelector('[data-radix-scroll-area-viewport]');
if (scrollContainer) {
scrollContainer.scrollTop = scrollContainer.scrollHeight;
}
}
}, [messages]);
const handleSendMessage = () => {
if (!input.trim()) return;
const userMessage = {
id: messages.length + 1,
type: 'user' as const,
text: input,
timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsProcessing(true);
// Simulate AI processing - in a real app, this would be an API call
setTimeout(() => {
processUserInput(userMessage.text);
}, 1000);
};
const processUserInput = (text: string) => {
// Enhanced NLP-like processing for both French and English inputs
// This is a simple simulation of NLP capabilities
const lowerText = text.toLowerCase();
// Check if this is about stock management or reports
if (
lowerText.includes('stock') ||
lowerText.includes('inventaire') ||
lowerText.includes('rupture') ||
lowerText.includes('rapport') ||
lowerText.includes('report')
) {
handleStockQuery(text);
return;
}
// Sales/expense transaction processing
const moneyRegex = /(\d+(?:\.\d{1,2})?)\s*(?:fcfa|cfa|\$|€)?/i;
const moneyMatch = lowerText.match(moneyRegex);
const amount = moneyMatch ? parseFloat(moneyMatch[1]) : undefined;
const quantityRegex = /(\d+)\s*(sac|sachet|boîte|kg|pièce|piece|carton|bouteille|paquet|article)/i;
const quantityMatch = lowerText.match(quantityRegex);
const quantity = quantityMatch ? parseInt(quantityMatch[1]) : 1;
const productRegex = /(riz|lait|sucre|huile|farine|café|cafe|thé|the|eau|savon|pâtes|pates|tomate|oignon|poisson|viande|poulet|œuf|oeuf)/i;
const productMatch = lowerText.match(productRegex);
const product = productMatch ? productMatch[1] : undefined;
const isSale =
lowerText.includes('vend') ||
lowerText.includes('vendu') ||
lowerText.includes('vendr') ||
lowerText.includes('sold') ||
lowerText.includes('sale');
const isPurchase =
lowerText.includes('achet') ||
lowerText.includes('acheter') ||
lowerText.includes('acheté') ||
lowerText.includes('bought') ||
lowerText.includes('purchas');
const data: TransactionData = {
amount: amount,
type: isSale ? 'income' : isPurchase ? 'expense' : undefined,
date: new Date().toISOString().split('T')[0],
product: product,
quantity: quantity,
description: product ? `${isSale ? 'Vente' : 'Achat'} de ${product}` : text.substring(0, 50),
category: product ? 'Marchandise' : 'Autre'
};
setExtractedData(data);
// Generate response based on extracted data
let response: string;
if (amount && (isSale || isPurchase) && product) {
const action = isSale ? 'vente' : 'achat';
response = `J'ai compris que vous avez ${isSale ? 'vendu' : 'acheté'} `;
if (quantity > 1) {
response += `${quantity} unités de ${product}`;
} else {
response += `du ${product}`;
}
if (amount) {
response += ` pour ${amount} FCFA`;
}
response += `. Voulez-vous enregistrer cette ${action} ?`;
} else if (amount) {
response = `J'ai détecté un montant de ${amount} FCFA. Pouvez-vous préciser s'il s'agit d'une vente ou d'un achat, et pour quel produit ?`;
} else if (product) {
response = `J'ai identifié le produit: ${product}. Pouvez-vous préciser la quantité et le montant de la transaction ?`;
} else {
response = `Je n'ai pas bien compris votre transaction. Pouvez-vous préciser le produit, la quantité et le montant ? Par exemple: "Vendu 5 sacs de riz à 2000 FCFA chacun"`;
}
const botMessage = {
id: messages.length + 2,
type: 'bot' as const,
text: response,
timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
};
setMessages(prev => [...prev, botMessage]);
setIsProcessing(false);
};
const handleStockQuery = (text: string) => {
const lowerText = text.toLowerCase();
let response: string;
// Check if it's a low stock query
if (
lowerText.includes('rupture') ||
lowerText.includes('faible') ||
lowerText.includes('low') ||
lowerText.includes('alerte')
) {
response = "D'après mon analyse, les produits suivants sont en stock faible : Riz (5 sacs), Lait (3 cartons), Huile (2 bidons). Je recommande de vous réapprovisionner bientôt.";
}
// Check if it's a sales report query
else if (
lowerText.includes('rapport') ||
lowerText.includes('report') ||
lowerText.includes('ventes') ||
lowerText.includes('sales')
) {
response = "Voici un résumé de vos ventes: Cette semaine, vous avez vendu pour 125,000 FCFA de marchandises, soit une augmentation de 12% par rapport à la semaine précédente. Vos produits les plus vendus sont le riz (45%), le lait (30%) et l'huile (15%).";
}
// Check if it's about price recommendations
else if (
lowerText.includes('prix') ||
lowerText.includes('price') ||
lowerText.includes('tarif') ||
lowerText.includes('recommand')
) {
response = "Basé sur les tendances actuelles du marché, je vous recommande d'augmenter le prix du riz de 5% (de 10,000 à 10,500 FCFA par sac) et de maintenir le prix des autres produits. Il y a une forte demande pour le riz cette semaine.";
}
// General stock query
else {
response = "Votre inventaire actuel comprend: Riz (25 sacs), Lait (18 cartons), Sucre (30 kg), Huile (12 bidons), Savon (40 pièces). Voulez-vous des informations sur un produit spécifique ?";
}
const botMessage = {
id: messages.length + 2,
type: 'bot' as const,
text: response,
timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
};
setMessages(prev => [...prev, botMessage]);
setIsProcessing(false);
// If it's a report request, offer to navigate to reports page
if (lowerText.includes('rapport') || lowerText.includes('report')) {
setTimeout(() => {
const reportMessage = {
id: messages.length + 3,
type: 'bot' as const,
text: "Voulez-vous voir des rapports plus détaillés? Je peux vous montrer la page des rapports.",
timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
};
setMessages(prev => [...prev, reportMessage]);
}, 1000);
}
};
const formatDateFromTerm = (term: string): string => {
const today = new Date();
switch(term.toLowerCase()) {
case 'yesterday':
case 'hier':
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
return yesterday.toISOString().split('T')[0];
case 'last week':
case 'semaine dernière':
const lastWeek = new Date(today);
lastWeek.setDate(lastWeek.getDate() - 7);
return lastWeek.toISOString().split('T')[0];
case 'this morning':
case 'ce matin':
case 'today':
case 'aujourd\'hui':
default:
return today.toISOString().split('T')[0];
}
};
const handleAddTransaction = () => {
if (extractedData?.amount) {
toast.success('Transaction ajoutée avec succès!');
// If it's a sale and we have product info, show stock alert if needed
if (extractedData.type === 'income' && extractedData.product && extractedData.quantity && extractedData.quantity > 3) {
setTimeout(() => {
const alertMessage = {
id: messages.length + 4,
type: 'bot' as const,
text: `⚠️ Alerte: Après cette vente, votre stock de ${extractedData.product} est faible. Il ne reste que ${Math.floor(Math.random() * 5) + 1} unités. Pensez à vous réapprovisionner.`,
timestamp: new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})
};
setMessages(prev => [...prev, alertMessage]);
}, 1500);
}
setTimeout(() => navigate('/transactions'), 2500);
} else {
toast.error('Veuillez fournir un montant valide');
}
};
return (
<div className="flex flex-col h-[calc(100vh-220px)] bg-white dark:bg-violet-darker rounded-lg border border-border overflow-hidden shadow-md">
<div className="p-4 border-b border-border bg-muted/30 dark:bg-violet-dark/30">
<h3 className="font-medium text-center">
Assistant IA de Gestion
</h3>
</div>
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
<div className="space-y-4 pb-4">
{messages.map((message) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className={cn(
"flex",
message.type === "user" ? "justify-end" : "justify-start"
)}
>
{message.type === "bot" && (
<Avatar className="h-8 w-8 mr-2 bg-primary text-white">
<span>AI</span>
</Avatar>
)}
<div
className={cn(
"max-w-[80%] p-3 rounded-xl",
message.type === "user"
? "bg-primary text-primary-foreground rounded-tr-none"
: "bg-muted dark:bg-violet-muted/30 rounded-tl-none"
)}
>
<p className="text-sm">{message.text}</p>
<span className={cn(
"text-xs mt-1 block opacity-70",
message.type === "user" ? "text-right" : ""
)}>
{message.timestamp}
</span>
</div>
</motion.div>
))}
{isProcessing && (
<div className="flex justify-start">
<Avatar className="h-8 w-8 mr-2 bg-primary text-white">
<span>AI</span>
</Avatar>
<div className="bg-muted dark:bg-violet-muted/30 p-3 rounded-xl rounded-tl-none">
<span className="flex items-center space-x-1">
<span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '0ms'}}></span>
<span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '150ms'}}></span>
<span className="w-2 h-2 bg-primary dark:bg-violet rounded-full animate-bounce" style={{animationDelay: '300ms'}}></span>
</span>
</div>
</div>
)}
</div>
</ScrollArea>
{extractedData?.amount && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
className="p-4 border-t border-border bg-muted/30 dark:bg-violet-dark/10"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">Transaction prête à ajouter:</p>
<p className="text-sm">
{extractedData.amount} FCFA - {extractedData.product || extractedData.category || 'Non catégorisé'} ({extractedData.type === 'income' ? 'Vente' : 'Achat'})
{extractedData.quantity && extractedData.quantity > 1 ? ` × ${extractedData.quantity}` : ''}
</p>
</div>
<Button size="sm" onClick={handleAddTransaction} className="gap-1">
<Plus size={16} /> Ajouter <ArrowRight size={14} />
</Button>
</div>
</motion.div>
)}
<div className="p-3 border-t border-border flex items-center gap-2 bg-card/50 dark:bg-violet-darker">
<Input
placeholder="Décrivez votre transaction ou demandez des informations..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSendMessage();
}
}}
className="flex-1 bg-background dark:bg-violet-darker border-slate-200 dark:border-violet-muted/30"
/>
<Button
size="icon"
className="bg-primary hover:bg-primary/90 dark:bg-violet dark:hover:bg-violet-dark"
onClick={handleSendMessage}
>
<Send size={18} />
</Button>
</div>
</div>
);
};
export default AITransactionChat;