import { useEffect, useMemo, useRef, useState } from 'react';
const QUICK_REPLIES = [
{ icon: '🔍', text: 'Find me a product' },
{ icon: '❤️', text: 'Track my order' },
{ icon: '🛒', text: 'View my cart' },
{ icon: '🚚', text: "What's your shipping policy?" },
{ icon: '✅', text: 'How do I return an item?' },
];
function ChatIconSvg({ className }) {
return (
);
}
function ThemeIcon({ mode, className }) {
if (mode === 'light') {
return (
);
}
return (
);
}
function ClearIcon({ className }) {
return (
);
}
function CloseIcon({ className }) {
return (
);
}
function chatStorageKey(isLoggedIn) {
return isLoggedIn ? 'acosmaiaHistoryUser' : 'acosmaiaHistorySession';
}
function decodeHtmlEntities(value) {
if (typeof value !== 'string') return value ?? '';
const textarea = document.createElement('textarea');
textarea.innerHTML = value;
return textarea.value;
}
function renderInlineBold(text) {
const parts = String(text).split(/(\*\*[^*]+\*\*)/g);
return parts.map((part, index) => {
if (part.startsWith('**') && part.endsWith('**') && part.length > 4) {
return {part.slice(2, -2)};
}
return {part};
});
}
function renderMessageContent(value) {
const decoded = decodeHtmlEntities(value || '');
const lines = String(decoded).split('\n');
return lines.map((line, index) => (
{renderInlineBold(line)}
));
}
function TypingIndicator() {
return (
);
}
function ProductCard({ product }) {
const hasSale = Boolean(product.sale_price);
const name = decodeHtmlEntities(product.name || '');
const description = decodeHtmlEntities(product.description || 'View product details.');
const price = decodeHtmlEntities(product.price || '');
const salePrice = decodeHtmlEntities(product.sale_price || '');
const [isAdding, setIsAdding] = useState(false);
const [added, setAdded] = useState(false);
const averageRating = Number(product.average_rating || 0);
const reviewCount = Number(product.review_count || 0);
const showRating = averageRating > 0 || reviewCount > 0;
const getAjaxAddToCartUrl = () => {
const wcAjaxTemplate = window.wc_add_to_cart_params?.wc_ajax_url;
if (typeof wcAjaxTemplate === 'string' && wcAjaxTemplate.includes('%%endpoint%%')) {
return wcAjaxTemplate.replace('%%endpoint%%', 'add_to_cart');
}
const fallback = new URL(window.location.href);
fallback.search = '';
fallback.searchParams.set('wc-ajax', 'add_to_cart');
return fallback.toString();
};
const getCartUrl = () => {
const localized = typeof window.acosmaiaChat?.cart_url === 'string' ? window.acosmaiaChat.cart_url : '';
if (localized) return localized;
const wcCart = window.wc_add_to_cart_params?.cart_url;
if (typeof wcCart === 'string' && wcCart) return wcCart;
const fallback = new URL(window.location.href);
fallback.search = '';
fallback.hash = '';
fallback.pathname = '/cart/';
return fallback.toString();
};
const handleAddToCart = async (e) => {
e.preventDefault();
e.stopPropagation();
if (added) {
window.location.href = getCartUrl();
return;
}
if (isAdding) return;
const productId = Number(product.id || 0);
if (!productId) return;
setIsAdding(true);
try {
const body = new URLSearchParams();
body.set('product_id', String(productId));
body.set('quantity', '1');
const response = await fetch(getAjaxAddToCartUrl(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
},
credentials: 'same-origin',
body: body.toString(),
});
const data = await response.json().catch(() => null);
if (!response.ok || !data || data.error) {
throw new Error('Add to cart failed');
}
// Let WooCommerce themes refresh cart fragments/minicart if they listen.
if (window.jQuery && typeof window.jQuery === 'function') {
window.jQuery(document.body).trigger('added_to_cart', [data.fragments, data.cart_hash]);
}
setAdded(true);
} catch {
// Fallback to classic add-to-cart URL if AJAX fails.
const fallbackUrl = typeof product.add_to_cart_url === 'string' ? product.add_to_cart_url.trim() : '';
if (fallbackUrl) {
window.location.href = fallbackUrl;
}
} finally {
setIsAdding(false);
}
};
return (
{product.image ?
: No image
}
{name}
{description}
{showRating ? (
Rating: {averageRating > 0 ? averageRating.toFixed(1) : 'N/A'}
({reviewCount} reviews)
) : null}
{product.stock_status === 'instock' ? 'In stock' : 'Out of stock'}
{hasSale ? (
<>
{salePrice}
{price}
>
) : (
{price}
)}
{product.add_to_cart_url ? (
) : null}
);
}
export default function App() {
const {
assistant_name,
welcome_message,
api_url,
nonce,
position = 'bottom-right',
primary_color = '#0073aa',
default_theme = 'light',
is_logged_in: isLoggedIn = false,
api_configured = false,
} = window.acosmaiaChat || {};
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [theme, setTheme] = useState(default_theme);
const [showSuggestions, setShowSuggestions] = useState(true);
const messagesEnd = useRef(null);
const storageKey = useMemo(() => chatStorageKey(isLoggedIn), [isLoggedIn]);
useEffect(() => {
document.documentElement.style.setProperty('--woo-chat-primary', primary_color);
}, [primary_color]);
useEffect(() => {
const raw = localStorage.getItem(storageKey);
if (!raw) return;
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed.messages)) {
setMessages(parsed.messages);
}
const parsedDefaultTheme = parsed?.default_theme === 'dark' ? 'dark' : 'light';
if ((parsed.theme === 'light' || parsed.theme === 'dark') && parsedDefaultTheme === default_theme) {
setTheme(parsed.theme);
}
} catch {
localStorage.removeItem(storageKey);
}
}, [storageKey]);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify({ messages, theme, default_theme }));
messagesEnd.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, theme, default_theme, storageKey]);
useEffect(() => {
if (!isOpen) return;
// Wait for chat window mount and then jump to latest message.
requestAnimationFrame(() => {
messagesEnd.current?.scrollIntoView({ behavior: 'auto', block: 'end' });
});
}, [isOpen]);
useEffect(() => {
if (isOpen && messages.length === 0) {
const initialMessage = api_configured
? (welcome_message || 'Hi! How can I help you today?')
: 'Please configure the AI API key in the admin settings to use this chat feature.';
setMessages([{ role: 'assistant', content: initialMessage, products: [] }]);
}
}, [isOpen, messages.length, welcome_message, api_configured]);
const send = async (forcedMessage) => {
const text = (forcedMessage ?? input).trim();
if (!text || isTyping) return;
const userMsg = { role: 'user', content: text, products: [] };
setMessages((prev) => [...prev, userMsg]);
setInput('');
setIsTyping(true);
try {
const res = await fetch(api_url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-AI-Nonce': nonce,
},
body: JSON.stringify({ message: text }),
});
const data = await res.json();
setMessages((prev) => [
...prev,
{
role: 'assistant',
content: data.message || data.error || 'No response.',
show_more_url: typeof data.show_more_url === 'string' ? data.show_more_url : '',
show_more_label: typeof data.show_more_label === 'string' && data.show_more_label ? data.show_more_label : 'Show more products',
products: Array.isArray(data.products) ? data.products : [],
},
]);
} catch {
setMessages((prev) => [...prev, {
role: 'assistant',
content: 'Connection error. Please try again.',
show_more_url: '',
show_more_label: '',
products: [],
}]);
} finally {
setIsTyping(false);
}
};
const clear = () => {
setMessages([]);
localStorage.removeItem(storageKey);
};
const toggleOpen = () => {
setIsOpen((prev) => !prev);
};
const bubbleClass = `woo-chat-bubble ${position}`;
const windowClass = `woo-chat-window ${position} ${theme}`;
return (
<>
{isOpen ? (
{assistant_name || 'AI Assistant'}
{messages.map((m, i) => (
{typeof m.content === 'string' && m.content.trim() ? (
{renderMessageContent(m.content)}
) : null}
{m.role === 'assistant' && Array.isArray(m.products) && m.products.length > 0 ? (
{m.products.slice(0, 6).map((product) => (
))}
) : null}
{m.role === 'assistant' && m.show_more_url ? (
) : null}
))}
{isTyping ?
: null}
{showSuggestions && (
{api_configured && QUICK_REPLIES.map((reply) => (
))}
)}
setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && send()}
placeholder={api_configured ? "Ask about products, price, stock, or policies..." : "API key not configured"}
disabled={!api_configured}
/>
) : null}
>
);
}