UNPKG

@botforge/widget

Version:

Official BotForge chatbot widget for easy integration into any website or web application

939 lines (932 loc) 78 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); var ReactDOM = require('react-dom/client'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var ReactDOM__namespace = /*#__PURE__*/_interopNamespaceDefault(ReactDOM); const ChatWindow = ({ messages, onSendMessage, onFileUpload, onClose, onRestart, isLoading, isConnected, config, error, isInitialized, conversationId, }) => { var _a, _b, _c, _d; const [inputValue, setInputValue] = React.useState(""); const [isDragging, setIsDragging] = React.useState(false); const messagesEndRef = React.useRef(null); const inputRef = React.useRef(null); const fileInputRef = React.useRef(null); React.useEffect(() => { var _a; (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" }); }, [messages]); React.useEffect(() => { if (inputRef.current && isInitialized) { inputRef.current.focus(); } }, [isInitialized]); const handleSubmit = (e) => { e.preventDefault(); if (inputValue.trim() && !isLoading && isInitialized && conversationId) { if (config.debug) { console.log("[BotForge Widget] Submitting message:", inputValue.trim()); } onSendMessage(inputValue.trim()); setInputValue(""); } }; const handleKeyPress = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSubmit(e); } }; const handleInputChange = (e) => { setInputValue(e.target.value); if (config.debug) { console.log("[BotForge Widget] Input value changed:", e.target.value); } }; const handleFileSelect = (e) => { var _a; const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0]; if (file && onFileUpload) { onFileUpload(file); } // Reset input if (e.target) { e.target.value = ""; } }; const handleDragOver = (e) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = (e) => { e.preventDefault(); setIsDragging(false); }; const handleDrop = (e) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file && onFileUpload) { onFileUpload(file); } }; const formatTime = (date) => { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; // Handle quick reply option clicks const handleQuickReply = (option) => { if (config.debug) { console.log("[BotForge Widget] Quick reply selected:", option); } onSendMessage(option); }; // Check if the chat is ready for user input const isChatReady = isInitialized && conversationId && !isLoading; const windowStyles = { width: ((_a = config.theme) === null || _a === void 0 ? void 0 : _a.chatWidth) || "400px", height: ((_b = config.theme) === null || _b === void 0 ? void 0 : _b.chatHeight) || "600px", backgroundColor: "#1f2937", border: "1px solid #374151", borderRadius: "16px", boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25)", display: "flex", flexDirection: "column", marginBottom: "80px", overflow: "hidden", fontFamily: ((_c = config.theme) === null || _c === void 0 ? void 0 : _c.fontFamily) || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }; const headerStyles = { backgroundColor: "#1f2937", color: "#f3f4f6", padding: "20px", display: "flex", justifyContent: "space-between", alignItems: "center", borderBottom: "1px solid #374151", }; const messagesStyles = { flex: 1, overflowY: "auto", padding: "20px", display: "flex", flexDirection: "column", gap: "16px", backgroundColor: "#111827", position: "relative", }; const inputContainerStyles = { padding: "20px", borderTop: "1px solid #374151", backgroundColor: "#1f2937", }; const inputStyles = { width: "100%", padding: "12px 16px", border: "1px solid #4b5563", borderRadius: "12px", outline: "none", fontSize: "14px", backgroundColor: "#374151", color: "#f3f4f6", fontFamily: "inherit", transition: "border-color 0.2s ease", }; const getConnectionStatusMessage = () => { if (!isConnected) { return "Offline mode - Limited functionality"; } if (error) { return "Connection issues - Using offline responses"; } return null; }; const connectionStatus = getConnectionStatusMessage(); if (config.debug) { console.log("[BotForge Widget] ChatWindow render:", { messagesCount: messages.length, inputValue, isLoading, isConnected, isInitialized, conversationId, isChatReady, }); } return (jsxRuntime.jsxs("div", { style: windowStyles, children: [jsxRuntime.jsxs("div", { style: headerStyles, children: [jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { style: { fontWeight: "600", fontSize: "18px", color: "#f3f4f6", marginBottom: "4px", }, children: config.title || "BotForge Support" }), config.subtitle && (jsxRuntime.jsx("div", { style: { fontSize: "14px", color: "#9ca3af", }, children: config.subtitle })), jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", marginTop: "8px", fontSize: "12px", color: isConnected ? "#10b981" : "#ef4444", }, children: [jsxRuntime.jsx("div", { style: { width: "8px", height: "8px", backgroundColor: isConnected ? "#10b981" : "#ef4444", borderRadius: "50%", marginRight: "6px", } }), isConnected ? "Online" : "Offline"] })] }), jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [onRestart && (jsxRuntime.jsx("button", { onClick: onRestart, style: { background: "none", border: "none", color: "#9ca3af", fontSize: "18px", cursor: "pointer", padding: "8px", borderRadius: "8px", transition: "all 0.2s ease", display: "flex", alignItems: "center", justifyContent: "center", width: "40px", height: "40px", }, onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = "#374151"; e.currentTarget.style.color = "#f3f4f6"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "transparent"; e.currentTarget.style.color = "#9ca3af"; }, title: "Restart chat", children: jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }), jsxRuntime.jsx("path", { d: "M21 3v5h-5" }), jsxRuntime.jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }), jsxRuntime.jsx("path", { d: "M8 16H3v5" })] }) })), jsxRuntime.jsx("button", { onClick: onClose, style: { background: "none", border: "none", color: "#9ca3af", fontSize: "24px", cursor: "pointer", padding: "8px", borderRadius: "8px", transition: "all 0.2s ease", display: "flex", alignItems: "center", justifyContent: "center", width: "40px", height: "40px", }, onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = "#374151"; e.currentTarget.style.color = "#f3f4f6"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "transparent"; e.currentTarget.style.color = "#9ca3af"; }, children: "\u00D7" })] })] }), connectionStatus && (jsxRuntime.jsxs("div", { style: { padding: "12px 20px", backgroundColor: isConnected ? "#fef3c7" : "#fee2e2", color: isConnected ? "#92400e" : "#991b1b", fontSize: "13px", textAlign: "center", display: "flex", alignItems: "center", justifyContent: "center", gap: "8px", }, children: [jsxRuntime.jsx("div", { style: { width: "8px", height: "8px", borderRadius: "50%", backgroundColor: isConnected ? "#f59e0b" : "#ef4444", } }), connectionStatus] })), jsxRuntime.jsxs("div", { style: messagesStyles, onDragOver: config.enableFileUpload ? handleDragOver : undefined, onDragLeave: config.enableFileUpload ? handleDragLeave : undefined, onDrop: config.enableFileUpload ? handleDrop : undefined, children: [isDragging && (jsxRuntime.jsx("div", { style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "rgba(59, 130, 246, 0.1)", border: "2px dashed #3b82f6", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 10, borderRadius: "8px", }, children: jsxRuntime.jsx("div", { style: { color: "#3b82f6", fontWeight: "600" }, children: "Drop file here to upload" }) })), messages.length === 0 && !isLoading && (jsxRuntime.jsx("div", { style: { textAlign: "center", color: "#6b7280", fontSize: "15px", padding: "40px 20px", lineHeight: "1.6", }, children: config.greeting || "Hi! I'm your BotForge assistant. I can help you create chatbots, understand features, troubleshoot issues, and more. What would you like to know?" })), messages.map((message) => { var _a, _b, _c; return (jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: message.sender === "user" ? "flex-end" : "flex-start", alignItems: "flex-start", gap: "12px", }, children: [message.sender === "bot" && (jsxRuntime.jsx("div", { style: { width: "32px", height: "32px", backgroundColor: "linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)", background: "linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)", borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, marginTop: "4px", }, children: jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", children: [jsxRuntime.jsx("path", { d: "M12 8V4H8" }), jsxRuntime.jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsxRuntime.jsx("path", { d: "m14 8-2 2-2-2" }), jsxRuntime.jsx("path", { d: "M18 12h2" }), jsxRuntime.jsx("path", { d: "M22 12v6" }), jsxRuntime.jsx("path", { d: "M4 12H2" }), jsxRuntime.jsx("path", { d: "M2 12v6" })] }) })), jsxRuntime.jsxs("div", { style: { maxWidth: "75%", display: "flex", flexDirection: "column", gap: "8px", }, children: [jsxRuntime.jsxs("div", { style: { padding: "12px 16px", borderRadius: message.sender === "user" ? "18px 18px 4px 18px" : "18px 18px 18px 4px", backgroundColor: message.sender === "user" ? "#3b82f6" : "#374151", color: message.sender === "user" ? "white" : "#f3f4f6", fontSize: "14px", lineHeight: "1.5", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)", border: message.isError ? "1px solid #ef4444" : "none", wordWrap: "break-word", position: "relative", }, children: [jsxRuntime.jsx("div", { children: message.content }), jsxRuntime.jsxs("div", { style: { fontSize: "11px", opacity: 0.7, marginTop: "6px", textAlign: message.sender === "user" ? "right" : "left", display: "flex", justifyContent: message.sender === "user" ? "flex-end" : "flex-start", alignItems: "center", gap: "6px", }, children: [jsxRuntime.jsx("span", { children: formatTime(message.timestamp) }), (((_a = message.metadata) === null || _a === void 0 ? void 0 : _a.fallback) || ((_b = message.metadata) === null || _b === void 0 ? void 0 : _b.offline)) && (jsxRuntime.jsx("span", { style: { fontSize: "10px", backgroundColor: "rgba(0, 0, 0, 0.2)", padding: "2px 6px", borderRadius: "6px", }, children: "Offline" }))] })] }), message.sender === "bot" && ((_c = message.metadata) === null || _c === void 0 ? void 0 : _c.options) && (jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "6px", }, children: message.metadata.options.map((option, index) => (jsxRuntime.jsx("button", { onClick: () => handleQuickReply(option), style: { padding: "8px 12px", backgroundColor: "#4b5563", color: "#f3f4f6", border: "1px solid #6b7280", borderRadius: "8px", fontSize: "13px", cursor: "pointer", transition: "all 0.2s ease", textAlign: "left", }, onMouseEnter: (e) => { e.currentTarget.style.backgroundColor = "#374151"; e.currentTarget.style.borderColor = "#9ca3af"; }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "#4b5563"; e.currentTarget.style.borderColor = "#6b7280"; }, children: option }, index))) }))] }), message.sender === "user" && (jsxRuntime.jsx("div", { style: { width: "32px", height: "32px", backgroundColor: "#3b82f6", borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, marginTop: "4px", }, children: jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", children: [jsxRuntime.jsx("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }), jsxRuntime.jsx("circle", { cx: "12", cy: "7", r: "4" })] }) }))] }, message.id)); }), isLoading && messages.length > 0 && config.enableTypingIndicator && (jsxRuntime.jsxs("div", { style: { display: "flex", justifyContent: "flex-start", alignItems: "flex-start", gap: "12px", }, children: [jsxRuntime.jsx("div", { style: { width: "32px", height: "32px", background: "linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)", borderRadius: "50%", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, marginTop: "4px", }, children: jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", children: [jsxRuntime.jsx("path", { d: "M12 8V4H8" }), jsxRuntime.jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsxRuntime.jsx("path", { d: "m14 8-2 2-2-2" }), jsxRuntime.jsx("path", { d: "M18 12h2" }), jsxRuntime.jsx("path", { d: "M22 12v6" }), jsxRuntime.jsx("path", { d: "M4 12H2" }), jsxRuntime.jsx("path", { d: "M2 12v6" })] }) }), jsxRuntime.jsx("div", { style: { padding: "12px 16px", borderRadius: "18px 18px 18px 4px", backgroundColor: "#374151", boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)", }, children: jsxRuntime.jsxs("div", { style: { display: "flex", gap: "4px", alignItems: "center" }, children: [jsxRuntime.jsx("div", { style: { width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#9ca3af", animation: "pulse 1.5s ease-in-out infinite", } }), jsxRuntime.jsx("div", { style: { width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#9ca3af", animation: "pulse 1.5s ease-in-out infinite 0.2s", } }), jsxRuntime.jsx("div", { style: { width: "8px", height: "8px", borderRadius: "50%", backgroundColor: "#9ca3af", animation: "pulse 1.5s ease-in-out infinite 0.4s", } })] }) })] })), jsxRuntime.jsx("div", { ref: messagesEndRef })] }), jsxRuntime.jsx("div", { style: inputContainerStyles, children: jsxRuntime.jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", gap: "12px", alignItems: "flex-end" }, children: [jsxRuntime.jsx("div", { style: { flex: 1, position: "relative" }, children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: inputValue, onChange: handleInputChange, onKeyPress: handleKeyPress, placeholder: !isChatReady ? "Initializing chat..." : config.placeholder || "Ask me about BotForge features, pricing, or how to build chatbots...", disabled: !isChatReady, style: { ...inputStyles, opacity: !isChatReady ? 0.5 : 1, cursor: !isChatReady ? "not-allowed" : "text", }, onFocus: (e) => { if (isChatReady) { e.currentTarget.style.borderColor = "#3b82f6"; e.currentTarget.style.boxShadow = "0 0 0 3px rgba(59, 130, 246, 0.1)"; } }, onBlur: (e) => { e.currentTarget.style.borderColor = "#4b5563"; e.currentTarget.style.boxShadow = "none"; } }) }), config.enableFileUpload && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("button", { type: "button", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, disabled: !isChatReady, style: { padding: "12px", border: "1px solid #4b5563", borderRadius: "12px", backgroundColor: "#374151", color: "#9ca3af", cursor: isChatReady ? "pointer" : "not-allowed", fontSize: "16px", opacity: !isChatReady ? 0.5 : 1, transition: "all 0.2s ease", display: "flex", alignItems: "center", justifyContent: "center", width: "44px", height: "44px", }, title: "Upload file", onMouseEnter: (e) => { if (isChatReady) { e.currentTarget.style.backgroundColor = "#4b5563"; e.currentTarget.style.color = "#f3f4f6"; } }, onMouseLeave: (e) => { e.currentTarget.style.backgroundColor = "#374151"; e.currentTarget.style.color = "#9ca3af"; }, children: "\uD83D\uDCCE" }), jsxRuntime.jsx("input", { ref: fileInputRef, type: "file", onChange: handleFileSelect, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt" })] })), jsxRuntime.jsx("button", { type: "submit", disabled: !inputValue.trim() || !isChatReady, style: { padding: "12px 20px", border: "none", borderRadius: "12px", backgroundColor: ((_d = config.theme) === null || _d === void 0 ? void 0 : _d.primaryColor) || "#3b82f6", color: "white", cursor: !inputValue.trim() || !isChatReady ? "not-allowed" : "pointer", fontSize: "14px", fontWeight: "600", opacity: !inputValue.trim() || !isChatReady ? 0.5 : 1, transition: "all 0.2s ease", display: "flex", alignItems: "center", justifyContent: "center", minWidth: "60px", height: "44px", }, onMouseEnter: (e) => { if (inputValue.trim() && isChatReady) { e.currentTarget.style.backgroundColor = "#2563eb"; e.currentTarget.style.transform = "translateY(-1px)"; } }, onMouseLeave: (e) => { var _a; e.currentTarget.style.backgroundColor = ((_a = config.theme) === null || _a === void 0 ? void 0 : _a.primaryColor) || "#3b82f6"; e.currentTarget.style.transform = "translateY(0)"; }, children: !isChatReady ? (jsxRuntime.jsx("div", { style: { width: "16px", height: "16px", border: "2px solid rgba(255, 255, 255, 0.3)", borderTop: "2px solid white", borderRadius: "50%", animation: "spin 1s linear infinite", } })) : (jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsxRuntime.jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), jsxRuntime.jsx("path", { d: "M22 2 11 13" })] })) })] }) }), config.showBranding && (jsxRuntime.jsxs("div", { style: { padding: "12px 20px", textAlign: "center", fontSize: "12px", color: "#6b7280", borderTop: "1px solid #374151", backgroundColor: "#1f2937", }, children: ["Powered by", " ", jsxRuntime.jsx("a", { href: "https://botforge.site", target: "_blank", rel: "noopener noreferrer", style: { color: "#3b82f6", textDecoration: "none", fontWeight: "600", }, children: "BotForge" })] })), jsxRuntime.jsx("style", { children: ` @keyframes pulse { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } ` })] })); }; const ChatButton = ({ onClick, isOpen, config, hasUnreadMessages, }) => { var _a, _b, _c; const getButtonSize = () => { var _a; switch ((_a = config.theme) === null || _a === void 0 ? void 0 : _a.buttonSize) { case "small": return { width: "56px", height: "56px", fontSize: "20px" }; case "large": return { width: "72px", height: "72px", fontSize: "28px" }; default: return { width: "64px", height: "64px", fontSize: "24px" }; } }; const buttonSize = getButtonSize(); const buttonStyles = { ...buttonSize, borderRadius: "50%", background: "linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)", border: "none", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", boxShadow: "0 8px 32px rgba(59, 130, 246, 0.3)", transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)", position: "relative", color: "white", fontFamily: ((_a = config.theme) === null || _a === void 0 ? void 0 : _a.fontFamily) || "inherit", }; const pulseStyles = { position: "absolute", top: "-3px", right: "-3px", width: "16px", height: "16px", backgroundColor: "#ef4444", borderRadius: "50%", border: "2px solid white", animation: "pulse 2s infinite", }; const iconSize = ((_b = config.theme) === null || _b === void 0 ? void 0 : _b.buttonSize) === "small" ? "24px" : ((_c = config.theme) === null || _c === void 0 ? void 0 : _c.buttonSize) === "large" ? "32px" : "28px"; return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("button", { onClick: onClick, style: buttonStyles, onMouseEnter: (e) => { e.currentTarget.style.transform = "scale(1.05) translateY(-2px)"; e.currentTarget.style.boxShadow = "0 12px 40px rgba(59, 130, 246, 0.4)"; }, onMouseLeave: (e) => { e.currentTarget.style.transform = "scale(1) translateY(0)"; e.currentTarget.style.boxShadow = "0 8px 32px rgba(59, 130, 246, 0.3)"; }, "aria-label": isOpen ? "Close chat" : "Open chat", title: isOpen ? "Close chat" : "Open chat", children: [hasUnreadMessages && !isOpen && jsxRuntime.jsx("div", { style: pulseStyles }), isOpen ? ( // Close icon (X) jsxRuntime.jsxs("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] })) : ( // Bot icon jsxRuntime.jsxs("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("path", { d: "M12 8V4H8" }), jsxRuntime.jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsxRuntime.jsx("path", { d: "m14 8-2 2-2-2" }), jsxRuntime.jsx("path", { d: "M18 12h2" }), jsxRuntime.jsx("path", { d: "M22 12v6" }), jsxRuntime.jsx("path", { d: "M4 12H2" }), jsxRuntime.jsx("path", { d: "M2 12v6" })] }))] }), jsxRuntime.jsx("style", { children: ` @keyframes pulse { 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } } ` })] })); }; class BotForgeAPIClient { constructor(chatbotId, apiUrl, anonKey, debug = false) { this.conversationId = null; this.userIdentifier = null; this.debug = false; this.isInitializing = false; this.retryCount = 0; this.maxRetries = 2; this.isOfflineMode = false; this.connectionCheckInterval = null; this.chatbotId = chatbotId; // Use provided API URL or throw error if not provided if (!apiUrl) { throw new Error("API URL is required for BotForge widget"); } this.baseUrl = apiUrl.replace(/\/$/, ""); // Use provided anonymous key or throw error if not provided if (!anonKey) { throw new Error("Anonymous key is required for BotForge widget"); } this.anonKey = anonKey; this.debug = debug; if (debug) { console.log("[BotForge Widget] Initialized with API URL:", this.baseUrl); } // Start connection monitoring this.startConnectionMonitoring(); } log(...args) { if (this.debug) { console.log("[BotForge Widget]", ...args); } } startConnectionMonitoring() { // Check connection every 30 seconds if in offline mode this.connectionCheckInterval = setInterval(() => { if (this.isOfflineMode) { this.checkConnection(); } }, 30000); } async checkConnection() { try { const response = await fetch(`${this.baseUrl}/functions/v1/widget-chat`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", Authorization: `Bearer ${this.anonKey}`, }, body: JSON.stringify({ chatbotId: this.chatbotId, action: "get_flow", }), signal: AbortSignal.timeout(5000), }); if (response.ok || response.status === 401) { this.isOfflineMode = false; this.log("Connection restored"); return true; } return false; } catch (error) { return false; } } async makeRequest(endpoint, data, retryAttempt = 0) { // If we're in offline mode and this isn't a connection check, return fallback immediately if (this.isOfflineMode && retryAttempt === 0) { this.log("In offline mode, using fallback response"); return { success: false, error: "Offline mode - using fallback response", }; } const url = `${this.baseUrl}/functions/v1/${endpoint}`; this.log("Making request to:", url, "with data:", data, "attempt:", retryAttempt + 1); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const headers = { "Content-Type": "application/json", Accept: "application/json", }; // Add authorization header if (this.anonKey) { headers["Authorization"] = `Bearer ${this.anonKey}`; } const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(data), signal: controller.signal, }); clearTimeout(timeoutId); this.log("Response status:", response.status); if (!response.ok) { const errorText = await response.text(); this.log("Error response:", errorText); // If it's a server error and we haven't exceeded retry limit, retry if (response.status >= 500 && retryAttempt < this.maxRetries) { this.log(`Server error, retrying in ${(retryAttempt + 1) * 1000}ms...`); await new Promise((resolve) => setTimeout(resolve, (retryAttempt + 1) * 1000)); return this.makeRequest(endpoint, data, retryAttempt + 1); } // If we've exhausted retries, enter offline mode if (retryAttempt >= this.maxRetries) { this.isOfflineMode = true; this.log("Entering offline mode due to repeated failures"); } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const result = await response.json(); this.log("Response data:", result); // Reset retry count and offline mode on successful request this.retryCount = 0; this.isOfflineMode = false; return { success: true, data: result, }; } catch (error) { this.log("Request failed:", error); // If it's a network error and we haven't exceeded retry limit, retry if (((typeof error === "object" && error !== null && "name" in error && error.name === "AbortError") || (typeof error === "object" && error !== null && "message" in error && (error.message.includes("fetch") || error.message.includes("Failed to fetch")))) && retryAttempt < this.maxRetries) { this.log(`Network error, retrying in ${(retryAttempt + 1) * 2000}ms...`); await new Promise((resolve) => setTimeout(resolve, (retryAttempt + 1) * 2000)); return this.makeRequest(endpoint, data, retryAttempt + 1); } // If we've exhausted retries, enter offline mode if (retryAttempt >= this.maxRetries) { this.isOfflineMode = true; this.log("Entering offline mode due to network errors"); } return { success: false, error: error instanceof Error ? error.message : "Unknown error occurred", }; } } async initializeConversation() { // Prevent multiple simultaneous initialization attempts if (this.isInitializing) { this.log("Initialization already in progress, waiting..."); let attempts = 0; while (this.isInitializing && attempts < 50) { await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } if (this.conversationId && this.userIdentifier) { return { conversationId: this.conversationId, userIdentifier: this.userIdentifier, }; } } // If already initialized, return existing data if (this.conversationId && this.userIdentifier) { this.log("Already initialized, returning existing data"); return { conversationId: this.conversationId, userIdentifier: this.userIdentifier, }; } this.isInitializing = true; this.log("Initializing conversation for chatbot:", this.chatbotId); try { const response = await this.makeRequest("widget-chat", { chatbotId: this.chatbotId, action: "initialize", userIdentifier: this.userIdentifier, }); if (!response.success || !response.data) { // Use fallback initialization this.conversationId = `offline-${Date.now()}`; this.userIdentifier = this.userIdentifier || generateUserIdentifier(); this.log("Using offline initialization:", { conversationId: this.conversationId, userIdentifier: this.userIdentifier, }); const welcomeMessage = { id: "welcome-offline", content: "Hello! I'm currently in offline mode, but I can still help you with basic questions.", sender: "bot", timestamp: new Date(), type: "text", metadata: { offline: true }, }; return { conversationId: this.conversationId, userIdentifier: this.userIdentifier, welcomeMessage, }; } this.conversationId = response.data.conversationId; this.userIdentifier = response.data.userIdentifier; this.log("Conversation initialized:", { conversationId: this.conversationId, userIdentifier: this.userIdentifier, }); // Process welcome message if provided let welcomeMessage; if (response.data.welcomeMessage) { welcomeMessage = { id: response.data.welcomeMessage.id || "welcome", content: response.data.welcomeMessage.content || "Hello! How can I help you today?", sender: "bot", timestamp: new Date(response.data.welcomeMessage.timestamp || Date.now()), type: "text", metadata: response.data.welcomeMessage.metadata || {}, }; // Check if the welcome message has options (quick replies) if (response.data.welcomeMessage.options) { welcomeMessage.metadata = { ...welcomeMessage.metadata, options: response.data.welcomeMessage.options, }; } } return { conversationId: this.conversationId, userIdentifier: this.userIdentifier, welcomeMessage, }; } finally { this.isInitializing = false; } } async sendMessage(content, type = "text") { var _a, _b, _c, _d, _e; if (!this.conversationId) { throw new Error("Conversation not initialized. Call initializeConversation() first."); } this.log("Sending message:", content, "type:", type); const userMessage = { id: `user-${Date.now()}`, content, sender: "user", timestamp: new Date(), type, }; // If we're in offline mode or the API fails, use fallback if (this.isOfflineMode) { const botMessage = createFallbackResponse(content); this.log("Using offline response for message"); return { userMessage, botMessage }; } const response = await this.makeRequest("widget-chat", { chatbotId: this.chatbotId, conversationId: this.conversationId, message: content, messageType: type, userIdentifier: this.userIdentifier, action: "send_message", }); if (!response.success || !response.data) { // Use fallback response const botMessage = createFallbackResponse(content); this.log("Using fallback response due to API failure"); return { userMessage, botMessage }; } // Process bot message and check for options/quick replies let botMessageMetadata = ((_a = response.data.botMessage) === null || _a === void 0 ? void 0 : _a.metadata) || {}; // Check if the response contains options (quick replies) if ((_b = response.data.botMessage) === null || _b === void 0 ? void 0 : _b.options) { botMessageMetadata = { ...botMessageMetadata, options: response.data.botMessage.options, }; } const botMessage = { id: ((_c = response.data.botMessage) === null || _c === void 0 ? void 0 : _c.id) || `bot-${Date.now()}`, content: ((_d = response.data.botMessage) === null || _d === void 0 ? void 0 : _d.content) || "I understand. How can I help you further?", sender: "bot", timestamp: new Date(((_e = response.data.botMessage) === null || _e === void 0 ? void 0 : _e.timestamp) || Date.now()), type: "text", metadata: botMessageMetadata, }; this.log("Messages created:", { userMessage, botMessage }); return { userMessage, botMessage }; } async getChatbotFlow() { this.log("Getting chatbot flow for:", this.chatbotId); const response = await this.makeRequest("widget-chat", { chatbotId: this.chatbotId, action: "get_flow", }); if (!response.success || !response.data) { // Return basic flow structure as fallback return { flow: { nodes: [ { id: "start", type: "start", data: { content: "Hello! How can I help you today?" }, }, ], edges: [], }, name: "Offline Bot", description: "Currently in offline mode", }; } return response.data; } setUserIdentifier(userIdentifier) { this.userIdentifier = userIdentifier; this.log("User identifier set to:", userIdentifier); } getConversationId() { return this.conversationId; } getUserIdentifier() { return this.userIdentifier; } isOffline() { return this.isOfflineMode; } reset() { this.conversationId = null; this.isInitializing = false; this.retryCount = 0; // Don't reset userIdentifier to maintain user identity // Don't reset isOfflineMode to maintain connection state awareness this.log("API client reset - conversation state only"); } destroy() { this.reset(); if (this.connectionCheckInterval) { clearInterval(this.connectionCheckInterval); this.connectionCheckInterval = null; } this.log("API client fully destroyed"); } } // Utility functions const generateUserIdentifier = () => { // Try to get existing identifier from localStorage const storageKey = "botforge-user-id"; let identifier = ""; try { identifier = localStorage.getItem(storageKey) || ""; } catch (e) { // localStorage might not be available } if (!identifier) { identifier = `user-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; try { localStorage.setItem(storageKey, identifier); } catch (e) { // localStorage might not be available } } return identifier; }; const createFallbackResponse = (userMessage) => { const input = userMessage.toLower