solomdev-chatbot
Version:
Tigerbase Chatbot Client for Product Management
402 lines (401 loc) • 26.3 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useEffect, useRef } from "react";
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
import { toast } from "react-toastify";
import { X, Maximize2, Minimize2, Send, RefreshCw, ShoppingCart, Trash2, Bot, User, Sparkles, } from "lucide-react";
import { Button } from "./components/ui/button";
import { Input } from "./components/ui/input";
import { Card, CardContent, CardHeader } from "./components/ui/card";
import { Badge } from "./components/ui/badge";
import { ScrollArea } from "./components/ui/scroll-area";
import { getConfig } from "./config";
const ChatbotPopup = () => {
const cfg = getConfig();
const MCP_URL = cfg.apiURL;
const genAIKey = cfg.aiKey;
const defaultLang = cfg.defaultLang || "ar";
if (!genAIKey) {
toast.error(defaultLang === "ar"
? "مفتاح Google GenAI غير موجود"
: "Google GenAI key not set");
throw new Error("Google GenAI key not set");
}
const genAI = new GoogleGenerativeAI(genAIKey);
const systemInstruction = `You are a helpful assistant for product management. Always respond in ${defaultLang === "ar" ? "Arabic" : "English"}. ${cfg.customPrompt || ""}
When user asks for product information with an ID, use the get-product-info tool and pass the ID in the arguments.
When user asks to list products, use the list-products tool.
When user asks to add a product, use the add-product tool with name, price, and description.`;
const model = genAI.getGenerativeModel({
model: "gemini-2.5-flash",
systemInstruction,
});
const [isOpen, setIsOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [messages, setMessages] = useState([
{
role: "model",
content: defaultLang === "ar"
? "أهلاً وسهلاً! أنا هنا لمساعدتك في إدارة المنتجات. كيف يمكنني مساعدتك؟"
: "Hello! I'm here to help with product management. How can I assist you?",
timestamp: new Date().toLocaleTimeString(defaultLang === "ar" ? "ar-EG" : "en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
}),
},
]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);
// Auto delete chats
useEffect(() => {
if (cfg.autoDeleteChats) {
const timer = setTimeout(() => {
setMessages([
{
role: "model",
content: defaultLang === "ar"
? "أهلاً وسهلاً! أنا هنا لمساعدتك في إدارة المنتجات. كيف يمكنني مساعدتك؟"
: "Hello! I'm here to help with product management. How can I assist you?",
timestamp: new Date().toLocaleTimeString(defaultLang === "ar" ? "ar-EG" : "en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
}),
},
]);
toast.info(defaultLang === "ar"
? "تم مسح المحادثة تلقائيًا"
: "Chat cleared automatically");
}, cfg.autoDeleteChats * 60 * 1000);
return () => clearTimeout(timer);
}
}, [messages, cfg.autoDeleteChats, defaultLang]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const formatTimestamp = () => {
return new Date().toLocaleTimeString(defaultLang === "ar" ? "ar-EG" : "en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
});
};
const handleMcpToolCall = async (toolName, args, retries = 2) => {
if (!cfg.validatedTools.includes(toolName)) {
throw new Error(defaultLang === "ar"
? `الأداة ${toolName} غير متاحة لهذا السيريال`
: `Tool ${toolName} not available for this serial`);
}
for (let i = 0; i <= retries; i++) {
try {
const methodName = "tools/call";
const response = await fetch(MCP_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
},
body: JSON.stringify({
jsonrpc: "2.0",
method: methodName,
params: {
name: toolName,
arguments: args,
_meta: { progressToken: 0 },
},
id: Date.now(),
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(`فشل استدعاء ${toolName}: ${data.error.message}`);
}
if (data.result &&
data.result.content &&
data.result.content[0]?.text) {
return JSON.parse(data.result.content[0].text);
}
return (data.result ||
(defaultLang === "ar"
? "تم تنفيذ الأمر بنجاح"
: "Command executed successfully"));
}
catch (e) {
if (i < retries) {
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
continue;
}
throw new Error(defaultLang === "ar"
? `فشل استدعاء ${toolName} بعد ${retries + 1} محاولة: ${e.message}`
: `Failed to call ${toolName} after ${retries + 1} attempts: ${e.message}`);
}
}
};
const handleSend = async (retryCount = 3) => {
if (!input.trim())
return;
const userMessage = {
role: "user",
content: input,
timestamp: formatTimestamp(),
};
setMessages((prev) => [...prev, userMessage]);
const userInput = input;
setInput("");
setLoading(true);
try {
const conversationHistory = messages
.map((m) => ({
role: m.role,
parts: [
{
text: typeof m.content === "string"
? m.content
: JSON.stringify(m.content),
},
],
}))
.concat({
role: "user",
parts: [{ text: userInput }],
});
const result = await model.generateContent({
contents: conversationHistory,
generationConfig: {
maxOutputTokens: 1024,
temperature: 0.7,
},
tools: [
{
functionDeclarations: [
{
name: "mcp_tool",
description: "Call MCP server tools for product management",
parameters: {
type: SchemaType.OBJECT,
properties: {
tool: {
type: SchemaType.STRING,
format: "enum",
enum: cfg.validatedTools,
description: "The MCP tool to call",
},
arguments: {
type: SchemaType.OBJECT,
properties: {
id: {
type: SchemaType.STRING,
description: "Product ID for get-product-info tool",
},
name: {
type: SchemaType.STRING,
description: "Product name for add-product tool",
},
price: {
type: SchemaType.NUMBER,
description: "Product price for add-product tool",
},
description: {
type: SchemaType.STRING,
description: "Product description for add-product tool",
},
},
description: "Arguments to pass to the selected tool",
},
},
required: ["tool"],
},
},
],
},
],
});
let assistantReply = defaultLang === "ar"
? "عذرًا، لم أفهم الطلب."
: "Sorry, I didn't understand the request.";
const response = await result.response;
const candidate = response.candidates?.[0];
if (candidate?.content?.parts) {
const parts = candidate.content.parts;
const functionCallPart = parts.find((part) => part.functionCall);
if (functionCallPart?.functionCall) {
const { name, args } = functionCallPart.functionCall;
if (name === "mcp_tool") {
try {
const functionArgs = args;
const toolResult = await handleMcpToolCall(functionArgs.tool || functionArgs.name || "list-products", functionArgs.arguments || functionArgs.params || {});
assistantReply = {
products: Array.isArray(toolResult) ? toolResult : [toolResult],
};
toast.success(defaultLang === "ar"
? "تم جلب البيانات بنجاح!"
: "Data fetched successfully!");
}
catch (e) {
assistantReply =
defaultLang === "ar"
? `عذرًا، حدث خطأ أثناء تنفيذ الأمر: ${e.message}`
: `Sorry, an error occurred while executing the command: ${e.message}`;
toast.error(e.message);
}
}
}
else {
const textParts = parts
.filter((part) => typeof part.text === "string")
.map((part) => part.text)
.join("\n");
assistantReply =
textParts ||
(defaultLang === "ar" ? "لم أتمكن من الرد." : "Couldn't respond.");
}
}
setMessages((prev) => [
...prev,
{
role: "model",
content: assistantReply,
timestamp: formatTimestamp(),
},
]);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "";
const isOverloaded = errorMessage.includes("overloaded") || errorMessage.includes("503");
const waitTime = isOverloaded ? 5000 : 1000;
if (retryCount > 0) {
toast.warn(defaultLang === "ar"
? `فشل الطلب، جاري إعادة المحاولة... (${retryCount} محاولة متبقية)`
: `Request failed, retrying... (${retryCount} attempts left)`);
setTimeout(() => handleSend(retryCount - 1), waitTime);
return;
}
const errorMsg = errorMessage ||
(defaultLang === "ar" ? "حدث خطأ غير متوقع" : "Unexpected error");
toast.error(`${defaultLang === "ar" ? "خطأ" : "Error"}: ${errorMsg}`);
setMessages((prev) => [
...prev,
{
role: "model",
content: `${defaultLang === "ar" ? "عذرًا، حدث خطأ" : "Sorry, an error occurred"}: ${errorMsg}`,
timestamp: formatTimestamp(),
},
]);
}
finally {
setLoading(false);
}
};
const handleKeyPress = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const refreshProducts = async () => {
setLoading(true);
try {
const mcpResult = await handleMcpToolCall("list-products", {});
setMessages((prev) => [
...prev,
{
role: "model",
content: {
products: Array.isArray(mcpResult) ? mcpResult : [mcpResult],
},
timestamp: formatTimestamp(),
},
]);
toast.success(defaultLang === "ar"
? "تم تحديث قائمة المنتجات!"
: "Product list updated!");
}
catch (e) {
toast.error(defaultLang === "ar"
? `فشل تحديث المنتجات: ${e.message}`
: `Failed to update products: ${e.message}`);
}
finally {
setLoading(false);
}
};
const clearChat = () => {
setMessages([
{
role: "model",
content: defaultLang === "ar"
? "أهلاً وسهلاً! أنا هنا لمساعدتك في إدارة المنتجات. كيف يمكنني مساعدتك؟"
: "Hello! I'm here to help with product management. How can I assist you?",
timestamp: formatTimestamp(),
},
]);
setInput("");
toast.info(defaultLang === "ar" ? "تم مسح المحادثة" : "Chat cleared");
};
const showCart = () => {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
if (cart.length === 0) {
toast.warn(defaultLang === "ar" ? "السلة فاضية!" : "Cart is empty!");
setMessages((prev) => [
...prev,
{
role: "model",
content: defaultLang === "ar"
? "السلة فاضية حالياً! يمكنك إضافة المنتجات من قائمة المنتجات."
: "The cart is empty! You can add products from the product list.",
timestamp: formatTimestamp(),
},
]);
return;
}
setMessages((prev) => [
...prev,
{
role: "model",
content: { products: cart },
timestamp: formatTimestamp(),
},
]);
toast.info(defaultLang === "ar"
? `عرض السلة (${cart.length} منتج)`
: `Showing cart (${cart.length} items)`);
};
const ProductCard = ({ product }) => (_jsx(Card, { className: "mb-3 border border-gray-200 shadow-sm hover:shadow-md transition-shadow", children: _jsx(CardContent, { className: "p-4", children: _jsxs("div", { className: "flex justify-between items-start", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h4", { className: "font-semibold text-gray-800 text-sm", children: product.name }), _jsx("p", { className: "text-xs text-gray-600 mt-1 line-clamp-2", children: product.description }), _jsxs("div", { className: "flex items-center gap-2 mt-2", children: [_jsxs(Badge, { variant: "secondary", className: "text-xs", children: [product.price, " ", defaultLang === "ar" ? "جنيه" : "EGP"] }), _jsxs(Badge, { variant: "outline", className: "text-xs", children: ["ID: ", product.id] })] })] }), _jsx(Button, { size: "sm", variant: "outline", className: "text-xs h-8 px-3", onClick: () => {
const cart = JSON.parse(localStorage.getItem("cart") || "[]");
cart.push(product);
localStorage.setItem("cart", JSON.stringify(cart));
toast.success(defaultLang === "ar"
? "تم إضافة المنتج للسلة!"
: "Product added to cart!");
}, children: defaultLang === "ar" ? "إضافة للسلة" : "Add to Cart" })] }) }) }));
const MessageBubble = ({ message }) => {
const isUser = message.role === "user";
return (_jsx("div", { className: `flex mb-4 ${isUser ? "justify-end" : "justify-start"}`, children: _jsxs("div", { className: `flex max-w-[85%] ${isUser ? "flex-row-reverse" : "flex-row"}`, children: [_jsx("div", { className: `w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${isUser ? "bg-blue-500 ml-3" : "bg-gray-200 mr-3"}`, children: isUser ? (_jsx(User, { className: "w-4 h-4 text-white" })) : (_jsx(Bot, { className: "w-4 h-4 text-gray-600" })) }), _jsxs("div", { className: `p-3 rounded-2xl ${isUser
? "bg-blue-500 text-white rounded-br-md"
: "bg-gray-100 text-gray-800 rounded-bl-md"}`, children: [typeof message.content === "string" ? (_jsx("p", { className: "text-sm leading-relaxed", children: message.content })) : message.content.products ? (_jsxs("div", { children: [_jsx("p", { className: "text-sm mb-3 font-medium", children: defaultLang === "ar"
? "المنتجات المتاحة:"
: "Available products:" }), _jsx("div", { className: "space-y-2", children: message.content.products.map((product, index) => (_jsx(ProductCard, { product: product }, product.id || index))) })] })) : null, _jsx("p", { className: `text-xs mt-2 ${isUser ? "text-blue-100" : "text-gray-500"}`, children: message.timestamp })] })] }) }));
};
if (!isOpen) {
return (_jsx("div", { className: "fixed bottom-6 right-6 z-50", children: _jsx(Button, { onClick: () => setIsOpen(true), className: "w-16 h-16 rounded-full shadow-xl bg-black hover:bg-gray-800 transition-all duration-300 hover:scale-105 flex items-center justify-center group cursor-pointer", children: _jsxs("div", { className: "flex items-center", children: [_jsx(Sparkles, { className: "text-white" }), _jsx("span", { className: "text-white font-medium text-sm opacity-0 group-hover:opacity-100 transition-opacity absolute -left-20 bg-black px-3 py-1 rounded-full whitespace-nowrap", children: defaultLang === "ar" ? "اسأل الذكاء الاصطناعي" : "Ask AI" })] }) }) }));
}
const chatClasses = isExpanded
? "fixed inset-0 z-50 animate-in slide-in-from-right-4 duration-300"
: "fixed bottom-0 right-0 w-96 h-screen z-50 animate-in slide-in-right-bottom-4 slide-in-from-right-4 duration-300";
return (_jsx("div", { className: chatClasses, children: _jsxs(Card, { className: "h-full shadow-2xl border-0 overflow-hidden bg-white flex flex-col", children: [_jsx(CardHeader, { className: "bg-white border-b p-4 flex-shrink-0", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center space-x-3 rtl:space-x-reverse", children: [_jsx("div", { className: "w-8 h-8 rounded-full bg-red-500 flex items-center justify-center text-white font-bold text-sm", children: "tb" }), _jsxs("div", { children: [_jsx("h3", { className: "font-semibold text-gray-800 text-sm", children: defaultLang === "ar" ? "اسأل Tigerbase" : "Ask Tigerbase" }), _jsx("p", { className: "text-xs text-gray-500", children: defaultLang === "ar"
? "متاح الآن للمساعدة"
: "Available now to assist" })] })] }), _jsxs("div", { className: "flex items-center space-x-2 rtl:space-x-reverse", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => setIsExpanded(!isExpanded), className: "text-gray-500 hover:bg-gray-100 w-8 h-8", children: isExpanded ? (_jsx(Minimize2, { className: "w-4 h-4" })) : (_jsx(Maximize2, { className: "w-4 h-4" })) }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => setIsOpen(false), className: "text-gray-500 hover:bg-gray-100 w-8 h-8", children: _jsx(X, { className: "w-4 h-4" }) })] })] }) }), _jsxs(CardContent, { className: "flex-1 p-0 flex flex-col overflow-hidden", children: [_jsx("div", { className: "flex-1 overflow-hidden", children: _jsx(ScrollArea, { className: "h-full", children: _jsxs("div", { className: "p-4", children: [messages.length === 1 && (_jsxs("div", { className: "text-center py-12", children: [_jsx("div", { className: "w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-purple-500 to-blue-600 rounded-full flex items-center justify-center", children: _jsx(Sparkles, { className: "w-8 h-8 text-white" }) }), _jsx("h3", { className: "text-lg font-semibold text-gray-800 mb-2", children: defaultLang === "ar"
? "اسأل أي شيء عن Tigerbase"
: "Ask anything about Tigerbase" }), _jsx("p", { className: "text-sm text-gray-500 max-w-xs mx-auto", children: defaultLang === "ar"
? "يستخدم Tigerbase أحدث البيانات في التوثيق للإجابة على أسئلتك."
: "Tigerbase uses the latest data in the documentation to answer your questions." })] })), messages.map((message, index) => (_jsx(MessageBubble, { message: message }, index))), loading && (_jsx("div", { className: "flex justify-start mb-4", children: _jsxs("div", { className: "flex", children: [_jsx("div", { className: "w-8 h-8 rounded-full bg-gray-200 mr-3 flex items-center justify-center", children: _jsx(Bot, { className: "w-4 h-4 text-gray-600" }) }), _jsx("div", { className: "bg-gray-100 p-3 rounded-2xl rounded-bl-md", children: _jsxs("div", { className: "flex space-x-1", children: [_jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce" }), _jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce", style: { animationDelay: "0.1s" } }), _jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce", style: { animationDelay: "0.2s" } })] }) })] }) })), _jsx("div", { ref: messagesEndRef })] }) }) }), messages.length > 1 && (_jsx("div", { className: "border-t bg-gray-50 p-3 flex-shrink-0", children: _jsxs("div", { className: "flex space-x-2 rtl:space-x-reverse mb-0 flex-wrap gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", onClick: refreshProducts, className: "text-xs h-8 px-3 flex items-center gap-1", disabled: loading, children: [_jsx(RefreshCw, { className: "w-3 h-3" }), defaultLang === "ar" ? "المنتجات" : "Products"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: showCart, className: "text-xs h-8 px-3 flex items-center gap-1", children: [_jsx(ShoppingCart, { className: "w-3 h-3" }), defaultLang === "ar" ? "السلة" : "Cart"] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: clearChat, className: "text-xs h-8 px-3 flex items-center gap-1", children: [_jsx(Trash2, { className: "w-3 h-3" }), defaultLang === "ar" ? "مسح" : "Clear"] })] }) })), _jsxs("div", { className: "border-t bg-white p-4 flex-shrink-0", children: [_jsxs("div", { className: "flex space-x-2 rtl:space-x-reverse items-center", children: [_jsx(Input, { value: input, onChange: (e) => setInput(e.target.value), onKeyPress: handleKeyPress, placeholder: defaultLang === "ar" ? "اسأل أي شيء" : "Ask anything", className: "flex-1 border-0 bg-gray-100 focus:bg-white transition-colors text-right placeholder:text-gray-500 text-sm", disabled: loading }), _jsx(Button, { onClick: () => handleSend(1), disabled: loading || !input.trim(), size: "icon", className: "bg-gray-300 hover:bg-gray-400 text-gray-600 w-8 h-8 rounded-full shrink-0", children: _jsx(Send, { className: "w-4 h-4" }) })] }), _jsx("div", { className: "mt-3 text-center", children: _jsxs("p", { className: "text-xs text-gray-500", children: [defaultLang === "ar" ? "مدعوم بواسطة" : "Powered by", " ", _jsx("span", { className: "text-red-500 font-semibold", children: "Tigerbase" })] }) })] })] })] }) }));
};
export default ChatbotPopup;