@helllo-ai/agent-chat-widget
Version:
Bot Swarm Agent Chat Widget - Embeddable chat widget for AI agents
892 lines (869 loc) • 127 kB
JavaScript
;(function () {
if (typeof window === 'undefined') return
if (window.AgentChatWidget) return
const VERSION = '0.1.0-staging'
/** 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 at