@aslaluroba/help-center
Version:
A powerful and customizable help center widget for Angular applications with real-time chat functionality, AI assistance, and multi-language support.
1 lines • 134 kB
Source Map (JSON)
{"version":3,"file":"aslaluroba-help-center.mjs","sources":["../../../src/app/services/ably.service.ts","../../../src/app/shared/components/card/card.component.ts","../../../src/app/shared/components/button/button.component.ts","../../../src/app/services/translation.service.ts","../../../src/app/pipes/translate.pipe.ts","../../../src/app/help-screen-data/help-screen-data.component.ts","../../../src/app/help-screen-data/help-screen-data.component.html","../../../src/app/shared/components/header/header.component.ts","../../../src/app/shared/components/loading/loading.component.ts","../../../src/app/chat/chat.component.ts","../../../src/app/chat/chat.component.html","../../../src/app/shared/components/confirmation-dialog/confirmation-dialog.component.ts","../../../src/app/services/api.service.ts","../../../src/app/help-center-widget/help-center-widget.component.ts","../../../src/app/help-center-widget/help-center-widget.component.html","../../../src/app/services/help-center-config.service.ts","../../../src/app/services/token.service.ts","../../../src/app/language.service.ts","../../../public_api.ts","../../../aslaluroba-help-center.ts"],"sourcesContent":["import * as Ably from 'ably';\n\nexport class ClientAblyService {\n private static client: Ably.Realtime | null = null;\n private static channel: Ably.RealtimeChannel | null = null;\n private static isConnected: boolean = false;\n private static sessionId: string | null = null;\n private static messageUnsubscribe: (() => void) | null = null;\n\n static async startConnection(\n sessionId: string,\n ablyToken: string,\n onMessageReceived: Function,\n tenantId: string\n ) {\n // Prevent multiple connections\n if (this.isConnected && this.sessionId === sessionId) {\n return;\n }\n\n // Close existing connection if connecting to a different session\n if (this.isConnected && this.sessionId !== sessionId) {\n await this.stopConnection();\n }\n\n try {\n // Initialize Ably client with the token\n this.client = new Ably.Realtime({\n authUrl: undefined,\n token: ablyToken,\n autoConnect: true,\n });\n\n // Wait for connection to be established\n await new Promise<void>((resolve, reject) => {\n if (!this.client) {\n reject(new Error('Failed to initialize Ably client'));\n return;\n }\n\n this.client.connection.once('connected', () => {\n this.isConnected = true;\n this.sessionId = sessionId;\n resolve();\n });\n\n this.client.connection.once('failed', (stateChange) => {\n console.error('Ably connection failed:', stateChange);\n reject(\n new Error(\n `Ably connection failed: ${\n stateChange.reason?.message || 'Unknown error'\n }`\n )\n );\n });\n\n this.client.connection.once('disconnected', (stateChange) => {\n console.error('Ably connection disconnected:', stateChange);\n reject(\n new Error(\n `Ably connection disconnected: ${\n stateChange.reason?.message || 'Unknown error'\n }`\n )\n );\n });\n\n // Set a timeout for connection\n setTimeout(() => {\n if (!this.isConnected) {\n reject(new Error('Ably connection timeout'));\n }\n }, 10000);\n });\n\n // Subscribe to the session room\n await this.joinChannel(sessionId, onMessageReceived, tenantId);\n } catch (error) {\n console.error('Error during Ably connection setup:', error);\n this.isConnected = false;\n this.sessionId = null;\n throw error;\n }\n }\n\n private static async joinChannel(\n sessionId: string,\n onMessageReceived: Function,\n tenantId: string\n ) {\n if (!this.client) {\n throw new Error('Chat client not initialized');\n }\n\n const roomName = `session:${tenantId}:${sessionId}`;\n\n // Set up raw channel subscription for server messages\n if (this.client) {\n this.channel = this.client.channels.get(roomName);\n\n // Subscribe to assistant/system responses\n this.channel.subscribe('ReceiveMessage', (message) => {\n try {\n const messageContent =\n typeof message.data === 'string'\n ? message.data\n : message.data?.content || message.data?.message;\n const senderType = message.data?.senderType || 3; // Assistant\n const needsAgent = message.data?.needsAgent || false;\n\n onMessageReceived(messageContent, senderType, needsAgent);\n } catch (error) {\n console.error('Error processing ReceiveMessage:', error);\n }\n });\n\n await this.channel.attach();\n }\n }\n\n static async stopConnection() {\n try {\n // Unsubscribe from room messages\n if (this.messageUnsubscribe) {\n this.messageUnsubscribe();\n this.messageUnsubscribe = null;\n }\n\n // Unsubscribe and detach from raw channel\n if (this.channel) {\n this.channel.unsubscribe();\n await this.channel.detach();\n this.channel = null;\n }\n\n // Close Ably connection\n if (this.client) {\n this.client.close();\n this.client = null;\n }\n\n this.isConnected = false;\n this.sessionId = null;\n } catch (error) {\n console.error('Error stopping Ably connection:', error);\n // Reset state even if there's an error\n this.isConnected = false;\n this.sessionId = null;\n this.client = null;\n this.channel = null;\n this.messageUnsubscribe = null;\n }\n }\n\n static isConnectionActive(): boolean {\n return this.isConnected && this.client?.connection.state === 'connected';\n }\n\n static getConnectionState(): string {\n return this.client?.connection.state || 'disconnected';\n }\n\n // Method to manually send a message (if needed for debugging or direct messaging)\n static async sendMessage(messageContent: string, senderType: number = 1) {\n if (!this.channel || !this.isConnected) {\n throw new Error('Connection not active');\n }\n\n try {\n const messageData = {\n text: messageContent,\n metadata: {\n senderType,\n sentAt: new Date().toISOString(),\n },\n };\n\n await this.channel.publish('message', messageData);\n } catch (error) {\n console.error('Error sending message:', error);\n throw error;\n }\n }\n}\n","import { Component, Input, HostBinding, ViewEncapsulation } from '@angular/core'\nimport { CommonModule } from '@angular/common'\n\n@Component({\n selector: 'app-card',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./card.component.scss'],\n template: `<ng-content></ng-content>`,\n encapsulation: ViewEncapsulation.None\n})\nexport class CardComponent {\n @Input() variant: 'default' | 'rounded' | 'shadowed' = 'default'\n @Input() class = ''\n\n @HostBinding('class')\n get hostClasses(): string {\n const classes = ['card']\n classes.push(`card--${this.variant}`)\n if (this.class) {\n classes.push(this.class)\n }\n return classes.join(' ')\n }\n}\n\n@Component({\n selector: 'app-card-header',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./card.component.scss'],\n template: `<ng-content></ng-content>`,\n encapsulation: ViewEncapsulation.None\n})\nexport class CardHeaderComponent {\n @Input() class = ''\n\n @HostBinding('class')\n get hostClasses(): string {\n const classes = ['card__header']\n if (this.class) {\n classes.push(this.class)\n }\n return classes.join(' ')\n }\n}\n\n@Component({\n selector: 'app-card-title',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./card.component.scss'],\n template: `<ng-content></ng-content>`,\n encapsulation: ViewEncapsulation.None\n})\nexport class CardTitleComponent {\n @Input() class = ''\n\n @HostBinding('class')\n get hostClasses(): string {\n const classes = ['card__title']\n if (this.class) {\n classes.push(this.class)\n }\n return classes.join(' ')\n }\n}\n\n@Component({\n selector: 'app-card-description',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./card.component.scss'],\n template: `<ng-content></ng-content>`,\n encapsulation: ViewEncapsulation.None\n})\nexport class CardDescriptionComponent {\n @Input() class = ''\n\n @HostBinding('class')\n get hostClasses(): string {\n const classes = ['card__description']\n if (this.class) {\n classes.push(this.class)\n }\n return classes.join(' ')\n }\n}\n\n@Component({\n selector: 'app-card-content',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./card.component.scss'],\n template: `<ng-content></ng-content>`,\n encapsulation: ViewEncapsulation.None\n})\nexport class CardContentComponent {\n @Input() class = ''\n\n @HostBinding('class')\n get hostClasses(): string {\n const classes = ['card__content']\n if (this.class) {\n classes.push(this.class)\n }\n return classes.join(' ')\n }\n}\n\n@Component({\n selector: 'app-card-footer',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./card.component.scss'],\n template: `<ng-content></ng-content>`,\n encapsulation: ViewEncapsulation.None\n})\nexport class CardFooterComponent {\n @Input() class = ''\n\n @HostBinding('class')\n get hostClasses(): string {\n const classes = ['card__footer']\n if (this.class) {\n classes.push(this.class)\n }\n return classes.join(' ')\n }\n}\n","import { CommonModule } from '@angular/common'\nimport { Component, EventEmitter, Input, Output } from '@angular/core'\n\ntype ButtonVariant = 'default' | 'icon-bg' | 'icon-only' | 'outline'\ntype ButtonType = 'button' | 'submit' | 'reset'\n\n@Component({\n selector: 'app-button',\n standalone: true,\n imports: [CommonModule],\n styleUrls: ['./button.component.scss'],\n template: `\n <button [type]=\"type\" [disabled]=\"disabled\" [class]=\"getButtonClasses()\" (click)=\"onClick.emit($event)\" [dir]=\"direction\">\n <ng-content></ng-content>\n </button>\n `\n})\nexport class ButtonComponent {\n @Input() variant: ButtonVariant = 'default'\n @Input() type: ButtonType = 'button'\n @Input() disabled = false\n @Input() fullWidth = false\n @Input() className = ''\n @Input() size: 'default' | 'small' = 'default'\n @Input() direction: 'ltr' | 'rtl' = 'ltr'\n @Output() onClick = new EventEmitter<MouseEvent>()\n\n getButtonClasses(): string {\n const classes = ['button']\n\n // Add variant class\n classes.push(`button--${this.variant}`)\n\n // Add full width class if needed\n if (this.fullWidth) {\n classes.push('button--full-width')\n }\n\n // Add size class if needed\n if (this.size) {\n classes.push(`button--${this.size}`)\n }\n\n // Add direction class\n classes.push(`button--${this.direction}`)\n\n // Add custom classes\n if (this.className) {\n classes.push(this.className)\n }\n\n return classes.join(' ')\n }\n}\n\n@Component({\n selector: 'app-button-content',\n standalone: true,\n imports: [CommonModule],\n template: ` <ng-content></ng-content> `\n})\nexport class ButtonContentComponent {}\n\n@Component({\n selector: 'app-icon-button',\n standalone: true,\n imports: [CommonModule, ButtonComponent],\n template: `\n <app-button variant=\"icon-bg\" [className]=\"className\" [disabled]=\"disabled\" (onClick)=\"onClick.emit($event)\">\n <ng-content></ng-content>\n </app-button>\n `\n})\nexport class IconButtonComponent {\n @Input() className = ''\n @Input() disabled = false\n @Output() onClick = new EventEmitter<MouseEvent>()\n}\n\n@Component({\n selector: 'app-transparent-icon-button',\n standalone: true,\n imports: [CommonModule, ButtonComponent],\n template: `\n <app-button variant=\"icon-only\" [className]=\"className\" [disabled]=\"disabled\" (onClick)=\"onClick.emit($event)\">\n <ng-content></ng-content>\n </app-button>\n `\n})\nexport class TransparentIconButtonComponent {\n @Input() className = ''\n @Input() disabled = false\n @Output() onClick = new EventEmitter<MouseEvent>()\n}\n","// src/app/services/translation.service.ts\nimport { Injectable } from '@angular/core'\nimport { BehaviorSubject, Observable } from 'rxjs'\nimport { Language } from '../types'\n\nconst defaultTranslations = {\n ChatIntroMessage: '',\n BabylaiTitle: '',\n BabylaiDescription: '',\n ChatNow: '',\n TryBableAI: '',\n ContactUs: '',\n PickTopicTitle: '',\n BabylAI: '',\n ChatPlaceholder: '',\n PoweredByBabylAI: '',\n EndChat: '',\n LeavingDialogTitle: '',\n LeavingDialogBody: '',\n Confirm: '',\n Cancel: '',\n title: ''\n} as const\n\nexport type TranslationKey = keyof typeof defaultTranslations\n\n@Injectable({\n providedIn: 'root'\n})\nexport class TranslationService {\n private translations = {\n en: {\n ChatIntroMessage: 'Chat with BabylAI 🚀',\n BabylaiTitle: 'BabylAI',\n BabylaiDescription: \"Hey there! 👋 I'm BabylAI, here to assist you.\",\n ChatNow: 'Chat Now',\n TryBableAI: 'Try BabylAI for Free 🎉',\n ContactUs: \"Contact us, Let's Talk! 💬\",\n PickTopicTitle: 'Pick a Topic to Get Started',\n BabylAI: 'BabylAI',\n ChatPlaceholder: 'Type your message...',\n PoweredByBabylAI: 'Powered by BabylAI',\n EndChat: 'End Chat',\n LeavingDialogTitle: 'Leaving so soon? 👋',\n LeavingDialogBody: \"Don't worry, you can come back anytime. We're always here if you need help or have questions.\",\n Confirm: 'Confirm',\n Cancel: 'Cancel',\n title: 'Help Center'\n },\n ar: {\n ChatIntroMessage: 'دردش مع BabylAI 🚀',\n BabylaiTitle: 'BabylAI',\n BabylaiDescription: 'مرحبا! 👋 أنا BabylAI، هنا لتساعدك.',\n ChatNow: 'دردش الآن',\n TryBableAI: 'جرب BabylAI مجانا 🎉',\n ContactUs: 'تواصل معنا, دعنا نتحدث! 💬',\n PickTopicTitle: 'اختر موضوع للبدء',\n BabylAI: 'BabylAI',\n ChatPlaceholder: 'اكتب رسالتك...',\n PoweredByBabylAI: 'مدعوم من BabylAI',\n EndChat: 'إنهاء الدردشة',\n LeavingDialogTitle: 'هل تغادر بالفعل؟ 👋',\n LeavingDialogBody: 'لا تقلق، يمكنك العودة في أي وقت. نحن دائماً هنا إذا كنت بحاجة إلى مساعدة أو لديك أسئلة.',\n Confirm: 'تأكيد',\n Cancel: 'إلغاء',\n title: 'مركز المساعدة'\n }\n }\n\n private _currentLang = new BehaviorSubject<Language>('en')\n public readonly currentLang: Observable<Language> = this._currentLang.asObservable()\n\n constructor() {}\n\n translate(key: TranslationKey): string {\n const lang = this._currentLang.value as Language\n return this.translations[lang][key] || key\n }\n\n setLanguage(lang: Language) {\n this._currentLang.next(lang)\n }\n\n getCurrentLang(): Language {\n return this._currentLang.value\n }\n}\n","// src/app/pipes/translate.pipe.ts\nimport { Pipe, PipeTransform } from '@angular/core'\nimport { TranslationService } from '../services/translation.service'\nimport { TranslationKey } from '../services/translation.service' // Add this import\n\n@Pipe({\n name: 'translate',\n standalone: true\n})\nexport class TranslatePipe implements PipeTransform {\n constructor(private translationService: TranslationService) {}\n\n transform(key: TranslationKey): string {\n return this.translationService.translate(key)\n }\n}\n","import { Component, Input, OnInit, Output, EventEmitter, ViewChild, ElementRef } from '@angular/core'\nimport { CommonModule } from '@angular/common'\nimport { CardComponent, CardContentComponent } from '../shared/components/card/card.component'\nimport { ButtonComponent } from '../shared/components/button/button.component'\nimport { TranslatePipe } from '../pipes/translate.pipe'\n\ninterface Tenant {\n id: string\n name: string\n key: string\n}\n\ninterface Assistant {\n id: string\n tenantId: string\n tenant: Tenant\n name: string\n openAIAssistantId: string\n greeting: string\n closing: string\n}\n\ninterface HelpScreenOption {\n id: string\n helpScreenId: string\n parentOptionId: string | null\n parentOption: HelpScreenOption | null\n nestedOptions: HelpScreenOption[]\n assistantId: string\n assistant: Assistant\n title: string\n paragraphs: string[]\n files: any[]\n chatWithUs: boolean\n hasNestedOptions: boolean\n order: number\n}\n\ninterface HelpScreenData {\n id: string\n tenantId: string\n tenant: Tenant\n title: string\n description: string\n options: HelpScreenOption[]\n chatWithUs: boolean\n}\n\n@Component({\n selector: 'app-help-screen-data',\n standalone: true,\n imports: [CommonModule, CardComponent, CardContentComponent, ButtonComponent, TranslatePipe],\n templateUrl: './help-screen-data.component.html',\n styleUrls: ['./help-screen-data.component.scss']\n})\nexport class HelpScreenDataComponent implements OnInit {\n @Input() helpScreenData: HelpScreenData | null = null\n @Input() title: string = ''\n @Output() handleStartNewChat = new EventEmitter<HelpScreenOption>()\n\n expandedItemId: string | null = null\n\n get helpScreenDataList() {\n if (!this.helpScreenData?.options) return []\n // Transform options to the format expected by HelpScreenDataComponent\n return this.helpScreenData.options.map((option: any) => ({\n icon: option.icon || 'assets/icons/default.svg',\n title: option.title,\n description: option.paragraphs?.[0] || '',\n actionLabel: option.chatWithUs ? 'Chat Now' : '',\n action: option.chatWithUs ? () => this.handleStartChat(option) : null\n }))\n }\n\n ngOnInit() {}\n\n toggleExpand(itemId: string): void {\n if (this.expandedItemId === itemId) {\n this.expandedItemId = null\n } else {\n this.expandedItemId = itemId\n setTimeout(() => {\n const element = document.getElementById(itemId)\n if (element) {\n element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })\n }\n }, 100)\n }\n }\n\n handleStartChat(option: HelpScreenOption): void {\n this.handleStartNewChat.emit(option)\n }\n}\n","<div class=\"help-screen\">\n <h1 class=\"help-screen__title\">\n {{ 'PickTopicTitle' | translate }}\n </h1>\n <ng-container *ngFor=\"let item of helpScreenData?.options\">\n <app-card [id]=\"item.id\" variant=\"rounded\" class=\"help-screen__card\" (click)=\"toggleExpand(item.id)\">\n <app-card-content>\n <!-- Header section (always visible) -->\n <div class=\"help-screen__header\">\n <h4 class=\"help-screen__title-text\">{{ item.title }}</h4>\n <div\n class=\"help-screen__arrow-container\"\n [ngClass]=\"{\n 'help-screen__arrow-container--expanded': expandedItemId === item.id,\n 'help-screen__arrow-container--collapsed': expandedItemId !== item.id\n }\"\n >\n <!-- <img src=\"/icons/arrow-stripped-colored.svg\" alt=\"arrow-down\" /> -->\n <svg class=\"header__back-button-icon\" width=\"8\" height=\"16\" viewBox=\"0 0 8 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M7 15L1 8L2.5 6.25M7 1L5 3.333\" stroke=\"#7A1CAC\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </div>\n </div>\n\n <!-- Expanded content (visible only when expanded) -->\n <div *ngIf=\"expandedItemId === item.id\" class=\"help-screen__content\">\n <ng-container>\n <div *ngFor=\"let paragraph of item.paragraphs\" class=\"help-screen__paragraph\">\n {{ paragraph }}\n </div>\n </ng-container>\n\n <app-button *ngIf=\"item?.chatWithUs\" variant=\"default\" [fullWidth]=\"true\" (click)=\"handleStartChat(item)\">{{\n 'ChatNow' | translate\n }}</app-button>\n </div>\n </app-card-content>\n </app-card>\n </ng-container>\n</div>\n","import { CommonModule } from '@angular/common'\nimport { Component, EventEmitter, HostListener, Input, Output } from '@angular/core'\nimport { ButtonComponent } from '../button'\nimport { TranslatePipe } from '../../../pipes/translate.pipe'\n\ntype HeaderType = 'standard' | 'minimal'\n\n@Component({\n selector: 'app-header',\n standalone: true,\n imports: [CommonModule, ButtonComponent],\n styleUrls: ['./header.component.scss'],\n template: `\n <div class=\"header\">\n <app-button\n *ngIf=\"showBackButton\"\n variant=\"icon-bg\"\n className=\"button--white-bg\"\n [direction]=\"isRtl ? 'rtl' : 'ltr'\"\n (click)=\"onBack.emit()\"\n >\n <svg\n [style.transform]=\"isRtl ? 'rotate(180deg)' : 'rotate(0deg)'\"\n class=\"header__back-button-icon\"\n width=\"8\"\n height=\"16\"\n viewBox=\"0 0 8 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M7 15L1 8L2.5 6.25M7 1L5 3.333\" stroke=\"#7A1CAC\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </app-button>\n <app-button\n *ngIf=\"showCloseButton\"\n variant=\"icon-bg\"\n [direction]=\"isRtl ? 'rtl' : 'ltr'\"\n className=\"button--close-button\"\n (click)=\"onClose.emit()\"\n >\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 55 55\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M33.8568 21.1458L21.1484 33.8541M21.1484 21.1458L33.8568 33.8541M14.7943 5.48404C18.6562 3.24921 23.0407 2.07593 27.5026 2.08329C41.5402 2.08329 52.9193 13.4623 52.9193 27.5C52.9193 41.5376 41.5402 52.9166 27.5026 52.9166C13.465 52.9166 2.08594 41.5376 2.08594 27.5C2.08594 22.8716 3.32373 18.5279 5.48669 14.7916\"\n stroke=\"white\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </app-button>\n <svg *ngIf=\"showLogo\" class=\"header__logo\" viewBox=\"0 0 55 53\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M8.53125 19.1353C8.53125 12.2804 14.0883 6.72339 20.9432 6.72339H41.6298C48.4847 6.72339 54.0418 12.2804 54.0418 19.1353V52.2339H20.9432C14.0883 52.2339 8.53125 46.6769 8.53125 39.8219V19.1353Z\"\n fill=\"#E5E5E5\"\n />\n <path\n d=\"M0 12.412C0 5.55702 5.55702 0 12.412 0H33.0985C39.9535 0 45.5105 5.55702 45.5105 12.412V33.0985C45.5105 39.9535 39.9535 45.5105 33.0985 45.5105H0V12.412Z\"\n fill=\"white\"\n />\n <path\n d=\"M14.3684 15.2203C14.3696 15.2162 14.3701 15.2142 14.3704 15.2132C14.5505 14.5816 15.4457 14.5816 15.6258 15.2132C15.6261 15.2142 15.6267 15.2162 15.6278 15.2203C15.6309 15.2311 15.6324 15.2365 15.6338 15.2416C16.4708 18.1971 18.7808 20.5071 21.7364 21.3441C21.7414 21.3455 21.7468 21.3471 21.7576 21.3501C21.7617 21.3512 21.7637 21.3518 21.7647 21.3521C22.3963 21.5322 22.3963 22.4274 21.7647 22.6075C21.7637 22.6078 21.7617 22.6084 21.7576 22.6095C21.7468 22.6126 21.7414 22.6141 21.7364 22.6155C18.7808 23.4525 16.4708 25.7625 15.6338 28.7181C15.6324 28.7231 15.6309 28.7285 15.6278 28.7393C15.6267 28.7434 15.6261 28.7454 15.6258 28.7464C15.4457 29.378 14.5505 29.378 14.3704 28.7464C14.3701 28.7454 14.3696 28.7434 14.3684 28.7393C14.3654 28.7285 14.3638 28.7231 14.3624 28.7181C13.5254 25.7625 11.2154 23.4525 8.25988 22.6155C8.25481 22.6141 8.24942 22.6126 8.23864 22.6095C8.23454 22.6084 8.2325 22.6078 8.23155 22.6075C7.5999 22.4274 7.5999 21.5322 8.23155 21.3521C8.2325 21.3518 8.23454 21.3512 8.23864 21.3501C8.24942 21.3471 8.25481 21.3455 8.25988 21.3441C11.2154 20.5071 13.5254 18.1971 14.3624 15.2416C14.3638 15.2365 14.3654 15.2311 14.3684 15.2203Z\"\n fill=\"#AD49E1\"\n />\n <path\n d=\"M36.7198 21.8503C36.7198 24.9207 34.2886 27.4098 31.2896 27.4098C28.2906 27.4098 25.8594 24.9207 25.8594 21.8503C25.8594 18.7799 28.2906 16.2908 31.2896 16.2908C34.2886 16.2908 36.7198 18.7799 36.7198 21.8503Z\"\n fill=\"#AD49E1\"\n />\n </svg>\n\n <app-button *ngIf=\"!showBackButton && !showCloseButton\" variant=\"icon-only\" class=\"header__close-button\" (click)=\"onClose.emit()\">\n <svg width=\"40\" height=\"40\" viewBox=\"0 0 55 55\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M33.8568 21.1458L21.1484 33.8541M21.1484 21.1458L33.8568 33.8541M14.7943 5.48404C18.6562 3.24921 23.0407 2.07593 27.5026 2.08329C41.5402 2.08329 52.9193 13.4623 52.9193 27.5C52.9193 41.5376 41.5402 52.9166 27.5026 52.9166C13.465 52.9166 2.08594 41.5376 2.08594 27.5C2.08594 22.8716 3.32373 18.5279 5.48669 14.7916\"\n stroke=\"white\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n />\n </svg>\n </app-button>\n </div>\n `\n})\nexport class HeaderComponent {\n @Input() headerType: HeaderType = 'standard'\n @Input() showBackButton = false\n @Input() showLogo = true\n @Input() logoSrc = '/logo-white.svg'\n @Input() logoAlt = 'BabylAI Logo'\n @Input() language = 'en'\n @Input() showCloseButton = false\n @Output() onBack = new EventEmitter<void>()\n @Output() onClose = new EventEmitter<void>()\n\n get isRtl(): boolean {\n return this.language === 'ar'\n }\n}\n\n@Component({\n selector: 'app-chat-header',\n standalone: true,\n imports: [CommonModule, ButtonComponent, TranslatePipe],\n styleUrls: ['./header.component.scss'],\n template: `\n <div class=\"chat-header\">\n <div class=\"chat-header__actions\">\n <app-button size=\"small\" variant=\"icon-bg\" className=\"chat-header__button button--light-bg\" (click)=\"onBack.emit()\">\n <svg\n width=\"8\"\n height=\"16\"\n viewBox=\"0 0 8 16\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n [style.transform]=\"isRtl ? 'rotate(180deg)' : 'rotate(0deg)'\"\n >\n <path d=\"M7 15L1 8L2.5 6.25M7 1L5 3.333\" stroke=\"#7A1CAC\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n </app-button>\n <div class=\"chat-header__menu\" data-menu-container>\n <app-button\n size=\"small\"\n variant=\"icon-bg\"\n className=\"chat-header__button button--light-bg\"\n (click)=\"$event.stopPropagation(); isMenuOpen = !isMenuOpen\"\n >\n <svg width=\"14\" height=\"4\" viewBox=\"0 0 14 4\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M3.25 1.8269C3.25 2.22473 3.09196 2.60626 2.81066 2.88756C2.52936 3.16887 2.14782 3.3269 1.75 3.3269C1.35218 3.3269 0.970644 3.16887 0.68934 2.88756C0.408035 2.60626 0.25 2.22473 0.25 1.8269C0.25 1.42908 0.408035 1.04755 0.68934 0.766244C0.970644 0.48494 1.35218 0.326904 1.75 0.326904C2.14782 0.326904 2.52936 0.48494 2.81066 0.766244C3.09196 1.04755 3.25 1.42908 3.25 1.8269ZM8.5 1.8269C8.5 2.22473 8.34196 2.60626 8.06066 2.88756C7.77936 3.16887 7.39782 3.3269 7 3.3269C6.60218 3.3269 6.22064 3.16887 5.93934 2.88756C5.65804 2.60626 5.5 2.22473 5.5 1.8269C5.5 1.42908 5.65804 1.04755 5.93934 0.766244C6.22064 0.48494 6.60218 0.326904 7 0.326904C7.39782 0.326904 7.77936 0.48494 8.06066 0.766244C8.34196 1.04755 8.5 1.42908 8.5 1.8269ZM13.75 1.8269C13.75 2.22473 13.592 2.60626 13.3107 2.88756C13.0294 3.16887 12.6478 3.3269 12.25 3.3269C11.8522 3.3269 11.4706 3.16887 11.1893 2.88756C10.908 2.60626 10.75 2.22473 10.75 1.8269C10.75 1.42908 10.908 1.04755 11.1893 0.766244C11.4706 0.48494 11.8522 0.326904 12.25 0.326904C12.6478 0.326904 13.0294 0.48494 13.3107 0.766244C13.592 1.04755 13.75 1.42908 13.75 1.8269Z\"\n fill=\"#AD49E1\"\n />\n </svg>\n </app-button>\n <div *ngIf=\"isMenuOpen\" class=\"chat-header__menu-dropdown\" [ngClass]=\"{ rtl: isRtl, ltr: !isRtl }\">\n <div class=\"chat-header__menu-dropdown-content\">\n <button (click)=\"onClose.emit(); isMenuOpen = false\" class=\"chat-header__menu-button\" [ngClass]=\"{ rtl: isRtl, ltr: !isRtl }\">\n {{ 'EndChat' | translate }}\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"chat-header__brand\">\n <svg width=\"39\" height=\"38\" viewBox=\"0 0 39 38\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M6.14844 13.794C6.14844 8.85255 10.1544 4.84668 15.0959 4.84668H30.0083C34.9499 4.84668 38.9558 8.85255 38.9558 13.794V37.6537H15.0959C10.1544 37.6537 6.14844 33.6478 6.14844 28.7063V13.794Z\"\n fill=\"#ECECEC\"\n />\n <path\n d=\"M0 8.94736C0 4.00587 4.00592 0 8.94746 0H23.8599C28.8014 0 32.8074 4.00587 32.8074 8.94736V23.8596C32.8074 28.8011 28.8014 32.807 23.8599 32.807H0V8.94736Z\"\n fill=\"#AD49E1\"\n />\n <path\n d=\"M10.3592 10.9721C10.36 10.9691 10.3604 10.9677 10.3606 10.967C10.4904 10.5117 11.1358 10.5117 11.2656 10.967C11.2658 10.9677 11.2662 10.9691 11.267 10.9721C11.2692 10.9799 11.2703 10.9838 11.2714 10.9874C11.8747 13.118 13.5399 14.7832 15.6705 15.3865C15.6742 15.3876 15.6781 15.3886 15.6858 15.3908C15.6888 15.3917 15.6903 15.3921 15.6909 15.3923C16.1463 15.5221 16.1463 16.1674 15.6909 16.2973C15.6903 16.2975 15.6888 16.2979 15.6858 16.2987C15.6781 16.3009 15.6742 16.302 15.6705 16.303C13.5399 16.9064 11.8747 18.5716 11.2714 20.7022C11.2703 20.7058 11.2692 20.7097 11.267 20.7175C11.2662 20.7204 11.2658 20.7219 11.2656 20.7226C11.1358 21.1779 10.4904 21.1779 10.3606 20.7226C10.3604 20.7219 10.36 20.7204 10.3592 20.7175C10.357 20.7097 10.3559 20.7058 10.3548 20.7022C9.75148 18.5716 8.08627 16.9064 5.95567 16.303C5.95202 16.302 5.94814 16.3009 5.94036 16.2987C5.93741 16.2979 5.93594 16.2975 5.93525 16.2973C5.47992 16.1674 5.47992 15.5221 5.93525 15.3923C5.93594 15.3921 5.93741 15.3917 5.94036 15.3908C5.94814 15.3886 5.95202 15.3876 5.95567 15.3865C8.08627 14.7832 9.75148 13.118 10.3548 10.9874C10.3559 10.9838 10.357 10.9799 10.3592 10.9721Z\"\n fill=\"white\"\n />\n <path\n d=\"M26.4618 15.7513C26.4618 17.9647 24.7093 19.759 22.5473 19.759C20.3854 19.759 18.6328 17.9647 18.6328 15.7513C18.6328 13.5379 20.3854 11.7437 22.5473 11.7437C24.7093 11.7437 26.4618 13.5379 26.4618 15.7513Z\"\n fill=\"white\"\n />\n </svg>\n\n <p>{{ 'BabylAI' | translate }}</p>\n </div>\n </div>\n `\n})\nexport class ChatHeaderComponent {\n isMenuOpen = false\n @Input() showBackButton = false\n @Input() showLogo = true\n @Input() logoSrc = '/logo-white.svg'\n @Input() logoAlt = 'BabylAI Logo'\n @Input() language = 'en'\n @Output() onBack = new EventEmitter<void>()\n @Output() onClose = new EventEmitter<void>()\n\n get isRtl(): boolean {\n return this.language === 'ar'\n }\n\n @HostListener('document:click', ['$event'])\n onDocumentClick(event: MouseEvent) {\n const target = event.target as HTMLElement\n const menuContainer = target.closest('[data-menu-container]')\n if (!menuContainer && this.isMenuOpen) {\n this.isMenuOpen = false\n }\n }\n}\n","import { Component, Input } from '@angular/core'\n\n@Component({\n selector: 'app-loading',\n styleUrls: ['./loading.component.scss'],\n template: `\n <div class=\"loading\">\n <div class=\"loader\" [class.loader--primary]=\"variant === 'primary'\"></div>\n </div>\n `,\n standalone: true\n})\nexport class LoadingComponent {\n @Input() variant: 'default' | 'primary' = 'default'\n}\n","import {\n Component,\n Input,\n Output,\n EventEmitter,\n ViewChild,\n ElementRef,\n OnInit,\n} from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { CardComponent, CardContentComponent } from '../shared/components/card';\nimport { LoadingComponent } from '../shared/components/loading/loading.component';\nimport { TranslatePipe } from '../pipes/translate.pipe';\nimport { MarkdownModule } from 'ngx-markdown';\nimport 'prismjs';\nimport 'prismjs/components/prism-typescript';\nimport 'prismjs/components/prism-javascript';\nimport 'prismjs/components/prism-css';\nimport 'prismjs/components/prism-json';\n\ninterface Message {\n id: string | number;\n sender: 'user' | 'assistant' | 'agent';\n senderType: number;\n messageContent: string;\n sentAt: Date;\n isSeen: boolean;\n}\n\n@Component({\n selector: 'app-chat',\n standalone: true,\n imports: [\n CommonModule,\n FormsModule,\n CardComponent,\n CardContentComponent,\n LoadingComponent,\n TranslatePipe,\n MarkdownModule,\n ],\n templateUrl: './chat.component.html',\n styleUrls: ['./chat.component.scss'],\n})\nexport class ChatComponent implements OnInit {\n @Input() messages: Message[] = [];\n @Input() needsAgent: boolean = false;\n @Input() assistantStatus: string = '';\n @Input() isAblyConnected: boolean = false;\n @Input() isChatClosed: boolean = false;\n @Input() currentLang: string = 'en';\n @Input() loading: boolean = false;\n @Output() sendMessageEvent = new EventEmitter<string>();\n @ViewChild('chatMessagesContainer') chatMessagesContainer!: ElementRef;\n @ViewChild('messageInput') messageInput!: ElementRef;\n\n messageContent = '';\n firstAgentMessageIndex = -1;\n\n ngOnInit(): void {\n this.findFirstAgentMessageIndex();\n }\n\n findFirstAgentMessageIndex(): void {\n this.firstAgentMessageIndex = this.messages.findIndex(\n (message) => message.senderType === 2\n );\n }\n\n handleSendMessage(): void {\n if (\n !this.messageContent.trim() ||\n this.loading ||\n this.assistantStatus === 'typing'\n )\n return;\n this.sendMessageEvent.emit(this.messageContent);\n this.messageContent = '';\n this.adjustTextareaHeight();\n }\n\n cleanMessageContent(content: string): string {\n return content.replace(/```/g, '\\\\`\\\\`\\\\`');\n }\n\n adjustTextareaHeight(): void {\n const textarea = this.messageInput?.nativeElement;\n if (textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = textarea.scrollHeight + 'px';\n }\n }\n\n ngAfterViewChecked(): void {\n this.scrollToBottom();\n }\n\n scrollToBottom(): void {\n try {\n this.chatMessagesContainer.nativeElement.scrollTop =\n this.chatMessagesContainer.nativeElement.scrollHeight;\n } catch (err) {\n console.error('Error scrolling to bottom:', err);\n }\n }\n\n hasAgentMessageBeenSent(messages: any[]): boolean {\n return messages.some(\n (message) => message.senderType === 2 || message.senderType === 3\n );\n }\n}\n","<div class=\"chat\">\n <div class=\"chat__messages\" #chatMessagesContainer>\n <div\n *ngFor=\"let message of messages; let i = index\"\n class=\"chat__message-group\"\n >\n <div\n class=\"chat__separator\"\n *ngIf=\"i === firstAgentMessageIndex && message.senderType === 2\"\n >\n <svg\n width=\"100%\"\n height=\"14\"\n viewBox=\"0 0 327 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <line x1=\"132.5\" y1=\"7.5\" y2=\"7.5\" stroke=\"#AD49E1\" />\n <path\n d=\"M162.891 0.464864C162.892 0.460907 162.893 0.458928 162.893 0.458012C163.067 -0.152671 163.933 -0.152671 164.107 0.458012C164.107 0.458928 164.108 0.460907 164.109 0.464864C164.112 0.475291 164.113 0.480505 164.115 0.4854C164.924 3.34287 167.157 5.57619 170.015 6.38539C170.019 6.38678 170.025 6.38825 170.035 6.39119C170.039 6.3923 170.041 6.39286 170.042 6.39312C170.653 6.56727 170.653 7.43274 170.042 7.60688C170.041 7.60714 170.039 7.6077 170.035 7.60881C170.025 7.61175 170.019 7.61322 170.015 7.61461C167.157 8.42381 164.924 10.6571 164.115 13.5146C164.113 13.5195 164.112 13.5247 164.109 13.5351C164.108 13.5391 164.107 13.5411 164.107 13.542C163.933 14.1527 163.067 14.1527 162.893 13.542C162.893 13.5411 162.892 13.5391 162.891 13.5351C162.888 13.5247 162.887 13.5195 162.885 13.5146C162.076 10.6571 159.843 8.42381 156.985 7.61461C156.981 7.61322 156.975 7.61175 156.965 7.60881C156.961 7.6077 156.959 7.60714 156.958 7.60688C156.347 7.43274 156.347 6.56727 156.958 6.39312C156.959 6.39286 156.961 6.3923 156.965 6.39119C156.975 6.38825 156.981 6.38678 156.985 6.38539C159.843 5.57619 162.076 3.34287 162.885 0.4854C162.887 0.480505 162.888 0.475291 162.891 0.464864Z\"\n fill=\"#AD49E1\"\n />\n <line x1=\"327\" y1=\"7.5\" x2=\"194.5\" y2=\"7.5\" stroke=\"#AD49E1\" />\n </svg>\n </div>\n\n <div\n class=\"chat__message-container\"\n [class.chat__message-container--user]=\"message.senderType === 1\"\n >\n <div\n class=\"chat__avatar\"\n [class.chat__avatar--hidden]=\"\n i > 0 && messages[i - 1].senderType === message.senderType\n \"\n >\n @if (message.senderType === 3) {\n <span class=\"chat__avatar-wrapper chat__avatar-wrapper--assistant\">\n <svg\n class=\"chat__avatar-image\"\n viewBox=\"0 0 55 53\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M8.53125 19.1353C8.53125 12.2804 14.0883 6.72339 20.9432 6.72339H41.6298C48.4847 6.72339 54.0418 12.2804 54.0418 19.1353V52.2339H20.9432C14.0883 52.2339 8.53125 46.6769 8.53125 39.8219V19.1353Z\"\n fill=\"#E5E5E5\"\n />\n <path\n d=\"M0 12.412C0 5.55702 5.55702 0 12.412 0H33.0985C39.9535 0 45.5105 5.55702 45.5105 12.412V33.0985C45.5105 39.9535 39.9535 45.5105 33.0985 45.5105H0V12.412Z\"\n fill=\"white\"\n />\n <path\n d=\"M14.3684 15.2203C14.3696 15.2162 14.3701 15.2142 14.3704 15.2132C14.5505 14.5816 15.4457 14.5816 15.6258 15.2132C15.6261 15.2142 15.6267 15.2162 15.6278 15.2203C15.6309 15.2311 15.6324 15.2365 15.6338 15.2416C16.4708 18.1971 18.7808 20.5071 21.7364 21.3441C21.7414 21.3455 21.7468 21.3471 21.7576 21.3501C21.7617 21.3512 21.7637 21.3518 21.7647 21.3521C22.3963 21.5322 22.3963 22.4274 21.7647 22.6075C21.7637 22.6078 21.7617 22.6084 21.7576 22.6095C21.7468 22.6126 21.7414 22.6141 21.7364 22.6155C18.7808 23.4525 16.4708 25.7625 15.6338 28.7181C15.6324 28.7231 15.6309 28.7285 15.6278 28.7393C15.6267 28.7434 15.6261 28.7454 15.6258 28.7464C15.4457 29.378 14.5505 29.378 14.3704 28.7464C14.3701 28.7454 14.3696 28.7434 14.3684 28.7393C14.3654 28.7285 14.3638 28.7231 14.3624 28.7181C13.5254 25.7625 11.2154 23.4525 8.25988 22.6155C8.25481 22.6141 8.24942 22.6126 8.23864 22.6095C8.23454 22.6084 8.2325 22.6078 8.23155 22.6075C7.5999 22.4274 7.5999 21.5322 8.23155 21.3521C8.2325 21.3518 8.23454 21.3512 8.23864 21.3501C8.24942 21.3471 8.25481 21.3455 8.25988 21.3441C11.2154 20.5071 13.5254 18.1971 14.3624 15.2416C14.3638 15.2365 14.3654 15.2311 14.3684 15.2203Z\"\n fill=\"#AD49E1\"\n />\n <path\n d=\"M36.7198 21.8503C36.7198 24.9207 34.2886 27.4098 31.2896 27.4098C28.2906 27.4098 25.8594 24.9207 25.8594 21.8503C25.8594 18.7799 28.2906 16.2908 31.2896 16.2908C34.2886 16.2908 36.7198 18.7799 36.7198 21.8503Z\"\n fill=\"#AD49E1\"\n />\n </svg>\n </span>\n } @else if (needsAgent || message.senderType === 2) {\n <span class=\"chat__avatar-wrapper chat__avatar-wrapper--agent\">\n <svg\n class=\"chat__avatar-image\"\n viewBox=\"0 0 12 14\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M5.99479 5.66658C7.46755 5.66658 8.66146 4.47268 8.66146 2.99992C8.66146 1.52716 7.46755 0.333252 5.99479 0.333252C4.52203 0.333252 3.32812 1.52716 3.32812 2.99992C3.32812 4.47268 4.52203 5.66658 5.99479 5.66658Z\"\n fill=\"#AD49E1\"\n />\n <path\n d=\"M11.3307 10.6665C11.3307 12.3232 11.3307 13.6665 5.9974 13.6665C0.664062 13.6665 0.664062 12.3232 0.664062 10.6665C0.664062 9.00984 3.05206 7.6665 5.9974 7.6665C8.94273 7.6665 11.3307 9.00984 11.3307 10.6665Z\"\n fill=\"#AD49E1\"\n />\n </svg>\n </span>\n }\n </div>\n <app-card\n variant=\"rounded\"\n [class]=\"\n 'chat__message ' +\n (message.senderType === 1\n ? 'chat__message--user'\n : 'chat__message--assistant')\n \"\n >\n <app-card-content>\n <div class=\"chat__message-content\">\n <markdown\n [data]=\"cleanMessageContent(message.messageContent)\"\n ngPreserveWhitespaces\n [inline]=\"false\"\n class=\"prose\"\n [class.prose-invert]=\"message.senderType === 1\"\n [dir]=\"currentLang === 'ar' ? 'rtl' : 'ltr'\"\n >\n </markdown>\n </div>\n </app-card-content>\n </app-card>\n </div>\n </div>\n\n <div\n *ngIf=\"assistantStatus === 'typing' && firstAgentMessageIndex === -1\"\n class=\"chat__typing\"\n >\n <div class=\"chat__avatar\">\n <span class=\"chat__avatar-wrapper chat__avatar-wrapper--agent\">\n <svg\n class=\"chat__avatar-image\"\n viewBox=\"0 0 55 53\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M8.53125 19.1353C8.53125 12.2804 14.0883 6.72339 20.9432 6.72339H41.6298C48.4847 6.72339 54.0418 12.2804 54.0418 19.1353V52.2339H20.9432C14.0883 52.2339 8.53125 46.6769 8.53125 39.8219V19.1353Z\"\n fill=\"#E5E5E5\"\n />\n <path\n d=\"M0 12.412C0 5.55702 5.55702 0 12.412 0H33.0985C39.9535 0 45.5105 5.55702 45.5105 12.412V33.0985C45.5105 39.9535 39.9535 45.5105 33.0985 45.5105H0V12.412Z\"\n fill=\"white\"\n />\n <path\n d=\"M14.3684 15.2203C14.3696 15.2162 14.3701 15.2142 14.3704 15.2132C14.5505 14.5816 15.4457 14.5816 15.6258 15.2132C15.6261 15.2142 15.6267 15.2162 15.6278 15.2203C15.6309 15.2311 15.6324 15.2365 15.6338 15.2416C16.4708 18.1971 18.7808 20.5071 21.7364 21.3441C21.7414 21.3455 21.7468 21.3471 21.7576 21.3501C21.7617 21.3512 21.7637 21.3518 21.7647 21.3521C22.3963 21.5322 22.3963 22.4274 21.7647 22.6075C21.7637 22.6078 21.7617 22.6084 21.7576 22.6095C21.7468 22.6126 21.7414 22.6141 21.7364 22.6155C18.7808 23.4525 16.4708 25.7625 15.6338 28.7181C15.6324 28.7231 15.6309 28.7285 15.6278 28.7393C15.6267 28.7434 15.6261 28.7454 15.6258 28.7464C15.4457 29.378 14.5505 29.378 14.3704 28.7464C14.3701 28.7454 14.3696 28.7434 14.3684 28.7393C14.3654 28.7285 14.3638 28.7231 14.3624 28.7181C13.5254 25.7625 11.2154 23.4525 8.25988 22.6155C8.25481 22.6141 8.24942 22.6126 8.23864 22.6095C8.23454 22.6084 8.2325 22.6078 8.23155 22.6075C7.5999 22.4274 7.5999 21.5322 8.23155 21.3521C8.2325 21.3518 8.23454 21.3512 8.23864 21.3501C8.24942 21.3471 8.25481 21.3455 8.25988 21.3441C11.2154 20.5071 13.5254 18.1971 14.3624 15.2416C14.3638 15.2365 14.3654 15.2311 14.3684 15.2203Z\"\n fill=\"#AD49E1\"\n />\n <path\n d=\"M36.7198 21.8503C36.7198 24.9207 34.2886 27.4098 31.2896 27.4098C28.2906 27.4098 25.8594 24.9207 25.8594 21.8503C25.8594 18.7799 28.2906 16.2908 31.2896 16.2908C34.2886 16.2908 36.7198 18.7799 36.7198 21.8503Z\"\n fill=\"#AD49E1\"\n />\n </svg>\n </span>\n </div>\n <app-card\n variant=\"rounded\"\n class=\"chat__message chat__message--assistant\"\n >\n <app-card-content>\n <div id=\"wave\">\n <span class=\"dot\"></span>\n <span class=\"dot\"></span>\n <span class=\"dot\"></span>\n </div>\n </app-card-content>\n </app-card>\n </div>\n <div *ngIf=\"loading\" class=\"chat__loading\">\n <app-loading variant=\"primary\" />\n </div>\n </div>\n\n <form (ngSubmit)=\"handleSendMessage()\" class=\"chat__input-container\">\n <div class=\"chat__input-wrapper\">\n <input\n type=\"text\"\n [(ngModel)]=\"messageContent\"\n name=\"messageContent\"\n [placeholder]=\"'ChatPlaceholder' | translate\"\n [disabled]=\"isChatClosed\"\n class=\"chat__input\"\n />\n <button\n type=\"submit\"\n [disabled]=\"\n !messageContent.trim() ||\n !isAblyConnected ||\n isChatClosed ||\n assistantStatus === 'typing'\n \"\n class=\"chat__send-button\"\n >\n <svg\n class=\"chat__send-button-icon\"\n [class.chat__send-button-icon--rtl]=\"currentLang === 'ar'\"\n viewBox=\"0 0 19 19\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M18.2346 2.68609C18.6666 1.49109 17.5086 0.33309 16.3136 0.76609L1.70855 6.04809C0.509554 6.48209 0.364554 8.11809 1.46755 8.75709L6.12955 11.4561L10.2926 7.29309C10.4812 7.11093 10.7338 7.01014 10.996 7.01242C11.2582 7.01469 11.509 7.11986 11.6944 7.30527C11.8798 7.49068 11.9849 7.74149 11.9872 8.00369C11.9895 8.26589 11.8887 8.51849 11.7066 8.70709L7.54355 12.8701L10.2436 17.5321C10.8816 18.6351 12.5176 18.4891 12.9516 17.2911L18.2346 2.68609Z\"\n fill=\"white\"\n />\n </svg>\n </button>\n </div>\n </form>\n</div>\n","import { Component, Input, Output, EventEmitter } from '@angular/core'\nimport { CommonModule } from '@angular/common'\nimport { ButtonComponent } from '../button'\n\n@Component({\n selector: 'app-confirmation-dialog',\n standalone: true,\n imports: [CommonModule, ButtonComponent],\n styleUrls: ['./confirmation-dialog.component.scss'],\n template: `\n <div class=\"dialog\">\n <div class=\"dialog__content\">\n <h3 class=\"dialog__title\">{{ title }}</h3>\n <p class=\"dialog__body\">{{ body }}</p>\n <div class=\"dialog__actions\">\n <app-button variant=\"outline\" [fullWidth]=\"true\" (click)=\"onCancel.emit()\">\n {{ cancelText }}\n </app-button>\n <app-button variant=\"default\" [fullWidth]=\"true\" (click)=\"onConfirm.emit()\">\n {{ confirmText }}\n </app-button>\n </div>\n </div>\n </div>\n `\n})\nexport class ConfirmationDialogComponent {\n @Input() title: string = ''\n @Input() body: string = ''\n @Input() confirmText: string = 'Confirm'\n @Input() cancelText: string = 'Cancel'\n @Output() onConfirm = new EventEmitter<void>()\n @Output() onCancel = new EventEmitter<void>()\n}\n","import { Injectable } from '@angular/core'\nimport { ApiConfig } from '../../../public_api'\n\n@Injectable({\n providedIn: 'root'\n})\nexport class ApiService {\n private getTokenFunction: (() => Promise<string>) | null = null\n private baseUrl: string = 'https://babylai.net/api'\n\n /**\n * Initialize the API service with optional configuration\n * @param config Configuration object containing token function and optional base URL\n */\n initialize(config: ApiConfig) {\n if (!config.getToken) {\n throw new Error('getToken function is required for API initialization')\n }\n\n this.getTokenFunction = config.getToken\n if (config.baseUrl) {\n this.baseUrl = config.baseUrl\n }\n }\n\n async getValidToken(forceRefresh = false): Promise<string> {\n if (!this.getTokenFunction) {\n throw new Error('API service not initialized. Call initialize({ getToken }) first.')\n }\n\n let storedToken = localStorage.getItem('chatbot-token')\n let storedExpiry = localStorage.getItem('chatbot-token-expiry')\n\n const currentTime = Math.floor(Date.now() / 1000)\n\n if (!storedToken || !storedExpiry || currentTime >= Number(storedExpiry) || forceRefresh) {\n const tokenResponse = await this.getTokenFunction()\n\n if (!tokenResponse) {\n throw new Error('Invalid token response from getToken function')\n }\n\n storedToken = tokenResponse\n storedExpiry = String(currentTime + 900) // 15 minutes expiry\n\n localStorage.setItem('chatbot-token', storedToken)\n localStorage.setItem('chatbot-token-expiry', storedExpiry)\n }\n\n return storedToken\n }\n\n private async fetchWithAuth(url: string, options: RequestInit, retry = true): Promise<Response> {\n if (!options.headers) {\n options.headers = {}\n }\n\n const headers = options.headers as Record<string, string>\n headers['Authorization'] = `Bearer ${await this.getValidToken()}`\n\n let response = await fetch(url, options)\n\n if ((response.status === 401 || response.status === 403) && retry) {\n console.warn('Token expired. Fetching new token...')\n\n const newToken = await this.getValidToken(true)\n headers['Authorization'] = `Bearer ${newToken}`\n\n response = await fetch(url, options)\n }\n\n return response\n }\n\n async apiRequest(endpoint: string, method = 'GET', body: any = null, customHeaders: Record<string, string> = {}): Promise<Response> {\n const url = `${this.baseUrl}/${endpoint}`\n\n const options: RequestInit = {\n method,\n headers: {\n 'Con