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
JavaScript
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: