@solcre-org/core-ui 2.12.17 → 2.12.19

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.
@@ -1341,11 +1341,11 @@ class ImagePreviewComponent {
1341
1341
  }
1342
1342
  }
1343
1343
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ImagePreviewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1344
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ImagePreviewComponent, isStandalone: true, selector: "core-image-preview", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, objectFit: { classPropertyName: "objectFit", publicName: "objectFit", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, cursor: { classPropertyName: "cursor", publicName: "cursor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, isRelative: { classPropertyName: "isRelative", publicName: "isRelative", isSignal: true, isRequired: false, transformFunction: null }, showSkeleton: { classPropertyName: "showSkeleton", publicName: "showSkeleton", isSignal: true, isRequired: false, transformFunction: null }, skeletonAnimation: { classPropertyName: "skeletonAnimation", publicName: "skeletonAnimation", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: GenericSkeletonComponent, selector: "core-generic-skeleton", inputs: ["config", "items", "type", "size", "width", "height", "animated", "animation", "lines", "customClass", "ariaLabel"] }] });
1344
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ImagePreviewComponent, isStandalone: true, selector: "core-image-preview", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: true, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, objectFit: { classPropertyName: "objectFit", publicName: "objectFit", isSignal: true, isRequired: false, transformFunction: null }, borderRadius: { classPropertyName: "borderRadius", publicName: "borderRadius", isSignal: true, isRequired: false, transformFunction: null }, cursor: { classPropertyName: "cursor", publicName: "cursor", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, isRelative: { classPropertyName: "isRelative", publicName: "isRelative", isSignal: true, isRequired: false, transformFunction: null }, showSkeleton: { classPropertyName: "showSkeleton", publicName: "showSkeleton", isSignal: true, isRequired: false, transformFunction: null }, skeletonAnimation: { classPropertyName: "skeletonAnimation", publicName: "skeletonAnimation", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading() && src()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: GenericSkeletonComponent, selector: "core-generic-skeleton", inputs: ["config", "items", "type", "size", "width", "height", "animated", "animation", "lines", "customClass", "ariaLabel"] }] });
1345
1345
  }
1346
1346
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ImagePreviewComponent, decorators: [{
1347
1347
  type: Component,
1348
- args: [{ selector: 'core-image-preview', standalone: true, imports: [CommonModule, GenericSkeletonComponent], template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>" }]
1348
+ args: [{ selector: 'core-image-preview', standalone: true, imports: [CommonModule, GenericSkeletonComponent], template: "<div class=\"c-img-preview\" \n [class.c-img-preview--loading]=\"isImageLoading() && showSkeleton()\"\n [class.c-img-preview--error]=\"hasImageError()\"\n [style.width]=\"width()\"\n [style.height]=\"height()\"\n [style.border-radius]=\"borderRadius()\"\n [style.position]=\"isRelative() ? 'relative' : 'static'\">\n \n @if (isImageLoading() && showSkeleton()) {\n <div class=\"c-img-preview__skeleton\"\n (click)=\"onSkeletonClick()\">\n <core-generic-skeleton \n [type]=\"SkeletonType.IMAGE\"\n [width]=\"width() || '100%'\"\n [height]=\"height() || '200px'\"\n [animation]=\"skeletonAnimation()\"\n [ariaLabel]=\"'Loading image: ' + (alt() || 'Image')\">\n </core-generic-skeleton>\n </div>\n }\n \n @if (!isImageLoading() && !hasImageError()) {\n <img \n [src]=\"src()\" \n [alt]=\"alt()\"\n [title]=\"title()\"\n [loading]=\"loading()\"\n [style.object-fit]=\"objectFit()\"\n [style.border-radius]=\"borderRadius()\"\n [style.cursor]=\"cursor()\"\n (click)=\"onImageClick()\">\n }\n \n @if (isImageLoading() && src()) {\n <img \n [src]=\"src()\"\n [alt]=\"alt()\"\n style=\"display: none;\"\n (load)=\"onImageLoad()\"\n (error)=\"onImageError()\">\n }\n \n @if (hasImageError()) {\n <div class=\"c-img-preview__error\"\n [style.width]=\"width() || '100%'\"\n [style.height]=\"height() || '200px'\"\n [style.border-radius]=\"borderRadius()\">\n <div class=\"c-img-preview__error-content\">\n <span class=\"c-img-preview__error-icon\">\uD83D\uDCF7</span>\n <span class=\"c-img-preview__error-text\">Error al cargar imagen</span>\n </div>\n </div>\n }\n \n</div>" }]
1349
1349
  }] });
1350
1350
 
