@solcre-org/core-ui 2.12.16 → 2.12.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11985,11 +11985,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
11985
11985
  // Este archivo es generado automáticamente por scripts/update-version.js
11986
11986
  // No edites manualmente este archivo
11987
11987
  const VERSION = {
11988
- full: '2.12.16',
11988
+ full: '2.12.17',
11989
11989
  major: 2,
11990
11990
  minor: 12,
11991
- patch: 16,
11992
- timestamp: '2025-09-08T20:21:04.064Z',
11991
+ patch: 17,
11992
+ timestamp: '2025-09-08T20:23:20.885Z',
11993
11993
  buildDate: '8/9/2025'
11994
11994
  };
11995
11995
 
@@ -14854,11 +14854,11 @@ class GenericChatComponent {
14854
14854
  return this.isSending();
14855
14855
  }
14856
14856
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: GenericChatComponent, deps: [{ token: GenericChatService }], target: i0.ɵɵFactoryTarget.Component });
14857
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: GenericChatComponent, isStandalone: true, selector: "app-generic-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isTyping: { classPropertyName: "isTyping", publicName: "isTyping", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messagesSent: "messagesSent", imageClicked: "imageClicked", fileClicked: "fileClicked", fileSelected: "fileSelected", filesSelected: "filesSelected", typingStatusChanged: "typingStatusChanged", modalClosed: "modalClosed", attachmentAction: "attachmentAction", sendingStatusChanged: "sendingStatusChanged", loadMoreMessages: "loadMoreMessages" }, host: { listeners: { "document:keydown": "handleKeyboardEvent($event)" } }, viewQueries: [{ propertyName: "messagesContainer", first: true, predicate: ["messagesContainer"], descendants: true, isSignal: true }, { propertyName: "modalMessagesContainer", first: true, predicate: ["modalMessagesContainer"], descendants: true, isSignal: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "scrollSentinelElement", first: true, predicate: ["scrollSentinel"], descendants: true, read: ElementRef }], ngImport: i0, template: "@if (mergedConfig().modalMode && mergedConfig().isModalOpen) {\n<div class=\"c-services-chat c-modal is-visible\" [class.is-closing]=\"isClosing()\">\n <div class=\"c-modal__overlay\" (click)=\"closeModal()\"></div>\n <div class=\"c-modal__holder\">\n <div class=\"c-modal__header\">\n <p>\n @if (mergedConfig().modalTitle) {\n <span>{{ mergedConfig().modalTitle }}</span>\n }\n @if (mergedConfig().modalSubtitle) {\n <time>{{ mergedConfig().modalSubtitle }}</time>\n }\n </p>\n <core-generic-button [config]=\"closeButtonConfig()\" (buttonClick)=\"closeModal()\">\n </core-generic-button>\n </div>\n\n <div class=\"c-modal__body c-chat-body\" #modalMessagesContainer>\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n @for (message of chronologicalMessages(); track trackByChronologicalMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n\n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n <div class=\"c-chat-bubble__image\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n @if (getAttachmentPreviewUrl(file)) {\n <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\n } @else {\n <div class=\"c-chat-image-placeholder\" (click)=\"onImageClick(message)\">\n <div class=\"c-chat-image-placeholder__content\">\n <span class=\"c-chat-image-placeholder__icon\">\uD83D\uDCF7</span>\n <span class=\"c-chat-image-placeholder__text\">{{ file.filename || 'Imagen no disponible' }}</span>\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview\n [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-chat-bubble__image\">\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-chat-bubble__image\">\n <picture class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </picture>\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> .c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-modal__bottom\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n </div>\n</div>\n} @else {\n<div class=\"c-chat-layout c-services-chat\" [ngClass]=\"mergedConfig().customCssClasses?.container\">\n <div #messagesContainer class=\"c-chat-layout__body c-chat-body\"\n [ngClass]=\"mergedConfig().customCssClasses?.messageList\">\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n\n @for (message of messages(); track trackByMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n \n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <div class=\"c-chat-bubble__image\">\n <core-image-preview [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"></core-image-preview>\n </div>\n }\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-chat-layout__bottom\" [ngClass]=\"mergedConfig().customCssClasses?.inputContainer\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n</div>\n}\n\n<!-- @if (showImageModal()) {\n<div class=\"c-img-modal js-img-modal is-visible\">\n <span \n class=\"c-img-modal__close icon-cross-thin js-gallery-modal-close\"\n (click)=\"closeImageModal()\"\n ></span>\n <div \n class=\"c-img-modal__overlay js-img-modal-close\"\n (click)=\"closeImageModal()\"\n ></div>\n <div class=\"c-img-modal__holder\">\n <img \n class=\"js-img-modal-img\" \n [src]=\"selectedImage()\" \n alt=\"Imagen Ampliada\"\n />\n </div>\n</div>\n} -->", styles: [".c-img-preview{position:relative;width:100%;height:100%}.c-img-preview__placeholder{position:absolute;bottom:.5rem;left:.5rem;right:.5rem;background-color:#000000b3;color:#fff;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;text-align:center}.c-chat-image-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem;background-color:#f8fafc;border:2px dashed #e2e8f0;border-radius:.5rem;min-height:8rem}.c-chat-image-placeholder .icon-image{font-size:2rem;color:#9ca3af;margin-bottom:.5rem}.c-chat-image-placeholder p{margin:0;color:#6b7280;font-size:.875rem;text-align:center}.c-chat-files__preview{width:50px;height:50px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-bottom:.25rem}.c-chat-loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.c-chat-input__submit.is-sending{opacity:.7;cursor:not-allowed}.c-chat-sending-global-indicator{margin-top:.5rem}.c-chat-sending-global-indicator .c-chat-bubble{background-color:#f8f9fa;border:1px solid #e9ecef}.c-chat-sending-global-indicator .c-chat-bubble .c-chat-bubble__content{padding:.5rem .75rem}.c-chat-sending-message{display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:#6c757d}.c-chat-sending-spinner{display:inline-block;width:12px;height:12px;border:2px solid #f3f3f3;border-top:2px solid #007bff;border-radius:50%;animation:spin 1s linear infinite}.c-chat-sending-text{font-style:italic}.c-chat-sending-indicator{display:flex;align-items:center;gap:.5rem;margin-top:.25rem;font-size:.75rem;color:#6c757d}.c-chat-sending-indicator .c-chat-dots{display:inline-flex;gap:2px}.c-chat-sending-indicator .c-chat-dots span{display:inline-block;width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-sending-indicator .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-sending-indicator .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-sending-indicator .c-chat-dots span:nth-child(3){animation-delay:0s}.c-chat-error-indicator{display:flex;align-items:center;gap:.25rem;margin-top:.25rem;font-size:.75rem;color:#dc3545}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.c-chat-scroll-sentinel{width:100%;min-height:1px;position:relative}.c-chat-loading-more{display:flex;justify-content:center;align-items:center;padding:1rem}.c-chat-loading-more .c-chat-dots{display:inline-flex;gap:4px}.c-chat-loading-more .c-chat-dots span{display:inline-block;width:8px;height:8px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-loading-more .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-loading-more .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-loading-more .c-chat-dots span:nth-child(3){animation-delay:0s}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: GenericButtonComponent, selector: "core-generic-button", inputs: ["config", "data"], outputs: ["buttonClick"] }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation"] }, { kind: "component", type: FilePreviewComponent, selector: "core-file-preview", inputs: ["fileModels", "files", "config"], outputs: ["actionTriggered"] }] });
14857
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: GenericChatComponent, isStandalone: true, selector: "core-generic-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, isTyping: { classPropertyName: "isTyping", publicName: "isTyping", isSignal: true, isRequired: false, transformFunction: null }, customTemplates: { classPropertyName: "customTemplates", publicName: "customTemplates", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { messagesSent: "messagesSent", imageClicked: "imageClicked", fileClicked: "fileClicked", fileSelected: "fileSelected", filesSelected: "filesSelected", typingStatusChanged: "typingStatusChanged", modalClosed: "modalClosed", attachmentAction: "attachmentAction", sendingStatusChanged: "sendingStatusChanged", loadMoreMessages: "loadMoreMessages" }, host: { listeners: { "document:keydown": "handleKeyboardEvent($event)" } }, viewQueries: [{ propertyName: "messagesContainer", first: true, predicate: ["messagesContainer"], descendants: true, isSignal: true }, { propertyName: "modalMessagesContainer", first: true, predicate: ["modalMessagesContainer"], descendants: true, isSignal: true }, { propertyName: "messageInput", first: true, predicate: ["messageInput"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "scrollSentinelElement", first: true, predicate: ["scrollSentinel"], descendants: true, read: ElementRef }], ngImport: i0, template: "@if (mergedConfig().modalMode && mergedConfig().isModalOpen) {\n<div class=\"c-services-chat c-modal is-visible\" [class.is-closing]=\"isClosing()\">\n <div class=\"c-modal__overlay\" (click)=\"closeModal()\"></div>\n <div class=\"c-modal__holder\">\n <div class=\"c-modal__header\">\n <p>\n @if (mergedConfig().modalTitle) {\n <span>{{ mergedConfig().modalTitle }}</span>\n }\n @if (mergedConfig().modalSubtitle) {\n <time>{{ mergedConfig().modalSubtitle }}</time>\n }\n </p>\n <core-generic-button [config]=\"closeButtonConfig()\" (buttonClick)=\"closeModal()\">\n </core-generic-button>\n </div>\n\n <div class=\"c-modal__body c-chat-body\" #modalMessagesContainer>\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n @for (message of chronologicalMessages(); track trackByChronologicalMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n\n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n <div class=\"c-chat-bubble__image\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n @if (getAttachmentPreviewUrl(file)) {\n <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\n } @else {\n <div class=\"c-chat-image-placeholder\" (click)=\"onImageClick(message)\">\n <div class=\"c-chat-image-placeholder__content\">\n <span class=\"c-chat-image-placeholder__icon\">\uD83D\uDCF7</span>\n <span class=\"c-chat-image-placeholder__text\">{{ file.filename || 'Imagen no disponible' }}</span>\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview\n [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-chat-bubble__image\">\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-chat-bubble__image\">\n <picture class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </picture>\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> .c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-modal__bottom\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n </div>\n</div>\n} @else {\n<div class=\"c-chat-layout c-services-chat\" [ngClass]=\"mergedConfig().customCssClasses?.container\">\n <div #messagesContainer class=\"c-chat-layout__body c-chat-body\"\n [ngClass]=\"mergedConfig().customCssClasses?.messageList\">\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n\n @for (message of messages(); track trackByMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n \n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <div class=\"c-chat-bubble__image\">\n <core-image-preview [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"></core-image-preview>\n </div>\n }\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-chat-layout__bottom\" [ngClass]=\"mergedConfig().customCssClasses?.inputContainer\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n</div>\n}\n\n<!-- @if (showImageModal()) {\n<div class=\"c-img-modal js-img-modal is-visible\">\n <span \n class=\"c-img-modal__close icon-cross-thin js-gallery-modal-close\"\n (click)=\"closeImageModal()\"\n ></span>\n <div \n class=\"c-img-modal__overlay js-img-modal-close\"\n (click)=\"closeImageModal()\"\n ></div>\n <div class=\"c-img-modal__holder\">\n <img \n class=\"js-img-modal-img\" \n [src]=\"selectedImage()\" \n alt=\"Imagen Ampliada\"\n />\n </div>\n</div>\n} -->", styles: [".c-img-preview{position:relative;width:100%;height:100%}.c-img-preview__placeholder{position:absolute;bottom:.5rem;left:.5rem;right:.5rem;background-color:#000000b3;color:#fff;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;text-align:center}.c-chat-image-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem;background-color:#f8fafc;border:2px dashed #e2e8f0;border-radius:.5rem;min-height:8rem}.c-chat-image-placeholder .icon-image{font-size:2rem;color:#9ca3af;margin-bottom:.5rem}.c-chat-image-placeholder p{margin:0;color:#6b7280;font-size:.875rem;text-align:center}.c-chat-files__preview{width:50px;height:50px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-bottom:.25rem}.c-chat-loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.c-chat-input__submit.is-sending{opacity:.7;cursor:not-allowed}.c-chat-sending-global-indicator{margin-top:.5rem}.c-chat-sending-global-indicator .c-chat-bubble{background-color:#f8f9fa;border:1px solid #e9ecef}.c-chat-sending-global-indicator .c-chat-bubble .c-chat-bubble__content{padding:.5rem .75rem}.c-chat-sending-message{display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:#6c757d}.c-chat-sending-spinner{display:inline-block;width:12px;height:12px;border:2px solid #f3f3f3;border-top:2px solid #007bff;border-radius:50%;animation:spin 1s linear infinite}.c-chat-sending-text{font-style:italic}.c-chat-sending-indicator{display:flex;align-items:center;gap:.5rem;margin-top:.25rem;font-size:.75rem;color:#6c757d}.c-chat-sending-indicator .c-chat-dots{display:inline-flex;gap:2px}.c-chat-sending-indicator .c-chat-dots span{display:inline-block;width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-sending-indicator .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-sending-indicator .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-sending-indicator .c-chat-dots span:nth-child(3){animation-delay:0s}.c-chat-error-indicator{display:flex;align-items:center;gap:.25rem;margin-top:.25rem;font-size:.75rem;color:#dc3545}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.c-chat-scroll-sentinel{width:100%;min-height:1px;position:relative}.c-chat-loading-more{display:flex;justify-content:center;align-items:center;padding:1rem}.c-chat-loading-more .c-chat-dots{display:inline-flex;gap:4px}.c-chat-loading-more .c-chat-dots span{display:inline-block;width:8px;height:8px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-loading-more .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-loading-more .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-loading-more .c-chat-dots span:nth-child(3){animation-delay:0s}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: GenericButtonComponent, selector: "core-generic-button", inputs: ["config", "data"], outputs: ["buttonClick"] }, { kind: "component", type: ImagePreviewComponent, selector: "core-image-preview", inputs: ["src", "alt", "title", "width", "height", "objectFit", "borderRadius", "cursor", "loading", "isRelative", "showSkeleton", "skeletonAnimation"] }, { kind: "component", type: FilePreviewComponent, selector: "core-file-preview", inputs: ["fileModels", "files", "config"], outputs: ["actionTriggered"] }] });
14858
14858
  }
14859
14859
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: GenericChatComponent, decorators: [{
14860
14860
  type: Component,
14861
- args: [{ selector: 'app-generic-chat', standalone: true, imports: [CommonModule, FormsModule, GenericButtonComponent, ImagePreviewComponent, FilePreviewComponent], template: "@if (mergedConfig().modalMode && mergedConfig().isModalOpen) {\n<div class=\"c-services-chat c-modal is-visible\" [class.is-closing]=\"isClosing()\">\n <div class=\"c-modal__overlay\" (click)=\"closeModal()\"></div>\n <div class=\"c-modal__holder\">\n <div class=\"c-modal__header\">\n <p>\n @if (mergedConfig().modalTitle) {\n <span>{{ mergedConfig().modalTitle }}</span>\n }\n @if (mergedConfig().modalSubtitle) {\n <time>{{ mergedConfig().modalSubtitle }}</time>\n }\n </p>\n <core-generic-button [config]=\"closeButtonConfig()\" (buttonClick)=\"closeModal()\">\n </core-generic-button>\n </div>\n\n <div class=\"c-modal__body c-chat-body\" #modalMessagesContainer>\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n @for (message of chronologicalMessages(); track trackByChronologicalMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n\n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n <div class=\"c-chat-bubble__image\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n @if (getAttachmentPreviewUrl(file)) {\n <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\n } @else {\n <div class=\"c-chat-image-placeholder\" (click)=\"onImageClick(message)\">\n <div class=\"c-chat-image-placeholder__content\">\n <span class=\"c-chat-image-placeholder__icon\">\uD83D\uDCF7</span>\n <span class=\"c-chat-image-placeholder__text\">{{ file.filename || 'Imagen no disponible' }}</span>\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview\n [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-chat-bubble__image\">\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-chat-bubble__image\">\n <picture class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </picture>\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> .c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-modal__bottom\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n </div>\n</div>\n} @else {\n<div class=\"c-chat-layout c-services-chat\" [ngClass]=\"mergedConfig().customCssClasses?.container\">\n <div #messagesContainer class=\"c-chat-layout__body c-chat-body\"\n [ngClass]=\"mergedConfig().customCssClasses?.messageList\">\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n\n @for (message of messages(); track trackByMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n \n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <div class=\"c-chat-bubble__image\">\n <core-image-preview [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"></core-image-preview>\n </div>\n }\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-chat-layout__bottom\" [ngClass]=\"mergedConfig().customCssClasses?.inputContainer\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n</div>\n}\n\n<!-- @if (showImageModal()) {\n<div class=\"c-img-modal js-img-modal is-visible\">\n <span \n class=\"c-img-modal__close icon-cross-thin js-gallery-modal-close\"\n (click)=\"closeImageModal()\"\n ></span>\n <div \n class=\"c-img-modal__overlay js-img-modal-close\"\n (click)=\"closeImageModal()\"\n ></div>\n <div class=\"c-img-modal__holder\">\n <img \n class=\"js-img-modal-img\" \n [src]=\"selectedImage()\" \n alt=\"Imagen Ampliada\"\n />\n </div>\n</div>\n} -->", styles: [".c-img-preview{position:relative;width:100%;height:100%}.c-img-preview__placeholder{position:absolute;bottom:.5rem;left:.5rem;right:.5rem;background-color:#000000b3;color:#fff;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;text-align:center}.c-chat-image-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem;background-color:#f8fafc;border:2px dashed #e2e8f0;border-radius:.5rem;min-height:8rem}.c-chat-image-placeholder .icon-image{font-size:2rem;color:#9ca3af;margin-bottom:.5rem}.c-chat-image-placeholder p{margin:0;color:#6b7280;font-size:.875rem;text-align:center}.c-chat-files__preview{width:50px;height:50px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-bottom:.25rem}.c-chat-loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.c-chat-input__submit.is-sending{opacity:.7;cursor:not-allowed}.c-chat-sending-global-indicator{margin-top:.5rem}.c-chat-sending-global-indicator .c-chat-bubble{background-color:#f8f9fa;border:1px solid #e9ecef}.c-chat-sending-global-indicator .c-chat-bubble .c-chat-bubble__content{padding:.5rem .75rem}.c-chat-sending-message{display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:#6c757d}.c-chat-sending-spinner{display:inline-block;width:12px;height:12px;border:2px solid #f3f3f3;border-top:2px solid #007bff;border-radius:50%;animation:spin 1s linear infinite}.c-chat-sending-text{font-style:italic}.c-chat-sending-indicator{display:flex;align-items:center;gap:.5rem;margin-top:.25rem;font-size:.75rem;color:#6c757d}.c-chat-sending-indicator .c-chat-dots{display:inline-flex;gap:2px}.c-chat-sending-indicator .c-chat-dots span{display:inline-block;width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-sending-indicator .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-sending-indicator .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-sending-indicator .c-chat-dots span:nth-child(3){animation-delay:0s}.c-chat-error-indicator{display:flex;align-items:center;gap:.25rem;margin-top:.25rem;font-size:.75rem;color:#dc3545}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.c-chat-scroll-sentinel{width:100%;min-height:1px;position:relative}.c-chat-loading-more{display:flex;justify-content:center;align-items:center;padding:1rem}.c-chat-loading-more .c-chat-dots{display:inline-flex;gap:4px}.c-chat-loading-more .c-chat-dots span{display:inline-block;width:8px;height:8px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-loading-more .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-loading-more .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-loading-more .c-chat-dots span:nth-child(3){animation-delay:0s}\n"] }]
14861
+ args: [{ selector: 'core-generic-chat', standalone: true, imports: [CommonModule, FormsModule, GenericButtonComponent, ImagePreviewComponent, FilePreviewComponent], template: "@if (mergedConfig().modalMode && mergedConfig().isModalOpen) {\n<div class=\"c-services-chat c-modal is-visible\" [class.is-closing]=\"isClosing()\">\n <div class=\"c-modal__overlay\" (click)=\"closeModal()\"></div>\n <div class=\"c-modal__holder\">\n <div class=\"c-modal__header\">\n <p>\n @if (mergedConfig().modalTitle) {\n <span>{{ mergedConfig().modalTitle }}</span>\n }\n @if (mergedConfig().modalSubtitle) {\n <time>{{ mergedConfig().modalSubtitle }}</time>\n }\n </p>\n <core-generic-button [config]=\"closeButtonConfig()\" (buttonClick)=\"closeModal()\">\n </core-generic-button>\n </div>\n\n <div class=\"c-modal__body c-chat-body\" #modalMessagesContainer>\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n @for (message of chronologicalMessages(); track trackByChronologicalMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n\n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n <div class=\"c-chat-bubble__image\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n @if (getAttachmentPreviewUrl(file)) {\n <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\n } @else {\n <div class=\"c-chat-image-placeholder\" (click)=\"onImageClick(message)\">\n <div class=\"c-chat-image-placeholder__content\">\n <span class=\"c-chat-image-placeholder__icon\">\uD83D\uDCF7</span>\n <span class=\"c-chat-image-placeholder__text\">{{ file.filename || 'Imagen no disponible' }}</span>\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview\n [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-chat-bubble__image\">\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-chat-bubble__image\">\n <picture class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </picture>\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> .c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-modal__bottom\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n </div>\n</div>\n} @else {\n<div class=\"c-chat-layout c-services-chat\" [ngClass]=\"mergedConfig().customCssClasses?.container\">\n <div #messagesContainer class=\"c-chat-layout__body c-chat-body\"\n [ngClass]=\"mergedConfig().customCssClasses?.messageList\">\n <!-- Sentinel para infinite scroll -->\n @if (mergedConfig().enableInfiniteScroll && hasMoreMessages()) {\n <div #scrollSentinel class=\"c-chat-scroll-sentinel\" style=\"height: 1px; margin-top: 10px;\">\n @if (isLoadingMore()) {\n <div class=\"c-chat-loading-more\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n </div>\n }\n \n <!-- <div class=\"c-chat-body__holder\"> -->\n\n @for (message of messages(); track trackByMessage($index, message)) {\n\n @if (shouldShowDateSeparator().get(message.id)) {\n <time class=\"c-chat-date\" [attr.datetime]=\"message.timestamp\">\n {{ getDateSeparatorText(message.timestamp) }}\n </time>\n }\n \n <div class=\"c-chat-bubble\" [ngClass]=\"message.position === 'right' ? 'c-chat-bubble--right' : ''\">\n <!-- @if (mergedConfig().showSenderNames && message.sender?.name) {\n <div class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</div>\n } -->\n\n <div class=\"c-chat-bubble__content\">\n @if (chatService.shouldUseCustomTemplate(message, mergedConfig())) {\n <ng-container *ngTemplateOutlet=\"\n getCustomTemplate(message);\n context: { $implicit: message }\n \">\n </ng-container>\n }\n @else if (message.type === messageTypes.TEXT) {\n @for (paragraph of message.content.split('\\n\\n'); track $index) {\n @if (paragraph.trim()) {\n <p [innerHTML]=\"paragraph.trim()\"></p>\n }\n }\n }\n @else if (message.type === messageTypes.SYSTEM || message.type === messageTypes.NOTIFICATION) {\n <p>{{ message.content }}</p>\n }\n\n @if (message.isSending) {\n <div class=\"c-chat-sending-indicator\">\n <span class=\"c-chat-dots\">\n <span></span> <span></span> <span></span>\n </span>\n </div>\n }\n @if (message.hasError) {\n <div class=\"c-chat-error-indicator\">\u274C Error al enviar</div>\n }\n </div>\n\n @if (message.attachments && message.attachments.length > 0) {\n @if (getImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__images\">\n @for (file of getImageAttachments(message.attachments); track $index) {\n <div class=\"c-chat-bubble__image\">\n <core-image-preview [src]=\"getAttachmentPreviewUrl(file)\"\n [alt]=\"file.filename || 'Imagen'\"></core-image-preview>\n </div>\n }\n </div>\n }\n\n @if (getNonImageAttachments(message.attachments).length > 0) {\n <div class=\"c-chat-bubble__files\">\n <core-file-preview [files]=\"convertAttachmentsToFilePreviewItems(getNonImageAttachments(message.attachments))\"\n [config]=\"getChatFilePreviewConfig()\" (actionTriggered)=\"onFilePreviewAction($event, message)\">\n </core-file-preview>\n </div>\n }\n }\n\n <!-- @if (message.type === messageTypes.IMAGE && (message.imageData || message.imagesData)) {\n <div class=\"c-chat-bubble__images\">\n @if (message.imageData) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"message.imageData.url\"\n [alt]=\"message.imageData.alt || 'Image'\"\n (click)=\"onImageClick(message)\"\n />\n </div>\n }\n @if (message.imagesData) {\n @for (image of message.imagesData; track image.url) {\n <div class=\"c-img-preview\">\n <img \n class=\"js-img-preview\" \n [src]=\"image.url\"\n [alt]=\"image.alt || 'Image'\"\n (click)=\"onMultipleImageClick(image, message)\"\n />\n </div>\n }\n }\n </div>\n } -->\n\n <!-- @if (message.type === messageTypes.FILE && message.fileData) {\n <div class=\"c-chat-file\" (click)=\"onFileClick(message)\">\n <div class=\"c-chat-file-icon\">\n @if (message.fileData.icon) {\n <img [src]=\"message.fileData.icon\" [alt]=\"message.fileData.type\" />\n } @else { <span>\uD83D\uDCC4</span> }\n </div>\n <div class=\"c-chat-file-info\">\n <div class=\"c-chat-file-name\">{{ message.fileData.name }}</div>\n <div class=\"c-chat-file-size\">{{ formatFileSize(message.fileData.size) }}</div>\n </div>\n </div>\n } -->\n\n <div class=\"c-chat-bubble__info\">\n @if (mergedConfig().showSenderNames && message.sender?.name) {\n <span class=\"c-chat-bubble__sender\">{{ message.sender?.name }}</span>\n }\n @if (mergedConfig().showTimestamps) {\n <time class=\"c-chat-bubble__date\" [attr.datetime]=\"message.timestamp\">\n {{ message.timestamp }}\n </time>\n }\n </div>\n\n </div>\n }\n\n @if (mergedConfig().showTypingIndicator && isTyping()) {\n <div class=\"c-chat-typing-indicator\">\n <div class=\"c-chat-bubble\">\n <div class=\"c-chat-bubble__content\">\n <div class=\"c-chat-typing-dots\">\n <span></span> <span></span> <span></span>\n </div>\n </div>\n </div>\n </div>\n }\n <!-- </div> c-chat-body__holder -->\n @if (selectedFiles().length > 0) {\n <div class=\"c-chat-files is-active\">\n <div class=\"c-chat-files__list\">\n @if (getSelectedImageFiles().length > 0) {\n @for (file of getSelectedImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <core-image-preview [src]=\"getImagePreviewUrl(file)\" [alt]=\"file.name\" (load)=\"onImageLoad(file.name)\"\n (error)=\"onImageError(file.name, $event)\" [width]=\"'50px'\" [height]=\"'50px'\"></core-image-preview>\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n @if (getSelectedNonImageFiles().length > 0) {\n @for (file of getSelectedNonImageFiles(); track trackByFile($index, file)) {\n <div class=\"c-chat-files__item\">\n <div class=\"c-chat-files__preview\">\n <img src=\"/images/static/file-placeholder.svg\" [alt]=\"file.name\" width=\"50px\" height=\"50px\">\n </div>\n <p class=\"c-chat-files__text\">{{ file.name }}</p>\n <button type=\"button\" class=\"c-chat-files__remove icon-cross-thin\"\n (click)=\"removeSelectedFile(getSelectedFileIndex(file))\" aria-label=\"Eliminar archivo\">\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (!mergedConfig().readOnly) {\n <div class=\"c-chat-layout__bottom\" [ngClass]=\"mergedConfig().customCssClasses?.inputContainer\">\n <div class=\"c-chat-input\">\n <div class=\"c-chat-input__textarea\">\n <label>\n <textarea #messageInput rows=\"1\" class=\"js-chat-textarea\" [value]=\"currentMessage()\"\n (input)=\"onTextareaInput($event)\" [placeholder]=\"mergedConfig().placeholder\"\n (keydown.enter)=\"sendMessage()\"></textarea>\n </label>\n <div class=\"c-chat-input__actions\">\n @if (mergedConfig().allowFileAttachments) {\n <button type=\"button\" class=\"icon-attach\" (click)=\"openFileSelector()\"></button>\n }\n </div>\n </div>\n <button type=\"button\" class=\"c-chat-input__submit\"\n [disabled]=\"!currentMessage().trim() && selectedFiles().length === 0\" (click)=\"sendMessage()\">\n <span class=\"icon-chat-send\"></span>\n </button>\n </div>\n @if (mergedConfig().allowFileAttachments) {\n <input #fileInput type=\"file\" multiple style=\"display: none\" (change)=\"onFileSelected($event)\" />\n }\n </div>\n }\n</div>\n}\n\n<!-- @if (showImageModal()) {\n<div class=\"c-img-modal js-img-modal is-visible\">\n <span \n class=\"c-img-modal__close icon-cross-thin js-gallery-modal-close\"\n (click)=\"closeImageModal()\"\n ></span>\n <div \n class=\"c-img-modal__overlay js-img-modal-close\"\n (click)=\"closeImageModal()\"\n ></div>\n <div class=\"c-img-modal__holder\">\n <img \n class=\"js-img-modal-img\" \n [src]=\"selectedImage()\" \n alt=\"Imagen Ampliada\"\n />\n </div>\n</div>\n} -->", styles: [".c-img-preview{position:relative;width:100%;height:100%}.c-img-preview__placeholder{position:absolute;bottom:.5rem;left:.5rem;right:.5rem;background-color:#000000b3;color:#fff;padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;text-align:center}.c-chat-image-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem;background-color:#f8fafc;border:2px dashed #e2e8f0;border-radius:.5rem;min-height:8rem}.c-chat-image-placeholder .icon-image{font-size:2rem;color:#9ca3af;margin-bottom:.5rem}.c-chat-image-placeholder p{margin:0;color:#6b7280;font-size:.875rem;text-align:center}.c-chat-files__preview{width:50px;height:50px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-bottom:.25rem}.c-chat-loading-spinner{display:inline-block;width:16px;height:16px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:spin 1s linear infinite}.c-chat-input__submit.is-sending{opacity:.7;cursor:not-allowed}.c-chat-sending-global-indicator{margin-top:.5rem}.c-chat-sending-global-indicator .c-chat-bubble{background-color:#f8f9fa;border:1px solid #e9ecef}.c-chat-sending-global-indicator .c-chat-bubble .c-chat-bubble__content{padding:.5rem .75rem}.c-chat-sending-message{display:flex;align-items:center;gap:.5rem;font-size:.875rem;color:#6c757d}.c-chat-sending-spinner{display:inline-block;width:12px;height:12px;border:2px solid #f3f3f3;border-top:2px solid #007bff;border-radius:50%;animation:spin 1s linear infinite}.c-chat-sending-text{font-style:italic}.c-chat-sending-indicator{display:flex;align-items:center;gap:.5rem;margin-top:.25rem;font-size:.75rem;color:#6c757d}.c-chat-sending-indicator .c-chat-dots{display:inline-flex;gap:2px}.c-chat-sending-indicator .c-chat-dots span{display:inline-block;width:4px;height:4px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-sending-indicator .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-sending-indicator .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-sending-indicator .c-chat-dots span:nth-child(3){animation-delay:0s}.c-chat-error-indicator{display:flex;align-items:center;gap:.25rem;margin-top:.25rem;font-size:.75rem;color:#dc3545}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes pulse{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.c-chat-scroll-sentinel{width:100%;min-height:1px;position:relative}.c-chat-loading-more{display:flex;justify-content:center;align-items:center;padding:1rem}.c-chat-loading-more .c-chat-dots{display:inline-flex;gap:4px}.c-chat-loading-more .c-chat-dots span{display:inline-block;width:8px;height:8px;background-color:#007bff;border-radius:50%;animation:pulse 1.4s ease-in-out infinite both}.c-chat-loading-more .c-chat-dots span:nth-child(1){animation-delay:-.32s}.c-chat-loading-more .c-chat-dots span:nth-child(2){animation-delay:-.16s}.c-chat-loading-more .c-chat-dots span:nth-child(3){animation-delay:0s}\n"] }]
14862
14862
  }], ctorParameters: () => [{ type: GenericChatService }], propDecorators: { scrollSentinelElement: [{
14863
14863
  type: ViewChild,
14864
14864
  args: ['scrollSentinel', { read: ElementRef }]