@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
JavaScript
;
"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
});