@prexo/ai-chat-sdk
Version:
AI Chat Component with Persistent History
339 lines (338 loc) • 12.2 kB
JavaScript
"use client";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { useEffect, useRef, useCallback, useState } from "react";
import "../../styles/chat.widget.css";
import { Message } from "./message.js";
import { TypingIndicator } from "./typing.indicator.js";
import { ChatInput } from "./chat.input.js";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
import {
Maximize,
Minimize,
X
} from "lucide-react";
import { useLocalStorage } from "../../../hooks/use.local.store.js";
import { useChat } from "@ai-sdk/react";
import { BASE_API_ENDPOINT } from "../../../lib/utils.js";
import { SuggestedActions } from "./suggested.actions.js";
import { getHistoryClient } from "../../../services/history/client.js";
import { getContextClient } from "../../../services/context/client.js";
function combineContextData(contextArr) {
return contextArr.map((obj) => obj.data).join("\n");
}
const PrexoAiChatBot = ({
apiKey,
suggestedActions,
sessionId,
sessionTTL,
onClose,
user,
theme,
placeholder = "Type your message...",
className = "",
width = 350,
height = 500,
position = "bottom-right",
redis,
vector,
RAGDisabled = false
}) => {
const [isOpen, setIsOpen] = useLocalStorage("@prexo-chat-bot-#isOpen", false);
const [loading, setLoading] = useState(false);
const [convo, setConvo] = useState([]);
const [cntxt, setCntxt] = useLocalStorage("@prexo-chat-bot-#cntxt", []);
const [cleanCntxt, setCleanCntxt] = useLocalStorage("@prexo-chat-bot-#cleanCntxt", "");
const messagesEndRef = useRef(null);
const messagesContainerRef = useRef(null);
const [isMinimized, setIsMinimized] = useLocalStorage("@prexo-chat-bot-#isMinimized", false);
const history = getHistoryClient({ redis });
const context = getContextClient({ vector, apiKey });
const [historyFetched, setHistoryFetched] = useState(false);
if (apiKey && apiKey.length === 0) {
console.error("API key is required for PrexoAiChatBot to function properly");
throw new Error("API key is required for PrexoAiChatBot to function properly");
}
if (suggestedActions && suggestedActions.length > 3) {
console.error("You can only add max 3 suggested actions!");
throw new Error("You can only add max 3 suggested actions!");
}
const { messages, input, handleInputChange, status, append: realAppend, setInput } = useChat({
api: `${BASE_API_ENDPOINT}/ai/stream`,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: {
history: convo,
context: cleanCntxt,
// always use the latest context
RAGDisabled
},
async onFinish(message) {
await history.addMessage({
message: {
id: message.id,
role: message.role,
content: message.content
},
sessionId,
sessionTTL
});
},
onError(error) {
console.log("ERROR OCCURED: ", error);
}
});
const customAppend = async (message) => {
setLoading(true);
let combinedContext = "";
if (context && !RAGDisabled) {
const contextResults = await context.getContext({ question: message.content });
if (contextResults.length > 0) {
combinedContext = combineContextData(contextResults);
setCntxt(contextResults);
setCleanCntxt(combinedContext);
}
}
setLoading(false);
return realAppend(
{
id: message.id,
role: message.role,
content: message.content
},
{
body: {
history: convo,
context: combinedContext,
// always use the latest context
RAGDisabled
}
}
);
};
const handleCustomSubmit = async (event) => {
event?.preventDefault?.();
setLoading(true);
await customAppend({
id: Date.now().toString(),
role: "user",
content: input
});
setLoading(false);
};
const handleSuggestedAction = async (content) => {
setLoading(true);
await customAppend({
id: Date.now().toString(),
role: "user",
content
});
setLoading(false);
setInput("");
};
useEffect(() => {
if (status === "submitted") {
setInput("");
}
if (RAGDisabled && cntxt.length > 0 && cleanCntxt.length > 0) {
setCleanCntxt("");
setCntxt([]);
}
}, [status, setInput, RAGDisabled, setCleanCntxt, setCntxt, cntxt, cleanCntxt]);
useEffect(() => {
const fetchHistory = async () => {
try {
setLoading(true);
const chatHistory = await history.getMessages({ sessionId });
if (chatHistory.length > 0) {
setConvo([]);
setConvo(chatHistory);
}
setHistoryFetched(true);
} catch (error) {
console.error("Error fetching chat history:", error);
} finally {
setLoading(false);
}
};
if (input.length > 0 && convo.length === 0 && !historyFetched) {
fetchHistory().then(() => {
console.log("History is set!");
});
}
}, [input]);
const isTyping = status === "submitted";
const scrollToBottom = useCallback(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, []);
useEffect(() => {
scrollToBottom();
}, [messages, isTyping, scrollToBottom]);
const handleMinimize = () => {
setIsMinimized(!isMinimized);
};
const handleClose = async () => {
await history.deleteMessages({ sessionId });
setIsOpen(false);
if (onClose) {
onClose();
}
};
const handleOpen = () => {
setIsOpen(true);
setIsMinimized(false);
};
const getPositionClasses = () => {
switch (position) {
case "bottom-left":
return "bottom-left";
default:
return "bottom-right";
}
};
const getWidgetStyle = () => {
return {
width: typeof width === "number" ? `${width}px` : width,
height: isMinimized ? "60px" : typeof height === "number" ? `${height}px` : height
};
};
const formatTime = (date) => {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
};
return /* @__PURE__ */ jsxs(Fragment, { children: [
!isOpen && /* @__PURE__ */ jsx("div", { className: `chat-bubble ${theme} ${getPositionClasses()}`, children: /* @__PURE__ */ jsx("button", { type: "button", onClick: handleOpen, className: "bubble-button", children: /* @__PURE__ */ jsx(
"img",
{
src: "https://raw.githubusercontent.com/plura-ai/prexo/refs/heads/main/apps/www/public/logo.png",
className: "w-12 h-12 rounded-lg object-cover",
alt: "Chat bot avatar",
onError: (e) => {
console.error("Failed to load image:", e);
e.currentTarget.style.display = "none";
}
}
) }) }),
isOpen && /* @__PURE__ */ jsxs(
"div",
{
className: `chat-widget ${theme} ${isMinimized ? "minimized" : ""} ${isOpen && position === "bottom-right" ? "open right" : "open left"} ${!isOpen && "close"} ${getPositionClasses()} ${className}`,
style: getWidgetStyle(),
children: [
/* @__PURE__ */ jsxs("div", { className: "chat-header", children: [
user ? /* @__PURE__ */ jsxs("div", { className: "chat-title", children: [
/* @__PURE__ */ jsxs(Avatar, { children: [
/* @__PURE__ */ jsx(AvatarImage, { src: user.pfp }),
/* @__PURE__ */ jsx(AvatarFallback, { children: "CN" })
] }),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
/* @__PURE__ */ jsx("span", { children: user.name }),
/* @__PURE__ */ jsxs("div", { className: "message-time", children: [
"Last seen ",
formatTime(user.lastSeen)
] })
] })
] }) : /* @__PURE__ */ jsxs("div", { className: "chat-title", children: [
/* @__PURE__ */ jsx(
"img",
{
src: "https://raw.githubusercontent.com/plura-ai/prexo/refs/heads/main/apps/www/public/logo.png",
className: "w-9 h-9 rounded-lg object-cover invert",
alt: "Chat bot avatar",
onError: (e) => {
console.error("Failed to load image:", e);
e.currentTarget.style.display = "none";
}
}
),
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
/* @__PURE__ */ jsx("span", { children: "Prexo Ai" }),
/* @__PURE__ */ jsxs("div", { className: "message-time", children: [
"Last seen ",
formatTime(/* @__PURE__ */ new Date())
] })
] })
] }),
/* @__PURE__ */ jsxs("div", { className: "chat-controls", children: [
/* @__PURE__ */ jsx(
"button",
{
className: "control-btn minimize-btn",
onClick: handleMinimize,
"aria-label": isMinimized ? "Expand chat" : "Minimize chat",
children: isMinimized ? /* @__PURE__ */ jsx(Maximize, { className: "size-4" }) : /* @__PURE__ */ jsx(Minimize, { className: "size-4" })
}
),
/* @__PURE__ */ jsx(
"button",
{
className: "control-btn close-btn",
onClick: handleClose,
"aria-label": "Close chat",
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
}
)
] })
] }),
!isMinimized && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("div", { className: "chat-messages", ref: messagesContainerRef, children: [
messages.length === 0 && /* @__PURE__ */ jsxs("div", { className: "message bot", children: [
/* @__PURE__ */ jsx("div", { className: "bot-avatar", children: /* @__PURE__ */ jsx(
"img",
{
src: "https://raw.githubusercontent.com/plura-ai/prexo/refs/heads/main/apps/www/public/logo.png",
className: "w-10 h-10 rounded-lg object-cover invert",
alt: "Chat bot avatar",
onError: (e) => {
console.error("Failed to load image:", e);
e.currentTarget.style.display = "none";
}
}
) }),
/* @__PURE__ */ jsx("div", { className: "message-content", children: /* @__PURE__ */ jsx("div", { className: "message-bubble", children: /* @__PURE__ */ jsx("p", { children: "Hello! I'm Prexo Ai. How can I help you today?" }) }) })
] }),
messages.map((message) => /* @__PURE__ */ jsx(
Message,
{
message
},
message.id
)),
isTyping && /* @__PURE__ */ jsx(TypingIndicator, {}),
/* @__PURE__ */ jsx("div", { ref: messagesEndRef })
] }),
messages.length === 0 && suggestedActions && suggestedActions.length < 3 && /* @__PURE__ */ jsx("div", { className: "message-content p-2", children: /* @__PURE__ */ jsx(
SuggestedActions,
{
append: handleSuggestedAction,
suggestedActions,
history,
sessionId,
sessionTTL
}
) }),
/* @__PURE__ */ jsx(
ChatInput,
{
input,
handleInputChange,
handleSubmit: handleCustomSubmit,
status,
placeholder,
sessionId,
sessionTTL,
isLoading: loading,
history
}
)
] })
]
}
)
] });
};
export {
PrexoAiChatBot
};