UNPKG

@helllo-ai/agent-chat-widget

Version:

Bot Swarm Agent Chat Widget - Embeddable chat widget for AI agents

892 lines (869 loc) 127 kB
;(function () { if (typeof window === 'undefined') return if (window.AgentChatWidget) return const VERSION = '0.1.0' /** Browser IIFE bundle (dist/daily-iframe.js). Older packages used build/daily.js — that path 404s on unpkg. */ const DEFAULT_DAILY_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/@daily-co/daily-js@0.87.0/dist/daily-iframe.js' const DEFAULT_SUPPORT_AGENT_IMAGE_URL = 'https://images.pexels.com/photos/6535381/pexels-photo-6535381.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940' /** Pipecat Daily public agent id: staging vs production bundles (VERSION suffix -prod / -staging). */ function defaultDailyAgentName() { if (VERSION.includes('-prod')) return 'voice-web-prod' return 'voice-web-001' } const ACW_SPARKLE_SVG = '<svg class="acw-launcher-sparkle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 2l1.2 5.2L18 9l-4.8 1.8L12 16l-1.2-5.2L6 9l4.8-1.8L12 2z" fill="currentColor"/><path d="M18.5 4l.4 1.4L20 6l-1.1.6-.4 1.4-.4-1.4L17 6l1.1-.6.4-1.4z" fill="currentColor"/><circle cx="7" cy="17" r="1.2" fill="currentColor"/></svg>' const ACW_LAUNCHER_X_SVG = '<svg class="acw-launcher-x" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12"/></svg>' const ACW_UI_LANG_CODES = { en: 1, id: 1, tl: 1, ms: 1, hi: 1, es: 1, fr: 1 } function normalizeWidgetUiLanguage(raw) { var s = String(raw == null ? 'en' : raw).trim().toLowerCase() var base = s.split('-')[0] return ACW_UI_LANG_CODES[base] ? base : 'en' } function acwT(uiLang, key) { var pack = ACW_I18N[uiLang] || ACW_I18N.en if (pack[key] != null) return pack[key] return ACW_I18N.en[key] != null ? ACW_I18N.en[key] : key } const ACW_I18N = { en: { always_online: 'Always Online', back_to_home: 'Back to home', end_chat_close: 'End chat and close', status_disconnected: 'Disconnected', status_connected: 'Connected', connecting_wait: 'Connecting... Please wait.', how_can_we_help: 'How can we help?', choose_communication_style: 'Choose your communication style', text_chat_title: 'Text Chat', text_chat_desc: 'Type messages with AI', voice_chat_title: 'Voice Chat', voice_chat_desc: 'Speak naturally with AI', whatsapp_title: 'WhatsApp', whatsapp_desc: 'Continue on WhatsApp', placeholder_type_message: 'Type a message...', placeholder_connect_first: 'Connect to start chatting', attach_image: 'Attach image', send_message: 'Send message', message_aria: 'Message', powered_by: 'Powered by ', form_title_start: "Let's get started", form_subtitle_details: 'We just need a few details', label_name: 'Your Name', placeholder_name: 'Enter your name', label_phone: 'Mobile Number', placeholder_phone: 'Enter your mobile', conversation_language: 'Conversation language', start_conversation: 'Start conversation →', skip: 'Skip', back_to_options: '← Back to options', alert_name_phone_required: 'Please fill in both name and phone number', alert_session_establish: 'Session not established. Please wait for connection.', ws_not_connected: 'WebSocket not connected. Please wait for connection.', submit_error: 'Error submitting information. Please try again.', voice_title: 'Voice Conversation', voice_tap_mic: 'Tap the mic to speak', voice_connecting: 'Connecting…', voice_loading_sdk: 'Loading voice SDK…', voice_url_or_key_required: 'Voice service URL or Daily API key required', voice_error: 'Voice error', voice_connected_speak: 'Connected — speak when ready', voice_starting_session: 'Starting voice session…', voice_joining_room: 'Joining voice room…', voice_connection_failed: 'Connection failed', voice_url_not_set: 'Voice service URL not set', voice_establishing_link: 'Establishing voice link…', voice_connecting_agent: 'Connecting to agent…', voice_negotiating: 'Negotiating with agent…', voice_signing_in: 'Signing in with agent…', voice_coming_soon: 'Voice coming soon', failed_connect: 'Failed to connect. Please try again or check your connection.', choose_text_chat_home: 'Please go to the home screen and choose Text Chat to start.', image_too_large: 'Image is too large (max 2MB).', image_read_error: 'Could not read the image. Please try again.', voice_end_conversation: 'End Conversation', voice_mic_aria: 'Microphone', launcher_default: 'Chat with us', }, id: { always_online: 'Selalu online', back_to_home: 'Kembali ke beranda', end_chat_close: 'Akhiri obrolan dan tutup', status_disconnected: 'Terputus', status_connected: 'Terhubung', connecting_wait: 'Menyambungkan... Harap tunggu.', how_can_we_help: 'Bagaimana kami bisa membantu?', choose_communication_style: 'Pilih gaya komunikasi Anda', text_chat_title: 'Obrolan teks', text_chat_desc: 'Ketik pesan dengan AI', voice_chat_title: 'Obrolan suara', voice_chat_desc: 'Bicara alami dengan AI', whatsapp_title: 'WhatsApp', whatsapp_desc: 'Lanjutkan di WhatsApp', placeholder_type_message: 'Ketik pesan...', placeholder_connect_first: 'Sambungkan untuk mulai mengobrol', attach_image: 'Lampirkan gambar', send_message: 'Kirim pesan', message_aria: 'Pesan', powered_by: 'Didayakan oleh ', form_title_start: 'Mari mulai', form_subtitle_details: 'Kami hanya perlu beberapa detail', label_name: 'Nama Anda', placeholder_name: 'Masukkan nama Anda', label_phone: 'Nomor ponsel', placeholder_phone: 'Masukkan nomor ponsel', conversation_language: 'Bahasa percakapan', start_conversation: 'Mulai percakapan →', skip: 'Lewati', back_to_options: '← Kembali ke pilihan', alert_name_phone_required: 'Harap isi nama dan nomor telepon', alert_session_establish: 'Sesi belum tersedia. Harap tunggu sambungan.', ws_not_connected: 'WebSocket tidak terhubung. Harap tunggu sambungan.', submit_error: 'Gagal mengirim informasi. Coba lagi.', voice_title: 'Percakapan suara', voice_tap_mic: 'Ketuk mikrofon untuk berbicara', voice_connecting: 'Menyambungkan…', voice_loading_sdk: 'Memuat SDK suara…', voice_url_or_key_required: 'URL layanan suara atau kunci Daily diperlukan', voice_error: 'Kesalahan suara', voice_connected_speak: 'Terhubung — silakan berbicara', voice_starting_session: 'Memulai sesi suara…', voice_joining_room: 'Bergabung ke ruang suara…', voice_connection_failed: 'Sambungan gagal', voice_url_not_set: 'URL layanan suara tidak diatur', voice_establishing_link: 'Menjalin tautan suara…', voice_connecting_agent: 'Menyambung ke agen…', voice_negotiating: 'Bernegosiasi dengan agen…', voice_signing_in: 'Masuk ke agen…', voice_coming_soon: 'Suara segera hadir', failed_connect: 'Gagal menyambung. Coba lagi atau periksa koneksi Anda.', choose_text_chat_home: 'Buka layar utama dan pilih Obrolan teks untuk memulai.', image_too_large: 'Gambar terlalu besar (maks. 2MB).', image_read_error: 'Tidak dapat membaca gambar. Coba lagi.', voice_end_conversation: 'Akhiri percakapan', voice_mic_aria: 'Mikrofon', launcher_default: 'Ngobrol dengan kami', }, tl: { always_online: 'Palaging online', back_to_home: 'Bumalik sa home', end_chat_close: 'Tapusin ang chat at isara', status_disconnected: 'Hindi konektado', status_connected: 'Nakakonekta', connecting_wait: 'Kumokonekta... Maghintay po.', how_can_we_help: 'Paano kami makakatulong?', choose_communication_style: 'Piliin ang istilo ng komunikasyon', text_chat_title: 'Text chat', text_chat_desc: 'Mag-type ng mensahe gamit ang AI', voice_chat_title: 'Voice chat', voice_chat_desc: 'Magsalita nang natural sa AI', whatsapp_title: 'WhatsApp', whatsapp_desc: 'Magpatuloy sa WhatsApp', placeholder_type_message: 'Mag-type ng mensahe...', placeholder_connect_first: 'Kumonekta para magsimula ng chat', attach_image: 'Maglakip ng larawan', send_message: 'Ipadala ang mensahe', message_aria: 'Mensahe', powered_by: 'Pinapagana ng ', form_title_start: 'Magsimula na tayo', form_subtitle_details: 'Kailangan lang namin ng ilang detalye', label_name: 'Iyong pangalan', placeholder_name: 'Ilagay ang iyong pangalan', label_phone: 'Mobile number', placeholder_phone: 'Ilagay ang iyong mobile', conversation_language: 'Wika ng pag-uusap', start_conversation: 'Simulan ang pag-uusap →', skip: 'Laktawan', back_to_options: '← Bumalik sa mga opsyon', alert_name_phone_required: 'Punan ang pangalan at numero ng telepono', alert_session_establish: 'Hindi pa handa ang session. Maghintay sa koneksyon.', ws_not_connected: 'Hindi nakakonekta ang WebSocket. Maghintay.', submit_error: 'Error sa pagsusumite. Subukan muli.', voice_title: 'Usapang boses', voice_tap_mic: 'I-tap ang mic para magsalita', voice_connecting: 'Kumokonekta…', voice_loading_sdk: 'Naglo-load ng voice SDK…', voice_url_or_key_required: 'Kailangan ang voice service URL o Daily API key', voice_error: 'Error sa boses', voice_connected_speak: 'Nakakonekta — magsalita kapag handa na', voice_starting_session: 'Nagsisimula ng voice session…', voice_joining_room: 'Sumasali sa voice room…', voice_connection_failed: 'Nabigo ang koneksyon', voice_url_not_set: 'Hindi naka-set ang voice service URL', voice_establishing_link: 'Itinatatag ang voice link…', voice_connecting_agent: 'Kumokonekta sa agent…', voice_negotiating: 'Nakikipag-negotiate sa agent…', voice_signing_in: 'Nag-sign in sa agent…', voice_coming_soon: 'Darating na ang boses', failed_connect: 'Nabigo ang koneksyon. Subukan muli o tingnan ang koneksyon.', choose_text_chat_home: 'Pumunta sa home at piliin ang Text chat para magsimula.', image_too_large: 'Masyadong malaki ang larawan (max 2MB).', image_read_error: 'Hindi mabasa ang larawan. Subukan muli.', voice_end_conversation: 'Tapusin ang usapan', voice_mic_aria: 'Mikropono', launcher_default: 'Makipag-chat sa amin', }, ms: { always_online: 'Sentiasa dalam talian', back_to_home: 'Kembali ke laman utama', end_chat_close: 'Tamatkan sembang dan tutup', status_disconnected: 'Terputus', status_connected: 'Bersambung', connecting_wait: 'Menyambung... Sila tunggu.', how_can_we_help: 'Bagaimana kami boleh membantu?', choose_communication_style: 'Pilih gaya komunikasi anda', text_chat_title: 'Sembang teks', text_chat_desc: 'Taip mesej dengan AI', voice_chat_title: 'Sembang suara', voice_chat_desc: 'Bercakap secara semula jadi dengan AI', whatsapp_title: 'WhatsApp', whatsapp_desc: 'Teruskan di WhatsApp', placeholder_type_message: 'Taip mesej...', placeholder_connect_first: 'Sambung untuk mula sembang', attach_image: 'Lampirkan imej', send_message: 'Hantar mesej', message_aria: 'Mesej', powered_by: 'Dikuasakan oleh ', form_title_start: 'Mari mulakan', form_subtitle_details: 'Kami hanya perlukan beberapa butiran', label_name: 'Nama anda', placeholder_name: 'Masukkan nama anda', label_phone: 'Nombor telefon bimbit', placeholder_phone: 'Masukkan nombor bimbit anda', conversation_language: 'Bahasa perbualan', start_conversation: 'Mula perbualan →', skip: 'Langkau', back_to_options: '← Kembali ke pilihan', alert_name_phone_required: 'Sila isi nama dan nombor telefon', alert_session_establish: 'Sesi belum sedia. Sila tunggu sambungan.', ws_not_connected: 'WebSocket tidak bersambung. Sila tunggu.', submit_error: 'Ralat menyerahkan maklumat. Cuba lagi.', voice_title: 'Perbualan suara', voice_tap_mic: 'Ketik mikrofon untuk bercakap', voice_connecting: 'Menyambung…', voice_loading_sdk: 'Memuatkan SDK suara…', voice_url_or_key_required: 'URL perkhidmatan suara atau kunci Daily diperlukan', voice_error: 'Ralat suara', voice_connected_speak: 'Bersambung — bercakap apabila bersedia', voice_starting_session: 'Memulakan sesi suara…', voice_joining_room: 'Menyertai bilik suara…', voice_connection_failed: 'Sambungan gagal', voice_url_not_set: 'URL perkhidmatan suara tidak ditetapkan', voice_establishing_link: 'Menubuhkan pautan suara…', voice_connecting_agent: 'Menyambung ke ejen…', voice_negotiating: 'Berunding dengan ejen…', voice_signing_in: 'Log masuk dengan ejen…', voice_coming_soon: 'Suara akan datang tidak lama lagi', failed_connect: 'Gagal menyambung. Cuba lagi atau semak sambungan anda.', choose_text_chat_home: 'Pergi ke skrin utama dan pilih Sembang teks untuk mula.', image_too_large: 'Imej terlalu besar (maks. 2MB).', image_read_error: 'Tidak boleh membaca imej. Cuba lagi.', voice_end_conversation: 'Tamatkan perbualan', voice_mic_aria: 'Mikrofon', launcher_default: 'Sembang dengan kami', }, hi: { always_online: 'हमेशा ऑनलाइन', back_to_home: 'होम पर वापस जाएँ', end_chat_close: 'चैट समाप्त करें और बंद करें', status_disconnected: 'डिस्कनेक्ट', status_connected: 'कनेक्ट', connecting_wait: 'कनेक्ट हो रहा है... कृपया प्रतीक्षा करें।', how_can_we_help: 'हम कैसे मदद कर सकते हैं?', choose_communication_style: 'अपनी संवाद शैली चुनें', text_chat_title: 'टेक्स्ट चैट', text_chat_desc: 'AI के साथ संदेश टाइप करें', voice_chat_title: 'वॉइस चैट', voice_chat_desc: 'AI से स्वाभाविक रूप से बात करें', whatsapp_title: 'WhatsApp', whatsapp_desc: 'WhatsApp पर जारी रखें', placeholder_type_message: 'संदेश लिखें...', placeholder_connect_first: 'चैट शुरू करने के लिए कनेक्ट करें', attach_image: 'छवि संलग्न करें', send_message: 'संदेश भेजें', message_aria: 'संदेश', powered_by: 'संचालित ', form_title_start: 'चलिए शुरू करते हैं', form_subtitle_details: 'हमें बस कुछ विवरण चाहिए', label_name: 'आपका नाम', placeholder_name: 'अपना नाम दर्ज करें', label_phone: 'मोबाइल नंबर', placeholder_phone: 'अपना मोबाइल दर्ज करें', conversation_language: 'बातचीत की भाषा', start_conversation: 'बातचीत शुरू करें →', skip: 'छोड़ें', back_to_options: '← विकल्पों पर वापस जाएँ', alert_name_phone_required: 'कृपया नाम और फ़ोन नंबर भरें', alert_session_establish: 'सत्र तैयार नहीं। कृपया कनेक्शन की प्रतीक्षा करें।', ws_not_connected: 'WebSocket कनेक्ट नहीं। कृपया प्रतीक्षा करें।', submit_error: 'जानकारी भेजने में त्रुटि। पुनः प्रयास करें।', voice_title: 'वॉइस बातचीत', voice_tap_mic: 'बोलने के लिए माइक टैप करें', voice_connecting: 'कनेक्ट हो रहा है…', voice_loading_sdk: 'वॉइस SDK लोड हो रहा है…', voice_url_or_key_required: 'वॉइस सेवा URL या Daily कुंजी आवश्यक', voice_error: 'वॉइस त्रुटि', voice_connected_speak: 'कनेक्ट — तैयार होने पर बोलें', voice_starting_session: 'वॉइस सेशन शुरू हो रहा है…', voice_joining_room: 'वॉइस रूम में शामिल हो रहे…', voice_connection_failed: 'कनेक्शन विफल', voice_url_not_set: 'वॉइस सेवा URL सेट नहीं', voice_establishing_link: 'वॉइस लिंक स्थापित हो रहा है…', voice_connecting_agent: 'एजेंट से कनेक्ट…', voice_negotiating: 'एजेंट से बातचीत…', voice_signing_in: 'एजेंट में साइन इन…', voice_coming_soon: 'वॉइस जल्द आ रहा है', failed_connect: 'कनेक्ट नहीं हो सका। पुनः प्रयास करें या अपना कनेक्शन जाँचें।', choose_text_chat_home: 'होम पर जाएँ और टेक्स्ट चैट चुनकर शुरू करें।', image_too_large: 'छवि बहुत बड़ी (अधिकतम 2MB)।', image_read_error: 'छवि नहीं पढ़ पा रहे। पुनः प्रयास करें।', voice_end_conversation: 'बातचीत समाप्त करें', voice_mic_aria: 'माइक्रोफ़ोन', launcher_default: 'हमसे चैट करें', }, es: { always_online: 'Siempre en línea', back_to_home: 'Volver al inicio', end_chat_close: 'Finalizar chat y cerrar', status_disconnected: 'Desconectado', status_connected: 'Conectado', connecting_wait: 'Conectando... Espere.', how_can_we_help: '¿Cómo podemos ayudarle?', choose_communication_style: 'Elija su estilo de comunicación', text_chat_title: 'Chat de texto', text_chat_desc: 'Escriba mensajes con la IA', voice_chat_title: 'Chat de voz', voice_chat_desc: 'Hable con naturalidad con la IA', whatsapp_title: 'WhatsApp', whatsapp_desc: 'Continuar en WhatsApp', placeholder_type_message: 'Escriba un mensaje...', placeholder_connect_first: 'Conéctese para empezar a chatear', attach_image: 'Adjuntar imagen', send_message: 'Enviar mensaje', message_aria: 'Mensaje', powered_by: 'Con tecnología de ', form_title_start: 'Empecemos', form_subtitle_details: 'Solo necesitamos algunos datos', label_name: 'Su nombre', placeholder_name: 'Introduzca su nombre', label_phone: 'Número móvil', placeholder_phone: 'Introduzca su móvil', conversation_language: 'Idioma de la conversación', start_conversation: 'Iniciar conversación →', skip: 'Omitir', back_to_options: '← Volver a opciones', alert_name_phone_required: 'Complete nombre y teléfono', alert_session_establish: 'Sesión no lista. Espere la conexión.', ws_not_connected: 'WebSocket no conectado. Espere.', submit_error: 'Error al enviar. Intente de nuevo.', voice_title: 'Conversación por voz', voice_tap_mic: 'Pulse el micrófono para hablar', voice_connecting: 'Conectando…', voice_loading_sdk: 'Cargando SDK de voz…', voice_url_or_key_required: 'Se requiere URL del servicio de voz o clave de Daily', voice_error: 'Error de voz', voice_connected_speak: 'Conectado — hable cuando esté listo', voice_starting_session: 'Iniciando sesión de voz…', voice_joining_room: 'Uniéndose a la sala de voz…', voice_connection_failed: 'Conexión fallida', voice_url_not_set: 'URL del servicio de voz no configurada', voice_establishing_link: 'Estableciendo enlace de voz…', voice_connecting_agent: 'Conectando con el agente…', voice_negotiating: 'Negociando con el agente…', voice_signing_in: 'Iniciando sesión con el agente…', voice_coming_soon: 'Voz próximamente', failed_connect: 'No se pudo conectar. Intente de nuevo o compruebe su conexión.', choose_text_chat_home: 'Vaya al inicio y elija chat de texto para empezar.', image_too_large: 'Imagen demasiado grande (máx. 2 MB).', image_read_error: 'No se pudo leer la imagen. Intente de nuevo.', voice_end_conversation: 'Finalizar conversación', voice_mic_aria: 'Micrófono', launcher_default: 'Chatea con nosotros', }, fr: { always_online: 'Toujours en ligne', back_to_home: "Retour à l'accueil", end_chat_close: 'Terminer le chat et fermer', status_disconnected: 'Déconnecté', status_connected: 'Connecté', connecting_wait: 'Connexion en cours... Veuillez patienter.', how_can_we_help: 'Comment pouvons-nous vous aider ?', choose_communication_style: 'Choisissez votre mode de communication', text_chat_title: 'Chat texte', text_chat_desc: 'Écrivez des messages avec l’IA', voice_chat_title: 'Chat vocal', voice_chat_desc: 'Parlez naturellement avec l’IA', whatsapp_title: 'WhatsApp', whatsapp_desc: 'Continuer sur WhatsApp', placeholder_type_message: 'Saisissez un message...', placeholder_connect_first: 'Connectez-vous pour démarrer le chat', attach_image: 'Joindre une image', send_message: 'Envoyer le message', message_aria: 'Message', powered_by: 'Propulsé par ', form_title_start: 'Commençons', form_subtitle_details: 'Il nous faut quelques informations', label_name: 'Votre nom', placeholder_name: 'Entrez votre nom', label_phone: 'Numéro de mobile', placeholder_phone: 'Entrez votre mobile', conversation_language: 'Langue de la conversation', start_conversation: 'Démarrer la conversation →', skip: 'Ignorer', back_to_options: '← Retour aux options', alert_name_phone_required: 'Veuillez remplir le nom et le téléphone', alert_session_establish: 'Session non établie. Veuillez attendre.', ws_not_connected: 'WebSocket non connecté. Veuillez attendre.', submit_error: "Erreur d'envoi. Réessayez.", voice_title: 'Conversation vocale', voice_tap_mic: 'Appuyez sur le micro pour parler', voice_connecting: 'Connexion…', voice_loading_sdk: 'Chargement du SDK vocal…', voice_url_or_key_required: 'URL du service vocal ou clé Daily requise', voice_error: 'Erreur vocale', voice_connected_speak: 'Connecté — parlez quand vous êtes prêt', voice_starting_session: 'Démarrage de la session vocale…', voice_joining_room: 'Connexion à la salle vocale…', voice_connection_failed: 'Échec de la connexion', voice_url_not_set: 'URL du service vocal non définie', voice_establishing_link: 'Établissement du lien vocal…', voice_connecting_agent: "Connexion à l'agent…", voice_negotiating: "Négociation avec l'agent…", voice_signing_in: "Connexion à l'agent…", voice_coming_soon: 'La voix arrive bientôt', failed_connect: 'Échec de la connexion. Réessayez ou vérifiez votre connexion.', choose_text_chat_home: "Allez à l'accueil et choisissez le chat texte pour commencer.", image_too_large: 'Image trop volumineuse (max 2 Mo).', image_read_error: 'Impossible de lire l’image. Réessayez.', voice_end_conversation: 'Terminer la conversation', voice_mic_aria: 'Microphone', launcher_default: 'Discutez avec nous', }, } function normalizeWhatsAppDigits(phone) { return String(phone || '').replace(/\D/g, '') } function domainAllowed(hostname, allowedList) { if (!Array.isArray(allowedList) || allowedList.length === 0) return true const host = (hostname || '').toLowerCase() return allowedList.some((d) => { const domain = (d || '').toLowerCase().trim() if (!domain) return false return host === domain || host.endsWith('.' + domain) }) } function createStyles(primary, background) { const safePrimary = primary || '#2563eb' const safeBg = background || '#ffffff' return ` :host, .acw * { box-sizing: border-box; } .acw-container { position: fixed; z-index: 2147483000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; } .acw-container.bottom-right { right: 24px; bottom: 24px; } .acw-container.middle-right { right: 24px; top: 50%; transform: translateY(-50%); } .acw-launcher-wrap { position: relative; display: inline-flex; border-radius: 999px; padding: 0; } .acw-launcher-wrap::before { content: ""; display: none; } @keyframes acw-launcher-glow { to { transform: rotate(360deg); } } .acw-launcher { position: relative; z-index: 1; background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 55%), ${safePrimary}; color: #fff; border: none; border-radius: 999px; padding: 10px 14px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 12px 40px rgba(37, 99, 235, 0.45), 0 4px 12px rgba(0,0,0,0.12); font-size: 14px; transition: transform 0.2s, box-shadow 0.2s; } .acw-launcher:hover { transform: scale(1.03); box-shadow: 0 16px 44px rgba(37, 99, 235, 0.5), 0 6px 14px rgba(0,0,0,0.14); } .acw-launcher:active { transform: scale(0.98); } .acw-launcher-fab { position: relative; width: 64px; height: 64px; padding: 0; border-radius: 50%; } .acw-launcher-fab .acw-launcher-sparkle, .acw-launcher-fab .acw-launcher-x { width: 28px; height: 28px; transition: transform 0.35s ease; } .acw-launcher-fab.is-open .acw-launcher-sparkle { transform: rotate(90deg) scale(0); opacity: 0; position: absolute; } .acw-launcher-fab.is-open .acw-launcher-x { transform: rotate(0deg) scale(1); opacity: 1; } .acw-launcher-fab:not(.is-open) .acw-launcher-x { transform: rotate(-90deg) scale(0); opacity: 0; position: absolute; } .acw-launcher-fab:not(.is-open) .acw-launcher-sparkle { position: relative; opacity: 1; } .acw-launcher img { width: 24px; height: 24px; object-fit: contain; border-radius: 50%; } .acw-launcher.vertical { flex-direction: column; padding: 14px 10px; min-width: 48px; height: auto; } .acw-launcher.vertical span { writing-mode: sideways-lr; text-orientation: mixed; letter-spacing: 0.5px; } .acw-header-left { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; } .acw-header-icon { width: 44px; height: 44px; object-fit: cover; object-position: center 78%; flex-shrink: 0; border-radius: 50%; border: 2px solid rgba(96, 165, 250, 0.35); box-shadow: 0 2px 10px rgba(0,0,0,0.08); position: relative; } .acw-header-icon-wrap { position: relative; display: inline-flex; } .acw-online-dot { position: absolute; right: 2px; bottom: 2px; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; border: 2px solid #fff; box-shadow: 0 0 0 2px rgba(34,197,94,0.25); } .acw-header-title-block { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .acw-header-title-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; } .acw-header-subtitle { font-size: 12px; font-weight: 600; color: ${safePrimary}; } .acw-unread-badge { background: rgba(255,255,255,0.95); color: ${safePrimary}; font-size: 11px; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: inline-flex; align-items: center; justify-content: center; padding: 0 5px; } .acw-unread-badge.hidden { display: none; } .acw-panel { position: absolute; width: 380px; max-width: 92vw; height: 640px; max-height: 85vh; background: rgba(255,255,255,0.95); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border: 1px solid rgba(191, 219, 254, 0.5); border-radius: 32px; box-shadow: 0 25px 50px rgba(15, 23, 42, 0.15), 0 12px 24px rgba(30, 58, 138, 0.12); display: none; flex-direction: column; overflow: hidden; } .acw-panel.bottom-right { right: 0; bottom: 76px; } .acw-panel.middle-right { right: 80px; top: 50%; transform: translateY(-50%); } .acw-panel.top-right { right: 0; top: 88px; } .acw-panel.bottom-left { left: 0; bottom: 76px; } .acw-panel.middle-left { left: 80px; top: 50%; transform: translateY(-50%); } .acw-panel.top-left { left: 0; top: 88px; } .acw-header { background: linear-gradient(90deg, rgba(236,254,255,0.85) 0%, rgba(239,246,255,0.85) 50%, rgba(238,242,255,0.85) 100%); color: #0f172a; padding: 16px 18px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(37, 99, 235, 0.1); } .acw-title { font-weight: 700; font-size: 15px; letter-spacing: -0.02em; } .acw-close { background: transparent; border: none; color: #94a3b8; cursor: pointer; font-size: 18px; line-height: 1; padding: 8px; width: 36px; height: 36px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; } .acw-close:hover { color: #64748b; background: rgba(0,0,0,0.05); } .acw-close svg { display: block; flex-shrink: 0; } .acw-messages { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 12px; background: linear-gradient(165deg, rgba(239,246,255,0.45) 0%, #ffffff 45%, rgba(238,242,255,0.4) 100%); } .acw-msg { padding: 12px 16px; border-radius: 24px; max-width: 88%; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; } .acw-msg-user { margin-left: auto; background: linear-gradient(135deg, #22d3ee, ${safePrimary}, #4f46e5); color: #fff; border-radius: 24px 24px 8px 24px; box-shadow: 0 8px 24px rgba(37, 99, 235, 0.35); } .acw-msg-bot { margin-right: auto; background: #fff; color: #0f172a; border: 1px solid rgba(191, 219, 254, 0.9); border-radius: 24px 24px 24px 8px; box-shadow: 0 2px 8px rgba(15,23,42,0.06); } .acw-msg-typing { margin-right: auto; display: inline-flex; align-items: center; gap: 3px; padding: 8px 12px; min-height: 0; background: #fff; border: 1px solid rgba(191, 219, 254, 0.9); border-radius: 18px 18px 18px 6px; box-shadow: 0 2px 8px rgba(15,23,42,0.06); } .acw-msg-typing span { width: 5px; height: 5px; border-radius: 50%; background: ${safePrimary}; opacity: 0.45; animation: acw-typing-dot 1.15s ease-in-out infinite; } .acw-msg-typing span:nth-child(2) { animation-delay: 0.2s; } .acw-msg-typing span:nth-child(3) { animation-delay: 0.4s; } @keyframes acw-typing-dot { 0%, 70%, 100% { opacity: 0.35; transform: translateY(0); } 35% { opacity: 1; transform: translateY(-2px); } } .acw-msg-img { max-width: 100%; border-radius: 16px; display: block; margin-top: 8px; max-height: 200px; object-fit: cover; } .acw-input { border-top: 1px solid rgba(0,0,0,0.06); padding: 10px 12px; display: flex; gap: 10px; align-items: center; background: rgba(250,250,250,0.95); } /* Image attach hidden until product enables uploads again */ .acw-input-attach { display: none !important; flex-shrink: 0; width: 48px; height: 48px; border-radius: 14px; border: 1px solid rgba(37, 99, 235, 0.2); background: #fff; cursor: pointer; align-items: center; justify-content: center; color: ${safePrimary}; transition: border-color 0.2s, box-shadow 0.2s; } .acw-input-attach:hover { border-color: ${safePrimary}; box-shadow: 0 4px 12px rgba(37,99,235,0.15); } .acw-input-attach svg { width: 22px; height: 22px; } .acw-input textarea { flex: 1; min-width: 0; min-height: 40px; max-height: 100px; padding: 9px 12px; resize: none; border: 2px solid rgba(37, 99, 235, 0.18); border-radius: 12px; font-size: 14px; font-family: inherit; line-height: 1.4; box-sizing: border-box; overflow: hidden; } .acw-input textarea:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(191, 219, 254, 0.6); } .acw-input textarea:disabled, .acw-input button:disabled { opacity: 0.6; cursor: not-allowed; } .acw-input-send { flex-shrink: 0; width: 40px; height: 40px; border-radius: 12px; border: none; cursor: pointer; background: linear-gradient(135deg, #22d3ee, ${safePrimary}, #4f46e5); color: #fff; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(37, 99, 235, 0.32); } .acw-input-send:hover { filter: brightness(1.05); transform: scale(1.02); } .acw-input-send:active { transform: scale(0.96); } .acw-input-send svg { width: 22px; height: 22px; } .acw-footer { font-size: 11px; color: #94a3b8; padding: 6px 12px 10px; text-align: center; border-top: 1px solid rgba(0,0,0,0.06); } .acw-footer a { color: #64748b; text-decoration: none; } .acw-footer a:hover { text-decoration: underline; } .acw-status { font-size: 12px; color: #64748b; padding: 8px 16px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid rgba(0,0,0,0.05); background: rgba(250,250,250,0.9); } .acw-status-disconnect { margin-left: auto; flex-shrink: 0; width: 32px; height: 32px; padding: 6px; } .acw-dot { width: 8px; height: 8px; border-radius: 999px; box-shadow: 0 0 0 6px rgba(0,0,0,0.04); } .acw-dot.connected { background: #22c55e; box-shadow: 0 0 0 6px rgba(34,197,94,0.18); } .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); } .acw-customer-form { position: absolute; inset: 0; background: rgba(255,255,255,0.98); backdrop-filter: blur(8px); z-index: 10; display: flex; flex-direction: column; align-items: stretch; padding: 24px 32px 32px; overflow-y: auto; overflow-x: hidden; box-sizing: border-box; scrollbar-gutter: stable; } .acw-form-hero { width: 52px; height: 52px; margin: 4px auto 14px; border-radius: 14px; display: flex; align-items: center; justify-content: center; } .acw-form-hero-chat { background: ${safePrimary}; } .acw-form-hero-voice { background: ${safePrimary}; } .acw-form-hero svg { width: 28px; height: 28px; color: #fff; } .acw-customer-form h3 { margin: 0 0 6px; font-size: 18px; font-weight: 700; color: #0f172a; text-align: center; letter-spacing: -0.02em; } .acw-customer-form p { margin: 0 0 18px; font-size: 13px; color: #64748b; text-align: center; line-height: 1.45; } .acw-customer-form .field { margin-bottom: 14px; min-width: 0; width: 100%; box-sizing: border-box; } .acw-customer-form label { display: block; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; color: #475569; } .acw-customer-form input { display: block; width: 100%; max-width: 100%; min-height: 0; height: auto; box-sizing: border-box; padding: 10px 12px; border: 2px solid rgba(37, 99, 235, 0.2); border-radius: 12px; font-size: 14px; line-height: 1.4; transition: border-color 0.2s, box-shadow 0.2s; } .acw-customer-form input:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(191, 219, 254, 0.55); } .acw-customer-form select { display: block; width: 100%; max-width: 100%; box-sizing: border-box; padding: 10px 12px; border: 2px solid rgba(37, 99, 235, 0.2); border-radius: 12px; font-size: 14px; line-height: 1.4; transition: border-color 0.2s, box-shadow 0.2s; background: #fff; } .acw-customer-form select:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(191, 219, 254, 0.55); } .acw-customer-form .actions { display: flex; flex-direction: column; gap: 10px; margin-top: 6px; min-width: 0; width: 100%; box-sizing: border-box; } .acw-customer-form button { width: 100%; max-width: 100%; box-sizing: border-box; padding: 12px 16px; border: none; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; } .acw-customer-form .submit-btn { min-height: 48px; padding-top: 12px; padding-bottom: 12px; background: linear-gradient(90deg, #22d3ee, ${safePrimary}, #6366f1); color: #fff; box-shadow: 0 8px 24px rgba(99, 102, 241, 0.35); } .acw-customer-form .submit-btn:hover { filter: brightness(1.05); transform: scale(1.02); } .acw-customer-form .skip-btn { background: #f8fafc; color: #64748b; border: 1px solid rgba(0,0,0,0.08); } .acw-customer-form .skip-btn:hover { background: #f1f5f9; } .acw-form-back { text-align: center; margin-top: 16px; font-size: 14px; color: #64748b; background: none; border: none; cursor: pointer; padding: 8px; } .acw-form-back:hover { color: #0f172a; } .acw-start-cta { flex: 1; display: flex; flex-direction: column; align-items: stretch; justify-content: center; padding: 20px 16px 24px; } .acw-start-cta-mode { justify-content: flex-start; padding-top: 20px; height: auto; } .acw-start-cta button { background: #fff; color: #0f172a; border: 2px solid rgba(37, 99, 235, 0.15); border-radius: 25px; font-size: 15px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.06); } .acw-start-cta button:hover { background: #f8fafc; border-color: ${safePrimary}; } /* Mode selector should size to its contents (no forced scroll height) */ .acw-mode-selector { flex: 0 0 auto; display: flex; flex-direction: column; align-items: stretch; padding: 0 4px 8px; gap: 0; overflow: visible; } .acw-mode-hero-icon { width: 72px; height: 72px; margin: 0 auto 16px; border-radius: 50%; background: transparent; display: flex; align-items: center; justify-content: center; box-shadow: 0 12px 28px rgba(15,23,42,0.10); border: 1px solid rgba(0,0,0,0.06); overflow: hidden; } .acw-mode-hero-icon svg { width: 36px; height: 36px; color: ${safePrimary}; } .acw-mode-hero-icon img { width: 100%; height: 100%; padding: 12px; object-fit: contain; border-radius: 0; background: transparent; } .acw-mode-heading { margin: 0 0 6px; font-size: 30px; font-weight: 600; line-height: 1.2; text-align: center; letter-spacing: 0em; background: linear-gradient(to right, #0891b2, #2563eb, #4f46e5); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .acw-mode-sub { margin: 0 0 20px; font-size: 14px; font-weight: 400; color: #6b7280; text-align: center; line-height: 1.4; } .acw-mode-options { display: flex; flex-direction: column; gap: 12px; flex: 0 0 auto; height: auto; } .acw-mode-btn { display: flex; flex-direction: row; align-items: center; gap: 14px; width: 100%; min-height: 0; padding: 16px 18px; background: #fff; border: 1px solid rgba(0,0,0,0.06); border-radius: 24px; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; text-align: left; box-shadow: 0 4px 14px rgba(15,23,42,0.06); position: relative; overflow: hidden; } .acw-mode-btn::before { /* Sweeping highlight band (moves left -> right) */ content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0px; opacity: 0; pointer-events: none; border-radius: inherit; border-top-left-radius: 0; border-bottom-left-radius: 0; z-index: 0; transform: translateX(-100%); background: linear-gradient( 90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 35%, var(--acw-sweep, rgba(37, 99, 235, 0.22)) 50%, rgba(255,255,255,0) 65%, rgba(255,255,255,0) 100% ); transition: opacity 0.15s cubic-bezier(.4, 0, .2, 1); mix-blend-mode: normal; } .acw-mode-btn::after { /* Static subtle highlight that remains after sweep */ content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0px; opacity: 0; pointer-events: none; border-radius: inherit; border-top-left-radius: 0; border-bottom-left-radius: 0; z-index: 0; transition: opacity 0.2s cubic-bezier(.4, 0, .2, 1) 0.12s; } @keyframes acw-sweep-ltr { 0% { transform: translateX(-60%); opacity: 0; } 10% { opacity: 1; } 100% { transform: translateX(60%); opacity: 0; } } .acw-mode-btn:hover { transform: translateY(-2px) scale(1.0); box-shadow: 0 12px 28px rgba(37, 99, 235, 0.18); } .acw-mode-btn:hover::before { animation: acw-sweep-ltr 0.65s cubic-bezier(.4, 0, .2, 1) both; } .acw-mode-btn:hover::after { opacity: 1; } .acw-mode-btn > * { position: relative; z-index: 1; } .acw-mode-btn .acw-mode-icon { width: 48px; height: 48px; flex-shrink: 0; border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 10px 15px -3px rgba(15,23,42,0.12); } .acw-mode-icon-chat { background: linear-gradient(to bottom right, #22d3ee, #2563eb); box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.3); } .acw-mode-icon-voice { background: linear-gradient(to bottom right, #6366f1, #9333ea); box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3); } .acw-mode-icon-whatsapp { background: linear-gradient(to bottom right, #10b981, #16a34a); box-shadow: 0 10px 15px -3px rgba(16, 185, 129, 0.3); } .acw-mode-btn .acw-mode-icon svg { width: 22px; height: 22px; color: #fff; } .acw-mode-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; } .acw-mode-title { font-size: 16px; font-weight: 700; color: #111827; margin-bottom: 2px; } .acw-mode-desc { font-size: 12px; font-weight: 400; color: #4b5563; line-height: 1.35; } .acw-mode-chevron { flex-shrink: 0; width: 22px; height: 22px; color: #94a3b8; opacity: 0.5; transition: transform 0.2s, opacity 0.2s; } .acw-mode-btn:hover .acw-mode-chevron { opacity: 1; transform: translateX(4px); } /* Per-mode borders + hover gradients + chevron colors (matches spec) */ .acw-mode-btn-chat { border: 2px solid #dbeafe; --acw-sweep: rgba(6, 182, 212, 0.24); } .acw-mode-btn-chat::after { background: linear-gradient(to right, rgba(6, 182, 212, 0.1), rgba(37, 99, 235, 0.1), rgba(79, 70, 229, 0.1)); } .acw-mode-btn-chat:hover { border-color: #60a5fa; } .acw-mode-btn-chat:hover .acw-mode-chevron { color: #2563eb; } .acw-mode-btn-voice { border: 2px solid #e0e7ff; --acw-sweep: rgba(99, 102, 241, 0.24); } .acw-mode-btn-voice::after { background: linear-gradient(to right, rgba(99, 102, 241, 0.1), rgba(168, 85, 247, 0.1), rgba(236, 72, 153, 0.1)); } .acw-mode-btn-voice:hover { border-color: #818cf8; } .acw-mode-btn-voice:hover .acw-mode-chevron { color: #4f46e5; } .acw-mode-btn-whatsapp { border: 2px solid #d1fae5; --acw-sweep: rgba(16, 185, 129, 0.24); } .acw-mode-btn-whatsapp::after { background: linear-gradient(to right, rgba(16, 185, 129, 0.1), rgba(34, 197, 94, 0.1), rgba(20, 184, 166, 0.1)); } .acw-mode-btn-whatsapp:hover { border-color: #34d399; } .acw-mode-btn-whatsapp:hover .acw-mode-chevron { color: #059669; } .acw-voice-panel { flex: 1; display: none; flex-direction: column; align-items: stretch; justify-content: flex-start; padding: 0; position: relative; overflow: hidden; background: #0b1220; color: #fff; } .acw-voice-panel.visible { display: flex; } .acw-voice-orb { position: absolute; border-radius: 50%; filter: blur(48px); opacity: 0.16; pointer-events: none; } .acw-voice-orb.a { width: 280px; height: 280px; background: ${safePrimary}; top: -80px; right: -60px; } .acw-voice-orb.b { width: 240px; height: 240px; background: #22d3ee; bottom: -40px; left: -50px; } @keyframes acw-orb-pulse { 0%, 100% { transform: scale(1); opacity: 0.35; } 50% { transform: scale(1.08); opacity: 0.55; } } .acw-voice-inner { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px 20px 24px; gap: 20px; } .acw-voice-title { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; text-align: center; } .acw-voice-status { font-size: 14px; font-weight: 500; line-height: 1.4; color: rgba(255,255,255,0.92); text-align: center; min-height: 2.8em; padding: 0 12px; } .acw-voice-visualizer { display: flex; align-items: flex-end; justify-content: center; gap: 6px; height: 72px; } .acw-voice-bar { width: 8px; border-radius: 4px; background: rgba(255,255,255,0.72); height: 24px; transition: height 0.08s ease-out; } .acw-voice-mic-wrap { display: flex; flex-direction: column; align-items: center; gap: 20px; width: 100%; } .acw-voice-mic { width: 96px; height: 96px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.35); cursor: pointer; display: flex; align-items: center; justify-content: center; background: rgba(255,255,255,0.15); color: #fff; transition: transform 0.2s, background 0.2s, box-shadow 0.2s; } .acw-voice-mic:hover { transform: scale(1.03); background: rgba(255,255,255,0.22); } .acw-voice-mic.recording { background: #fff; color: ${safePrimary}; border-color: #fff; box-shadow: 0 0 0 12px rgba(255,255,255,0.15); animation: none; } @keyframes acw-pulse-mic { 0%, 100% { box-shadow: 0 0 0 12px rgba(255,255,255,0.12); } 50% { box-shadow: 0 0 0 20px rgba(255,255,255,0.2); } } .acw-voice-mic svg { width: 36px; height: 36px; } .acw-voice-end-row { width: 100%; max-width: 280px; height: 56px; border-radius: 16px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.1); backdrop-filter: blur(8px); color: #fff; font-size: 15px; font-weight: 600; display: none; align-items: center; justify-content: center; gap: 10px; cursor: pointer; } .acw-voice-end-row:hover { background: rgba(255,255,255,0.18); } .acw-voice-end-row svg { width: 20px; height: 20px; } @keyframes acw-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.75; } } @media (max-width: 480px) { .acw-panel { width: calc(100vw - 24px); } .acw-panel:not(.acw-panel-fit-content) { height: min(640px, 85vh); } } ` } function createWidget(config) { const { agentId, embedKey, primaryColor, backgroundColor, allowedDomains = [], apiBaseUrl, voiceServiceUrl, wsUrl, title = 'Chat with us', greeting = null, captureCustomerInfo = false, allowSkipCustomerInfo = true, position = 'bottom-right', iconUrl = null, showIcon = true, launcherText, enableVoice = true, isLocalWebrtc = true, useDailyVoice = false, dailyApiKey = '', dailyAgentName = defaultDailyAgentName(), dailyScriptUrl = DEFAULT_DAILY_SCRIPT_URL, voiceTenant = 'default', liveTranscript = false, voiceSessionData = {}, whatsappPhone = '', whatsappMessage = 'Hi! I need assistance.', enableWhatsApp = true, language = 'en', } = config const uiLang = normalizeWidgetUiLanguage(language) function t(key) { return acwT(uiLang, key) } const trimmedVoiceUrl = voiceServiceUrl != null ? String(voiceServiceUrl).trim() : '' const voiceHttpBase = trimmedVoiceUrl ? trimmedVoiceUrl.replace(/\/$/, '') : VERSION.includes('-staging') ? 'https://talk2ai-staging.helllo.ai' : VERSION.includes('-prod') ? 'https://talk2ai-prod.helllo.ai' : '' const whatsappDigits = normalizeWhatsAppDigits(whatsappPhone) const hasWhatsAppOption = enableWhatsApp !== false && whatsappDigits.length > 0 // Resolve favicon for hero/header (always). const resolvedFaviconUrl = (() => { try { const link = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]') if (link && link.href) return link.href } catch (_) {} try { const origin = window.location.origin || '' if (origin) return origin + '/favicon.ico' } catch (_) {} return null })() // Resolve widget icon (only used when showIcon is true): explicit iconUrl, else favicon. const resolvedIconUrl = (() => { if (!showIcon) return null if (iconUrl && typeof iconUrl === 'string' && iconUrl.trim()) return iconUrl.trim() return resolvedFaviconUrl })() const launcherLabel = (typeof launcherText === 'string' && launcherText.trim()) ? launcherText.trim() : t('launcher_default') var conversationLanguageCode = null var supportedLanguagesList = [] var languageSelect = null var languageFieldWrap = null function resolveLanguagesApiBase() { var b = voiceHttpBase || apiBaseUrl if (b) return String(b).replace(/\/$/, '') try { var sc = document.currentScript || document.querySelector('script[data-agent-id]') if (sc && sc.src) return new URL(sc.src).origin } catch (_) {} try { return window.location.origin } catch (_) { return '' } } function fetchSupportedLanguages(done) { var base = resolveLanguagesApiBase() if (!base || !embedKey || !agentId) { if (done) done(null) return } var url = base + '/api/v1/agent-voice/' + encodeURIComponent(String(agentId)) + '/supported-languages?embed_key=' + encodeURIComponent(embedKey) fetch(url) .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('languages')) }) .then(function (data) { var langs = data && data.languages && Array.isArray(data.languages) ? data.languages : [] supportedLanguagesList = langs if (done) done(langs) }) .catch(function () { if (done) done(null) }) } // Valid positions const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left'] const finalPosition = validPositions.includes(position) ? position : 'bottom-right' const isVertical = finalPosition === 'middle-right' || finalPosition === 'middle-left' const isLeft = finalPosition.includes('left') if (!agentId) { console.warn('[AgentChatWidget] Missing agentId. Provide it via init({ agentId: "..." }) or data-agent-id attribute'