UNPKG

@candoa/chatbot

Version:

Headless React chatbot widget with TypeScript, TailwindCSS support. Customizable AI chat component with hooks for modern React applications.

373 lines (368 loc) 12.3 kB
"use strict"; "use client"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.tsx var src_exports = {}; __export(src_exports, { ChatbotWidget: () => ChatbotWidget, useChatbot: () => useChatbot }); module.exports = __toCommonJS(src_exports); // src/components/ChatbotWidget.tsx var import_react_portal = require("@radix-ui/react-portal"); // src/hooks/useChatbot.ts var import_react = require("react"); function useChatbot(projectId, options = {}) { const { greetings = [], title, icon, initialConversationId, apiUrl, isDev } = options; const getApiUrl = () => { if (apiUrl) return apiUrl; if (isDev) { return "http://localhost:3000"; } return "https://crm.candoa.app"; }; const baseApiUrl = getApiUrl(); const [isOpen, setIsOpen] = (0, import_react.useState)(false); const [messages, setMessages] = (0, import_react.useState)([]); const [error, setError] = (0, import_react.useState)(null); const [isTyping, setIsTyping] = (0, import_react.useState)(false); const [conversationId, setConversationId] = (0, import_react.useState)( initialConversationId || null ); const [humanTakenOver, setHumanTakenOver] = (0, import_react.useState)(false); const [hasUnreadMessages, setHasUnreadMessages] = (0, import_react.useState)(false); (0, import_react.useEffect)(() => { if (isOpen && hasUnreadMessages) { setHasUnreadMessages(false); } }, [isOpen, hasUnreadMessages]); const pollingIntervalRef = (0, import_react.useRef)(null); const lastMessageCountRef = (0, import_react.useRef)(0); (0, import_react.useEffect)(() => { if (typeof window === "undefined" || initialConversationId) return; const storedConversationId = localStorage.getItem("conversationId"); if (storedConversationId && !conversationId) { setConversationId(storedConversationId); } }, [initialConversationId, conversationId]); (0, import_react.useEffect)(() => { if (typeof window === "undefined" || !conversationId) return; localStorage.setItem("conversationId", conversationId); }, [conversationId]); const pollForNewMessages = (0, import_react.useCallback)(async () => { if (!conversationId || !humanTakenOver) return; try { const response = await fetch( `${baseApiUrl}/api/messages?conversationId=${conversationId}` ); if (!response.ok) return; const data = await response.json(); if (!data.success) return; const serverMessages = data.messages.map((msg) => ({ id: msg.id, text: msg.content, isUser: msg.role === "user", timestamp: new Date(msg.createdAt) })); if (serverMessages.length > lastMessageCountRef.current) { const newMessages = serverMessages.slice(lastMessageCountRef.current); const hasNewAgentMessages = newMessages.some((msg) => !msg.isUser); setMessages(serverMessages); lastMessageCountRef.current = serverMessages.length; if (!isOpen && hasNewAgentMessages) { setHasUnreadMessages(true); if (typeof window !== "undefined" && "Notification" in window && Notification.permission === "granted") { const lastAgentMessage = newMessages.filter((msg) => !msg.isUser).pop(); if (lastAgentMessage) { new Notification("New message from support", { body: lastAgentMessage.text.length > 100 ? lastAgentMessage.text.substring(0, 100) + "..." : lastAgentMessage.text, icon: "/favicon.ico" }); } } } } } catch (err) { console.warn("Polling error:", err); } }, [conversationId, humanTakenOver, baseApiUrl]); (0, import_react.useEffect)(() => { if (humanTakenOver && conversationId) { pollingIntervalRef.current = setInterval(pollForNewMessages, 3e3); } else { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } } return () => { if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } }; }, [humanTakenOver, conversationId, pollForNewMessages]); const loadConversation = (0, import_react.useCallback)( async (convoId) => { if (!convoId) return; setIsTyping(true); setError(null); try { const response = await fetch( `${baseApiUrl}/api/messages?conversationId=${convoId}` ); if (!response.ok) { if (response.status === 404) { if (typeof window !== "undefined") { localStorage.removeItem("conversationId"); } setConversationId(null); setMessages([]); setIsTyping(false); return; } throw new Error("Failed to load message history"); } const data = await response.json(); if (!data.success) { throw new Error(data.message || "Failed to load messages"); } const historyMessages = data.messages.map((msg) => ({ id: msg.id, text: msg.content, isUser: msg.role === "user", timestamp: new Date(msg.createdAt) })); setMessages(historyMessages); setConversationId(convoId); lastMessageCountRef.current = historyMessages.length; if (data.conversation?.humanTakenOver) { setHumanTakenOver(true); } } catch (err) { setError( err instanceof Error ? err.message : "Failed to load message history" ); setMessages([]); } finally { setIsTyping(false); } }, [projectId, baseApiUrl] ); (0, import_react.useEffect)(() => { if (conversationId && messages.length === 0 && !initialConversationId) { loadConversation(conversationId); } }, [conversationId, loadConversation, messages.length, initialConversationId]); (0, import_react.useEffect)(() => { if (initialConversationId && messages.length === 0) { loadConversation(initialConversationId); } }, [initialConversationId, loadConversation, messages.length]); (0, import_react.useEffect)(() => { if (isOpen && messages.length === 0 && greetings.length > 0 && !initialConversationId && !conversationId) { const greetingMessages = greetings.map((greeting) => ({ id: crypto.randomUUID(), text: greeting, isUser: false, timestamp: /* @__PURE__ */ new Date() })); setMessages(greetingMessages); } }, [ isOpen, messages.length, greetings, initialConversationId, conversationId ]); const sendMessage = (0, import_react.useCallback)( async (text) => { if (!text.trim()) return; const newMessage = { id: crypto.randomUUID(), text: text.trim(), isUser: true, timestamp: /* @__PURE__ */ new Date() }; const updatedMessages = [...messages, newMessage]; setMessages(updatedMessages); if (!humanTakenOver) { setIsTyping(true); } setError(null); try { const requestBody = { projectId, access: "AI_AGENT", messages: updatedMessages.map((m) => ({ role: m.isUser ? "user" : "assistant", content: m.text })) }; if (conversationId) { requestBody.conversationId = conversationId; } else { requestBody.greetingMessages = greetings.length > 0 ? greetings : []; } const response = await fetch(`${baseApiUrl}/api/chatbot`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) }); if (!response.ok) throw new Error("Failed to send message"); const data = await response.json(); if (!data.success) { throw new Error(data.message || "AI response failed"); } if (data.conversationId && !conversationId) { setConversationId(data.conversationId); } if (data.humanTakenOver) { setHumanTakenOver(true); lastMessageCountRef.current = updatedMessages.length; if (data.message && data.message.trim()) { const botMessage2 = { id: crypto.randomUUID(), text: data.message, isUser: false, timestamp: /* @__PURE__ */ new Date() }; setMessages((prev) => [...prev, botMessage2]); lastMessageCountRef.current = updatedMessages.length + 1; } return; } const botMessage = { id: crypto.randomUUID(), text: data.message, isUser: false, timestamp: /* @__PURE__ */ new Date() }; setMessages((prev) => [...prev, botMessage]); lastMessageCountRef.current = updatedMessages.length + 1; } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); } finally { setIsTyping(false); } }, [projectId, messages, conversationId, baseApiUrl, greetings] ); const clearMessages = (0, import_react.useCallback)(() => { setMessages([]); setConversationId(null); setHumanTakenOver(false); lastMessageCountRef.current = 0; if (pollingIntervalRef.current) { clearInterval(pollingIntervalRef.current); pollingIntervalRef.current = null; } if (typeof window !== "undefined") { localStorage.removeItem("conversationId"); } }, []); const clearError = (0, import_react.useCallback)(() => { setError(null); }, []); const markAsRead = (0, import_react.useCallback)(() => { setHasUnreadMessages(false); }, []); return { state: { isOpen, messages, error, isTyping, hasActiveConversation: !!conversationId, conversationId, humanTakenOver, hasUnreadMessages }, actions: { setIsOpen, sendMessage, clearMessages, clearError, loadConversation, markAsRead } }; } // src/components/ChatbotWidget.tsx var import_jsx_runtime = require("react/jsx-runtime"); var ChatbotWidget = ({ projectId, title = "Chat with us", icon, greetings, apiUrl, isDev, children }) => { const { state, actions } = useChatbot(projectId, { title, greetings, icon, apiUrl, isDev }); const renderProps = { isOpen: state.isOpen, messages: state.messages, error: state.error, isTyping: state.isTyping, hasActiveConversation: state.hasActiveConversation, conversationId: state.conversationId, humanTakenOver: state.humanTakenOver, hasUnreadMessages: state.hasUnreadMessages, onSendMessage: actions.sendMessage, onToggleOpen: () => actions.setIsOpen(!state.isOpen), onClose: () => actions.setIsOpen(false), onClearMessages: actions.clearMessages, onClearError: actions.clearError, onLoadConversation: actions.loadConversation, onMarkAsRead: actions.markAsRead }; return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_portal.Portal, { children: children(renderProps) }); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ChatbotWidget, useChatbot });