UNPKG

@franklinhelp/sdk-react

Version:

Embeddable AI-native help center for modern SaaS applications

898 lines (892 loc) 35.3 kB
import { createContext, forwardRef, useImperativeHandle, useEffect, useRef, useContext, useState, useCallback, useMemo } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; import { useChat } from '@ai-sdk/react'; import { PromptInput, PromptInputTextarea, PromptInputFooter, PromptInputTools, PromptInputButton, PromptInputSubmit, Suggestion } from '@franklin/ui'; import { ArrowLeft, Search, Settings, X, FileText, Folder, Plus, MessageSquare, Paperclip, ImageIcon, Mic, Sparkles, Bot, User } from 'lucide-react'; var FranklinContext = createContext( void 0 ); function FranklinProvider({ children, orgId, sdkKey, defaultTab = "ai", theme = {}, apiEndpoint = "/api/franklin", onOpen, onClose }) { const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTabState] = useState(defaultTab); const [context, setContext] = useState({}); const open = useCallback(() => { setIsOpen(true); onOpen?.(); }, [onOpen]); const close = useCallback(() => { setIsOpen(false); onClose?.(); }, [onClose]); const toggle = useCallback(() => { if (isOpen) { close(); } else { open(); } }, [isOpen, open, close]); const setActiveTab = useCallback((tab) => { setActiveTabState(tab); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("franklin:tab-change", { detail: { tab } }) ); } }, []); const pushContext = useCallback((payload) => { setContext((prev) => ({ ...prev, ...payload, metadata: payload.metadata || prev.metadata ? { ...prev.metadata ?? {}, ...payload.metadata ?? {} } : void 0 })); }, []); const value = { isOpen, activeTab, context, theme, orgId, sdkKey, apiEndpoint, open, close, toggle, setActiveTab, pushContext }; useEffect(() => { if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("franklin:tab-change", { detail: { tab: defaultTab } }) ); } }, [defaultTab]); return /* @__PURE__ */ React.createElement(FranklinContext.Provider, { value }, children); } function useFranklinContext() { const context = useContext(FranklinContext); if (context === void 0) { throw new Error( "useFranklinContext must be used within a FranklinProvider" ); } return context; } function cn(...inputs) { return twMerge(clsx(inputs)); } function formatDate(timestamp) { const date = new Date(timestamp); const now = /* @__PURE__ */ new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1e3); if (diffInSeconds < 60) { return "Just now"; } if (diffInSeconds < 3600) { const minutes = Math.floor(diffInSeconds / 60); return `${minutes}m ago`; } if (diffInSeconds < 86400) { const hours = Math.floor(diffInSeconds / 3600); return `${hours}h ago`; } if (diffInSeconds < 604800) { const days = Math.floor(diffInSeconds / 86400); return `${days}d ago`; } return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: date.getFullYear() !== now.getFullYear() ? "numeric" : void 0 }); } function Message({ role, content }) { if (role === "system") { return null; } const isUser = role === "user"; const containerClass = cn( "fade-in-50 slide-in-from-bottom-1 flex animate-in gap-3 duration-500", isUser ? "justify-end" : "justify-start" ); const avatarClass = cn( "flex h-8 w-8 shrink-0 items-center justify-center rounded-full", isUser ? "bg-gray-900 text-white" : "border border-gray-200 bg-gray-100" ); const bubbleClass = cn( "max-w-[80%] rounded-2xl px-4 py-2.5 text-[15px] leading-[22px]", isUser ? "bg-black text-white" : "border border-gray-200 bg-gray-100 text-[var(--franklin-foreground)]" ); return /* @__PURE__ */ React.createElement("div", { className: containerClass }, !isUser && /* @__PURE__ */ React.createElement("div", { className: avatarClass }, /* @__PURE__ */ React.createElement(Bot, { className: "h-4 w-4 text-gray-700", strokeWidth: 2 })), /* @__PURE__ */ React.createElement("div", { className: bubbleClass }, /* @__PURE__ */ React.createElement("div", { className: "whitespace-pre-wrap break-words" }, content)), isUser && /* @__PURE__ */ React.createElement("div", { className: avatarClass }, /* @__PURE__ */ React.createElement(User, { className: "h-4 w-4", strokeWidth: 2 }))); } function LoadingMessage() { return /* @__PURE__ */ React.createElement("div", { className: "fade-in-50 slide-in-from-bottom-1 flex animate-in justify-start gap-3 duration-500" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-gray-100" }, /* @__PURE__ */ React.createElement(Bot, { className: "h-4 w-4 text-gray-700", strokeWidth: 2 })), /* @__PURE__ */ React.createElement("div", { className: "rounded-2xl border border-gray-200 bg-gray-100 px-4 py-2.5" }, /* @__PURE__ */ React.createElement("div", { className: "flex gap-1" }, [0, 150, 300].map((delay) => /* @__PURE__ */ React.createElement( "div", { className: "h-2 w-2 animate-bounce rounded-full bg-gray-400", key: delay, style: { animationDelay: `${delay}ms` } } ))))); } var QUICK_ACTIONS = [ "Ask a knowledge question", "Get support", "Status of my open case", "Speak with support" ]; function WelcomeScreen({ onQuickAction, inputValue, onInputChange, onSubmit, isLoading }) { return /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col items-center justify-center px-6" }, /* @__PURE__ */ React.createElement("div", { className: "mb-8 flex max-w-[320px] flex-col items-center gap-4" }, /* @__PURE__ */ React.createElement("div", { className: "flex h-14 w-14 items-center justify-center" }, /* @__PURE__ */ React.createElement( Sparkles, { className: "h-8 w-8", strokeWidth: 1.5, style: { color: "var(--franklin-accent)" } } )), /* @__PURE__ */ React.createElement("div", { className: "text-center" }, /* @__PURE__ */ React.createElement("h3", { className: "mb-2 font-semibold text-[var(--franklin-foreground)] text-lg" }, "Hi! I'm Franklin"), /* @__PURE__ */ React.createElement("p", { className: "text-[var(--franklin-muted)] text-sm" }, "Your AI-powered assistant. How can I help you today?"))), /* @__PURE__ */ React.createElement("div", { className: "mb-6 flex w-full flex-col items-center gap-2" }, QUICK_ACTIONS.map((action) => /* @__PURE__ */ React.createElement( Suggestion, { key: action, onClick: onQuickAction, suggestion: action } ))), /* @__PURE__ */ React.createElement("div", { className: "w-full" }, /* @__PURE__ */ React.createElement( PromptInput, { onSubmit: (message, event) => { event.preventDefault(); if (message.text?.trim()) { onSubmit(event); } } }, /* @__PURE__ */ React.createElement( PromptInputTextarea, { onChange: onInputChange, placeholder: "Message Franklin...", value: inputValue } ), /* @__PURE__ */ React.createElement(PromptInputFooter, null, /* @__PURE__ */ React.createElement(PromptInputTools, null, /* @__PURE__ */ React.createElement(PromptInputButton, { title: "Attach file" }, /* @__PURE__ */ React.createElement(Paperclip, { className: "size-4" })), /* @__PURE__ */ React.createElement(PromptInputButton, { title: "Add image" }, /* @__PURE__ */ React.createElement(ImageIcon, { className: "size-4" })), /* @__PURE__ */ React.createElement(PromptInputButton, { title: "Voice input" }, /* @__PURE__ */ React.createElement(Mic, { className: "size-4" }))), /* @__PURE__ */ React.createElement( PromptInputSubmit, { disabled: !inputValue.trim(), status: isLoading ? "streaming" : "ready" } )) ))); } // src/components/AIChat/ai-chat.tsx function AIChat() { const { apiEndpoint, orgId, sdkKey, context } = useFranklinContext(); const [inputValue, setInputValue] = useState(""); const messagesEndRef = useRef(null); const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ api: apiEndpoint, headers: { "X-Franklin-Org-Id": orgId, "X-Franklin-SDK-Key": sdkKey }, body: { context }, initialMessages: [] }); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const onQuickAction = (action) => { const changeEvent = { currentTarget: { value: action }, preventDefault: () => { }, stopPropagation: () => { }, target: { value: action } }; setInputValue(action); handleInputChange(changeEvent); setTimeout(() => { const submitEvent = { preventDefault: () => { }, stopPropagation: () => { } }; handleSubmit(submitEvent); setInputValue(""); }, 100); }; const handleFormSubmit = (event) => { event.preventDefault(); if (inputValue.trim() || input?.trim()) { handleSubmit(event); setInputValue(""); } }; const isSubmitDisabled = !(inputValue.trim() || input?.trim()); return /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col bg-[var(--franklin-surface)]" }, /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-y-auto" }, messages.length === 0 ? /* @__PURE__ */ React.createElement( WelcomeScreen, { inputValue: inputValue || input || "", isLoading, onInputChange: (e) => { setInputValue(e.target.value); handleInputChange(e); }, onQuickAction, onSubmit: handleFormSubmit } ) : /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-4 px-6 pt-6 pb-4" }, messages.map((message) => /* @__PURE__ */ React.createElement( Message, { content: message.content, key: message.id, role: message.role } )), isLoading && /* @__PURE__ */ React.createElement(LoadingMessage, null), /* @__PURE__ */ React.createElement("div", { ref: messagesEndRef }))), messages.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "px-4 pb-4" }, /* @__PURE__ */ React.createElement( PromptInput, { onSubmit: (message, event) => { event.preventDefault(); if (message.text?.trim()) { handleFormSubmit(event); } } }, /* @__PURE__ */ React.createElement( PromptInputTextarea, { onChange: (event) => { setInputValue(event.target.value); handleInputChange(event); }, placeholder: "Message Franklin...", value: inputValue || input || "" } ), /* @__PURE__ */ React.createElement(PromptInputFooter, null, /* @__PURE__ */ React.createElement(PromptInputTools, null, /* @__PURE__ */ React.createElement(PromptInputButton, { title: "Attach file" }, /* @__PURE__ */ React.createElement(Paperclip, { className: "size-4" })), /* @__PURE__ */ React.createElement(PromptInputButton, { title: "Add image" }, /* @__PURE__ */ React.createElement(ImageIcon, { className: "size-4" })), /* @__PURE__ */ React.createElement(PromptInputButton, { title: "Voice input" }, /* @__PURE__ */ React.createElement(Mic, { className: "size-4" }))), /* @__PURE__ */ React.createElement( PromptInputSubmit, { disabled: isSubmitDisabled, status: isLoading ? "streaming" : "ready" } )) ))); } var STATUS_CLASSES = { closed: "bg-gray-100 text-gray-700", open: "bg-green-100 text-green-700", pending: "bg-yellow-100 text-yellow-700", resolved: "bg-gray-100 text-gray-700" }; var AUTHOR_LABEL = { agent: "Support Agent", ai: "Franklin AI", user: "You" }; function Cases() { const { apiEndpoint, orgId, sdkKey, context } = useFranklinContext(); const [cases, setCases] = useState([]); const [selectedCase, setSelectedCase] = useState(null); const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(true); const resolvedUserEmail = resolveUserEmail(context); useEffect(() => { fetchCases(resolvedUserEmail).catch((error) => { console.error("Failed to initialise cases:", error); }); }, [resolvedUserEmail]); const fetchCases = async (userEmail) => { try { setIsLoading(true); const headers = { "X-Franklin-Org-Id": orgId, "X-Franklin-SDK-Key": sdkKey }; if (userEmail) { headers["X-Franklin-User-Email"] = userEmail; } const response = await fetch(`${apiEndpoint}/cases`, { headers }); const data = await response.json(); setCases(data.cases ?? []); } catch (error) { console.error("Failed to fetch cases:", error); } finally { setIsLoading(false); } }; const fetchCaseMessages = async (caseId) => { try { const response = await fetch(`${apiEndpoint}/cases/${caseId}/messages`, { headers: { "X-Franklin-Org-Id": orgId, "X-Franklin-SDK-Key": sdkKey } }); const data = await response.json(); setMessages(data.messages ?? []); } catch (error) { console.error("Failed to fetch case messages:", error); } }; const handleCaseClick = (caseItem) => { setSelectedCase(caseItem); fetchCaseMessages(caseItem.id).catch((error) => { console.error("Failed to load case messages:", error); }); }; const selectedCaseStatusClass = useMemo(() => { if (!selectedCase) { return ""; } return STATUS_CLASSES[selectedCase.status] ?? STATUS_CLASSES.resolved; }, [selectedCase]); if (isLoading) { return /* @__PURE__ */ React.createElement("div", { className: "flex h-full items-center justify-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-gray-500 text-sm" }, "Loading cases...")); } if (selectedCase) { return /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "border-[var(--franklin-border)] border-b px-6 py-4" }, /* @__PURE__ */ React.createElement( "button", { className: "mb-2 text-blue-600 text-sm hover:text-blue-700", onClick: () => setSelectedCase(null), type: "button" }, "\u2190 Back to cases" ), /* @__PURE__ */ React.createElement("h2", { className: "font-semibold text-[var(--franklin-foreground)] text-lg" }, selectedCase.subject), /* @__PURE__ */ React.createElement("div", { className: "mt-2 flex items-center gap-2" }, /* @__PURE__ */ React.createElement( "span", { className: `inline-flex items-center rounded-full px-2 py-1 font-medium text-xs ${selectedCaseStatusClass}` }, selectedCase.status ), selectedCase.priority && /* @__PURE__ */ React.createElement("span", { className: "text-gray-500 text-xs" }, "Priority: ", selectedCase.priority))), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-y-auto px-6 py-4" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-4" }, messages.map((message) => /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-1", key: message.id }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, /* @__PURE__ */ React.createElement("span", { className: "font-medium text-[var(--franklin-foreground)] text-xs" }, AUTHOR_LABEL[message.authorType]), /* @__PURE__ */ React.createElement("span", { className: "text-gray-500 text-xs" }, formatDate(message.createdAt))), /* @__PURE__ */ React.createElement("div", { className: "text-gray-700 text-sm" }, message.body)))))); } return /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "border-[var(--franklin-border)] border-b px-6 py-4" }, /* @__PURE__ */ React.createElement("h2", { className: "font-semibold text-[var(--franklin-foreground)] text-lg" }, "My Cases")), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-y-auto px-6 py-4" }, cases.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "py-8 text-center text-gray-500 text-sm" }, "You don't have any open cases") : /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, cases.map((caseItem) => /* @__PURE__ */ React.createElement( "button", { className: "w-full rounded-lg border border-[var(--franklin-border)] p-4 text-left transition-colors hover:bg-gray-50", key: caseItem.id, onClick: () => handleCaseClick(caseItem), type: "button" }, /* @__PURE__ */ React.createElement("div", { className: "mb-2 flex items-start justify-between" }, /* @__PURE__ */ React.createElement("h3", { className: "font-medium text-[var(--franklin-foreground)] text-sm" }, caseItem.subject), /* @__PURE__ */ React.createElement( "span", { className: `inline-flex items-center rounded-full px-2 py-1 font-medium text-xs ${STATUS_CLASSES[caseItem.status] ?? STATUS_CLASSES.resolved}` }, caseItem.status )), /* @__PURE__ */ React.createElement("p", { className: "text-[var(--franklin-muted)] text-xs" }, "Updated ", formatDate(caseItem.updatedAt)) ))))); } function resolveUserEmail(context) { const metadataEmail = typeof context.metadata === "object" && context.metadata !== null ? context.metadata.email ?? context.metadata.userEmail : void 0; if (metadataEmail) { return metadataEmail; } if (typeof context.userId === "string" && context.userId.includes("@")) { return context.userId; } return; } function KnowledgeBase() { const { apiEndpoint, orgId, sdkKey } = useFranklinContext(); const [articles, setArticles] = useState([]); const [_categories, setCategories] = useState([]); const [selectedArticle, setSelectedArticle] = useState(null); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { fetchKnowledgeBase(); }, []); const fetchKnowledgeBase = async () => { try { setIsLoading(true); const response = await fetch(`${apiEndpoint}/knowledge-base`, { headers: { "X-Franklin-Org-Id": orgId, "X-Franklin-SDK-Key": sdkKey } }); const data = await response.json(); setArticles(data.articles || []); setCategories(data.categories || []); } catch (error) { console.error("Failed to fetch knowledge base:", error); } finally { setIsLoading(false); } }; const filteredArticles = articles.filter( (article) => article.title.toLowerCase().includes(searchQuery.toLowerCase()) ); if (isLoading) { return /* @__PURE__ */ React.createElement("div", { className: "flex h-full items-center justify-center" }, /* @__PURE__ */ React.createElement("div", { className: "text-gray-500 text-sm" }, "Loading knowledge base...")); } if (selectedArticle) { return /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "border-[var(--franklin-border)] border-b px-6 py-4" }, /* @__PURE__ */ React.createElement( "button", { className: "mb-2 text-blue-600 text-sm hover:text-blue-700", onClick: () => setSelectedArticle(null), type: "button" }, "\u2190 Back to articles" ), /* @__PURE__ */ React.createElement("h2", { className: "font-semibold text-[var(--franklin-foreground)] text-xl" }, selectedArticle.title)), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-y-auto px-6 py-6" }, /* @__PURE__ */ React.createElement( "div", { className: "prose prose-sm max-w-none text-gray-700", dangerouslySetInnerHTML: { __html: selectedArticle.content } } ))); } return /* @__PURE__ */ React.createElement("div", { className: "flex h-full flex-col" }, /* @__PURE__ */ React.createElement("div", { className: "border-[var(--franklin-border)] border-b px-6 py-4" }, /* @__PURE__ */ React.createElement("h2", { className: "mb-3 font-semibold text-[var(--franklin-foreground)] text-lg" }, "Knowledge Base"), /* @__PURE__ */ React.createElement( "input", { className: "w-full rounded-lg border border-[var(--franklin-border)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500", onChange: (event) => setSearchQuery(event.target.value), placeholder: "Search articles...", type: "text", value: searchQuery } )), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-y-auto px-6 py-4" }, filteredArticles.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "py-8 text-center text-gray-500 text-sm" }, searchQuery ? "No articles found" : "No articles available") : /* @__PURE__ */ React.createElement("div", { className: "space-y-3" }, filteredArticles.map((article) => /* @__PURE__ */ React.createElement( "button", { className: "w-full rounded-lg border border-[var(--franklin-border)] p-4 text-left transition-colors hover:bg-gray-50", key: article.id, onClick: () => setSelectedArticle(article), type: "button" }, /* @__PURE__ */ React.createElement("h3", { className: "mb-1 font-medium text-[var(--franklin-foreground)] text-sm" }, article.title), article.excerpt && /* @__PURE__ */ React.createElement("p", { className: "line-clamp-2 text-[var(--franklin-muted)] text-xs" }, article.excerpt) ))))); } var NAV_ITEMS = [ { icon: FileText, label: "Knowledge Base", tab: "knowledge" }, { icon: Folder, label: "My Cases", tab: "cases" }, { icon: Plus, label: "Submit a New Case", tab: "ai" }, { icon: MessageSquare, label: "Speak with Support", tab: "ai" } ]; function SidebarFooter() { const { setActiveTab } = useFranklinContext(); return /* @__PURE__ */ React.createElement( "div", { className: "border-t", style: { borderColor: "rgba(0, 0, 0, 0.06)", backgroundColor: "var(--franklin-surface)", paddingBottom: "20px" } }, /* @__PURE__ */ React.createElement("div", { className: "space-y-3 px-6 pt-6 pb-2" }, NAV_ITEMS.map((item) => { const Icon = item.icon; return /* @__PURE__ */ React.createElement( "button", { className: "flex w-full items-center gap-3 rounded-lg px-3 py-2.5 font-normal text-sm transition-all hover:bg-gray-50", key: item.label, onClick: () => setActiveTab(item.tab), style: { color: "#6B7280" }, type: "button" }, /* @__PURE__ */ React.createElement(Icon, { className: "h-4 w-4", strokeWidth: 1.5 }), /* @__PURE__ */ React.createElement("span", null, item.label) ); })), /* @__PURE__ */ React.createElement( "div", { className: "mx-6 mt-6 border-t pt-4 pb-16", style: { borderColor: "rgba(0, 0, 0, 0.06)" } }, /* @__PURE__ */ React.createElement( "p", { className: "text-center font-normal text-xs", style: { color: "#9CA3AF" } }, "Powered by", " ", /* @__PURE__ */ React.createElement("span", { className: "font-medium", style: { color: "#6B7280" } }, "Franklin") ) ) ); } function SidebarHeader() { const { close } = useFranklinContext(); return /* @__PURE__ */ React.createElement( "div", { className: "flex h-16 items-center justify-between border-b px-6", style: { borderColor: "rgba(0, 0, 0, 0.06)", backgroundColor: "var(--franklin-surface)" } }, /* @__PURE__ */ React.createElement( "button", { className: "flex h-9 items-center gap-2 rounded-lg px-3 font-normal text-sm transition-colors hover:bg-gray-50", onClick: close, style: { color: "#6B7280" }, type: "button" }, /* @__PURE__ */ React.createElement(ArrowLeft, { className: "h-4 w-4", strokeWidth: 1.5 }), /* @__PURE__ */ React.createElement("span", null, "Back to App") ), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1" }, /* @__PURE__ */ React.createElement( "button", { className: "flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-gray-50", style: { color: "#9CA3AF" }, title: "Search", type: "button" }, /* @__PURE__ */ React.createElement(Search, { className: "h-[18px] w-[18px]", strokeWidth: 1.5 }) ), /* @__PURE__ */ React.createElement( "button", { className: "flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-gray-50", style: { color: "#9CA3AF" }, title: "Settings", type: "button" }, /* @__PURE__ */ React.createElement(Settings, { className: "h-[18px] w-[18px]", strokeWidth: 1.5 }) ), /* @__PURE__ */ React.createElement( "button", { className: "flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-gray-50", onClick: close, style: { color: "#9CA3AF" }, title: "Close", type: "button" }, /* @__PURE__ */ React.createElement(X, { className: "h-[18px] w-[18px]", strokeWidth: 1.5 }) )) ); } // src/components/Sidebar/sidebar.tsx function Sidebar() { const { isOpen, activeTab } = useFranklinContext(); const sidebarWidth = "var(--franklin-width, 420px)"; const renderTabContent = () => { switch (activeTab) { case "ai": return /* @__PURE__ */ React.createElement(AIChat, null); case "knowledge": return /* @__PURE__ */ React.createElement(KnowledgeBase, null); case "cases": return /* @__PURE__ */ React.createElement(Cases, null); default: return /* @__PURE__ */ React.createElement(AIChat, null); } }; return /* @__PURE__ */ React.createElement(AnimatePresence, null, isOpen && /* @__PURE__ */ React.createElement( motion.aside, { animate: { width: sidebarWidth, opacity: 1 }, className: cn( "franklin-sidebar fixed top-0 right-0 z-[var(--franklin-z-index,9999)] h-full shadow-[0px_25px_50px_-12px_rgba(0,0,0,0.25)]", "flex flex-col overflow-hidden" ), exit: { width: 0, opacity: 0 }, initial: { width: 0, opacity: 0 }, style: { maxWidth: "100vw", width: sidebarWidth, backgroundColor: "var(--franklin-surface)", color: "var(--franklin-foreground)" }, transition: { type: "spring", damping: 30, stiffness: 300 } }, /* @__PURE__ */ React.createElement(SidebarHeader, null), /* @__PURE__ */ React.createElement("div", { className: "flex-1 overflow-hidden" }, renderTabContent()), /* @__PURE__ */ React.createElement(SidebarFooter, null) )); } // src/components/franklin-help-center.tsx var DEFAULT_SIDEBAR_WIDTH = "420px"; var FranklinHelpCenterInner = forwardRef( (_, ref) => { const context = useFranklinContext(); const { isOpen } = context; useImperativeHandle(ref, () => ({ open: context.open, close: context.close, toggle: context.toggle, pushContext: context.pushContext, setTab: context.setActiveTab })); useEffect(() => { if (isOpen) { document.body.classList.add("franklin-sidebar-open"); } else { document.body.classList.remove("franklin-sidebar-open"); } }, [isOpen]); return /* @__PURE__ */ React.createElement(Sidebar, null); } ); FranklinHelpCenterInner.displayName = "FranklinHelpCenterInner"; var FranklinHelpCenter = forwardRef( ({ orgId, sdkKey, shiftTargetSelector, defaultTab = "ai", theme, onOpen, onClose, apiEndpoint = "/api/franklin" }, ref) => { const targetRef = useRef(null); const previousWidthRef = useRef(null); useEffect(() => { if (typeof document === "undefined") { return; } let element = document.body; if (shiftTargetSelector) { const queried = document.querySelector( shiftTargetSelector ); if (queried) { element = queried; } } targetRef.current = element; if (element) { element.setAttribute("data-franklin-shift", ""); } return () => { element?.removeAttribute("data-franklin-shift"); }; }, [shiftTargetSelector]); useEffect(() => { if (typeof document === "undefined") { return; } const root = document.documentElement; const current = root.style.getPropertyValue("--franklin-width"); if (current) { previousWidthRef.current = current; } else { previousWidthRef.current = null; root.style.setProperty("--franklin-width", DEFAULT_SIDEBAR_WIDTH); } return () => { if (previousWidthRef.current === null) { root.style.removeProperty("--franklin-width"); } else if (previousWidthRef.current) { root.style.setProperty("--franklin-width", previousWidthRef.current); } }; }, []); useEffect(() => { if (typeof document === "undefined" || !theme) { return; } const root = document.documentElement; const previousValues = /* @__PURE__ */ new Map(); let previousThemeAttr = null; const setVariable = (token, value) => { if (value === void 0) { return; } const formatted = typeof value === "number" ? `${value}px` : value.toString(); previousValues.set(token, root.style.getPropertyValue(token)); root.style.setProperty(token, formatted); }; setVariable("--franklin-accent", theme.accent); setVariable("--franklin-surface", theme.surface); setVariable("--franklin-border", theme.border); setVariable("--franklin-radius", theme.radius); setVariable("--franklin-font", theme.fontFamily); if (typeof document !== "undefined") { previousThemeAttr = document.body.getAttribute("data-franklin-theme"); if (theme.mode && theme.mode !== "auto") { document.body.setAttribute("data-franklin-theme", theme.mode); } else if (theme.mode === "auto") { document.body.removeAttribute("data-franklin-theme"); } } return () => { previousValues.forEach((value, token) => { if (value) { root.style.setProperty(token, value); } else { root.style.removeProperty(token); } }); if (typeof document !== "undefined") { if (previousThemeAttr) { document.body.setAttribute( "data-franklin-theme", previousThemeAttr ); } else { document.body.removeAttribute("data-franklin-theme"); } } }; }, [theme]); useEffect(() => { const handleShift = (event) => { const { detail } = event; const target = targetRef.current; if (!target) { return; } const computedWidth = typeof document !== "undefined" ? getComputedStyle(document.documentElement).getPropertyValue("--franklin-width").trim() : ""; const sidebarWidth = computedWidth || DEFAULT_SIDEBAR_WIDTH; target.style.transition = "margin-right 0.4s cubic-bezier(0.4, 0, 0.2, 1)"; if (detail?.isOpen) { target.style.marginRight = sidebarWidth; } else { target.style.marginRight = ""; } }; window.addEventListener("franklin:state-change", handleShift); return () => { window.removeEventListener("franklin:state-change", handleShift); if (targetRef.current) { targetRef.current.style.marginRight = ""; targetRef.current.style.transition = ""; } }; }, [shiftTargetSelector]); return /* @__PURE__ */ React.createElement( FranklinProvider, { apiEndpoint, defaultTab, onClose: () => { window.dispatchEvent( new CustomEvent("franklin:state-change", { detail: { isOpen: false, target: shiftTargetSelector ?? "body" } }) ); onClose?.(); }, onOpen: () => { window.dispatchEvent( new CustomEvent("franklin:state-change", { detail: { isOpen: true, target: shiftTargetSelector ?? "body" } }) ); onOpen?.(); }, orgId, sdkKey, theme }, /* @__PURE__ */ React.createElement(FranklinHelpCenterInner, { ref }) ); } ); FranklinHelpCenter.displayName = "FranklinHelpCenter"; function useFranklin() { const franklinRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState("ai"); useEffect(() => { const handleStateChange = (event) => { const detail = event.detail; if (typeof detail?.isOpen === "boolean") { setIsOpen(detail.isOpen); } if (detail?.tab) { setActiveTab(detail.tab); } }; const handleTabChange = (event) => { const detail = event.detail; if (detail?.tab) { setActiveTab(detail.tab); } }; window.addEventListener("franklin:state-change", handleStateChange); window.addEventListener("franklin:tab-change", handleTabChange); return () => { window.removeEventListener("franklin:state-change", handleStateChange); window.removeEventListener("franklin:tab-change", handleTabChange); }; }, []); const open = useCallback(() => { franklinRef.current?.open(); }, []); const close = useCallback(() => { franklinRef.current?.close(); }, []); const toggle = useCallback(() => { franklinRef.current?.toggle(); }, []); const pushContext = useCallback((payload) => { franklinRef.current?.pushContext(payload); }, []); const setTab = useCallback((tab) => { franklinRef.current?.setTab(tab); }, []); return { ref: franklinRef, open, close, toggle, pushContext, setTab, isOpen, activeTab }; } // src/index.ts var FranklinHelpCenter2 = FranklinHelpCenter; var FranklinProvider2 = FranklinProvider; var useFranklinContext2 = useFranklinContext; var useFranklin2 = useFranklin; var cn2 = cn; var formatDate2 = formatDate; var FRANKLIN_SDK_REACT_ENTRY = true; export { FRANKLIN_SDK_REACT_ENTRY, FranklinHelpCenter2 as FranklinHelpCenter, FranklinProvider2 as FranklinProvider, cn2 as cn, formatDate2 as formatDate, useFranklin2 as useFranklin, useFranklinContext2 as useFranklinContext }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map