UNPKG

@prexo/ai-chat-sdk

Version:

AI Chat Component with Persistent History

339 lines (338 loc) 12.2 kB
"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 };