1351
1351
  class FileFieldComponent extends BaseFieldComponent {
@@ -2790,7 +2790,7 @@ class ServerSelectFieldComponent extends BaseFieldComponent {
2790
2790
  return currentValue === optionValue;
2791
2791
  }
2792
2792
  onBlurInput() {
2793
- if (!this.hasSearched()) {
2793
+ if (!this.hasSearched() && !this.hasValue()) {
2794
2794
  this.itemsSignal.set([]);
2795
2795
  }
2796
2796
  this.onBlur();
@@ -2866,15 +2866,15 @@ class ServerSelectFieldComponent extends BaseFieldComponent {
2866
2866
  return;
2867
2867
  }
2868
2868
  if (this.hasValue()) {
2869
- this.initializeWithOptions();
2870
- if (this.items().length === 0) {
2871
- this.initializeWithPreloadedOptions();
2872
- }
2869
+ this.initializeWithPreloadedOptions();
2873
2870
  if (this.items().length === 0) {
2874
2871
  this.initializeWithExistingValues();
2875
2872
  }
2873
+ if (this.items().length === 0 && this.field().dynamicOptions) {
2874
+ this.throttledEvaluateDynamicOptions();
2875
+ }
2876
2876
  }
2877
- if (this.items().length === 0 && !this.hasSearched()) {
2877
+ if (this.items().length === 0 && !this.hasSearched() && !this.hasValue()) {
2878
2878
  this.hasSearchedSignal.set(false);
2879
2879
  }
2880
2880
  }
@@ -2890,7 +2890,18 @@ class ServerSelectFieldComponent extends BaseFieldComponent {
2890
2890
  return;
2891
2891
  }
2892
2892
  if (this.items().length === 0 && this.hasValue()) {
2893
- this.initializeWithExistingValues();
2893
+ const options = this.field().options;
2894
+ if (options && options.length > 0) {
2895
+ this.initializeWithOptions();
2896
+ return;
2897
+ }
2898
+ this.initializeWithPreloadedOptions();
2899
+ if (this.items().length === 0) {
2900
+ this.initializeWithExistingValues();
2901
+ }
2902
+ if (this.items().length === 0 && this.field().dynamicOptions) {
2903
+ this.throttledEvaluateDynamicOptions();
2904
+ }
2894
2905
  return;
2895
2906
  }
2896
2907
  if (!hasSearchResults && !hasCurrentItems && !hasValue) {
@@ -11985,12 +11996,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImpor
11985
11996
  // Este archivo es generado automáticamente por scripts/update-version.js
11986
11997
  // No edites manualmente este archivo
11987
11998
  const VERSION = {
11988
- full: '2.12.17',
11999
+ full: '2.12.19',
11989
12000
  major: 2,
11990
12001
  minor: 12,
11991
- patch: 17,
11992
- timestamp: '2025-09-08T20:23:20.885Z',
11993
- buildDate: '8/9/2025'
12002
+ patch: 19,
12003
+ timestamp: '2025-09-09T12:48:52.457Z',
12004
+ buildDate: '9/9/2025'
11994
12005
  };
11995
12006
 
11996
12007
  class MainNavComponent {
@@ -14405,9 +14416,14 @@ class GenericChatComponent {
14405
14416
  if (!file) {
14406
14417
  return null;
14407
14418
  }
14408
- if (this.isImageFilePreview(file) && file.thumbnailUrl) {
14409
- return file.thumbnailUrl;
14419
+ // Para archivos de imagen, intentar obtener URL en este orden de prioridad:
14420
+ // 1. thumbnailUrl (URL optimizada para preview)
14421
+ // 2. url (URL principal del archivo)
14422
+ // 3. src (URL alternativa)
14423
+ if (this.isImageFilePreview(file)) {
14424
+ return file.thumbnailUrl || file.url || file.src || null;
14410
14425
  }
14426
+ // Para archivos no imagen, usar URL principal
14411
14427
  return file.url || file.src || null;
14412
14428
  }
14413
14429
  getAttachmentPlaceholderUrl(file) {
@@ -14854,11 +14870,11 @@ class GenericChatComponent {
14854
14870
  return this.isSending();
14855
14871
  }
14856
14872
  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: "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"] }] });
14873
+ 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 <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file) || ''\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\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
14874
  }
14859
14875
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: GenericChatComponent, decorators: [{
14860
14876
  type: Component,
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"] }]
14877
+ 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 <core-image-preview \n [src]=\"getAttachmentPreviewUrl(file) || ''\"\n [alt]=\"file.filename || 'Imagen'\"\n [showSkeleton]=\"true\"\n (click)=\"onImageClick(message)\">\n </core-image-preview>\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
14878
  }], ctorParameters: () => [{ type: GenericChatService }], propDecorators: { scrollSentinelElement: [{
14863
14879
  type: ViewChild,
14864
14880
  args: ['scrollSentinel', { read: ElementRef }]