UNPKG

bettercx-widget

Version:

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

584 lines (583 loc) 23.1 kB
import { Host, h } from "@stencil/core"; import { AuthService } from "../../services/auth.service"; import { ApiService } from "../../services/api.service"; import { ThemeService } from "../../services/theme.service"; export class BetterCXWidget { el; // Simplified public properties - only essential ones publicKey; theme = 'auto'; debug = false; baseUrl = 'https://dev-api.bettercx.ai'; aiServiceUrl = 'https://dev-ai.bettercx.ai'; autoInit = true; position = 'right'; // Internal state state = { isOpen: false, isAuthenticated: false, isLoading: true, messages: [], isTyping: false, }; // Language state language = 'en'; // Services authService; apiService; themeService; // Refs messagesContainerRef; // Events widgetEvent; async onPublicKeyChange() { if (this.publicKey) { await this.initialize(); } } async componentWillLoad() { if (this.publicKey && this.autoInit) { await this.initialize(); } } async componentDidLoad() { if (this.themeService) { this.themeService.watchWebsiteTheme(() => { this.themeService.setDefaultTheme(); }); } } async initialize() { if (!this.publicKey) { this.setState({ isLoading: false, isAuthenticated: false }); return; } try { this.setState({ isLoading: true, error: undefined }); this.authService = new AuthService(this.baseUrl); const origin = window.location.origin; const sessionData = await this.authService.createSession(this.publicKey, origin); this.apiService = new ApiService(this.baseUrl, this.aiServiceUrl, this.authService); this.themeService = new ThemeService(this.el); this.language = await this.themeService.detectWebsiteLanguage(); this.themeService.setDefaultTheme(); if ('attrs' in sessionData && sessionData.attrs) { this.applyCustomColors(sessionData.attrs); this.applyColorsToMessageComposer(); } this.setState({ isAuthenticated: true, exampleQuestions: ('example_questions' in sessionData ? sessionData.example_questions : []), }); this.emitEvent('session-created', { origin }); this.setState({ isLoading: false }); this.setState({ isOpen: false }); } catch (error) { this.setState({ isLoading: false, isAuthenticated: false, }); this.emitEvent('error', { error: error.message }); } } async open() { if (this.state.isAuthenticated) { this.setState({ isOpen: true }); this.emitEvent('opened'); this.applyColorsToMessageComposer(); setTimeout(() => { this.scrollToBottom(false); }, 100); } } async close() { this.setState({ isOpen: false }); this.emitEvent('closed'); } async toggle() { if (this.state.isOpen) { await this.close(); } else { await this.open(); } } async sendMessage(content) { if (!this.state.isAuthenticated || !content.trim()) { return; } const userMessage = { content: content.trim(), author: 'user', timestamp: new Date().toISOString(), id: this.generateId(), }; this.setState({ messages: [...this.state.messages, userMessage], isTyping: true, }); setTimeout(() => { this.scrollToBottom(true); }, 50); this.emitEvent('message-sent', userMessage); try { const stream = await this.apiService.sendMessage(content); if (stream) { let assistantMessage = null; let isStreamingStarted = false; const streamParser = await this.apiService.parseStreamResponse(stream); for await (const chunk of streamParser) { if (chunk.type === 'streaming_output') { if (!isStreamingStarted) { assistantMessage = { content: '', author: 'assistant', timestamp: new Date().toISOString(), id: this.generateId(), }; this.setState({ messages: [...this.state.messages, assistantMessage], isTyping: false, }); isStreamingStarted = true; } if (assistantMessage) { assistantMessage.content += chunk.content; this.setState({ messages: [...this.state.messages.slice(0, -1), { ...assistantMessage }], }); } } else { if (!isStreamingStarted) { this.setState({ isTyping: true }); } } } if (assistantMessage) { this.emitEvent('message-received', assistantMessage); } } } catch (error) { this.setState({ error: 'Failed to send message' }); this.emitEvent('error', { error: error.message }); } finally { this.setState({ isTyping: false }); } } setState(updates) { const previousMessages = this.state.messages; this.state = { ...this.state, ...updates }; if (updates.messages && updates.messages !== previousMessages) { setTimeout(() => { this.scrollToBottom(true); }, 0); } } emitEvent(type, data) { const event = { type, data, timestamp: new Date().toISOString(), }; this.widgetEvent.emit(event); } generateId() { return Math.random().toString(36).substr(2, 9); } getTranslation(key) { const translations = { common_questions: { en: 'Common questions', pl: 'Często zadawane pytania', }, message_placeholder: { en: 'Type your message...', pl: 'Wpisz swoją wiadomość...', }, }; return translations[key]?.[this.language] || translations[key]?.['en'] || key; } scrollToBottom(smooth = true) { if (this.messagesContainerRef) { this.messagesContainerRef.scrollTo({ top: this.messagesContainerRef.scrollHeight, behavior: smooth ? 'smooth' : 'auto', }); } } handleToggleClick = () => { if (this.state.isOpen) { this.close(); } else { this.open(); } }; handleMessageSubmit = (event) => { this.sendMessage(event.detail); }; applyCustomColors(attrs) { const currentTheme = this.themeService.getCurrentTheme(); const colorMode = currentTheme === 'dark' ? attrs.dark_mode : attrs.light_mode; if (colorMode && typeof colorMode === 'object') { this.el.style.setProperty('--bcx-primary', String(colorMode.primary_color || '')); this.el.style.setProperty('--bcx-secondary', String(colorMode.secondary_color || '')); this.el.style.setProperty('--bcx-background', String(colorMode.background_color || '')); this.el.style.setProperty('--bcx-text', String(colorMode.text_color || '')); document.documentElement.style.setProperty('--bcx-primary', String(colorMode.primary_color || '')); document.documentElement.style.setProperty('--bcx-secondary', String(colorMode.secondary_color || '')); document.documentElement.style.setProperty('--bcx-background', String(colorMode.background_color || '')); document.documentElement.style.setProperty('--bcx-text', String(colorMode.text_color || '')); } } applyColorsToMessageComposer() { setTimeout(() => { const messageComposer = this.el.querySelector('bcx-message-composer'); if (messageComposer) { const primaryColor = this.el.style.getPropertyValue('--bcx-primary') || '#007bff'; const secondaryColor = this.el.style.getPropertyValue('--bcx-secondary') || '#6c757d'; const backgroundColor = this.el.style.getPropertyValue('--bcx-background') || '#ffffff'; const textColor = this.el.style.getPropertyValue('--bcx-text') || '#212529'; messageComposer.style.setProperty('--bcx-primary', primaryColor); messageComposer.style.setProperty('--bcx-secondary', secondaryColor); messageComposer.style.setProperty('--bcx-background', backgroundColor); messageComposer.style.setProperty('--bcx-text', textColor); } }, 100); } handleExampleQuestionClick = (question) => { const userMessage = { content: question.question_text, author: 'user', timestamp: new Date().toISOString(), id: this.generateId(), }; this.setState({ messages: [...this.state.messages, userMessage], }); this.simulateTypingResponse(question.answer_text); }; simulateTypingResponse(fullText) { const assistantMessage = { content: '', author: 'assistant', timestamp: new Date().toISOString(), id: this.generateId(), }; this.setState({ messages: [...this.state.messages, assistantMessage], isTyping: false, }); let currentText = ''; let index = 0; const typingSpeed = 25; const typeNextCharacter = () => { if (index < fullText.length) { currentText += fullText[index]; index++; const updatedMessage = { ...assistantMessage, content: currentText }; const updatedMessages = [...this.state.messages]; updatedMessages[updatedMessages.length - 1] = updatedMessage; this.setState({ messages: updatedMessages, }); setTimeout(typeNextCharacter, typingSpeed); } else { this.emitEvent('message-received', assistantMessage); } }; setTimeout(typeNextCharacter, 200); } render() { if (!this.state.isAuthenticated) { return null; } if (this.state.isLoading) { return (h(Host, { class: "bcx-widget bcx-widget--loading" }, h("div", { class: "bcx-widget__loading" }, h("div", { class: "bcx-widget__spinner" }), h("span", null, "Loading...")))); } return (h(Host, { class: `bcx-widget ${this.state.isOpen ? 'bcx-widget--open' : ''} bcx-widget--${this.position}` }, h("button", { class: "bcx-widget__toggle", onClick: this.handleToggleClick, "aria-label": this.state.isOpen ? 'Close chat' : 'Open chat' }, h("span", { class: "bcx-widget__toggle-icon" }, this.state.isOpen ? (h("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))) : (h("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }))))), this.state.isOpen && (h("div", { class: "bcx-widget__chat" }, h("div", { class: "bcx-widget__header" }, h("h3", null, "Chat AI"), h("button", { class: "bcx-widget__close", onClick: () => this.close(), "aria-label": "Close chat" }, h("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))), h("div", { class: "bcx-widget__messages", ref: el => (this.messagesContainerRef = el) }, this.state.messages.map(message => (h("div", { key: message.id, class: `bcx-widget__message bcx-widget__message--${message.author}` }, h("div", { class: "bcx-widget__message-content" }, message.content), h("div", { class: "bcx-widget__message-time" }, new Date(message.timestamp).toLocaleTimeString())))), this.state.messages.length === 0 && this.state.exampleQuestions && this.state.exampleQuestions.length > 0 && (h("div", { class: "bcx-widget__example-questions" }, h("div", { class: "bcx-widget__example-questions-title" }, this.getTranslation('common_questions')), this.state.exampleQuestions.slice(0, 3).map(question => (h("button", { key: question.id, class: "bcx-widget__example-question", onClick: () => this.handleExampleQuestionClick(question) }, question.question_text))))), this.state.isTyping && (h("div", { class: "bcx-widget__typing" }, h("div", { class: "bcx-widget__typing-indicator" }, h("span", null), h("span", null), h("span", null))))), h("div", { class: "bcx-widget__composer" }, h("bcx-message-composer", { onMessageSubmit: this.handleMessageSubmit, disabled: this.state.isTyping, loading: this.state.isTyping, placeholder: this.getTranslation('message_placeholder') })))))); } static get is() { return "bettercx-widget"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["bettercx-widget.scss"] }; } static get styleUrls() { return { "$": ["bettercx-widget.css"] }; } static get properties() { return { "publicKey": { "type": "string", "attribute": "public-key", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false }, "theme": { "type": "string", "attribute": "theme", "mutable": false, "complexType": { "original": "'light' | 'dark' | 'auto'", "resolved": "\"auto\" | \"dark\" | \"light\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'auto'" }, "debug": { "type": "boolean", "attribute": "debug", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "baseUrl": { "type": "string", "attribute": "base-url", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'https://dev-api.bettercx.ai'" }, "aiServiceUrl": { "type": "string", "attribute": "ai-service-url", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'https://dev-ai.bettercx.ai'" }, "autoInit": { "type": "boolean", "attribute": "auto-init", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "true" }, "position": { "type": "string", "attribute": "position", "mutable": false, "complexType": { "original": "'left' | 'right'", "resolved": "\"left\" | \"right\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'right'" } }; } static get states() { return { "state": {}, "language": {} }; } static get events() { return [{ "method": "widgetEvent", "name": "widgetEvent", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "" }, "complexType": { "original": "WidgetEvent", "resolved": "WidgetEvent", "references": { "WidgetEvent": { "location": "import", "path": "../../types/api", "id": "src/types/api.ts::WidgetEvent" } } } }]; } static get methods() { return { "open": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } }, "close": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } }, "toggle": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } }, "sendMessage": { "complexType": { "signature": "(content: string) => Promise<void>", "parameters": [{ "name": "content", "type": "string", "docs": "" }], "references": { "Promise": { "location": "global", "id": "global::Promise" }, "ChatMessage": { "location": "import", "path": "../../types/api", "id": "src/types/api.ts::ChatMessage" }, "Record": { "location": "global", "id": "global::Record" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "publicKey", "methodName": "onPublicKeyChange" }]; } } //# sourceMappingURL=bettercx-widget.js.map