UNPKG

@jutech-devs/agent-sdk

Version:

Modern embeddable AI agent chat widget with voice support and payment integration

1 lines 266 kB
{"version":3,"sources":["../src/AgentWidget.tsx","../src/components/ChatBubble.tsx","../src/components/ChatWindow.tsx","../src/components/MessageList.tsx","../src/hooks/useVoice.ts","../src/components/MessageInput.tsx","../src/components/ProductCard.tsx","../src/components/OrdersView.tsx","../src/components/ServiceCard.tsx","../src/components/BookingsView.tsx","../src/components/PaymentOptionsView.tsx","../src/components/InvoiceView.tsx","../src/components/FeedbackView.tsx","../src/components/CartView.tsx","../src/components/OnlineStatus.tsx","../src/components/PaymentModal.tsx","../src/components/AddressForm.tsx","../src/components/BookingForm.tsx","../src/hooks/useChat.ts","../src/api.ts","../src/pages/ChatbotEmbed.tsx","../src/components/EmbeddedChatWindow.tsx"],"sourcesContent":["\"use client\"\n\nimport { useState, useEffect } from \"react\"\nimport { ChatBubble } from \"./components/ChatBubble\"\nimport { ChatWindow } from \"./components/ChatWindow\"\nimport { PaymentModal } from \"./components/PaymentModal\"\nimport { AddressForm } from \"./components/AddressForm\"\nimport { BookingForm } from \"./components/BookingForm\"\nimport { CartView } from \"./components/CartView\"\nimport { useChat } from \"./hooks/useChat\"\nimport type { AgentWidgetProps, AgentConfig } from \"./types\"\nimport \"./styles/widget.css\"\n\nexport function AgentWidget(props: AgentWidgetProps) {\n const {\n apiKey,\n agentId,\n baseUrl = typeof window !== 'undefined' && window.location.hostname === 'localhost' \n ? 'http://localhost:3001' \n : \"https://jutech-agent-server.onrender.com\",\n user,\n onUserUpdate,\n className = \"\",\n disableAutoConfig = false,\n // Default values that can be overridden by server config or props\n theme,\n position,\n primaryColor,\n allowFileUpload: propAllowFileUpload = false,\n placeholder,\n title,\n subtitle,\n } = props\n\n const [isOpen, setIsOpen] = useState(false)\n const [unreadCount, setUnreadCount] = useState(0)\n const [config, setConfig] = useState<AgentConfig | null>(null)\n const [configLoading, setConfigLoading] = useState(!disableAutoConfig)\n\n useEffect(() => {\n if (disableAutoConfig) {\n setConfigLoading(false)\n return\n }\n\n const fetchConfig = async () => {\n try {\n const url = `${baseUrl}/api/chat/agents/${agentId}/public-config?apiKey=${apiKey}`\n console.log(\"Fetching config from:\", url)\n const response = await fetch(url)\n console.log(\"Response status:\", response.status)\n if (response.ok) {\n const agentConfig = await response.json()\n console.log(\"Raw agent config:\", agentConfig)\n setConfig(agentConfig)\n } else {\n const errorText = await response.text()\n console.error(\"Config fetch failed:\", response.status, errorText)\n }\n } catch (error) {\n console.warn(\"Failed to fetch agent config, using props:\", error)\n } finally {\n setConfigLoading(false)\n }\n }\n\n fetchConfig()\n }, [agentId, baseUrl, disableAutoConfig])\n console.log(\"Fetched agent config:\", config)\n\n const agentConfig = (config as any)?.config || config\n\n const finalConfig = {\n theme: agentConfig?.behavior?.theme || theme || \"light\",\n position: agentConfig?.behavior?.position || position || \"bottom-right\",\n primaryColor: agentConfig?.behavior?.primaryColor || primaryColor || \"#3b82f6\",\n allowFileUpload: agentConfig?.paymentEnabled || propAllowFileUpload || false,\n bookingEnabled: agentConfig?.features?.bookingEnabled || false,\n placeholder: agentConfig?.behavior?.placeholder || placeholder || \"Type your message...\",\n title: agentConfig?.name || title || \"AI Assistant\",\n subtitle: agentConfig?.businessDescription || subtitle || \"How can I help you today?\",\n }\n\n // Apply theming via inline styles to override any conflicts\n const widgetStyle = {\n '--aw-primary-color': finalConfig.primaryColor,\n '--aw-primary-hover': adjustColor(finalConfig.primaryColor, -10),\n } as React.CSSProperties\n\n function adjustColor(color: string, percent: number): string {\n const num = parseInt(color.replace(\"#\", \"\"), 16)\n const amt = Math.round(2.55 * percent)\n const R = (num >> 16) + amt\n const G = (num >> 8 & 0x00FF) + amt\n const B = (num & 0x0000FF) + amt\n return \"#\" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +\n (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +\n (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)\n }\n\n console.log(\"Final config:\", finalConfig)\n console.log(\"Agent config paystackPublicKey:\", agentConfig?.paystackPublicKey)\n console.log(\"Full agent config:\", agentConfig)\n\n const {\n messages,\n isLoading,\n error,\n sendMessage,\n clearMessages,\n initializeChat,\n user: chatUser,\n isCollectingUserInfo,\n products,\n loadingProducts,\n loadProducts,\n createOrder,\n completePaymentOrder,\n completePaymentBooking,\n // completePayment, --- IGNORE ---\n conversationId,\n orders,\n loadOrders,\n services,\n loadServices,\n createBooking,\n bookings,\n loadBookings,\n setUserInfo,\n endConversation,\n } = useChat(apiKey, agentId, baseUrl, user, agentConfig?.behavior?.voiceEnabled, onUserUpdate)\n\n console.log(\"Products loaded:\", products)\n console.log(\"Products length:\", products.length)\n\n const [showProducts, setShowProducts] = useState(false)\n const [showOrders, setShowOrders] = useState(false)\n const [showServices, setShowServices] = useState(false)\n const [showBookings, setShowBookings] = useState(false)\n const [showPaymentOptions, setShowPaymentOptions] = useState(false)\n const [showInvoice, setShowInvoice] = useState(false)\n const [showFeedback, setShowFeedback] = useState(false)\n const [showCart, setShowCart] = useState(false)\n const [cartItems, setCartItems] = useState<any[]>([])\n const [paymentModal, setPaymentModal] = useState<{\n isOpen: boolean\n product: any\n order: any\n }>({ isOpen: false, product: null, order: null })\n const [addressForm, setAddressForm] = useState<{\n isOpen: boolean\n product: any\n }>({ isOpen: false, product: null })\n const [bookingForm, setBookingForm] = useState<{\n isOpen: boolean\n service: any\n }>({ isOpen: false, service: null })\n const [isAddressLoading, setIsAddressLoading] = useState(false)\n const [isPaymentLoading, setIsPaymentLoading] = useState(false)\n\n useEffect(() => {\n if (isOpen && messages.length === 0 && !user) {\n initializeChat()\n }\n if (isOpen && products.length === 0) {\n loadProducts()\n }\n if (isOpen && services.length === 0) {\n loadServices()\n }\n }, [isOpen, messages.length, user, initializeChat, products.length, loadProducts])\n\n // Set user info when provided via props or when conversation starts\n useEffect(() => {\n if (user && setUserInfo) {\n setUserInfo(user)\n }\n }, [user, setUserInfo])\n\n const handleBuyProduct = async (product: any) => {\n setAddressForm({ isOpen: true, product })\n }\n\n const handleAddressSubmit = async (address: any) => {\n setIsAddressLoading(true)\n try {\n const order = await createOrder(addressForm.product, address)\n console.log('Order created with Paystack key:', order.paystackPublicKey)\n console.log('Agent config Paystack key:', agentConfig?.paystackPublicKey)\n setAddressForm({ isOpen: false, product: null })\n setPaymentModal({ isOpen: true, product: addressForm.product, order })\n } catch (error) {\n alert(`Failed to create order: ${error instanceof Error ? error.message : 'Unknown error'}`)\n } finally {\n setIsAddressLoading(false)\n }\n }\n\n const handlePayOrder = async (order: any) => {\n const product = { _id: order.productId._id, name: order.productId.name }\n setPaymentModal({ isOpen: true, product, order: { ...order, orderId: order._id } })\n }\n\n const handlePaymentSuccess = async (reference: string) => {\n setIsPaymentLoading(true)\n try {\n if (paymentModal.order?.bookingId) {\n // This is a booking payment\n await completePaymentBooking(paymentModal.order.bookingId, reference)\n alert('Payment successful! Your booking has been confirmed.')\n loadBookings()\n } else if (paymentModal.order.orderId) {\n // This is a product order payment\n await completePaymentOrder(paymentModal.order.orderId, reference)\n alert('Payment successful! Your order has been confirmed.')\n loadOrders()\n }\n\n setPaymentModal({ isOpen: false, product: null, order: null })\n } catch (error) {\n alert('Payment verification failed. Please contact support.')\n } finally {\n setIsPaymentLoading(false)\n }\n }\n\n const handlePaymentError = (error: string) => {\n alert(`Payment failed: ${error}`)\n }\n\n const handleBookService = async (service: any) => {\n setBookingForm({ isOpen: true, service })\n }\n\n const handleBookingSubmit = async (bookingData: any) => {\n try {\n if (bookingData.requiresPayment) {\n // Create booking first\n const bookingResponse = await fetch(`${baseUrl}/api/chat/agents/${agentId}/bookings?apiKey=${apiKey}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n serviceId: bookingForm.service._id,\n customerName: chatUser?.name,\n customerEmail: chatUser?.email,\n serviceName: bookingForm.service.name,\n date: bookingData.date,\n time: bookingData.time,\n duration: bookingData.duration,\n location: bookingData.location,\n notes: bookingData.notes\n })\n })\n\n if (!bookingResponse.ok) {\n throw new Error(`Failed to create booking: ${bookingResponse.statusText}`)\n }\n\n const booking = await bookingResponse.json()\n \n // Create payment order for the booking\n const order = {\n orderId: `booking_${booking.booking._id}`,\n bookingId: booking.booking._id,\n amount: bookingData.paymentAmount || bookingForm.service.price,\n currency: bookingForm.service.currency || 'GHS',\n itemType: 'service',\n itemName: bookingForm.service.name,\n paystackPublicKey: agentConfig?.paystackPublicKey\n }\n \n setBookingForm({ isOpen: false, service: null })\n setPaymentModal({ isOpen: true, product: bookingForm.service, order })\n } else {\n // Create booking directly without payment\n const response = await fetch(`${baseUrl}/api/chat/agents/${agentId}/bookings?apiKey=${apiKey}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n serviceId: bookingForm.service._id,\n customerName: chatUser?.name,\n customerEmail: chatUser?.email,\n serviceName: bookingForm.service.name,\n date: bookingData.date,\n time: bookingData.time,\n duration: bookingData.duration,\n location: bookingData.location,\n notes: bookingData.notes\n })\n })\n\n if (!response.ok) {\n throw new Error(`Failed to create booking: ${response.statusText}`)\n }\n\n alert('Booking created successfully!')\n setBookingForm({ isOpen: false, service: null })\n }\n } catch (error) {\n alert(`Failed to create booking: ${error instanceof Error ? error.message : 'Unknown error'}`)\n }\n }\n\n useEffect(() => {\n if (!isOpen && messages.length > 0) {\n const lastMessage = messages[messages.length - 1]\n if (lastMessage.role === \"assistant\" && !lastMessage.isStreaming) {\n setUnreadCount((prev) => prev + 1)\n }\n }\n }, [messages, isOpen])\n\n // Auto-open views based on metadata\n useEffect(() => {\n const lastMessage = messages[messages.length - 1]\n if (lastMessage?.metadata && !lastMessage.isStreaming) {\n if (lastMessage.metadata.showProducts) setShowProducts(true)\n if (lastMessage.metadata.showServices) setShowServices(true)\n if (lastMessage.metadata.showCart) setShowCart(true)\n if (lastMessage.metadata.showPaymentOptions) setShowPaymentOptions(true)\n if (lastMessage.metadata.showInvoice) setShowInvoice(true)\n if (lastMessage.metadata.requestFeedback) setShowFeedback(true)\n }\n }, [messages])\n\n const handleOpen = () => {\n setIsOpen(true)\n setUnreadCount(0)\n }\n\n const handleClose = () => {\n setIsOpen(false)\n }\n\n useEffect(() => {\n if (finalConfig.theme === \"dark\") {\n document.documentElement.classList.add(\"dark\")\n } else if (finalConfig.theme === \"light\") {\n document.documentElement.classList.remove(\"dark\")\n }\n }, [finalConfig.theme])\n\n if (configLoading) {\n return (\n <div className={`agent-widget ${className}`}>\n <div className=\"fixed bottom-4 right-4 w-14 h-14 bg-gray-200 rounded-full animate-pulse\" />\n </div>\n )\n }\n\n return (\n <div\n className={`agent-widget ${finalConfig.theme === 'dark' ? 'dark' : ''} ${className}`}\n style={widgetStyle}\n >\n <ChatBubble\n isOpen={isOpen}\n onClick={handleOpen}\n primaryColor={finalConfig.primaryColor}\n unreadCount={unreadCount}\n position={finalConfig.position}\n />\n\n <ChatWindow\n isOpen={isOpen}\n onClose={handleClose}\n messages={messages}\n isLoading={isLoading}\n error={error}\n onSendMessage={sendMessage}\n onClearMessages={clearMessages}\n primaryColor={finalConfig.primaryColor}\n title={finalConfig.title}\n subtitle={finalConfig.subtitle}\n placeholder={finalConfig.placeholder}\n position={finalConfig.position}\n products={products}\n onBuyProduct={handleBuyProduct}\n showProducts={showProducts}\n onToggleProducts={() => setShowProducts(!showProducts)}\n voiceEnabled={agentConfig?.behavior?.voiceEnabled}\n orders={orders}\n showOrders={showOrders}\n onToggleOrders={() => {\n setShowOrders(!showOrders)\n if (!showOrders) {\n loadOrders()\n }\n }}\n onPayOrder={handlePayOrder}\n services={services}\n onBookService={handleBookService}\n showServices={showServices}\n onToggleServices={() => {\n setShowServices(!showServices)\n if (!showServices) {\n loadServices()\n }\n }}\n bookings={bookings}\n showBookings={showBookings}\n onToggleBookings={() => {\n setShowBookings(!showBookings)\n if (!showBookings) {\n loadBookings()\n }\n }}\n bookingEnabled={finalConfig.bookingEnabled}\n onEndConversation={endConversation}\n showPaymentOptions={showPaymentOptions}\n onTogglePaymentOptions={() => setShowPaymentOptions(!showPaymentOptions)}\n showInvoice={showInvoice}\n onToggleInvoice={() => setShowInvoice(!showInvoice)}\n showFeedback={showFeedback}\n onToggleFeedback={() => setShowFeedback(!showFeedback)}\n onSubmitFeedback={async (feedback) => {\n console.log('Feedback submitted:', feedback)\n // TODO: Send feedback to server\n }}\n showCart={showCart}\n onToggleCart={() => setShowCart(!showCart)}\n cartItems={cartItems}\n onUpdateCartQuantity={(id, quantity) => {\n if (quantity === 0) {\n setCartItems(prev => prev.filter(item => item.id !== id))\n } else {\n setCartItems(prev => prev.map(item => \n item.id === id ? { ...item, quantity } : item\n ))\n }\n }}\n onRemoveCartItem={(id) => {\n setCartItems(prev => prev.filter(item => item.id !== id))\n }}\n onAddCartItem={(productName) => {\n const product = products.find(p => \n p.name.toLowerCase().includes(productName.toLowerCase())\n )\n if (product) {\n const existingItem = cartItems.find(item => item.id === product._id)\n if (existingItem) {\n setCartItems(prev => prev.map(item => \n item.id === product._id \n ? { ...item, quantity: item.quantity + 1 }\n : item\n ))\n } else {\n setCartItems(prev => [...prev, {\n id: product._id,\n name: product.name,\n price: product.price,\n quantity: 1,\n image: product.images?.[0]\n }])\n }\n }\n }}\n onCartCheckout={async () => {\n if (cartItems.length === 0) return\n \n try {\n // Create a combined order for all cart items\n const total = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)\n const itemNames = cartItems.map(item => `${item.name} (x${item.quantity})`).join(', ')\n \n const order = await createOrder(\n null, // No single product\n null, // No delivery address for now\n {\n amount: total,\n currency: products[0]?.currency || 'GHS',\n itemName: `Cart Items: ${itemNames}`,\n itemType: 'cart',\n cartItems: cartItems\n }\n )\n \n setShowCart(false)\n setCartItems([]) // Clear cart after checkout\n setPaymentModal({ \n isOpen: true, \n product: { name: `Cart (${cartItems.length} items)` }, \n order \n })\n } catch (error) {\n alert(`Failed to create order: ${error instanceof Error ? error.message : 'Unknown error'}`)\n }\n }}\n />\n\n <PaymentModal\n isOpen={paymentModal.isOpen}\n onClose={() => setPaymentModal({ isOpen: false, product: null, order: null })}\n product={paymentModal.product}\n order={paymentModal.order}\n customerEmail={chatUser?.email || \"\"}\n paystackPublicKey={paymentModal.order?.paystackPublicKey || agentConfig?.paystackPublicKey || \"\"}\n onPaymentSuccess={handlePaymentSuccess}\n onPaymentError={handlePaymentError}\n isLoading={isPaymentLoading}\n />\n\n {addressForm.isOpen && (\n <AddressForm\n onSubmit={handleAddressSubmit}\n onCancel={() => setAddressForm({ isOpen: false, product: null })}\n isLoading={isAddressLoading}\n />\n )}\n\n {bookingForm.isOpen && (\n <BookingForm\n service={bookingForm.service}\n onSubmit={handleBookingSubmit}\n onCancel={() => setBookingForm({ isOpen: false, service: null })}\n />\n )}\n\n <CartView\n isOpen={showCart}\n onClose={() => setShowCart(false)}\n items={cartItems}\n onUpdateQuantity={(id, quantity) => {\n if (quantity === 0) {\n setCartItems(prev => prev.filter(item => item.id !== id))\n } else {\n setCartItems(prev => prev.map(item => \n item.id === id ? { ...item, quantity } : item\n ))\n }\n }}\n onRemoveItem={(id) => {\n setCartItems(prev => prev.filter(item => item.id !== id))\n }}\n onAddItem={(productName) => {\n const product = products.find(p => \n p.name.toLowerCase().includes(productName.toLowerCase())\n )\n if (product) {\n const existingItem = cartItems.find(item => item.id === product._id)\n if (existingItem) {\n setCartItems(prev => prev.map(item => \n item.id === product._id \n ? { ...item, quantity: item.quantity + 1 }\n : item\n ))\n } else {\n setCartItems(prev => [...prev, {\n id: product._id,\n name: product.name,\n price: product.price,\n quantity: 1,\n image: product.images?.[0]\n }])\n }\n }\n }}\n onCheckout={async () => {\n if (cartItems.length === 0) return\n \n try {\n // Create a combined order for all cart items\n const total = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)\n const itemNames = cartItems.map(item => `${item.name} (x${item.quantity})`).join(', ')\n \n const order = await createOrder(\n null, // No single product\n null, // No delivery address for now\n {\n amount: total,\n currency: products[0]?.currency || 'GHS',\n itemName: `Cart Items: ${itemNames}`,\n itemType: 'cart',\n cartItems: cartItems\n }\n )\n \n setShowCart(false)\n setCartItems([]) // Clear cart after checkout\n setPaymentModal({ \n isOpen: true, \n product: { name: `Cart (${cartItems.length} items)` }, \n order \n })\n } catch (error) {\n alert(`Failed to create order: ${error instanceof Error ? error.message : 'Unknown error'}`)\n }\n }}\n primaryColor={finalConfig.primaryColor}\n currency={products[0]?.currency || 'GHS'}\n position={finalConfig.position}\n />\n </div>\n )\n}\n","\"use client\"\n\ninterface ChatBubbleProps {\n isOpen: boolean\n onClick: () => void\n primaryColor?: string\n unreadCount?: number\n position?: \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\"\n}\n\nexport function ChatBubble({ isOpen, onClick, primaryColor = \"#3b82f6\", unreadCount = 0, position = \"bottom-right\" }: ChatBubbleProps) {\n const adjustColor = (color: string, percent: number): string => {\n const num = parseInt(color.replace(\"#\", \"\"), 16)\n const amt = Math.round(2.55 * percent)\n const R = (num >> 16) + amt\n const G = (num >> 8 & 0x00FF) + amt\n const B = (num & 0x0000FF) + amt\n return \"#\" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +\n (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +\n (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)\n }\n\n return (\n <button\n onClick={onClick}\n style={{\n position: 'fixed',\n bottom: position.includes('bottom') ? '20px' : 'auto',\n top: position.includes('top') ? '20px' : 'auto',\n right: position.includes('right') ? '20px' : 'auto',\n left: position.includes('left') ? '20px' : 'auto',\n width: '60px',\n height: '60px',\n borderRadius: '50%',\n backgroundColor: primaryColor,\n color: 'white',\n border: 'none',\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: '24px',\n boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',\n zIndex: 9999,\n transition: 'all 0.2s ease'\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = adjustColor(primaryColor, -10)\n e.currentTarget.style.transform = 'scale(1.05)'\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = primaryColor\n e.currentTarget.style.transform = 'scale(1)'\n }}\n aria-label=\"Open chat\"\n >\n {unreadCount > 0 && (\n <span style={{\n position: 'absolute',\n top: '-2px',\n right: '-2px',\n background: 'linear-gradient(135deg, #ef4444, #dc2626)',\n color: 'white',\n borderRadius: '50%',\n minWidth: '20px',\n height: '20px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: '11px',\n fontWeight: '600',\n padding: '0 4px',\n border: '2px solid white'\n }}>\n {unreadCount > 9 ? \"9+\" : unreadCount}\n </span>\n )}\n\n <svg \n width=\"24\" \n height=\"24\" \n fill=\"none\" \n stroke=\"currentColor\" \n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.5}\n d=\"M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z\"\n />\n </svg>\n </button>\n )\n}","\"use client\"\n\nimport { useState, useRef, useEffect } from \"react\"\nimport { MessageList } from \"./MessageList\"\nimport { MessageInput } from \"./MessageInput\"\nimport { ProductCard } from \"./ProductCard\"\nimport { OrdersView } from \"./OrdersView\"\nimport { ServiceCard } from \"./ServiceCard\"\nimport { BookingsView } from \"./BookingsView\"\nimport { PaymentOptionsView } from \"./PaymentOptionsView\"\nimport { InvoiceView } from \"./InvoiceView\"\nimport { FeedbackView } from \"./FeedbackView\"\nimport { CartView } from \"./CartView\"\nimport type { Message, Product } from \"../types\"\nimport OnlineStatusBadge from \"./OnlineStatus\"\n\ninterface ChatWindowProps {\n isOpen: boolean\n onClose: () => void\n messages: Message[]\n isLoading: boolean\n error: string | null\n onSendMessage: (message: string) => void\n onClearMessages: () => void\n primaryColor?: string\n title?: string\n subtitle?: string\n placeholder?: string\n position?: \"bottom-right\" | \"bottom-left\" | \"top-right\" | \"top-left\"\n products?: Product[]\n onBuyProduct?: (product: Product) => void\n showProducts?: boolean\n onToggleProducts?: () => void\n voiceEnabled?: boolean\n orders?: any[]\n onToggleOrders?: () => void\n showOrders?: boolean\n onPayOrder?: (order: any) => void\n services?: any[]\n onBookService?: (service: any) => void\n showServices?: boolean\n onToggleServices?: () => void\n bookings?: any[]\n onToggleBookings?: () => void\n showBookings?: boolean\n bookingEnabled?: boolean\n onEndConversation?: () => void\n showPaymentOptions?: boolean\n onTogglePaymentOptions?: () => void\n showInvoice?: boolean\n onToggleInvoice?: () => void\n showFeedback?: boolean\n onToggleFeedback?: () => void\n onSubmitFeedback?: (feedback: { rating: number; comment: string }) => void\n showCart?: boolean\n onToggleCart?: () => void\n cartItems?: any[]\n onUpdateCartQuantity?: (id: string, quantity: number) => void\n onRemoveCartItem?: (id: string) => void\n onAddCartItem?: (productName: string) => void\n onCartCheckout?: () => void\n}\n\nexport function ChatWindow({\n isOpen,\n onClose,\n messages,\n isLoading,\n error,\n onSendMessage,\n onClearMessages,\n primaryColor,\n title,\n subtitle,\n placeholder,\n position = \"bottom-right\",\n products = [],\n onBuyProduct,\n showProducts,\n onToggleProducts,\n voiceEnabled,\n orders = [],\n onToggleOrders,\n showOrders,\n onPayOrder,\n services = [],\n onBookService,\n showServices,\n onToggleServices,\n bookings = [],\n onToggleBookings,\n showBookings,\n bookingEnabled,\n onEndConversation,\n showPaymentOptions,\n onTogglePaymentOptions,\n showInvoice,\n onToggleInvoice,\n showFeedback,\n onToggleFeedback,\n onSubmitFeedback,\n showCart,\n onToggleCart,\n cartItems = [],\n onUpdateCartQuantity,\n onRemoveCartItem,\n onAddCartItem,\n onCartCheckout,\n}: ChatWindowProps) {\n const [input, setInput] = useState(\"\")\n const messagesEndRef = useRef<HTMLDivElement>(null)\n\n const scrollToBottom = () => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" })\n }\n\n useEffect(() => {\n if (isOpen) {\n scrollToBottom()\n }\n }, [messages, isOpen])\n\n const handleSend = () => {\n if (input.trim() && !isLoading) {\n onSendMessage(input.trim())\n setInput(\"\")\n }\n }\n\n if (!isOpen) return null\n\n return (\n <div\n style={{\n position: 'fixed',\n width: 'min(380px, calc(100vw - 20px))',\n height: 'min(600px, calc(100vh - 100px))',\n maxWidth: '380px',\n maxHeight: '600px',\n backgroundColor: 'white',\n borderRadius: '12px',\n boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n zIndex: 9998,\n border: '1px solid #e5e7eb',\n bottom: position.includes('bottom') ? '90px' : 'auto',\n top: position.includes('top') ? '90px' : 'auto',\n right: position.includes('right') ? '20px' : 'auto',\n left: position.includes('left') ? '20px' : 'auto'\n }}\n className=\"chat-window-responsive\"\n >\n {/* Header with integrated chat bubble */}\n <div style={{\n background: `linear-gradient(135deg, ${primaryColor}, ${primaryColor}dd)`,\n color: 'white',\n padding: 'min(16px, 12px)',\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n flexShrink: 0,\n position: 'relative'\n }}>\n <div>\n <h3 style={{ fontWeight: '600', fontSize: '16px', margin: 0 }}>{title}</h3>\n <OnlineStatusBadge status=\"online\" />\n </div>\n\n <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>\n {products.length > 0 && onToggleProducts && (\n <button onClick={onToggleProducts} style={{\n background: 'rgba(255, 255, 255, 0.15)',\n border: 'none',\n color: 'white',\n cursor: 'pointer',\n padding: '8px',\n borderRadius: '6px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n }}>\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z\" />\n </svg>\n </button>\n )}\n {onToggleCart && (\n <button onClick={onToggleCart} style={{\n background: 'rgba(255, 255, 255, 0.15)',\n border: 'none',\n color: 'white',\n cursor: 'pointer',\n padding: '8px',\n borderRadius: '6px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n position: 'relative'\n }}>\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 7M7 13l-2.8 8.4a2 2 0 002 2.6h7.6\" />\n </svg>\n {cartItems.length > 0 && (\n <span style={{\n position: 'absolute',\n top: '-4px',\n right: '-4px',\n backgroundColor: '#ef4444',\n color: 'white',\n borderRadius: '50%',\n width: '16px',\n height: '16px',\n fontSize: '10px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontWeight: 'bold'\n }}>\n {cartItems.reduce((sum, item) => sum + item.quantity, 0)}\n </span>\n )}\n </button>\n )}\n {products?.length > 0 && onToggleOrders && (\n <button onClick={onToggleOrders} style={{\n background: 'rgba(255, 255, 255, 0.15)',\n border: 'none',\n color: 'white',\n cursor: 'pointer',\n padding: '8px',\n borderRadius: '6px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n }}>\n <svg xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" width=\"16\" height=\"16\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 7M7 13l-2 9m5-9v9m6-9v9m-6 0h6\" />\n </svg>\n {/* <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\" />\n </svg> */}\n </button>\n )}\n {bookingEnabled && services.length > 0 && onToggleServices && (\n <button onClick={onToggleServices} style={{\n background: 'rgba(255, 255, 255, 0.15)',\n border: 'none',\n color: 'white',\n cursor: 'pointer',\n padding: '8px',\n borderRadius: '6px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n }}>\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" width=\"16\" height=\"16\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n {/* <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 7V3a2 2 0 012-2h4a2 2 0 012 2v4m-6 0V6a2 2 0 012-2h4a2 2 0 012 2v1m-6 0h6m-6 0l-.5 8.5A2 2 0 0013.5 21h-3A2 2 0 018.5 15.5L8 7z\" />\n </svg> */}\n </button>\n )}\n {bookingEnabled && services.length > 0 && onToggleBookings && (\n <button onClick={onToggleBookings} style={{\n background: 'rgba(255, 255, 255, 0.15)',\n border: 'none',\n color: 'white',\n cursor: 'pointer',\n padding: '8px',\n borderRadius: '6px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n }}>\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" width=\"16\" height=\"16\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\" />\n </svg>\n {/* <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 7V3a2 2 0 012-2h4a2 2 0 012 2v4m-6 0V6a2 2 0 012-2h4a2 2 0 012 2v1m-6 0h6m-6 0l-.5 8.5A2 2 0 0013.5 21h-3A2 2 0 018.5 15.5L8 7z\" />\n </svg> */}\n </button>\n )}\n\n <button onClick={onClose} style={{\n background: 'none',\n border: 'none',\n color: 'white',\n cursor: 'pointer',\n padding: '8px',\n borderRadius: '6px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center'\n }} onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.15)'\n }} onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent'\n }}>\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Messages */}\n <div style={{\n flex: 1,\n overflowY: 'auto',\n padding: '16px',\n backgroundColor: '#f9fafb'\n }}>\n {messages.length === 0 ? (\n <div style={{ padding: '40px 20px', textAlign: 'center' }}>\n <div style={{\n width: '80px',\n height: '80px',\n borderRadius: '50%',\n background: 'var(--background-secondary)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n margin: '0 auto 20px',\n }}>\n <svg width=\"36\" height=\"36\" fill=\"none\" stroke=\"var(--text-muted)\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z\" />\n </svg>\n </div>\n <h4 style={{ fontSize: '20px', marginBottom: '12px', fontWeight: '600' }}>{title}</h4>\n <p style={{ color: 'var(--text-muted)', marginBottom: '8px' }}>{subtitle}</p>\n <p style={{ color: 'var(--text-muted)', fontSize: '13px' }}>Type a message to get started</p>\n </div>\n ) : showOrders ? (\n <OrdersView\n orders={orders}\n onClose={() => onToggleOrders && onToggleOrders()}\n onPayOrder={onPayOrder}\n />\n ) : showProducts ? (\n <div>\n <h4 style={{ fontSize: '18px', marginBottom: '16px', fontWeight: '600' }}>Products</h4>\n <div style={{\n display: 'grid',\n gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',\n gap: '12px',\n margin: '16px 0'\n }}>\n {products.map((product) => (\n <ProductCard\n key={product._id}\n product={product}\n onBuyNow={onBuyProduct!}\n primaryColor={primaryColor as string}\n />\n ))}\n </div>\n </div>\n ) : showServices ? (\n <div>\n <h4 style={{ fontSize: '18px', marginBottom: '16px', fontWeight: '600' }}>Services</h4>\n {services.map((service) => (\n <ServiceCard\n key={service._id}\n service={service}\n onBook={onBookService!}\n />\n ))}\n </div>\n ) : showBookings ? (\n <BookingsView\n bookings={bookings}\n onClose={() => onToggleBookings && onToggleBookings()}\n />\n ) : showPaymentOptions ? (\n <PaymentOptionsView\n onClose={() => onTogglePaymentOptions && onTogglePaymentOptions()}\n primaryColor={primaryColor}\n />\n ) : showInvoice ? (\n <InvoiceView\n onClose={() => onToggleInvoice && onToggleInvoice()}\n primaryColor={primaryColor}\n />\n ) : showFeedback ? (\n <FeedbackView\n onClose={() => onToggleFeedback && onToggleFeedback()}\n onSubmit={onSubmitFeedback!}\n primaryColor={primaryColor}\n />\n ) : (\n <MessageList\n messages={messages}\n primaryColor={primaryColor as string}\n isLoading={isLoading}\n onEndConversation={onEndConversation}\n onShowProducts={onToggleProducts}\n onShowServices={onToggleServices}\n onShowBookingForm={() => {}}\n onShowPaymentOptions={onTogglePaymentOptions}\n onShowInvoice={onToggleInvoice}\n onRequestFeedback={onToggleFeedback}\n onShowCart={onToggleCart}\n />\n )}\n\n {error && (\n <div style={{\n backgroundColor: '#fef2f2',\n border: '1px solid #fecaca',\n color: '#dc2626',\n padding: '12px',\n borderRadius: '6px',\n fontSize: '14px',\n margin: '12px 0'\n }}>\n <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n <svg width=\"20\" height=\"20\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\" />\n </svg>\n <p style={{ margin: 0 }}>{error}</p>\n </div>\n </div>\n )}\n\n <div ref={messagesEndRef} />\n </div>\n\n {/* Input */}\n <MessageInput\n value={input}\n onChange={setInput}\n onSend={handleSend}\n isLoading={isLoading}\n placeholder={placeholder as string}\n primaryColor={primaryColor as string}\n voiceEnabled={voiceEnabled}\n />\n\n {/* Cart Sheet */}\n <CartView\n isOpen={showCart || false}\n onClose={() => onToggleCart && onToggleCart()}\n items={cartItems}\n onUpdateQuantity={onUpdateCartQuantity!}\n onRemoveItem={onRemoveCartItem!}\n onAddItem={onAddCartItem!}\n onCheckout={onCartCheckout!}\n primaryColor={primaryColor}\n currency={products[0]?.currency || 'GHS'}\n />\n </div>\n )\n}","import type { Message } from \"../types\"\n\ninterface MessageListProps {\n messages: Message[]\n primaryColor: string\n isLoading?: boolean\n onEndConversation?: () => void\n onShowProducts?: () => void\n onShowServices?: () => void\n onShowBookingForm?: () => void\n onShowPaymentOptions?: () => void\n onShowInvoice?: () => void\n onRequestFeedback?: () => void\n onShowCart?: () => void\n}\n\nexport function MessageList({ \n messages, \n primaryColor, \n isLoading = false, \n onEndConversation,\n onShowProducts,\n onShowServices,\n onShowBookingForm,\n onShowPaymentOptions,\n onShowInvoice,\n onRequestFeedback,\n onShowCart\n}: MessageListProps) {\n const adjustColor = (color: string, percent: number): string => {\n const num = parseInt(color.replace(\"#\", \"\"), 16)\n const amt = Math.round(2.55 * percent)\n const R = (num >> 16) + amt\n const G = (num >> 8 & 0x00FF) + amt\n const B = (num & 0x0000FF) + amt\n return \"#\" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +\n (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +\n (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)\n }\n\n const buttonStyle = {\n padding: '8px 16px',\n backgroundColor: primaryColor,\n color: 'white',\n border: 'none',\n borderRadius: '16px',\n fontSize: '13px',\n fontWeight: '500',\n cursor: 'pointer',\n display: 'inline-flex',\n alignItems: 'center',\n gap: '6px',\n transition: 'all 0.2s ease'\n }\n\n const ThinkingIndicator = () => (\n <div style={{\n marginBottom: '16px',\n display: 'flex',\n gap: '8px',\n justifyContent: 'flex-start',\n alignItems: 'flex-end'\n }}>\n <div style={{\n width: '28px',\n height: '28px',\n borderRadius: '50%',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: '12px',\n flexShrink: 0,\n background: `linear-gradient(135deg, ${primaryColor}, ${adjustColor(primaryColor, -10)})`,\n color: 'white',\n marginBottom: '2px'\n }}>\n <svg width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z\" />\n </svg>\n </div>\n \n <div style={{\n maxWidth: '85%',\n padding: '12px 16px',\n borderRadius: '16px',\n borderBottomLeftRadius: '4px',\n fontSize: '14px',\n lineHeight: '1.5',\n backgroundColor: '#f8f9fa',\n color: '#1f2937',\n