@franklinhelp/sdk-react
Version:
Embeddable AI-native help center for modern SaaS applications
898 lines (892 loc) • 35.3 kB
JavaScript
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