@botforge/widget
Version:
Official BotForge chatbot widget for easy integration into any website or web application
951 lines (947 loc) • 76.1 kB
JavaScript
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import React, { useState, useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react';
import * as ReactDOM from 'react-dom/client';
const ChatWindow = ({ messages, onSendMessage, onFileUpload, onClose, onRestart, isLoading, isConnected, config, error, isInitialized, conversationId, }) => {
var _a, _b, _c, _d;
const [inputValue, setInputValue] = useState("");
const [isDragging, setIsDragging] = useState(false);
const messagesEndRef = useRef(null);
const inputRef = useRef(null);
const fileInputRef = useRef(null);
useEffect(() => {
var _a;
(_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
}, [messages]);
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 (jsxs("div", { style: windowStyles, children: [jsxs("div", { style: headerStyles, children: [jsxs("div", { children: [jsx("div", { style: {
fontWeight: "600",
fontSize: "18px",
color: "#f3f4f6",
marginBottom: "4px",
}, children: config.title || "BotForge Support" }), config.subtitle && (jsx("div", { style: {
fontSize: "14px",
color: "#9ca3af",
}, children: config.subtitle })), jsxs("div", { style: {
display: "flex",
alignItems: "center",
marginTop: "8px",
fontSize: "12px",
color: isConnected ? "#10b981" : "#ef4444",
}, children: [jsx("div", { style: {
width: "8px",
height: "8px",
backgroundColor: isConnected ? "#10b981" : "#ef4444",
borderRadius: "50%",
marginRight: "6px",
} }), isConnected ? "Online" : "Offline"] })] }), jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [onRestart && (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: jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }), jsx("path", { d: "M21 3v5h-5" }), jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }), jsx("path", { d: "M8 16H3v5" })] }) })), 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 && (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: [jsx("div", { style: {
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: isConnected ? "#f59e0b" : "#ef4444",
} }), connectionStatus] })), jsxs("div", { style: messagesStyles, onDragOver: config.enableFileUpload ? handleDragOver : undefined, onDragLeave: config.enableFileUpload ? handleDragLeave : undefined, onDrop: config.enableFileUpload ? handleDrop : undefined, children: [isDragging && (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: jsx("div", { style: { color: "#3b82f6", fontWeight: "600" }, children: "Drop file here to upload" }) })), messages.length === 0 && !isLoading && (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 (jsxs("div", { style: {
display: "flex",
justifyContent: message.sender === "user" ? "flex-end" : "flex-start",
alignItems: "flex-start",
gap: "12px",
}, children: [message.sender === "bot" && (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: jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", children: [jsx("path", { d: "M12 8V4H8" }), jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsx("path", { d: "m14 8-2 2-2-2" }), jsx("path", { d: "M18 12h2" }), jsx("path", { d: "M22 12v6" }), jsx("path", { d: "M4 12H2" }), jsx("path", { d: "M2 12v6" })] }) })), jsxs("div", { style: {
maxWidth: "75%",
display: "flex",
flexDirection: "column",
gap: "8px",
}, children: [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: [jsx("div", { children: message.content }), 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: [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)) && (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) && (jsx("div", { style: {
display: "flex",
flexDirection: "column",
gap: "6px",
}, children: message.metadata.options.map((option, index) => (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" && (jsx("div", { style: {
width: "32px",
height: "32px",
backgroundColor: "#3b82f6",
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
marginTop: "4px",
}, children: jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", children: [jsx("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }), jsx("circle", { cx: "12", cy: "7", r: "4" })] }) }))] }, message.id));
}), isLoading && messages.length > 0 && config.enableTypingIndicator && (jsxs("div", { style: {
display: "flex",
justifyContent: "flex-start",
alignItems: "flex-start",
gap: "12px",
}, children: [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: jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "white", strokeWidth: "2", children: [jsx("path", { d: "M12 8V4H8" }), jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsx("path", { d: "m14 8-2 2-2-2" }), jsx("path", { d: "M18 12h2" }), jsx("path", { d: "M22 12v6" }), jsx("path", { d: "M4 12H2" }), jsx("path", { d: "M2 12v6" })] }) }), jsx("div", { style: {
padding: "12px 16px",
borderRadius: "18px 18px 18px 4px",
backgroundColor: "#374151",
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.1)",
}, children: jsxs("div", { style: { display: "flex", gap: "4px", alignItems: "center" }, children: [jsx("div", { style: {
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: "#9ca3af",
animation: "pulse 1.5s ease-in-out infinite",
} }), jsx("div", { style: {
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: "#9ca3af",
animation: "pulse 1.5s ease-in-out infinite 0.2s",
} }), jsx("div", { style: {
width: "8px",
height: "8px",
borderRadius: "50%",
backgroundColor: "#9ca3af",
animation: "pulse 1.5s ease-in-out infinite 0.4s",
} })] }) })] })), jsx("div", { ref: messagesEndRef })] }), jsx("div", { style: inputContainerStyles, children: jsxs("form", { onSubmit: handleSubmit, style: { display: "flex", gap: "12px", alignItems: "flex-end" }, children: [jsx("div", { style: { flex: 1, position: "relative" }, children: 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 && (jsxs(Fragment, { children: [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" }), jsx("input", { ref: fileInputRef, type: "file", onChange: handleFileSelect, style: { display: "none" }, accept: "image/*,.pdf,.doc,.docx,.txt" })] })), 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 ? (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",
} })) : (jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [jsx("path", { d: "m22 2-7 20-4-9-9-4Z" }), jsx("path", { d: "M22 2 11 13" })] })) })] }) }), config.showBranding && (jsxs("div", { style: {
padding: "12px 20px",
textAlign: "center",
fontSize: "12px",
color: "#6b7280",
borderTop: "1px solid #374151",
backgroundColor: "#1f2937",
}, children: ["Powered by", " ", jsx("a", { href: "https://botforge.site", target: "_blank", rel: "noopener noreferrer", style: {
color: "#3b82f6",
textDecoration: "none",
fontWeight: "600",
}, children: "BotForge" })] })), 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 (jsxs(Fragment, { children: [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 && jsx("div", { style: pulseStyles }), isOpen ? (
// Close icon (X)
jsxs("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] })) : (
// Bot icon
jsxs("svg", { width: iconSize, height: iconSize, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsx("path", { d: "M12 8V4H8" }), jsx("rect", { width: "16", height: "12", x: "4", y: "8", rx: "2" }), jsx("path", { d: "m14 8-2 2-2-2" }), jsx("path", { d: "M18 12h2" }), jsx("path", { d: "M22 12v6" }), jsx("path", { d: "M4 12H2" }), jsx("path", { d: "M2 12v6" })] }))] }), 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.toLowerCase();
let response = "";
if (input.includes("hello") ||
input.includes("hi") ||
input.includes("hey")) {
response =
"Hello! I'm currently in offline mode, but I'm here to help. What can I assist you with?";
}
else if (input.includes("help") || input.includes("support")) {
response =
"I'd be happy to help! While I'm in offline mode, I can provide general assistance. What do you need help with?";
}
else if (input.includes("price") ||
input.includes("cost") ||
input.includes("pricing")) {
response =
"For current pricing information, please visit our website or contact our sales team when the connection is restored.";
}
else if (input.includes("thank")) {
response =
"You're very welcome! Is there anything else I can help you with?";
}
else if (input.includes("bye") || input.includes("goodbye")) {
response = "Goodbye! Feel free to reach out anytime. Have a great day!";
}
else if (input.includes("contact") ||
input.includes("email") ||
input.includes("phone")) {
response =
"You can reach our support team at support@botforge.site. We'll get back to you as soon as possible!";
}
else if (input.includes("feature") || input.includes("what can you do")) {
response =
"I can help with general questions about our services. For detailed feature information, please visit our website or cont