UNPKG

@jutech-devs/agent-sdk

Version:

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

1 lines 153 kB
{"version":3,"sources":["../src/AgentWidget.tsx","../src/components/ChatBubble.tsx","../src/components/ChatWindow.tsx","../src/components/MessageList.tsx","../src/components/VoiceButton.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/PaymentModal.tsx","../src/components/AddressForm.tsx","../src/components/BookingForm.tsx","../src/hooks/useChat.ts","../src/api.ts"],"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 { 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 = \"http://localhost:3001\",\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 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 completePayment,\n conversationId,\n orders,\n loadOrders,\n services,\n loadServices,\n createBooking,\n bookings,\n loadBookings,\n setUserInfo,\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 [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\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\n useEffect(() => {\n if (user && conversationId && setUserInfo) {\n setUserInfo(user)\n }\n }, [user, conversationId, setUserInfo])\n\n const handleBuyProduct = async (product: any) => {\n setAddressForm({ isOpen: true, product })\n }\n\n const handleAddressSubmit = async (address: any) => {\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 }\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 try {\n await completePayment(paymentModal.order.orderId, reference)\n\n // If this was a booking payment, create the booking\n if (paymentModal.order.bookingData) {\n await createBooking({\n ...paymentModal.order.bookingData,\n paymentReference: reference,\n paymentStatus: 'completed'\n })\n alert('Payment successful! Your booking has been confirmed.')\n } else {\n alert('Payment successful! Thank you for your purchase.')\n loadOrders()\n }\n\n setPaymentModal({ isOpen: false, product: null, order: null })\n } catch (error) {\n alert('Payment verification failed. Please contact support.')\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 order for payment\n const order = await createOrder({\n ...bookingForm.service,\n paymentAmount: bookingData.paymentAmount\n }, null)\n\n // Store booking data for after payment\n order.bookingData = bookingData\n setBookingForm({ isOpen: false, service: null })\n setPaymentModal({ isOpen: true, product: bookingForm.service, order })\n } else {\n await createBooking(bookingData)\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 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 className={`agent-widget ${className}`}>\n <ChatBubble\n isOpen={isOpen}\n onClick={handleOpen}\n primaryColor={finalConfig.primaryColor}\n unreadCount={unreadCount}\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 />\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 />\n\n {addressForm.isOpen && (\n <AddressForm\n onSubmit={handleAddressSubmit}\n onCancel={() => setAddressForm({ isOpen: false, product: null })}\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 </div>\n )\n}\n","\"use client\"\n\ninterface ChatBubbleProps {\n isOpen: boolean\n onClick: () => void\n primaryColor?: string\n unreadCount?: number\n}\n\nexport function ChatBubble({ isOpen, onClick, primaryColor = \"#3b82f6\", unreadCount = 0 }: ChatBubbleProps) {\n return (\n <button\n onClick={onClick}\n style={{\n position: 'fixed',\n bottom: '24px',\n right: '24px',\n width: '64px',\n height: '64px',\n borderRadius: '50%',\n background: `linear-gradient(135deg, ${primaryColor}, ${primaryColor}dd)`,\n border: 'none',\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n boxShadow: '0 8px 25px rgba(102, 126, 234, 0.35), 0 3px 10px rgba(0, 0, 0, 0.1)',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n zIndex: 999999,\n }}\n aria-label=\"Open chat\"\n >\n {unreadCount > 0 && (\n <div style={{\n position: 'absolute',\n top: '-8px',\n right: '-8px',\n width: '24px',\n height: '24px',\n background: 'linear-gradient(135deg, #ef4444, #dc2626)',\n color: 'white',\n borderRadius: '50%',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: '12px',\n fontWeight: 'bold',\n border: '2px solid white',\n boxShadow: '0 4px 12px rgba(239, 68, 68, 0.4)',\n }}>\n {unreadCount > 9 ? \"9+\" : unreadCount}\n </div>\n )}\n\n <svg \n width=\"24\" \n height=\"24\" \n fill=\"none\" \n stroke=\"white\" \n viewBox=\"0 0 24 24\"\n style={{ filter: 'drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2))' }}\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 type { Message, Product } from \"../types\"\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}\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,\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}: 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 style={{\n position: 'fixed',\n bottom: '96px',\n right: '24px',\n width: '400px',\n height: '600px',\n maxWidth: 'calc(100vw - 48px)',\n maxHeight: 'calc(100vh - 140px)',\n background: 'linear-gradient(145deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.95) 100%)',\n backdropFilter: 'blur(24px) saturate(180%)',\n WebkitBackdropFilter: 'blur(24px) saturate(180%)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n borderRadius: '24px',\n boxShadow: '0 32px 80px rgba(0, 0, 0, 0.12), 0 16px 40px rgba(0, 0, 0, 0.08), 0 8px 20px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(255, 255, 255, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.6)',\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden',\n transformOrigin: 'bottom right',\n animation: 'slideInUp 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275)',\n zIndex: 999999,\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif',\n }}>\n {/* Header */}\n <div style={{\n padding: '24px 28px',\n background: `linear-gradient(135deg, ${primaryColor} 0%, ${primaryColor}dd 100%)`,\n color: 'white',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n position: 'relative',\n overflow: 'hidden',\n borderRadius: '24px 24px 0 0',\n }}>\n <div style={{\n position: 'absolute',\n inset: 0,\n background: 'radial-gradient(circle at 15% 15%, rgba(255,255,255,0.3) 0%, transparent 40%), radial-gradient(circle at 85% 85%, rgba(255,255,255,0.15) 0%, transparent 40%), linear-gradient(135deg, rgba(255,255,255,0.15) 0%, transparent 70%)',\n }} />\n \n <div style={{ zIndex: 1 }}>\n <h3 style={{\n fontSize: '20px',\n fontWeight: '700',\n margin: '0 0 6px 0',\n letterSpacing: '-0.02em',\n textShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',\n }}>{title}</h3>\n <p style={{\n fontSize: '14px',\n opacity: 0.95,\n margin: 0,\n fontWeight: '400',\n letterSpacing: '-0.01em',\n }}>{subtitle}</p>\n </div>\n \n <div style={{ display: 'flex', gap: '8px', zIndex: 2 }}>\n {products.length > 0 && onToggleProducts && (\n <button \n onClick={onToggleProducts}\n style={{\n background: 'rgba(255, 255, 255, 0.2)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n borderRadius: '50%',\n width: '36px',\n height: '36px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: 'pointer',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n backdropFilter: 'blur(10px)',\n boxShadow: '0 4px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)',\n }}\n >\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"white\" 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 {products?.length > 0 && onToggleOrders && (\n <button \n onClick={onToggleOrders}\n style={{\n background: 'rgba(255, 255, 255, 0.2)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n borderRadius: '50%',\n width: '36px',\n height: '36px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: 'pointer',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n backdropFilter: 'blur(10px)',\n boxShadow: '0 4px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)',\n }}\n >\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"white\" 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 \n onClick={onToggleServices}\n style={{\n background: 'rgba(255, 255, 255, 0.2)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n borderRadius: '50%',\n width: '36px',\n height: '36px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: 'pointer',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n backdropFilter: 'blur(10px)',\n boxShadow: '0 4px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)',\n }}\n >\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"white\" 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 && onToggleBookings && (\n <button \n onClick={onToggleBookings}\n style={{\n background: 'rgba(255, 255, 255, 0.2)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n borderRadius: '50%',\n width: '36px',\n height: '36px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: 'pointer',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n backdropFilter: 'blur(10px)',\n boxShadow: '0 4px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)',\n }}\n >\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"white\" 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 <button \n onClick={onClose}\n style={{\n background: 'rgba(255, 255, 255, 0.2)',\n border: '1px solid rgba(255, 255, 255, 0.3)',\n borderRadius: '50%',\n width: '36px',\n height: '36px',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n cursor: 'pointer',\n transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',\n zIndex: 2,\n backdropFilter: 'blur(10px)',\n boxShadow: '0 4px 16px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.4)',\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.background = 'rgba(255, 255, 255, 0.3)'\n e.currentTarget.style.transform = 'scale(1.1) rotate(90deg)'\n e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.5)'\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)'\n e.currentTarget.style.transform = 'scale(1) rotate(0deg)'\n e.currentTarget.style.borderColor = 'rgba(255, 255, 255, 0.3)'\n }}\n >\n <svg width=\"16\" height=\"16\" fill=\"none\" stroke=\"white\" 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 padding: '28px',\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: '20px',\n background: 'radial-gradient(circle at 30% 20%, rgba(102, 126, 234, 0.02) 0%, transparent 60%), radial-gradient(circle at 70% 80%, rgba(139, 92, 246, 0.01) 0%, transparent 60%), linear-gradient(180deg, rgba(248, 250, 252, 0.6) 0%, rgba(255, 255, 255, 0.8) 100%)',\n position: 'relative',\n }}>\n {messages.length === 0 ? (\n <div style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n textAlign: 'center',\n padding: '40px 20px',\n }}>\n <div style={{\n width: '80px',\n height: '80px',\n borderRadius: '50%',\n background: `linear-gradient(135deg, ${primaryColor}15, ${primaryColor}08)`,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n marginBottom: '20px',\n boxShadow: '0 8px 32px rgba(102, 126, 234, 0.1)',\n }}>\n <svg width=\"36\" height=\"36\" fill=\"none\" stroke=\"#9ca3af\" 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={{\n fontSize: '20px',\n fontWeight: '600',\n color: '#1f2937',\n margin: '0 0 12px 0',\n letterSpacing: '-0.01em',\n }}>{title}</h4>\n <p style={{\n fontSize: '15px',\n color: '#6b7280',\n margin: '0 0 8px 0',\n lineHeight: '1.5',\n }}>{subtitle}</p>\n <p style={{\n fontSize: '13px',\n color: '#9ca3af',\n margin: 0,\n }}>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', fontWeight: '600', marginBottom: '16px', color: '#1f2937' }}>Products</h4>\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 ) : showServices ? (\n <div>\n <h4 style={{ fontSize: '18px', fontWeight: '600', marginBottom: '16px', color: '#1f2937' }}>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 ) : (\n <MessageList messages={messages} primaryColor={primaryColor as string} />\n )}\n\n {error && (\n <div style={{\n background: 'rgba(254, 242, 242, 0.95)',\n border: '1px solid rgba(252, 165, 165, 0.6)',\n borderRadius: '16px',\n padding: '16px',\n margin: '16px 0',\n backdropFilter: 'blur(10px)',\n }}>\n <div style={{\n display: 'flex',\n alignItems: 'center',\n gap: '12px',\n }}>\n <svg width=\"20\" height=\"20\" fill=\"none\" stroke=\"#ef4444\" 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={{\n fontSize: '14px',\n color: '#dc2626',\n margin: 0,\n fontWeight: '500',\n }}>{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 </div>\n )\n}","import type { Message } from \"../types\"\n\n// Add message animations\nconst messageStyles = `\n @keyframes fade-in {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n .animate-fade-in {\n animation: fade-in 0.3s ease-out;\n }\n`\n\n// Inject styles\nif (typeof document !== 'undefined') {\n const styleSheet = document.createElement('style')\n styleSheet.textContent = messageStyles\n document.head.appendChild(styleSheet)\n}\n\ninterface MessageListProps {\n messages: Message[]\n primaryColor: string\n}\n\nexport function MessageList({ messages, primaryColor }: MessageListProps) {\n return (\n <div className=\"space-y-4\">\n {messages.map((message, index) => (\n <div\n key={message._id}\n className={`flex ${message.role === \"user\" ? \"justify-end\" : \"justify-start\"} animate-fade-in`}\n style={{ animationDelay: `${index * 100}ms` }}\n >\n {message.role === \"assistant\" && (\n <div className=\"flex-shrink-0 mr-3\">\n <div\n className=\"w-8 h-8 rounded-full flex items-center justify-center shadow-md\"\n style={{\n background: `linear-gradient(135deg, ${primaryColor}, ${primaryColor}dd)`\n }}\n >\n <svg className=\"w-4 h-4 text-white\" 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 </div>\n )}\n <div\n className={`max-w-[280px] rounded-2xl px-4 py-3 text-sm shadow-lg backdrop-blur-sm transition-all duration-200 hover:shadow-xl ${message.role === \"user\"\n ? \"text-white rounded-br-md\"\n : \"bg-white/90 text-gray-800 rounded-bl-md border border-gray-200/50\"\n }`}\n style={{\n background: message.role === \"user\"\n ? `linear-gradient(135deg, ${primaryColor}, ${primaryColor}dd)`\n : undefined,\n boxShadow: message.role === \"user\"\n ? `0 8px 25px -8px ${primaryColor}40`\n : '0 4px 20px rgba(0,0,0,0.08)'\n }}\n >\n {message.content ||\n (message.isStreaming ? (\n <div className=\"flex items-center gap-2\">\n <div className=\"flex space-x-1\">\n <div\n className=\"w-2 h-2 bg-gray-400 rounded-full animate-bounce\"\n style={{ animationDelay: \"0ms\" }}\n ></div>\n <div\n className=\"w-2 h-2 bg-gray-400 rounded-full animate-bounce\"\n style={{ animationDelay: \"150ms\" }}\n ></div>\n <div\n className=\"w-2 h-2 bg-gray-400 rounded-full animate-bounce\"\n style={{ animationDelay: \"300ms\" }}\n ></div>\n </div>\n <span className=\"text-xs text-gray-500\">AI is typing...</span>\n </div>\n ) : (\n <div className=\"flex items-center gap-2\">\n <div className=\"flex space-x-1\">\n <div className=\"w-2 h-2 bg-gray-400 rounded-full animate-pulse\"></div>\n <div className=\"w-2 h-2 bg-gray-400 rounded-full animate-pulse\" style={{ animationDelay: \"0.5s\" }}></div>\n <div className=\"w-2 h-2 bg-gray-400 rounded-full animate-pulse\" style={{ animationDelay: \"1s\" }}></div>\n </div>\n <span className=\"text-xs text-gray-500\">Thinking...</span>\n </div>\n ))}\n </div>\n {message.role === \"user\" && (\n <div className=\"flex-shrink-0 ml-3\">\n <div className=\"w-8 h-8 rounded-full flex items-center justify-center shadow-md\"\n style={{\n background: `linear-gradient(135deg, ${primaryColor}, ${primaryColor}dd)`\n }}>\n <svg\n className=\"w-4 h-4 text-white\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z\"\n />\n </svg>\n\n </div>\n </div>\n )}\n </div>\n ))}\n </div>\n )\n}\n","import React from \"react\"\n\ninterface VoiceButtonProps {\n isListening: boolean\n isSupported: boolean\n onToggle: () => void\n primaryColor: string\n}\n\nexport function VoiceButton({ isListening, isSupported, onToggle, primaryColor }: VoiceButtonProps) {\n if (!isSupported) return null\n\n return (\n <button\n onClick={onToggle}\n style={{\n background: isListening ? primaryColor : \"rgba(255, 255, 255, 0.1)\",\n border: `1px solid ${isListening ? \"white\" : \"rgba(255, 255, 255, 0.3)\"}`,\n borderRadius: \"50%\",\n width: \"40px\",\n height: \"40px\",\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n cursor: \"pointer\",\n transition: \"all 0.3s cubic-bezier(0.4, 0, 0.2, 1)\",\n backdropFilter: \"blur(10px)\",\n boxShadow: isListening \n ? `0 0 20px ${primaryColor}40, 0 4px 16px rgba(0, 0, 0, 0.1)` \n : \"0 4px 16px rgba(0, 0, 0, 0.1)\",\n animation: isListening ? \"pulse 2s infinite\" : \"none\",\n }}\n title={isListening ? \"Stop listening\" : \"Start voice input\"}\n >\n <svg \n width=\"18\" \n height=\"18\" \n fill=\"none\" \n stroke=\"white\" \n viewBox=\"0 0 24 24\"\n style={{\n filter: isListening ? \"drop-shadow(0 0 4px rgba(255,255,255,0.5))\" : \"none\"\n }}\n >\n <path \n strokeLinecap=\"round\" \n strokeLinejoin=\"round\" \n strokeWidth={2} \n d=\"M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z\" \n />\n <path \n strokeLinecap=\"round\" \n strokeLinejoin=\"round\" \n strokeWidth={2} \n d=\"M19 10v2a7 7 0 0 1-14 0v-2\" \n />\n <line \n strokeLinecap=\"round\" \n strokeLinejoin=\"round\" \n strokeWidth={2} \n x1=\"12\" \n y1=\"19\" \n x2=\"12\" \n y2=\"23\" \n />\n <line \n strokeLinecap=\"round\" \n strokeLinejoin=\"round\" \n strokeWidth={2} \n x1=\"8\" \n y1=\"23\" \n x2=\"16\" \n y2=\"23\" \n />\n </svg>\n </button>\n )\n}","\"use client\"\n\nimport { useState, useCallback, useRef, useEffect } from \"react\"\n\ninterface UseVoiceProps {\n onTranscript: (text: string) => void\n enabled?: boolean\n}\n\nexport function useVoice({ onTranscript, enabled = true }: UseVoiceProps) {\n const [isListening, setIsListening] = useState(false)\n const [isSupported, setIsSupported] = useState(false)\n const recognitionRef = useRef<SpeechRecognition | null>(null)\n\n useEffect(() => {\n if (typeof window !== \"undefined\") {\n const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition\n setIsSupported(!!SpeechRecognition && enabled)\n \n if (SpeechRecognition && enabled) {\n const recognition = new SpeechRecognition()\n recognition.continuous = false\n recognition.interimResults = false\n recognition.lang = \"en-US\"\n\n recognition.onstart = () => {\n setIsListening(true)\n }\n\n recognition.onresult = (event) => {\n const transcript = event.results[0]?.transcript\n if (transcript) {\n onTranscript(transcript)\n }\n }\n\n recognition.onend = () => {\n setIsListening(false)\n }\n\n recognition.onerror = (event) => {\n console.error(\"Speech recognition error:\", event.error)\n setIsListening(false)\n }\n\n recognitionRef.current = recognition\n }\n }\n\n return () => {\n if (recognitionRef.current) {\n recognitionRef.current.abort()\n }\n }\n }, [onTranscript, enabled])\n\n const startListening = useCallback(() => {\n if (recognitionRef.current && !isListening) {\n try {\n recognitionRef.current.start()\n } catch (error) {\n console.error(\"Failed to start speech recognition:\", error)\n }\n }\n }, [isListening])\n\n const stopListening = useCallback(() => {\n if (recognitionRef.current && isListening) {\n recognitionRef.current.stop()\n }\n }, [isListening])\n\n const speak = useCallback((text: string) => {\n if (typeof window !== \"undefined\" && window.speechSynthesis && enabled) {\n // Cancel any ongoing speech\n window.speechSynthesis.cancel()\n \n const utterance = new SpeechSynthesisUtterance(text)\n utterance.rate = 0.9\n utterance.pitch = 1\n utterance.volume = 0.8\n \n window.speechSynthesis.speak(utterance)\n }\n }, [enabled])\n\n return {\n isListening,\n isSupported,\n startListening,\n stopListening,\n speak,\n }\n}","\"use client\"\n\nimport type { KeyboardEvent } from \"react\"\nimport { VoiceButton } from \"./VoiceButton\"\nimport { useVoice } from \"../hooks/useVoice\"\n\ninterface MessageInputProps {\n value: string\n onChange: (value: string) => void\n onSend: () => void\n isLoading: boolean\n placeholder: string\n primaryColor: string\n voiceEnabled?: boolean\n}\n\nexport function MessageInput({ value, onChange, onSend, isLoading, placeholder, primaryColor, voiceEnabled = false }: MessageInputProps) {\n const { isListening, isSupported, startListening, stopListening } = useVoice({\n onTranscript: (text) => {\n onChange(value + (value ? \" \" : \"\") + text)\n },\n enabled: voiceEnabled,\n })\n\n const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n onSend()\n }\n }\n\n const handleVoiceToggle = () => {\n if (isListening) {\n stopListening()\n } else {\n startListening()\n }\n }\n\n return (\n <div className=\"flex items-center gap-3 p-4 bg-gradient-to-t from-gray-50/80 to-transparent backdrop-blur-sm\">\n <div className=\"flex-1 relative\">\n <input\n type=\"text\"\n value={value}\n onChange={(e) => onChange(e.target.value)}\n onKeyPress={handleKeyPress}\n placeholder={placeholder}\n disabled={isLoading}\n className=\"w-full rounded-2xl border-0 bg-white/90 backdrop-blur-sm px-4 py-3 pr-12 text-sm placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-2 focus:ring-opacity-50 disabled:opacity-50 shadow-lg transition-all duration-200\"\n style={{\n boxShadow: `0 4px 20px rgba(0,0,0,0.08), 0 0 0 1px ${primaryColor}20`,\n focusRingColor: `${primaryColor}80`\n }}\n />\n {value.trim() && (\n <div className=\"absolute right-4 top-1/2 transform -translate-y-1/2\">\n <div className=\"w-2 h-2 bg-green-500 rounded-full animate-pulse\"></div>\n </div>\n )}\n </div>\n {voiceEnabled && (\n <VoiceButton\n isListening={isListening}\n isSupported={isSupported}\n onToggle={handleVoiceToggle}\n primaryColor={primaryColor}\n />\n )}\n <button\n onClick={onSend}\n disabled={!value.trim() || isLoading}\n className={`flex h-12 w-12 items-center justify-center rounded-2xl text-white transition-all duration-200 transform ${\n !value.trim() || isLoading \n ? 'bg-gray-300 cursor-not-allowed scale-95' \n : 'hover:scale-105 active:scale-95 shadow-lg hover:shadow-xl'\n }`}\n style={{\n background: !value.trim() || isLoading \n ? undefined \n : `linear-gradient(135deg, ${primaryColor}, ${primaryColor}dd)`,\n boxShadow: !value.trim() || isLoading \n ? undefined \n : `0 8px 25px -8px ${primaryColor}60`\n }}\n aria-label=\"Send message\"\n >\n {isLoading ? (\n <div className=\"h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent\" />\n ) : (\n <svg className=\"h-5 w-5 transition-transform duration-200\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2.5} d=\"M12 19l9 2-9-18-9 18 9-2zm0 0v-8\" />\n </svg>\n )}\n </button>\n </div>\n )\n}\n","import React from \"react\"\nimport type { Product } from \"../types\"\n\ninterface ProductCardProps {\n product: Product\n onBuyNow: (product: Product) => void\n primaryColor: string\n}\n\nexport function ProductCard({ product, onBuyNow, primaryColor }: ProductCardProps) {\n return (\n <div\n style={{\n border: \"1px solid #e5e7eb\",\n borderRadius: \"8px\",\n padding: \"16px\",\n margin: \"8px 0\",\n backgroundColor: \"#ffffff\",\n }}\n >\n {(product.imageUrl || product.images?.[0]) && (\n <img\n src={product.imageUrl || product.images[0]}\n alt={product.name}\n style={{\n width: \"100%\",\n height: \"120px\",\n objectFit: \"cover\",\n borderRadius: \"6px\",\n marginBottom: \"12px\",\n }}\n />\n )}\n <h3\n style={{\n fontSize: \"16px\",\n fontWeight: \"600\",\n margin: \"0 0 8px 0\",\n color: \"#1f2937\",\n }}\n >\n {product.name}\n </h3>\n <p\n style={{\n fontSize: \"14px\",\n color: \"#6b7280\",\n margin: \"0 0 12px 0\",\n lineHeight: \"1.4\",\n }}\n >\n {product.description}\n </p>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <span\n style={{\n fontSize: \"18px\",\n fontWeight: \"700\",\n color: primaryColor,\n }}\n >\n {product.currency} {product.price}\n </span>\n <button\n onClick={() => onBuyNow(product)}\n style={{\n backgroundColor: primaryColor,\n color: \"white\",\n border: \"none\",\n borderRadius: \"6px\",\n padding: \"8px 16px\",\n fontSize: \"14px\",\n fontWeight: \"500\",\n cursor: \"pointer\",\n }}\n >\n Buy Now\n </button>\n </div>\n </div>\n )\n}","import React from \"react\"\n\ninterface Order {\n _id: string\n productId?: {\n name: string\n description: string\n price: number\n currency: string\n imageUrl?: string\n }\n amount: number\n currency: string\n status: string\n createdAt: string\n paymentReference?: string\n deliveryAddress?: {\n street: string\n city: string\n state: string\n zipCode: string\n country: string\n }\n trackingNumber?: string\n itemName?: string\n itemType?: string\n}\n\ninterface OrdersViewProps {\n orders: Order[]\n onClose: () => void\n onPayOrder?: (order: Order) => void\n}\n\nexport function OrdersView({ orders, onClose, onPayOrder }: OrdersViewProps) {\n const formatDate = (dateString: string) => {\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n })\n }\n\n const getStatusColor = (status: string) => {\n switch (status) {\n case 'completed': return '#10b981'\n case 'processing': return '#3b82f6'\n case 'shipped': return '#8b5cf6'\n case 'delivered': return '#059669'\n case 'pending': return '#f59e0b'\n case 'failed': return '#ef4444'\n case 'cancelled': return '#6b7280'\n default: return '#6b7280'\n }\n }\n\n return (\n <div style={{ padding: '20px' }}>\n <div style={{ \n display: 'flex', \n justifyContent: 'space-between', \n alignItems: 'center',\n marginBottom: '20px'\n }}>\n <h3 style={{ \n fontSize: '18px', \n fontWeight: '600', \n margin: 0,\n color: '#1f2937'\n }}>\n My Orders\n </h3>\n <button\n onClick={onClose}\n style={{\n background: 'none',\n border: 'none',\n fontSize: '24px',\n cursor: 'pointer',\n color: '#6b7280',\n padding: '4px'\n }}\n >\n ×\n </button>\n </div>\n\n {orders.length === 0 ? (\n <div style={{\n textAlign: 'center',\n padding: '40px 20px',\n color: '#6b7280'\n }}>\n <p>No orders found</p>\n </div>\n ) : (\n <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>\n {orders.map((order) => (\n <div\n key={order._id}\n style={{\n border: '1px solid #e5e7eb',\n borderRadius: '8px',\n