UNPKG

bettercx-widget

Version:

Professional AI-powered chat widget for BetterCX platform. Seamlessly integrate intelligent customer support into any website.

883 lines (879 loc) 71.7 kB
import { d as defineCustomElement$1, p as proxyCustomElement, H, c as createEvent, h, e as Host } from './p-BdIfuIyd.js'; /** * Authentication Service * Handles widget session creation and token management */ class AuthService { baseUrl; sessionToken; sessionExpiresAt; constructor(baseUrl = 'http://localhost:8000') { this.baseUrl = baseUrl; } /** * Create a new widget session */ async createSession(widgetKey, origin) { const request = { widget_key: widgetKey, origin: origin, }; const response = await fetch(`${this.baseUrl}/api/widgets/session/create/`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(request), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`); } const sessionData = await response.json(); // Store session data in memory only this.sessionToken = sessionData.data.token; this.sessionExpiresAt = new Date(sessionData.data.expires_at); return sessionData.data; } /** * Get current session token */ getToken() { if (this.isTokenValid()) { return this.sessionToken; } return undefined; } /** * Check if current token is valid and not expired */ isTokenValid() { if (!this.sessionToken || !this.sessionExpiresAt) { return false; } return new Date() < this.sessionExpiresAt; } /** * Clear current session */ clearSession() { this.sessionToken = undefined; this.sessionExpiresAt = undefined; } /** * Get authorization header for API requests */ getAuthHeader() { const token = this.getToken(); return token ? { Authorization: `Bearer ${token}` } : {}; } /** * Refresh session if needed */ async refreshSessionIfNeeded(widgetKey, origin) { if (this.isTokenValid()) { return true; } try { await this.createSession(widgetKey, origin); return true; } catch { return false; } } } /** * API Service * Handles all API communication with backend services */ class ApiService { authService; dbServiceUrl; aiServiceUrl; constructor(dbServiceUrl = 'http://localhost:8000', aiServiceUrl = 'http://localhost:8081', authService) { this.dbServiceUrl = dbServiceUrl; this.aiServiceUrl = aiServiceUrl; this.authService = authService || new AuthService(dbServiceUrl); } /** * Fetch widget configuration from backend */ async getWidgetConfig(organizationId) { const token = this.authService.getToken(); if (!token) { throw new Error('No valid session token available'); } try { const response = await fetch(`${this.dbServiceUrl}/api/widgets/org/${organizationId}/widget-config/`, { method: 'GET', headers: { 'Content-Type': 'application/json', ...this.authService.getAuthHeader(), }, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { throw error; } } /** * Send a chat message to the AI service */ async sendMessage(message) { const token = this.authService.getToken(); if (!token) { throw new Error('No valid session token available'); } const request = { content: message, }; try { const response = await fetch(`${this.aiServiceUrl}/widget/ai/respond/`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...this.authService.getAuthHeader(), }, body: JSON.stringify(request), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || `HTTP ${response.status}: ${response.statusText}`); } // Return the response stream for streaming return response.body; } catch (error) { throw error; } } /** * Parse streaming response from AI service */ async *parseStreamResponse(stream) { const reader = stream.getReader(); const decoder = new TextDecoder(); let currentEventType = 'streaming_output'; // Default event type try { while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('event: ')) { currentEventType = line.slice(7).trim(); continue; } if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { return; } try { const parsed = JSON.parse(data); if (parsed.content) { yield { type: currentEventType, content: parsed.content }; } } catch (e) { // Skip invalid JSON continue; } } } } } finally { reader.releaseLock(); } } /** * Get the auth service instance */ getAuthService() { return this.authService; } } /** * Theme Service * Handles widget theming and CSS custom properties */ class ThemeService { hostElement; currentTheme = 'auto'; constructor(hostElement) { this.hostElement = hostElement; } /** * Detect the language of the website the widget is embedded on * Returns 'pl' for Polish, 'en' for English (default) */ async detectWebsiteLanguage() { try { // Method 1: Check meta http-equiv="content-language" const metaLang = document.querySelector('meta[http-equiv="content-language"]'); if (metaLang) { const content = metaLang.getAttribute('content')?.toLowerCase(); if (content?.includes('pl')) return 'pl'; if (content?.includes('en')) return 'en'; } // Method 2: Check meta name="language" const metaNameLang = document.querySelector('meta[name="language"]'); if (metaNameLang) { const content = metaNameLang.getAttribute('content')?.toLowerCase(); if (content?.includes('pl')) return 'pl'; if (content?.includes('en')) return 'en'; } // Method 3: Check for Polish text patterns in the page (improved) const bodyText = document.body.textContent?.toLowerCase() || ''; // More comprehensive Polish word detection const polishWords = [ // Common Polish words 'i', 'w', 'na', 'z', 'do', 'od', 'po', 'przy', 'dla', 'przez', 'bez', 'pod', 'nad', 'między', 'przed', 'za', // Polish articles and pronouns 'jest', 'są', 'ma', 'mają', 'być', 'może', 'można', 'możemy', 'można', 'możemy', // Polish common verbs 'jest', 'są', 'ma', 'mają', 'być', 'może', 'można', 'możemy', 'można', 'możemy', // Polish common nouns 'strona', 'stronie', 'strony', 'stron', 'stroną', 'stronę', 'stronie', 'strony', 'stron', 'stroną', 'stronę', 'informacje', 'informacji', 'informacjami', 'informacjami', 'informacjami', 'informacjami', 'informacjami', 'kontakt', 'kontakcie', 'kontaktu', 'kontaktem', 'kontaktem', 'kontaktem', 'kontaktem', 'kontaktem', // Polish common adjectives 'nowy', 'nowa', 'nowe', 'nowego', 'nowej', 'nowego', 'nowym', 'nową', 'nowym', 'nowym', 'dobry', 'dobra', 'dobre', 'dobrego', 'dobrej', 'dobrego', 'dobrym', 'dobrą', 'dobrym', 'dobrym', // Polish common conjunctions 'oraz', 'lub', 'ale', 'jednak', 'więc', 'dlatego', 'ponieważ', 'gdy', 'gdyż', 'jeśli', 'jeżeli', // Polish common prepositions 'w', 'na', 'z', 'do', 'od', 'po', 'przy', 'dla', 'przez', 'bez', 'pod', 'nad', 'między', 'przed', 'za', // Polish common particles 'nie', 'już', 'jeszcze', 'tylko', 'także', 'również', 'też', 'tak', 'nie', 'już', 'jeszcze', 'tylko', 'także', 'również', 'też', 'tak', ]; // Count Polish words with better matching const polishWordCount = polishWords.filter(word => { // Look for word boundaries to avoid false positives const regex = new RegExp(`\\b${word}\\b`, 'gi'); return regex.test(bodyText); }).length; // Lower threshold for detection if (polishWordCount >= 2) { return 'pl'; } // Method 4: Check for Polish characters (ą, ć, ę, ł, ń, ó, ś, ź, ż) const polishChars = /[ąćęłńóśźż]/gi; const polishCharCount = (bodyText.match(polishChars) || []).length; if (polishCharCount >= 5) { return 'pl'; } // Method 5: Check navigator.language as fallback const browserLang = navigator.language.toLowerCase().split('-')[0]; if (browserLang === 'pl') return 'pl'; // Method 6: Check for common Polish website patterns const polishPatterns = [ 'www.', '.pl', 'strona główna', 'o nas', 'kontakt', 'oferta', 'usługi', 'produkty', 'cennik', 'galeria', 'aktualności', 'news', 'blog', 'pomoc', 'faq', 'regulamin', 'polityka prywatności', 'cookies', ]; const polishPatternCount = polishPatterns.filter(pattern => bodyText.includes(pattern.toLowerCase())).length; if (polishPatternCount >= 2) { return 'pl'; } // Method 7: Try to use Google Translate API for detection (optional) try { const detectedLang = await this.detectLanguageWithGoogleTranslate(bodyText.substring(0, 1000)); if (detectedLang) { if (detectedLang === 'pl') return 'pl'; if (detectedLang === 'en') return 'en'; } } catch (error) { console.error('Error detecting language with online translation:', error); } // Method 8: Check HTML lang attribute (highest priority) const htmlLang = document.documentElement.lang; if (htmlLang) { const langCode = htmlLang.toLowerCase().split('-')[0]; if (langCode === 'pl') return 'pl'; if (langCode === 'en') return 'en'; } return 'en'; } catch { return 'en'; } } /** * Detect language using Google Translate API (fallback method) */ async detectLanguageWithGoogleTranslate(text) { try { // Use Google Translate's language detection API const response = await fetch(`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=${encodeURIComponent(text)}`); if (!response.ok) { throw new Error('Google Translate API request failed'); } const data = await response.json(); const detectedLang = data[2]; // The detected language code return detectedLang; } catch { return null; } } /** * Apply widget configuration to CSS custom properties */ applyConfig(config, theme = 'auto') { this.currentTheme = theme; const resolvedTheme = this.resolveTheme(theme); const colorConfig = resolvedTheme === 'dark' ? config.dark_mode : config.light_mode; if (colorConfig) { this.applyColorConfig(colorConfig); } } /** * Apply color configuration to CSS custom properties */ applyColorConfig(config) { const properties = { '--bcx-primary': config.primary_color, '--bcx-secondary': config.secondary_color, '--bcx-background': config.background_color, '--bcx-text': config.text_color, }; Object.entries(properties).forEach(([property, value]) => { if (value) { this.hostElement.style.setProperty(property, value); } }); } /** * Resolve theme based on preference and system settings */ resolveTheme(theme) { if (theme === 'auto') { return this.detectWebsiteColorScheme(); } return theme; } /** * Universal color scheme detection for any website * Checks multiple methods in order of reliability */ detectWebsiteColorScheme() { try { // Method 1: Check CSS custom properties (most reliable for modern sites) const cssTheme = this.detectCSSTheme(); if (cssTheme) return cssTheme; // Method 2: Check data attributes (common in frameworks) const dataTheme = this.detectDataTheme(); if (dataTheme) return dataTheme; // Method 3: Check class names on html/body const classTheme = this.detectClassTheme(); if (classTheme) return classTheme; // Method 4: Check meta theme-color const metaTheme = this.detectMetaTheme(); if (metaTheme) return metaTheme; // Method 5: Check computed styles of body/html const computedTheme = this.detectComputedTheme(); if (computedTheme) return computedTheme; // No system preference fallback - default to light mode return 'light'; } catch { return 'light'; } } /** * Method 1: Detect CSS custom properties * Checks for common CSS variables used by frameworks */ detectCSSTheme() { const root = document.documentElement; const computedStyle = window.getComputedStyle(root); // Check common CSS custom properties const cssVars = ['--color-scheme', '--theme', '--mode', '--color-mode', '--dark-mode', '--light-mode', '--app-theme', '--ui-theme']; for (const varName of cssVars) { const value = computedStyle.getPropertyValue(varName).trim(); if (value) { if (value.includes('dark') || value === 'dark') return 'dark'; if (value.includes('light') || value === 'light') return 'light'; } } // Check for dark mode indicators in CSS variables const darkIndicators = ['--bg-color', '--background-color', '--primary-bg', '--surface-color']; for (const varName of darkIndicators) { const value = computedStyle.getPropertyValue(varName).trim(); if (value) { // Check if it's a dark color (basic heuristic) if (this.isDarkColor(value)) return 'dark'; if (this.isLightColor(value)) return 'light'; } } return null; } /** * Method 2: Detect data attributes * Common in frameworks like Next.js, Nuxt.js, etc. */ detectDataTheme() { const elements = [document.documentElement, document.body]; const dataAttrs = ['data-theme', 'data-mode', 'data-color-scheme', 'data-color-mode']; for (const element of elements) { if (!element) continue; for (const attr of dataAttrs) { const value = element.getAttribute(attr); if (value) { if (value.includes('dark') || value === 'dark') return 'dark'; if (value.includes('light') || value === 'light') return 'light'; } } } return null; } /** * Method 3: Detect class names * Common in Tailwind, Bootstrap, and other frameworks */ detectClassTheme() { const elements = [document.documentElement, document.body]; const darkClasses = ['dark', 'dark-mode', 'theme-dark', 'dark-theme', 'is-dark', 'dark-theme', 'night-mode']; for (const element of elements) { if (!element) continue; const classList = element.classList; for (const className of darkClasses) { if (classList.contains(className)) return 'dark'; } } return null; } /** * Method 4: Detect meta theme-color * Some sites use meta theme-color to indicate dark mode */ detectMetaTheme() { const metaTheme = document.querySelector('meta[name="theme-color"]'); if (metaTheme) { const content = metaTheme.getAttribute('content'); if (content && this.isDarkColor(content)) return 'dark'; if (content && this.isLightColor(content)) return 'light'; } return null; } /** * Method 5: Detect computed styles * Analyze background and text colors of the page */ detectComputedTheme() { try { const body = document.body; if (!body) return null; const computedStyle = window.getComputedStyle(body); const bgColor = computedStyle.backgroundColor; const textColor = computedStyle.color; // If we can't get colors, skip this method if (!bgColor || bgColor === 'rgba(0, 0, 0, 0)' || !textColor) return null; const isDarkBg = this.isDarkColor(bgColor); const isDarkText = this.isDarkColor(textColor); // If background is dark and text is light, it's dark mode if (isDarkBg && !isDarkText) return 'dark'; // If background is light and text is dark, it's light mode if (!isDarkBg && isDarkText) return 'light'; return null; } catch { return null; } } /** * Helper: Check if a color is dark */ isDarkColor(color) { try { // Convert to RGB values const rgb = this.parseColor(color); if (!rgb) return false; // Calculate luminance const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; return luminance < 0.5; } catch { return false; } } /** * Helper: Check if a color is light */ isLightColor(color) { try { const rgb = this.parseColor(color); if (!rgb) return false; const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; return luminance > 0.7; } catch { return false; } } /** * Helper: Parse color string to RGB */ parseColor(color) { // Remove whitespace color = color.trim(); // Handle hex colors if (color.startsWith('#')) { const hex = color.slice(1); if (hex.length === 3) { return { r: parseInt(hex[0] + hex[0], 16), g: parseInt(hex[1] + hex[1], 16), b: parseInt(hex[2] + hex[2], 16), }; } else if (hex.length === 6) { return { r: parseInt(hex.slice(0, 2), 16), g: parseInt(hex.slice(2, 4), 16), b: parseInt(hex.slice(4, 6), 16), }; } } // Handle rgb/rgba colors const rgbMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (rgbMatch) { return { r: parseInt(rgbMatch[1]), g: parseInt(rgbMatch[2]), b: parseInt(rgbMatch[3]), }; } // Handle hsl colors (basic conversion) const hslMatch = color.match(/hsla?\((\d+),\s*(\d+)%,\s*(\d+)%/); if (hslMatch) { const h = parseInt(hslMatch[1]) / 360; const s = parseInt(hslMatch[2]) / 100; const l = parseInt(hslMatch[3]) / 100; const hue2rgb = (p, q, t) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; return { r: Math.round(hue2rgb(p, q, h + 1 / 3) * 255), g: Math.round(hue2rgb(p, q, h) * 255), b: Math.round(hue2rgb(p, q, h - 1 / 3) * 255), }; } return null; } /** * Set default theme colors */ setDefaultTheme() { const defaultColors = { '--bcx-primary': '#007bff', '--bcx-secondary': '#6c757d', '--bcx-background': '#ffffff', '--bcx-text': '#212529', '--bcx-border': '#dee2e6', '--bcx-shadow': 'rgba(0, 0, 0, 0.1)', '--bcx-success': '#28a745', '--bcx-warning': '#ffc107', '--bcx-error': '#dc3545', '--bcx-info': '#17a2b8', }; Object.entries(defaultColors).forEach(([property, value]) => { this.hostElement.style.setProperty(property, value); }); // Also apply to document root for inheritance Object.entries(defaultColors).forEach(([property, value]) => { document.documentElement.style.setProperty(property, value); }); } /** * Apply custom CSS properties from host page */ applyCustomProperties(customProperties) { Object.entries(customProperties).forEach(([property, value]) => { this.hostElement.style.setProperty(property, value); }); } /** * Listen for system theme changes */ watchSystemTheme(callback) { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handler = (e) => { callback(e.matches ? 'dark' : 'light'); }; mediaQuery.addEventListener('change', handler); // Return cleanup function return () => { mediaQuery.removeEventListener('change', handler); }; } /** * Watch for website theme changes dynamically * This is useful for sites that change themes without page reload */ watchWebsiteTheme(callback) { // Check if MutationObserver is available (not available in test environment) if (typeof MutationObserver === 'undefined') { return () => { }; } const observers = []; // Watch for class changes on html and body const watchClassChanges = () => { const elements = [document.documentElement, document.body]; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const newTheme = this.detectWebsiteColorScheme(); callback(newTheme); } }); }); elements.forEach(element => { if (element) { observer.observe(element, { attributes: true, attributeFilter: ['class'] }); } }); observers.push(() => observer.disconnect()); }; // Watch for data attribute changes const watchDataChanges = () => { const elements = [document.documentElement, document.body]; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && ['data-theme', 'data-mode', 'data-color-scheme'].includes(mutation.attributeName)) { const newTheme = this.detectWebsiteColorScheme(); callback(newTheme); } }); }); elements.forEach(element => { if (element) { observer.observe(element, { attributes: true, attributeFilter: ['data-theme', 'data-mode', 'data-color-scheme'], }); } }); observers.push(() => observer.disconnect()); }; // Watch for CSS custom property changes const watchCSSChanges = () => { const root = document.documentElement; const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { const newTheme = this.detectWebsiteColorScheme(); callback(newTheme); } }); }); if (root) { observer.observe(root, { attributes: true, attributeFilter: ['style'] }); } observers.push(() => observer.disconnect()); }; // Start all watchers watchClassChanges(); watchDataChanges(); watchCSSChanges(); // Return cleanup function return () => { observers.forEach(cleanup => cleanup()); }; } /** * Get current detected theme */ getCurrentDetectedTheme() { return this.detectWebsiteColorScheme(); } /** * Get current applied theme */ getCurrentTheme() { return this.currentTheme === 'auto' ? this.detectWebsiteColorScheme() : this.currentTheme; } } const bettercxWidgetCss = ":host{--bcx-widget-size:60px;--bcx-widget-chat-width:400px;--bcx-widget-chat-height:640px;--bcx-widget-border-radius:20px;--bcx-widget-shadow:0 20px 60px rgba(0, 0, 0, 0.08), 0 8px 25px rgba(0, 0, 0, 0.04);--bcx-widget-shadow-hover:0 25px 80px rgba(0, 0, 0, 0.12), 0 12px 35px rgba(0, 0, 0, 0.06);--bcx-widget-shadow-active:0 15px 40px rgba(0, 0, 0, 0.1), 0 6px 20px rgba(0, 0, 0, 0.05);--bcx-primary:#007bff;--bcx-background:#ffffff;--bcx-text:#212529;--bcx-primary-50:color-mix(in srgb, var(--bcx-primary) 5%, var(--bcx-background));--bcx-primary-100:color-mix(in srgb, var(--bcx-primary) 10%, var(--bcx-background));--bcx-primary-200:color-mix(in srgb, var(--bcx-primary) 20%, var(--bcx-background));--bcx-primary-300:color-mix(in srgb, var(--bcx-primary) 30%, var(--bcx-background));--bcx-primary-400:color-mix(in srgb, var(--bcx-primary) 40%, var(--bcx-background));--bcx-primary-500:var(--bcx-primary);--bcx-primary-600:color-mix(in srgb, var(--bcx-primary) 80%, #000000);--bcx-primary-700:color-mix(in srgb, var(--bcx-primary) 70%, #000000);--bcx-primary-800:color-mix(in srgb, var(--bcx-primary) 60%, #000000);--bcx-primary-900:color-mix(in srgb, var(--bcx-primary) 50%, #000000);--bcx-text-primary:var(--bcx-text);--bcx-text-secondary:color-mix(in srgb, var(--bcx-text) 70%, var(--bcx-background));--bcx-text-tertiary:color-mix(in srgb, var(--bcx-text) 50%, var(--bcx-background));--bcx-text-quaternary:color-mix(in srgb, var(--bcx-text) 30%, var(--bcx-background));--bcx-bg-primary:var(--bcx-background);--bcx-bg-secondary:color-mix(in srgb, var(--bcx-text) 2%, var(--bcx-background));--bcx-bg-tertiary:color-mix(in srgb, var(--bcx-text) 4%, var(--bcx-background));--bcx-bg-elevated:color-mix(in srgb, var(--bcx-background) 95%, #ffffff);--bcx-border-subtle:color-mix(in srgb, var(--bcx-text) 8%, var(--bcx-background));--bcx-border-soft:color-mix(in srgb, var(--bcx-text) 12%, var(--bcx-background));--bcx-border-medium:color-mix(in srgb, var(--bcx-text) 16%, var(--bcx-background));--bcx-space-1:4px;--bcx-space-2:8px;--bcx-space-3:12px;--bcx-space-4:16px;--bcx-space-5:20px;--bcx-space-6:24px;--bcx-space-8:32px;--bcx-space-10:40px;--bcx-space-12:48px;--bcx-space-16:64px;--bcx-text-xs:11px;--bcx-text-sm:12px;--bcx-text-base:14px;--bcx-text-lg:16px;--bcx-text-xl:18px;--bcx-text-2xl:20px;--bcx-radius-sm:6px;--bcx-radius-md:8px;--bcx-radius-lg:12px;--bcx-radius-xl:16px;--bcx-radius-2xl:20px;--bcx-radius-full:9999px;--bcx-transition-fast:150ms cubic-bezier(0.4, 0, 0.2, 1);--bcx-transition-normal:250ms cubic-bezier(0.4, 0, 0.2, 1);--bcx-transition-slow:350ms cubic-bezier(0.4, 0, 0.2, 1);position:fixed;bottom:var(--bcx-space-6);right:var(--bcx-space-6);z-index:9999}:host--left{right:auto !important;left:var(--bcx-space-6) !important}:host{font-family:-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Helvetica Neue\", Arial, sans-serif;font-size:var(--bcx-text-base);line-height:1.5;color:var(--bcx-text-primary);font-weight:400;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:host(.bcx-widget--left){right:auto !important;left:var(--bcx-space-6) !important}:host(.bcx-widget--left) .bcx-widget__toggle{position:relative;z-index:10}:host(.bcx-widget--left) .bcx-widget__chat{left:0;right:auto}.bcx-widget--loading .bcx-widget__toggle,.bcx-widget--error .bcx-widget__toggle{display:none}.bcx-widget--open .bcx-widget__toggle{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.bcx-widget__toggle{width:var(--bcx-widget-size);height:var(--bcx-widget-size);border:2px solid var(--bcx-bg-elevated);border-radius:var(--bcx-radius-full);background:var(--bcx-primary-500);color:var(--bcx-bg-primary);cursor:pointer;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;font-size:24px;font-weight:500;-webkit-box-shadow:var(--bcx-widget-shadow);box-shadow:var(--bcx-widget-shadow);-webkit-transition:all var(--bcx-transition-normal);transition:all var(--bcx-transition-normal);position:relative;z-index:1;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.bcx-widget__toggle::before{content:\"\";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg, color-mix(in srgb, var(--bcx-primary-500) 20%, transparent) 0%, transparent 50%, color-mix(in srgb, var(--bcx-primary-500) 10%, transparent) 100%);opacity:0;-webkit-transition:opacity var(--bcx-transition-fast);transition:opacity var(--bcx-transition-fast)}.bcx-widget__toggle:hover{-webkit-transform:translateY(-2px) scale(1.04);transform:translateY(-2px) scale(1.04);-webkit-box-shadow:var(--bcx-widget-shadow-hover);box-shadow:var(--bcx-widget-shadow-hover);background:var(--bcx-primary-600);border-color:var(--bcx-bg-elevated)}.bcx-widget__toggle:hover::before{opacity:1}.bcx-widget__toggle:focus{outline:none;-webkit-box-shadow:var(--bcx-widget-shadow-hover);box-shadow:var(--bcx-widget-shadow-hover)}.bcx-widget__toggle:active{-webkit-transform:translateY(-1px) scale(1.02);transform:translateY(-1px) scale(1.02);-webkit-box-shadow:var(--bcx-widget-shadow-active);box-shadow:var(--bcx-widget-shadow-active);background:var(--bcx-primary-700)}.bcx-widget__toggle:active::after{content:\"\";position:absolute;inset:-8px;border-radius:inherit;background:var(--bcx-primary-200);opacity:0.3;-webkit-animation:bcx-ripple 300ms ease-out;animation:bcx-ripple 300ms ease-out}.bcx-widget__toggle-icon{-webkit-transition:-webkit-transform var(--bcx-transition-normal);transition:-webkit-transform var(--bcx-transition-normal);transition:transform var(--bcx-transition-normal);transition:transform var(--bcx-transition-normal), -webkit-transform var(--bcx-transition-normal);-webkit-filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.bcx-widget__toggle-icon svg{width:20px;height:20px;stroke-width:2;-webkit-transition:all var(--bcx-transition-fast);transition:all var(--bcx-transition-fast);display:block;-ms-flex-negative:0;flex-shrink:0}.bcx-widget__toggle:hover .bcx-widget__toggle-icon{-webkit-transform:scale(1.1);transform:scale(1.1)}.bcx-widget__toggle:hover .bcx-widget__toggle-icon svg{stroke-width:2.2}.bcx-widget__toggle:active .bcx-widget__toggle-icon{-webkit-transform:scale(0.96);transform:scale(0.96)}.bcx-widget__toggle:active .bcx-widget__toggle-icon svg{stroke-width:1.8}.bcx-widget__chat{position:absolute;bottom:calc(var(--bcx-widget-size) + var(--bcx-space-3));right:0;width:var(--bcx-widget-chat-width);height:var(--bcx-widget-chat-height);background:var(--bcx-bg-elevated);border:1px solid var(--bcx-border-subtle);border-radius:var(--bcx-widget-border-radius);-webkit-box-shadow:var(--bcx-widget-shadow);box-shadow:var(--bcx-widget-shadow);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;overflow:hidden;-webkit-animation:bcx-slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1);animation:bcx-slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1);backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px)}.bcx-widget--left .bcx-widget__chat{right:auto !important;left:0 !important}.bcx-widget__chat::before{content:\"\";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg, color-mix(in srgb, var(--bcx-bg-primary) 50%, transparent) 0%, transparent 50%, color-mix(in srgb, var(--bcx-bg-primary) 30%, transparent) 100%);pointer-events:none}.bcx-widget__header{background:var(--bcx-primary-500);color:var(--bcx-bg-primary);padding:var(--bcx-space-4);display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-negative:0;flex-shrink:0;border-radius:var(--bcx-widget-border-radius) var(--bcx-widget-border-radius) 0 0;position:relative;z-index:1}.bcx-widget__header::before{content:\"\";position:absolute;inset:0;background:linear-gradient(135deg, color-mix(in srgb, var(--bcx-primary-500) 80%, transparent) 0%, transparent 50%, color-mix(in srgb, var(--bcx-primary-500) 90%, transparent) 100%);border-radius:inherit;pointer-events:none}.bcx-widget__header h3{margin:0;font-size:var(--bcx-text-lg);font-weight:600;letter-spacing:-0.02em;position:relative;z-index:1;text-shadow:0 1px 2px rgba(0, 0, 0, 0.1)}.bcx-widget__close{background:none;border:none;color:color-mix(in srgb, var(--bcx-bg-primary) 80%, transparent);cursor:pointer;font-size:18px;padding:var(--bcx-space-2);border-radius:var(--bcx-radius-md);-webkit-transition:all var(--bcx-transition-fast);transition:all var(--bcx-transition-fast);display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:36px;height:36px;position:relative;z-index:1}.bcx-widget__close svg{width:18px;height:18px;stroke-width:2;-webkit-transition:all var(--bcx-transition-fast);transition:all var(--bcx-transition-fast);display:block;-ms-flex-negative:0;flex-shrink:0}.bcx-widget__close:hover{background:color-mix(in srgb, var(--bcx-bg-primary) 15%, transparent);color:var(--bcx-bg-primary);-webkit-transform:scale(1.1);transform:scale(1.1)}.bcx-widget__close:hover svg{stroke-width:2.2}.bcx-widget__close:focus{outline:none;background:color-mix(in srgb, var(--bcx-bg-primary) 20%, transparent);color:var(--bcx-bg-primary);-webkit-box-shadow:0 0 0 2px color-mix(in srgb, var(--bcx-bg-primary) 30%, transparent);box-shadow:0 0 0 2px color-mix(in srgb, var(--bcx-bg-primary) 30%, transparent)}.bcx-widget__close:focus svg{stroke-width:2.2}.bcx-widget__close:active{-webkit-transform:scale(0.95);transform:scale(0.95)}.bcx-widget__close:active svg{stroke-width:1.8}.bcx-widget__messages{-ms-flex:1;flex:1;overflow-y:auto;padding:var(--bcx-space-4);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:var(--bcx-space-4);background:var(--bcx-bg-primary);color:var(--bcx-text-primary);scroll-behavior:smooth;scrollbar-width:thin;scrollbar-color:var(--bcx-border-soft) transparent;position:relative}.bcx-widget__messages::-webkit-scrollbar{width:4px}.bcx-widget__messages::-webkit-scrollbar-track{background:transparent}.bcx-widget__messages::-webkit-scrollbar-thumb{background:var(--bcx-border-soft);border-radius:var(--bcx-radius-sm);-webkit-transition:background var(--bcx-transition-fast);transition:background var(--bcx-transition-fast)}.bcx-widget__messages::-webkit-scrollbar-thumb:hover{background:var(--bcx-border-medium)}.bcx-widget__messages::before,.bcx-widget__messages::after{content:\"\";position:-webkit-sticky;position:sticky;left:0;right:0;height:var(--bcx-space-4);background:-webkit-gradient(linear, left top, left bottom, from(var(--bcx-bg-primary)), to(transparent));background:linear-gradient(to bottom, var(--bcx-bg-primary), transparent);pointer-events:none;z-index:1}.bcx-widget__messages::before{top:0;margin-bottom:calc(-1 * var(--bcx-space-4))}.bcx-widget__messages::after{bottom:0;background:-webkit-gradient(linear, left bottom, left top, from(var(--bcx-bg-primary)), to(transparent));background:linear-gradient(to top, var(--bcx-bg-primary), transparent);margin-top:calc(-1 * var(--bcx-space-4))}.bcx-widget__message{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;max-width:85%;-webkit-animation:bcx-message-appear 0.3s cubic-bezier(0.16, 1, 0.3, 1);animation:bcx-message-appear 0.3s cubic-bezier(0.16, 1, 0.3, 1)}.bcx-widget__message--user{-ms-flex-item-align:end;align-self:flex-end}.bcx-widget__message--user .bcx-widget__message-content{background:var(--bcx-primary-500);color:var(--bcx-bg-primary);border-radius:var(--bcx-radius-2xl) var(--bcx-radius-2xl) var(--bcx-radius-sm) var(--bcx-radius-2xl);-webkit-box-shadow:0 4px 12px color-mix(in srgb, var(--bcx-primary-500) 25%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-primary-500) 15%, transparent);box-shadow:0 4px 12px color-mix(in srgb, var(--bcx-primary-500) 25%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-primary-500) 15%, transparent);text-align:start;position:relative}.bcx-widget__message--user .bcx-widget__message-content::before{content:\"\";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg, color-mix(in srgb, var(--bcx-primary-500) 20%, transparent) 0%, transparent 50%, color-mix(in srgb, var(--bcx-primary-500) 10%, transparent) 100%);pointer-events:none}.bcx-widget__message--user .bcx-widget__message-time{text-align:right;color:var(--bcx-text-tertiary)}.bcx-widget__message--assistant{-ms-flex-item-align:start;align-self:flex-start}.bcx-widget__message--assistant .bcx-widget__message-content{background:var(--bcx-bg-elevated);color:var(--bcx-text-primary);border-radius:var(--bcx-radius-2xl) var(--bcx-radius-2xl) var(--bcx-radius-2xl) var(--bcx-radius-sm);-webkit-box-shadow:0 2px 8px color-mix(in srgb, var(--bcx-text-primary) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);box-shadow:0 2px 8px color-mix(in srgb, var(--bcx-text-primary) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);border:1px solid var(--bcx-border-subtle);text-align:left;position:relative}.bcx-widget__message--assistant .bcx-widget__message-time{text-align:left;color:var(--bcx-text-tertiary)}.bcx-widget__message-content{padding:var(--bcx-space-3) var(--bcx-space-4);word-wrap:break-word;white-space:pre-wrap;font-size:var(--bcx-text-base);line-height:1.5;font-weight:400;position:relative;z-index:1}.bcx-widget__message--user .bcx-widget__message-content{color:var(--bcx-bg-primary);text-shadow:0 1px 2px rgba(0, 0, 0, 0.1)}.bcx-widget__message--assistant .bcx-widget__message-content{color:var(--bcx-text-primary)}.bcx-widget__message-time{font-size:var(--bcx-text-xs);margin-top:var(--bcx-space-1);font-weight:500;letter-spacing:0.025em}.bcx-widget__example-questions{padding:var(--bcx-space-4) 0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;gap:var(--bcx-space-2);margin-top:var(--bcx-space-2);border-top:1px solid var(--bcx-border-subtle);position:relative}.bcx-widget__example-questions::before{content:\"\";position:absolute;top:0;left:0;right:0;height:1px;background:-webkit-gradient(linear, left top, right top, from(transparent), color-stop(20%, var(--bcx-border-soft)), color-stop(80%, var(--bcx-border-soft)), to(transparent));background:linear-gradient(90deg, transparent 0%, var(--bcx-border-soft) 20%, var(--bcx-border-soft) 80%, transparent 100%)}.bcx-widget__example-questions-title{font-size:var(--bcx-text-xs);color:var(--bcx-text-tertiary);font-weight:600;margin-bottom:var(--bcx-space-3);text-transform:uppercase;letter-spacing:0.05em;text-align:center;position:relative}.bcx-widget__example-questions-title::after{content:\"\";position:absolute;bottom:-6px;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);width:24px;height:1px;background:var(--bcx-primary-300)}.bcx-widget__example-question{background:var(--bcx-bg-secondary);border:1px solid var(--bcx-border-subtle);border-radius:var(--bcx-radius-lg);padding:var(--bcx-space-3) var(--bcx-space-4);text-align:left;font-size:var(--bcx-text-sm);color:var(--bcx-text-secondary);cursor:pointer;-webkit-transition:all var(--bcx-transition-normal);transition:all var(--bcx-transition-normal);-webkit-box-shadow:0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);box-shadow:0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);word-wrap:break-word;white-space:pre-wrap;position:relative;font-weight:500;line-height:1.4}.bcx-widget__example-question::before{content:\"\";position:absolute;inset:0;border-radius:inherit;background:linear-gradient(135deg, color-mix(in srgb, var(--bcx-primary-500) 5%, transparent) 0%, transparent 50%, color-mix(in srgb, var(--bcx-primary-500) 3%, transparent) 100%);opacity:0;-webkit-transition:opacity var(--bcx-transition-fast);transition:opacity var(--bcx-transition-fast);pointer-events:none}.bcx-widget__example-question:hover{background:var(--bcx-bg-tertiary);border-color:var(--bcx-primary-200);-webkit-transform:translateY(-2px);transform:translateY(-2px);-webkit-box-shadow:0 4px 12px color-mix(in srgb, var(--bcx-text-primary) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);box-shadow:0 4px 12px color-mix(in srgb, var(--bcx-text-primary) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);color:var(--bcx-text-primary)}.bcx-widget__example-question:hover::before{opacity:1}.bcx-widget__example-question:active{-webkit-transform:translateY(-1px);transform:translateY(-1px);background:var(--bcx-bg-tertiary);-webkit-box-shadow:0 2px 6px color-mix(in srgb, var(--bcx-text-primary) 6%, transparent);box-shadow:0 2px 6px color-mix(in srgb, var(--bcx-text-primary) 6%, transparent)}.bcx-widget__example-question:focus{outline:none;border-color:var(--bcx-primary-300);-webkit-box-shadow:0 0 0 3px var(--bcx-primary-100);box-shadow:0 0 0 3px var(--bcx-primary-100);color:var(--bcx-text-primary)}.bcx-widget__typing{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:var(--bcx-space-2) 0;-webkit-animation:bcx-typing-appear 0.3s cubic-bezier(0.16, 1, 0.3, 1);animation:bcx-typing-appear 0.3s cubic-bezier(0.16, 1, 0.3, 1)}.bcx-widget__typing-indicator{display:-ms-flexbox;display:flex;gap:var(--bcx-space-1);padding:var(--bcx-space-3) var(--bcx-space-4);background:var(--bcx-bg-elevated);border-radius:var(--bcx-radius-2xl) var(--bcx-radius-2xl) var(--bcx-radius-2xl) var(--bcx-radius-sm);-ms-flex-item-align:start;align-self:flex-start;-webkit-box-shadow:0 2px 8px color-mix(in srgb, var(--bcx-text-primary) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);box-shadow:0 2px 8px color-mix(in srgb, var(--bcx-text-primary) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--bcx-text-primary) 4%, transparent);border:1px solid var(--bcx-border-subtle);position:relative}.bcx-widget__typing-indicator span{width:8px;height:8px;border-radius:var(--bcx-radius-full);background:var(--bcx-primary-400);-webkit-animation:bcx-pulse 1.6s ease-in-out infinite both;animation:bcx-pulse 1.6s ease-in-out infinite both;-webkit-box-shadow:0 1px 2px color-mix(in srgb, var(--bcx-primary-500) 20%, transparent);box-shadow:0 1px 2px color-mix(in srgb, var(--bcx-primary-500) 20%, transparent)}.bcx-widget__typing-indicator span:nth-child(1){-webkit-animation-delay:-0.4s;animation-delay:-0.4s}.bcx-widget__typing-indicator span:nth-child(2){-webkit-animation-delay:-0.2s;animation-delay:-0.2s}.bcx-widget__typing-indicator span:nth-child(3){-webkit-animation-delay:0s;animation-delay:0s}.bcx-widget__composer{border-top:1px solid var(--bcx-border-subtle);padding:var(--bcx-space-4);-ms-flex-negative:0;flex-shrink:0;background:var(--bcx-bg-elevated);width:100%;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative}.bcx-widget__composer::before{content:\"\";position:absolute;top:0;left:0;right:0;height:1px;background:-webkit-gradient(linear, left top, right top, from(transparent), color-stop(20%, var(--bcx-border-soft)), color-stop(80%, var(--bcx-border-soft)), to(transparent));background:linear-gradient(90deg, transparent 0%, var(--bcx-border-soft) 20%, var(--bcx-border-soft) 80%, transparent 100%)}.bcx-widget__composer bcx-message-composer{display:block;width:100%}.bcx-widget__loading{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;padding:var(--bcx-spacing-xl);color:var(--bcx-text)}.bcx-widget__spinner{width:32px;height:32px;border:3px solid var(--bcx-border);border-top:3px solid var(--bcx-primary);border-radius:50%;-webkit-animation:bcx-spin 1s linear infinite;animation:bcx-spin 1s linear infinite;margin-bottom:var(--bcx-spacing-md)}.bcx-widget__error{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;padding:var(--bcx-spacing-xl);text-align:center;color: