@shival99/z-ui 2.2.0 → 2.2.2

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.
@@ -1,14 +1,32 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, model, output, signal, viewChild, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { input, model, output, signal, viewChild, viewChildren, afterRenderEffect, computed, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormsModule } from '@angular/forms';
5
5
  import { TranslatePipe } from '@ngx-translate/core';
6
6
  import { ZIconComponent } from '@shival99/z-ui/components/z-icon';
7
7
  import { ZModalComponent } from '@shival99/z-ui/components/z-modal';
8
8
  import { zTransform, zMergeClasses, zUuid } from '@shival99/z-ui/utils';
9
+ import { injectVirtualizer, elementScroll } from '@tanstack/angular-virtual';
9
10
  import { NgScrollbar } from 'ngx-scrollbar';
10
11
  import { cva } from 'class-variance-authority';
11
12
 
13
+ /** Renders a pin gets to land the list on the newest message before it gives up. */
14
+ const Z_CHAT_PIN_TO_NEWEST_ATTEMPTS = 12;
15
+ /** Gợi ý mặc định hiển thị khi khung chat chưa có tin nhắn nào. */
16
+ const Z_CHAT_DEFAULT_SUGGESTIONS = [
17
+ {
18
+ label: 'i18n_z_ui_chat_ai_suggestion_1',
19
+ prompt: 'Create release notes from recent commits',
20
+ icon: 'lucideSparkles',
21
+ },
22
+ {
23
+ label: 'i18n_z_ui_chat_ai_suggestion_2',
24
+ prompt: 'Suggest test cases for a new feature',
25
+ icon: 'lucideFlaskConical',
26
+ },
27
+ { label: 'i18n_z_ui_chat_ai_suggestion_3', prompt: 'Generate a concise PR summary', icon: 'lucideGitPullRequest' },
28
+ ];
29
+
12
30
  const zChatWrapperVariants = cva('z-chat fixed z-[95] pointer-events-none', {
13
31
  variants: {
14
32
  zPosition: {
@@ -54,19 +72,6 @@ const zChatFabVariants = cva('relative inline-flex size-14 items-center justify-
54
72
  },
55
73
  });
56
74
 
57
- const DEFAULT_SUGGESTIONS = [
58
- {
59
- label: 'i18n_z_ui_chat_ai_suggestion_1',
60
- prompt: 'Create release notes from recent commits',
61
- icon: 'lucideSparkles',
62
- },
63
- {
64
- label: 'i18n_z_ui_chat_ai_suggestion_2',
65
- prompt: 'Suggest test cases for a new feature',
66
- icon: 'lucideFlaskConical',
67
- },
68
- { label: 'i18n_z_ui_chat_ai_suggestion_3', prompt: 'Generate a concise PR summary', icon: 'lucideGitPullRequest' },
69
- ];
70
75
  class ZChatComponent {
71
76
  class = input('', /* @ts-ignore */
72
77
  ...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
@@ -86,7 +91,7 @@ class ZChatComponent {
86
91
  ...(ngDevMode ? [{ debugName: "zOffset" }] : /* istanbul ignore next */ []));
87
92
  zSize = input('default', /* @ts-ignore */
88
93
  ...(ngDevMode ? [{ debugName: "zSize" }] : /* istanbul ignore next */ []));
89
- zSuggestions = input(DEFAULT_SUGGESTIONS, /* @ts-ignore */
94
+ zSuggestions = input(Z_CHAT_DEFAULT_SUGGESTIONS, /* @ts-ignore */
90
95
  ...(ngDevMode ? [{ debugName: "zSuggestions" }] : /* istanbul ignore next */ []));
91
96
  zShowSuggestions = input(true, { ...(ngDevMode ? { debugName: "zShowSuggestions" } : /* istanbul ignore next */ {}), transform: zTransform });
92
97
  zShowPulse = input(true, { ...(ngDevMode ? { debugName: "zShowPulse" } : /* istanbul ignore next */ {}), transform: zTransform });
@@ -130,9 +135,69 @@ class ZChatComponent {
130
135
  ...(ngDevMode ? [{ debugName: "_chatInputRef" }] : /* istanbul ignore next */ []));
131
136
  _fileInputRef = viewChild('fileInput', /* @ts-ignore */
132
137
  ...(ngDevMode ? [{ debugName: "_fileInputRef" }] : /* istanbul ignore next */ []));
138
+ _virtualMessageRefs = viewChildren('virtualMessage', /* @ts-ignore */
139
+ ...(ngDevMode ? [{ debugName: "_virtualMessageRefs" }] : /* istanbul ignore next */ []));
140
+ /** Signal, not a field: requesting a pin must itself wake `_pinToNewest`. */
141
+ _pinAttemptsLeft = signal(0, /* @ts-ignore */
142
+ ...(ngDevMode ? [{ debugName: "_pinAttemptsLeft" }] : /* istanbul ignore next */ []));
133
143
  _mockReplyTimer = null;
134
144
  _rippleTimer = null;
135
- _scrollTimer = null;
145
+ // Declared before the virtualizer so measurement runs before its post-render scroll
146
+ // reconciliation — otherwise anchoring settles against stale sizes.
147
+ _measureMessages = afterRenderEffect({
148
+ mixedReadWrite: () => {
149
+ for (const ref of this._virtualMessageRefs()) {
150
+ this.messageVirtualizer.measureElement(ref.nativeElement);
151
+ }
152
+ },
153
+ });
154
+ messageVirtualizer = injectVirtualizer(() => ({
155
+ scrollElement: this._getMessagesViewportElement(),
156
+ count: this.zMessages().length,
157
+ // Seed only: every row is measured, so this just sizes rows not yet rendered.
158
+ estimateSize: () => 74,
159
+ getItemKey: (index) => this.zMessages()[index]?.id ?? index,
160
+ // Chat semantics: pin to the bottom, and follow new messages only when already pinned
161
+ // there, so a reader scrolled up into history is never yanked down by an incoming reply.
162
+ anchorTo: 'end',
163
+ followOnAppend: true,
164
+ scrollEndThreshold: 80,
165
+ overscan: 10,
166
+ useApplicationRefTick: false,
167
+ // ng-scrollbar does not emit a scroll event for a programmatic scrollTo, so the virtualizer
168
+ // would keep rendering the old offset while the box sits somewhere else — a viewport stuck on
169
+ // stale rows. Push the offset in by hand, exactly as z-table does over the same scroller.
170
+ scrollToFn: (offset, options, instance) => {
171
+ instance.scrollOffset = offset;
172
+ elementScroll(offset, options, instance);
173
+ },
174
+ }));
175
+ /**
176
+ * Drives a requested pin to completion across renders.
177
+ *
178
+ * One `scrollToEnd()` is not enough on a cold list: the end is computed from estimates, and it
179
+ * moves as rows are measured. The scroll box also arrives late — ng-scrollbar reports its
180
+ * viewport only after init, and a scroll issued before then leaves the virtualizer at offset 0
181
+ * with no event to resync it. Re-asserting each render until `isAtEnd` holds covers both.
182
+ */
183
+ _pinToNewest = afterRenderEffect({
184
+ mixedReadWrite: () => {
185
+ // Tracked so this re-runs as rows mount and measurements land.
186
+ this.messageVirtualizer.getVirtualItems();
187
+ const attemptsLeft = this._pinAttemptsLeft();
188
+ const hasRows = this.zMessages().length > 0;
189
+ if (attemptsLeft <= 0 || !this.zOpen() || !hasRows || !this._getMessagesViewportElement()) {
190
+ return;
191
+ }
192
+ if (this.messageVirtualizer.isAtEnd(1)) {
193
+ this._pinAttemptsLeft.set(0);
194
+ return;
195
+ }
196
+ // Decrementing re-triggers this effect, so the pin keeps re-asserting until it lands.
197
+ this._pinAttemptsLeft.set(attemptsLeft - 1);
198
+ this.messageVirtualizer.scrollToEnd();
199
+ },
200
+ });
136
201
  wrapperClasses = computed(() => zMergeClasses(zChatWrapperVariants({ zPosition: this.zPosition() }), this.class()), /* @ts-ignore */
137
202
  ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []));
138
203
  wrapperTop = computed(() => this._resolveOffsetValue('top'), /* @ts-ignore */
@@ -159,6 +224,12 @@ class ZChatComponent {
159
224
  ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
160
225
  hasMessages = computed(() => this.zMessages().length > 0, /* @ts-ignore */
161
226
  ...(ngDevMode ? [{ debugName: "hasMessages" }] : /* istanbul ignore next */ []));
227
+ /** Only the newest bubble plays the push-in animation; recycled rows must not replay it. */
228
+ newestMessageId = computed(() => {
229
+ const messages = this.zMessages();
230
+ return messages.length > 0 ? messages[messages.length - 1].id : null;
231
+ }, /* @ts-ignore */
232
+ ...(ngDevMode ? [{ debugName: "newestMessageId" }] : /* istanbul ignore next */ []));
162
233
  showEmptyState = computed(() => !this.hasMessages() && this.zShowSuggestions(), /* @ts-ignore */
163
234
  ...(ngDevMode ? [{ debugName: "showEmptyState" }] : /* istanbul ignore next */ []));
164
235
  previewImageSizeLabel = computed(() => {
@@ -173,9 +244,6 @@ class ZChatComponent {
173
244
  if (this._rippleTimer) {
174
245
  clearTimeout(this._rippleTimer);
175
246
  }
176
- if (this._scrollTimer) {
177
- clearTimeout(this._scrollTimer);
178
- }
179
247
  const filesInMessages = this.zMessages().flatMap(item => item.files ?? []);
180
248
  this._releaseAttachmentUrls(filesInMessages);
181
249
  this._releaseAttachmentUrls(this.attachments());
@@ -334,9 +402,7 @@ class ZChatComponent {
334
402
  this._triggerRipple();
335
403
  if (isOpen) {
336
404
  this._focusInputSoon();
337
- requestAnimationFrame(() => {
338
- this._scheduleScrollToBottom(250);
339
- });
405
+ this._ensureBottomVisible();
340
406
  }
341
407
  }
342
408
  _createMessage(role, content, files) {
@@ -393,31 +459,20 @@ class ZChatComponent {
393
459
  this.closeImagePreview();
394
460
  }
395
461
  }
396
- _scheduleScrollToBottom(delay = 0, duration = 300) {
397
- if (this._scrollTimer) {
398
- clearTimeout(this._scrollTimer);
399
- }
400
- this._scrollTimer = setTimeout(() => {
401
- this._scrollToBottom(duration);
402
- }, delay);
403
- }
462
+ /**
463
+ * Requests a pin to the newest message; `_pinToNewest` performs it once rendering can honour it.
464
+ *
465
+ * Never scroll synchronously from here. Callers fire right after mutating `zMessages` or
466
+ * `zOpen`, before change detection has fed the new length into the virtualizer, so the scroll
467
+ * would target the previous last index and strand the list mid-history.
468
+ */
404
469
  _ensureBottomVisible() {
405
- this._scrollToBottom(0);
406
- requestAnimationFrame(() => {
407
- this._scrollToBottom(0);
408
- this._scheduleScrollToBottom(90, 0);
409
- });
470
+ this._pinAttemptsLeft.set(Z_CHAT_PIN_TO_NEWEST_ATTEMPTS);
410
471
  }
411
- _scrollToBottom(duration = 300) {
472
+ /** ng-scrollbar owns the real scrolling box; the virtualizer must observe that, not the host. */
473
+ _getMessagesViewportElement() {
412
474
  const scrollbar = this._messagesScrollbarRef();
413
- if (!scrollbar) {
414
- return;
415
- }
416
- void scrollbar.scrollTo({
417
- bottom: 0,
418
- duration,
419
- easing: { x1: 0.25, y1: 0.1, x2: 0.25, y2: 1 },
420
- });
475
+ return scrollbar?.adapter.initialized() ? scrollbar.adapter.viewportElement : undefined;
421
476
  }
422
477
  _focusInputSoon() {
423
478
  setTimeout(() => this._chatInputRef()?.nativeElement.focus(), 50);
@@ -460,15 +515,15 @@ class ZChatComponent {
460
515
  return defaults[side];
461
516
  }
462
517
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ZChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
463
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: ZChatComponent, isStandalone: true, selector: "z-chat", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, zOpen: { classPropertyName: "zOpen", publicName: "zOpen", isSignal: true, isRequired: false, transformFunction: null }, zMessages: { classPropertyName: "zMessages", publicName: "zMessages", isSignal: true, isRequired: false, transformFunction: null }, zTitle: { classPropertyName: "zTitle", publicName: "zTitle", isSignal: true, isRequired: false, transformFunction: null }, zSubtitle: { classPropertyName: "zSubtitle", publicName: "zSubtitle", isSignal: true, isRequired: false, transformFunction: null }, zPlaceholder: { classPropertyName: "zPlaceholder", publicName: "zPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, zPosition: { classPropertyName: "zPosition", publicName: "zPosition", isSignal: true, isRequired: false, transformFunction: null }, zOffset: { classPropertyName: "zOffset", publicName: "zOffset", isSignal: true, isRequired: false, transformFunction: null }, zSize: { classPropertyName: "zSize", publicName: "zSize", isSignal: true, isRequired: false, transformFunction: null }, zSuggestions: { classPropertyName: "zSuggestions", publicName: "zSuggestions", isSignal: true, isRequired: false, transformFunction: null }, zShowSuggestions: { classPropertyName: "zShowSuggestions", publicName: "zShowSuggestions", isSignal: true, isRequired: false, transformFunction: null }, zShowPulse: { classPropertyName: "zShowPulse", publicName: "zShowPulse", isSignal: true, isRequired: false, transformFunction: null }, zDisabled: { classPropertyName: "zDisabled", publicName: "zDisabled", isSignal: true, isRequired: false, transformFunction: null }, zLoading: { classPropertyName: "zLoading", publicName: "zLoading", isSignal: true, isRequired: false, transformFunction: null }, zAllowAttachments: { classPropertyName: "zAllowAttachments", publicName: "zAllowAttachments", isSignal: true, isRequired: false, transformFunction: null }, zAcceptedFiles: { classPropertyName: "zAcceptedFiles", publicName: "zAcceptedFiles", isSignal: true, isRequired: false, transformFunction: null }, zAutoReply: { classPropertyName: "zAutoReply", publicName: "zAutoReply", isSignal: true, isRequired: false, transformFunction: null }, zAutoReplyDelay: { classPropertyName: "zAutoReplyDelay", publicName: "zAutoReplyDelay", isSignal: true, isRequired: false, transformFunction: null }, zFabIcon: { classPropertyName: "zFabIcon", publicName: "zFabIcon", isSignal: true, isRequired: false, transformFunction: null }, zBotIcon: { classPropertyName: "zBotIcon", publicName: "zBotIcon", isSignal: true, isRequired: false, transformFunction: null }, zSendIcon: { classPropertyName: "zSendIcon", publicName: "zSendIcon", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { zOpen: "zOpenChange", zMessages: "zMessagesChange", zSend: "zSend", zResponse: "zResponse", zToggle: "zToggle", zClear: "zClear" }, host: { listeners: { "document:keydown.escape": "onEscapeKey()" }, classAttribute: "z-chat block" }, viewQueries: [{ propertyName: "_messagesScrollbarRef", first: true, predicate: ["messagesScrollbar"], descendants: true, isSignal: true }, { propertyName: "_chatInputRef", first: true, predicate: ["chatInput"], descendants: true, isSignal: true }, { propertyName: "_fileInputRef", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], exportAs: ["zChat"], ngImport: i0, template: "<div\n [class]=\"wrapperClasses()\"\n [style.top]=\"wrapperTop()\"\n [style.right]=\"wrapperRight()\"\n [style.bottom]=\"wrapperBottom()\"\n [style.left]=\"wrapperLeft()\">\n <div class=\"relative\">\n <section class=\"z-chat-panel-shell\" [class]=\"panelClasses()\">\n <header class=\"from-primary/95 to-primary/75 text-primary-foreground bg-gradient-to-r px-4 py-3.5\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"bg-primary-foreground/15 relative inline-flex size-8 shrink-0 items-center justify-center rounded-full\">\n <z-icon [zType]=\"zBotIcon()\" zSize=\"16\" class=\"text-primary-foreground\" />\n <span\n class=\"ring-primary inline-block size-2.5 rounded-full bg-emerald-300 ring-2\"\n style=\"position: absolute; right: -0.0625rem; bottom: -0.0625rem\"></span>\n </span>\n <h3 class=\"truncate text-sm font-semibold\">{{ zTitle() | translate }}</h3>\n </div>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"clearMessages()\"\n title=\"{{ 'i18n_z_ui_chat_ai_new_chat' | translate }}\">\n <z-icon zType=\"lucideRefreshCcw\" zSize=\"17\" />\n </button>\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"close()\"\n title=\"{{ 'i18n_z_ui_chat_ai_close' | translate }}\">\n <z-icon zType=\"lucideChevronDown\" zSize=\"18\" />\n </button>\n </div>\n </div>\n </header>\n\n <div class=\"z-chat-lower-shell\">\n <ng-scrollbar\n #messagesScrollbar\n (afterInit)=\"onMessagesScrollbarInit()\"\n class=\"z-chat-scrollbar z-chat-content-surface flex-1 overflow-x-hidden\"\n track=\"vertical\"\n orientation=\"vertical\"\n appearance=\"compact\"\n visibility=\"hover\">\n <div class=\"z-chat-messages\">\n @if (hasMessages()) {\n @for (item of zMessages(); track item.id) {\n <div\n class=\"z-chat-message z-chat-message--enter\"\n [class.justify-end]=\"item.role === 'user'\"\n [class.justify-start]=\"item.role === 'assistant'\">\n <div\n class=\"z-chat-message-bubble max-w-[88%] min-w-0 rounded-2xl px-3 py-2.5 text-sm leading-5 [overflow-wrap:anywhere] break-all shadow-xs\"\n [class.bg-primary]=\"item.role === 'user'\"\n [class.text-primary-foreground]=\"item.role === 'user'\"\n [class.rounded-br-md]=\"item.role === 'user'\"\n [class.bg-background]=\"item.role === 'assistant'\"\n [class.border-border/60]=\"item.role === 'assistant'\"\n [class.text-foreground]=\"item.role === 'assistant'\"\n [class.rounded-bl-md]=\"item.role === 'assistant'\"\n [class.border]=\"item.role === 'assistant'\">\n @if (item.files && item.files.length > 0) {\n <div class=\"mb-2 flex flex-col gap-1.5\">\n @for (file of item.files; track file.id) {\n <div\n class=\"flex items-center gap-2 rounded-[0.5rem] px-2 py-1.5 text-xs\"\n [class.bg-white/15]=\"item.role === 'user'\"\n [class.bg-muted/40]=\"item.role === 'assistant'\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background/80 text-muted-foreground inline-flex size-8 items-center justify-center rounded-[0.5rem]\"\n [class.bg-white/20]=\"item.role === 'user'\"\n [class.text-white]=\"item.role === 'user'\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"truncate text-[0.625rem] opacity-80\">{{ formatFileSize(file.size) }}</p>\n </div>\n </div>\n }\n </div>\n }\n {{ item.content }}\n </div>\n </div>\n }\n } @else if (showEmptyState()) {\n <div class=\"flex h-full min-h-60 flex-col items-center justify-center px-2 text-center\">\n <div\n class=\"from-primary/20 to-primary/5 mb-3 inline-flex size-14 items-center justify-center rounded-full bg-gradient-to-br\">\n <z-icon zType=\"lucideBot\" zSize=\"28\" class=\"text-primary\" />\n </div>\n <h4 class=\"text-foreground text-sm font-semibold\">{{ 'i18n_z_ui_chat_ai_empty_title' | translate }}</h4>\n <p class=\"text-muted-foreground mt-1 text-xs\">{{ 'i18n_z_ui_chat_ai_empty_desc' | translate }}</p>\n\n @if (zSuggestions().length > 0) {\n <div class=\"mt-4 flex w-full flex-col gap-2\">\n @for (item of zSuggestions(); track item.label) {\n <button\n type=\"button\"\n class=\"z-chat-suggestion-item bg-background/95 hover:bg-background border-border/60 text-foreground flex w-full cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-left text-xs transition hover:shadow-sm\"\n [style.animation-delay.ms]=\"$index * 70\"\n (click)=\"onSuggestionSelect(item)\">\n @if (item.icon) {\n <z-icon [zType]=\"item.icon!\" zSize=\"14\" class=\"text-primary shrink-0\" />\n }\n <span class=\"line-clamp-2\">{{ item.label | translate }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (isTyping()) {\n <div class=\"z-chat-message z-chat-message--enter justify-start\">\n <div class=\"bg-background border-border/60 rounded-2xl rounded-bl-md border px-3 py-2\">\n <div class=\"flex items-center gap-1\">\n <span class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.15s\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.3s\"></span>\n </div>\n </div>\n </div>\n }\n </div>\n </ng-scrollbar>\n\n <footer class=\"z-chat-footer-surface p-3\">\n @if (attachments().length > 0) {\n <div class=\"mb-2 flex max-h-32 flex-col gap-2 overflow-y-auto pr-1\">\n @for (file of attachments(); track file.id) {\n <div\n class=\"bg-muted/45 border-border/55 relative flex w-full items-center gap-2 rounded-[0.5rem] border px-2 py-1.5\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 shrink-0 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background text-muted-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-[0.5rem]\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"text-muted-foreground truncate text-[0.625rem]\">{{ formatFileSize(file.size) }}</p>\n </div>\n <button\n type=\"button\"\n class=\"bg-background/80 hover:bg-background text-muted-foreground inline-flex size-5 cursor-pointer items-center justify-center rounded-full transition\"\n (click)=\"removeAttachment(file.id)\">\n <z-icon zType=\"lucideX\" zSize=\"12\" />\n </button>\n </div>\n }\n </div>\n }\n\n <div class=\"z-chat-composer-shell\">\n <div class=\"z-chat-composer-main\" [class.z-chat-composer__field--overflow]=\"isComposerOverflowing()\">\n <textarea\n #chatInput\n class=\"z-chat-composer-textarea\"\n [placeholder]=\"zPlaceholder() | translate\"\n [disabled]=\"zDisabled()\"\n [ngModel]=\"draftMessage()\"\n (ngModelChange)=\"draftMessage.set($event)\"\n (input)=\"onTextareaInput($event)\"\n (paste)=\"onTextareaPaste($event)\"\n (keydown)=\"onInputKeydown($event)\"\n rows=\"3\"></textarea>\n </div>\n\n <div class=\"z-chat-composer-toolbar\">\n <div class=\"z-chat-composer-toolbar-left\">\n @if (zAllowAttachments()) {\n <input\n #fileInput\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"zAcceptedFiles()\"\n multiple\n (change)=\"onFileChange($event)\" />\n <button\n type=\"button\"\n class=\"z-chat-composer-tool-btn\"\n [disabled]=\"zDisabled()\"\n (click)=\"onClickAttach()\"\n title=\"{{ 'i18n_z_ui_chat_ai_attach' | translate }}\">\n <z-icon zType=\"lucidePlus\" zSize=\"18\" />\n </button>\n }\n </div>\n\n <div class=\"z-chat-composer-toolbar-right\">\n <button\n type=\"button\"\n class=\"z-chat-composer-send-btn\"\n [disabled]=\"!canSend()\"\n (click)=\"onSendMessage()\"\n title=\"{{ 'i18n_z_ui_chat_ai_send' | translate }}\">\n <z-icon\n [zType]=\"isTyping() ? 'lucideLoaderCircle' : zSendIcon()\"\n zSize=\"16\"\n [class]=\"isTyping() ? 'animate-spin' : ''\" />\n </button>\n </div>\n </div>\n </div>\n </footer>\n </div>\n\n <z-modal\n [zVisible]=\"isPreviewVisible()\"\n [zTitle]=\"previewImage()?.name\"\n [zDescription]=\"previewImageSizeLabel()\"\n zWidth=\"min(92vw, 720px)\"\n [zHideFooter]=\"true\"\n [zMaskClosable]=\"true\"\n zOverlay=\"blur\"\n (zCancel)=\"closeImagePreview()\">\n <div class=\"z-chat-preview-modal-body\">\n <img [src]=\"previewImage()?.url\" [alt]=\"previewImage()?.name\" class=\"z-chat-preview-image\" />\n </div>\n </z-modal>\n </section>\n\n <button\n type=\"button\"\n class=\"pointer-events-auto cursor-pointer\"\n [class]=\"fabClasses()\"\n (click)=\"onToggle()\"\n [attr.aria-expanded]=\"zOpen()\"\n [attr.aria-label]=\"'i18n_z_ui_chat_ai_toggle' | translate\"\n title=\"{{ 'i18n_z_ui_chat_ai_toggle' | translate }}\">\n @if (zShowPulse() && !zOpen()) {\n <span class=\"bg-primary/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n @if (showRipple()) {\n <span class=\"bg-primary-foreground/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n <z-icon\n [zType]=\"zOpen() ? 'lucideX' : zFabIcon()\"\n zSize=\"24\"\n class=\"relative z-1 transition-transform duration-200\"\n [class.rotate-180]=\"zOpen()\" />\n </button>\n </div>\n</div>\n", styles: [".z-chat textarea{scrollbar-width:thin}.z-chat-panel-shell{box-shadow:0 0 28px #00000024!important}:is(.dark,[data-theme=dark]) .z-chat-panel-shell{box-shadow:0 0 28px #00000057!important}.z-chat-composer__field--overflow:after{position:absolute;right:0;bottom:0;left:0;height:1rem;pointer-events:none;content:\"\";background:linear-gradient(to top,var(--background) 22%,transparent)}.z-chat-composer-shell{position:relative;display:flex;width:100%;flex-direction:column;border:1px solid color-mix(in oklab,var(--border) 82%,transparent);border-radius:1rem;background:linear-gradient(180deg,color-mix(in oklab,var(--card) 98%,var(--background) 2%),color-mix(in oklab,var(--card) 92%,var(--background) 8%));box-shadow:0 6px 18px #00000014;transition:box-shadow .2s ease}.z-chat-composer-shell:focus-within{box-shadow:0 6px 18px #00000014}:is(.dark,[data-theme=dark]) .z-chat-composer-shell{border-color:var(--border);background:linear-gradient(180deg,color-mix(in oklab,var(--card) 96%,var(--background) 4%),color-mix(in oklab,var(--card) 88%,var(--background) 12%));box-shadow:0 10px 24px #00000057}.z-chat-composer-main{position:relative;overflow:hidden;padding:.7rem .35rem .45rem .85rem}.z-chat-composer-textarea{width:100%;height:4.125rem;max-height:4.125rem;border:none;background:transparent;resize:none;overflow-x:hidden;font-size:.875rem;line-height:1.4rem;color:var(--foreground);outline:none;padding-right:.2rem;scrollbar-width:thin;overflow-wrap:anywhere;word-break:break-word}.z-chat-composer-textarea::placeholder{color:var(--muted-foreground)}.z-chat-composer-textarea::-webkit-scrollbar{width:.375rem}.z-chat-composer-textarea::-webkit-scrollbar-track{background:transparent}.z-chat-composer-textarea::-webkit-scrollbar-thumb{border-radius:62.4375rem;background:#64748b73}:is(.dark,[data-theme=dark]) .z-chat-composer-textarea::-webkit-scrollbar-thumb{background:#94a3b88c}.z-chat-composer-toolbar{display:flex;align-items:center;justify-content:space-between;border-top:.0625rem solid rgba(219,219,219,.6);padding:.55rem .7rem .65rem}:is(.dark,[data-theme=dark]) .z-chat-composer-toolbar{border-top-color:#00000038}.z-chat-composer-toolbar-left,.z-chat-composer-toolbar-right{display:flex;align-items:center}.z-chat-composer-tool-btn{display:inline-flex;height:1.75rem;width:1.75rem;cursor:pointer;align-items:center;justify-content:center;border-radius:.6rem;border:none;background:transparent;color:#374151;transition:background-color .2s ease}.z-chat-composer-tool-btn:hover{background:#0000000f}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn{color:#e5e7ebe6}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn:hover{background:#ffffff1a}.z-chat-composer-tool-btn:disabled{cursor:not-allowed;opacity:.5}.z-chat-composer-send-btn{display:inline-flex;height:2rem;width:2rem;cursor:pointer;align-items:center;justify-content:center;border-radius:62.4375rem;border:1px solid rgba(0,0,0,.1);background:var(--primary);color:var(--primary-foreground);box-shadow:0 2px 8px #0000002e,inset 0 1px #ffffff2e;transition:transform .15s ease,opacity .2s ease,box-shadow .2s ease}.z-chat-composer-send-btn:hover{transform:translateY(-.0625rem);box-shadow:0 4px 12px #00000038,inset 0 1px #ffffff38}.z-chat-composer-send-btn:disabled{cursor:not-allowed;opacity:.45;box-shadow:none}.z-chat-scrollbar{contain:layout}.z-chat-content-surface{background:var(--card)}.z-chat-lower-shell{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;background:var(--card);color:var(--card-foreground);border:1px solid transparent;border-top:0;border-bottom-right-radius:1rem;border-bottom-left-radius:1rem}:is(.dark,[data-theme=dark]) .z-chat-lower-shell{border-color:var(--border);border-top:0}.z-chat-footer-surface{background:transparent;border-top:.0625rem solid color-mix(in oklab,var(--border) 70%,transparent)}:is(.dark,[data-theme=dark]) .z-chat-footer-surface{background:transparent;border-top-color:var(--border)}.z-chat-messages{min-height:100%;overflow-x:hidden;padding:.875rem .75rem;contain:content}.z-chat-message{display:flex;margin:.5rem 0;padding:0 2px;transform-origin:bottom center;contain:layout style}.z-chat-message-bubble{transform:translateZ(0);backface-visibility:hidden}.z-chat-message--enter{animation:z-chat-message-push-in .3s ease-out;will-change:transform,opacity}.z-chat-suggestion-item{animation:z-chat-suggestion-reveal .28s ease-out both;will-change:transform,opacity}.z-chat-preview-modal-body{display:flex;align-items:center;justify-content:center;max-height:min(70vh,40rem);padding:.25rem}.z-chat-preview-image{max-width:100%;max-height:min(66vh,37.5rem);border-radius:.5rem;object-fit:contain}@keyframes z-chat-message-push-in{0%{opacity:0;transform:translateY(.9375rem) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes z-chat-suggestion-reveal{0%{opacity:0;transform:translateY(.5rem) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ZIconComponent, selector: "z-icon, [z-icon]", inputs: ["class", "zType", "zAnimatedType", "zAnimate", "zAnimationTrigger", "zSize", "zStrokeWidth", "zSvg"] }, { kind: "component", type: ZModalComponent, selector: "z-modal", inputs: ["class", "zBodyClass", "zVisible", "zTitle", "zDescription", "zWidth", "zClosable", "zMaskClosable", "zDisableShadow", "zHeaderBorder", "zFooterBorder", "zHideHeader", "zHideFooter", "zOkText", "zCancelText", "zOkDestructive", "zOkDisabled", "zLoading", "zContentLoading", "zSkeletonRows", "zOverlay"], outputs: ["zOk", "zCancel", "zAfterClose", "zScrollbar", "zVisibleChange"], exportAs: ["zModal"] }, { kind: "component", type: NgScrollbar, selector: "ng-scrollbar:not([externalViewport]), [ngScrollbar]", exportAs: ["ngScrollbar"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
518
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: ZChatComponent, isStandalone: true, selector: "z-chat", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, zOpen: { classPropertyName: "zOpen", publicName: "zOpen", isSignal: true, isRequired: false, transformFunction: null }, zMessages: { classPropertyName: "zMessages", publicName: "zMessages", isSignal: true, isRequired: false, transformFunction: null }, zTitle: { classPropertyName: "zTitle", publicName: "zTitle", isSignal: true, isRequired: false, transformFunction: null }, zSubtitle: { classPropertyName: "zSubtitle", publicName: "zSubtitle", isSignal: true, isRequired: false, transformFunction: null }, zPlaceholder: { classPropertyName: "zPlaceholder", publicName: "zPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, zPosition: { classPropertyName: "zPosition", publicName: "zPosition", isSignal: true, isRequired: false, transformFunction: null }, zOffset: { classPropertyName: "zOffset", publicName: "zOffset", isSignal: true, isRequired: false, transformFunction: null }, zSize: { classPropertyName: "zSize", publicName: "zSize", isSignal: true, isRequired: false, transformFunction: null }, zSuggestions: { classPropertyName: "zSuggestions", publicName: "zSuggestions", isSignal: true, isRequired: false, transformFunction: null }, zShowSuggestions: { classPropertyName: "zShowSuggestions", publicName: "zShowSuggestions", isSignal: true, isRequired: false, transformFunction: null }, zShowPulse: { classPropertyName: "zShowPulse", publicName: "zShowPulse", isSignal: true, isRequired: false, transformFunction: null }, zDisabled: { classPropertyName: "zDisabled", publicName: "zDisabled", isSignal: true, isRequired: false, transformFunction: null }, zLoading: { classPropertyName: "zLoading", publicName: "zLoading", isSignal: true, isRequired: false, transformFunction: null }, zAllowAttachments: { classPropertyName: "zAllowAttachments", publicName: "zAllowAttachments", isSignal: true, isRequired: false, transformFunction: null }, zAcceptedFiles: { classPropertyName: "zAcceptedFiles", publicName: "zAcceptedFiles", isSignal: true, isRequired: false, transformFunction: null }, zAutoReply: { classPropertyName: "zAutoReply", publicName: "zAutoReply", isSignal: true, isRequired: false, transformFunction: null }, zAutoReplyDelay: { classPropertyName: "zAutoReplyDelay", publicName: "zAutoReplyDelay", isSignal: true, isRequired: false, transformFunction: null }, zFabIcon: { classPropertyName: "zFabIcon", publicName: "zFabIcon", isSignal: true, isRequired: false, transformFunction: null }, zBotIcon: { classPropertyName: "zBotIcon", publicName: "zBotIcon", isSignal: true, isRequired: false, transformFunction: null }, zSendIcon: { classPropertyName: "zSendIcon", publicName: "zSendIcon", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { zOpen: "zOpenChange", zMessages: "zMessagesChange", zSend: "zSend", zResponse: "zResponse", zToggle: "zToggle", zClear: "zClear" }, host: { listeners: { "document:keydown.escape": "onEscapeKey()" }, classAttribute: "z-chat block" }, viewQueries: [{ propertyName: "_messagesScrollbarRef", first: true, predicate: ["messagesScrollbar"], descendants: true, isSignal: true }, { propertyName: "_chatInputRef", first: true, predicate: ["chatInput"], descendants: true, isSignal: true }, { propertyName: "_fileInputRef", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }, { propertyName: "_virtualMessageRefs", predicate: ["virtualMessage"], descendants: true, isSignal: true }], exportAs: ["zChat"], ngImport: i0, template: "<div\n [class]=\"wrapperClasses()\"\n [style.top]=\"wrapperTop()\"\n [style.right]=\"wrapperRight()\"\n [style.bottom]=\"wrapperBottom()\"\n [style.left]=\"wrapperLeft()\">\n <div class=\"relative\">\n <section class=\"z-chat-panel-shell\" [class]=\"panelClasses()\">\n <header class=\"from-primary/95 to-primary/75 text-primary-foreground bg-gradient-to-r px-4 py-3.5\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"bg-primary-foreground/15 relative inline-flex size-8 shrink-0 items-center justify-center rounded-full\">\n <z-icon [zType]=\"zBotIcon()\" zSize=\"16\" class=\"text-primary-foreground\" />\n <span\n class=\"ring-primary inline-block size-2.5 rounded-full bg-emerald-300 ring-2\"\n style=\"position: absolute; right: -0.0625rem; bottom: -0.0625rem\"></span>\n </span>\n <h3 class=\"truncate text-sm font-semibold\">{{ zTitle() | translate }}</h3>\n </div>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"clearMessages()\"\n title=\"{{ 'i18n_z_ui_chat_ai_new_chat' | translate }}\">\n <z-icon zType=\"lucideRefreshCcw\" zSize=\"17\" />\n </button>\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"close()\"\n title=\"{{ 'i18n_z_ui_chat_ai_close' | translate }}\">\n <z-icon zType=\"lucideChevronDown\" zSize=\"18\" />\n </button>\n </div>\n </div>\n </header>\n\n <div class=\"z-chat-lower-shell\">\n <ng-scrollbar\n #messagesScrollbar\n (afterInit)=\"onMessagesScrollbarInit()\"\n class=\"z-chat-scrollbar z-chat-content-surface flex-1 overflow-x-hidden\"\n track=\"vertical\"\n orientation=\"vertical\"\n appearance=\"compact\"\n visibility=\"hover\">\n <div class=\"z-chat-messages\">\n @if (hasMessages()) {\n <div class=\"z-chat-messages-sizer\" [style.height.px]=\"messageVirtualizer.getTotalSize()\">\n @for (row of messageVirtualizer.getVirtualItems(); track row.key) {\n @let item = zMessages()[row.index];\n <div\n #virtualMessage\n class=\"z-chat-virtual-row\"\n [attr.data-index]=\"row.index\"\n [style.transform]=\"'translateY(' + row.start + 'px)'\">\n <div\n class=\"z-chat-message\"\n [class.z-chat-message--enter]=\"item.id === newestMessageId()\"\n [class.justify-end]=\"item.role === 'user'\"\n [class.justify-start]=\"item.role === 'assistant'\">\n <div\n class=\"z-chat-message-bubble max-w-[88%] min-w-0 rounded-2xl px-3 py-2.5 text-sm leading-5 [overflow-wrap:anywhere] break-all shadow-xs\"\n [class.bg-primary]=\"item.role === 'user'\"\n [class.text-primary-foreground]=\"item.role === 'user'\"\n [class.rounded-br-md]=\"item.role === 'user'\"\n [class.bg-background]=\"item.role === 'assistant'\"\n [class.border-border/60]=\"item.role === 'assistant'\"\n [class.text-foreground]=\"item.role === 'assistant'\"\n [class.rounded-bl-md]=\"item.role === 'assistant'\"\n [class.border]=\"item.role === 'assistant'\">\n @if (item.files && item.files.length > 0) {\n <div class=\"mb-2 flex flex-col gap-1.5\">\n @for (file of item.files; track file.id) {\n <div\n class=\"flex items-center gap-2 rounded-[0.5rem] px-2 py-1.5 text-xs\"\n [class.bg-white/15]=\"item.role === 'user'\"\n [class.bg-muted/40]=\"item.role === 'assistant'\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background/80 text-muted-foreground inline-flex size-8 items-center justify-center rounded-[0.5rem]\"\n [class.bg-white/20]=\"item.role === 'user'\"\n [class.text-white]=\"item.role === 'user'\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"truncate text-[0.625rem] opacity-80\">{{ formatFileSize(file.size) }}</p>\n </div>\n </div>\n }\n </div>\n }\n {{ item.content }}\n </div>\n </div>\n </div>\n }\n </div>\n } @else if (showEmptyState()) {\n <div class=\"flex h-full min-h-60 flex-col items-center justify-center px-2 text-center\">\n <div\n class=\"from-primary/20 to-primary/5 mb-3 inline-flex size-14 items-center justify-center rounded-full bg-gradient-to-br\">\n <z-icon zType=\"lucideBot\" zSize=\"28\" class=\"text-primary\" />\n </div>\n <h4 class=\"text-foreground text-sm font-semibold\">{{ 'i18n_z_ui_chat_ai_empty_title' | translate }}</h4>\n <p class=\"text-muted-foreground mt-1 text-xs\">{{ 'i18n_z_ui_chat_ai_empty_desc' | translate }}</p>\n\n @if (zSuggestions().length > 0) {\n <div class=\"mt-4 flex w-full flex-col gap-2\">\n @for (item of zSuggestions(); track item.label) {\n <button\n type=\"button\"\n class=\"z-chat-suggestion-item bg-background/95 hover:bg-background border-border/60 text-foreground flex w-full cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-left text-xs transition hover:shadow-sm\"\n [style.animation-delay.ms]=\"$index * 70\"\n (click)=\"onSuggestionSelect(item)\">\n @if (item.icon) {\n <z-icon [zType]=\"item.icon!\" zSize=\"14\" class=\"text-primary shrink-0\" />\n }\n <span class=\"line-clamp-2\">{{ item.label | translate }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (isTyping()) {\n <div class=\"z-chat-message z-chat-message--enter justify-start\">\n <div class=\"bg-background border-border/60 rounded-2xl rounded-bl-md border px-3 py-2\">\n <div class=\"flex items-center gap-1\">\n <span class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.15s\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.3s\"></span>\n </div>\n </div>\n </div>\n }\n </div>\n </ng-scrollbar>\n\n <footer class=\"z-chat-footer-surface p-3\">\n @if (attachments().length > 0) {\n <div class=\"mb-2 flex max-h-32 flex-col gap-2 overflow-y-auto pr-1\">\n @for (file of attachments(); track file.id) {\n <div\n class=\"bg-muted/45 border-border/55 relative flex w-full items-center gap-2 rounded-[0.5rem] border px-2 py-1.5\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 shrink-0 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background text-muted-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-[0.5rem]\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"text-muted-foreground truncate text-[0.625rem]\">{{ formatFileSize(file.size) }}</p>\n </div>\n <button\n type=\"button\"\n class=\"bg-background/80 hover:bg-background text-muted-foreground inline-flex size-5 cursor-pointer items-center justify-center rounded-full transition\"\n (click)=\"removeAttachment(file.id)\">\n <z-icon zType=\"lucideX\" zSize=\"12\" />\n </button>\n </div>\n }\n </div>\n }\n\n <div class=\"z-chat-composer-shell\">\n <div class=\"z-chat-composer-main\" [class.z-chat-composer__field--overflow]=\"isComposerOverflowing()\">\n <textarea\n #chatInput\n class=\"z-chat-composer-textarea\"\n [placeholder]=\"zPlaceholder() | translate\"\n [disabled]=\"zDisabled()\"\n [ngModel]=\"draftMessage()\"\n (ngModelChange)=\"draftMessage.set($event)\"\n (input)=\"onTextareaInput($event)\"\n (paste)=\"onTextareaPaste($event)\"\n (keydown)=\"onInputKeydown($event)\"\n rows=\"3\"></textarea>\n </div>\n\n <div class=\"z-chat-composer-toolbar\">\n <div class=\"z-chat-composer-toolbar-left\">\n @if (zAllowAttachments()) {\n <input\n #fileInput\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"zAcceptedFiles()\"\n multiple\n (change)=\"onFileChange($event)\" />\n <button\n type=\"button\"\n class=\"z-chat-composer-tool-btn\"\n [disabled]=\"zDisabled()\"\n (click)=\"onClickAttach()\"\n title=\"{{ 'i18n_z_ui_chat_ai_attach' | translate }}\">\n <z-icon zType=\"lucidePlus\" zSize=\"18\" />\n </button>\n }\n </div>\n\n <div class=\"z-chat-composer-toolbar-right\">\n <button\n type=\"button\"\n class=\"z-chat-composer-send-btn\"\n [disabled]=\"!canSend()\"\n (click)=\"onSendMessage()\"\n title=\"{{ 'i18n_z_ui_chat_ai_send' | translate }}\">\n <z-icon\n [zType]=\"isTyping() ? 'lucideLoaderCircle' : zSendIcon()\"\n zSize=\"16\"\n [class]=\"isTyping() ? 'animate-spin' : ''\" />\n </button>\n </div>\n </div>\n </div>\n </footer>\n </div>\n\n <z-modal\n [zVisible]=\"isPreviewVisible()\"\n [zTitle]=\"previewImage()?.name\"\n [zDescription]=\"previewImageSizeLabel()\"\n zWidth=\"min(92vw, 720px)\"\n [zHideFooter]=\"true\"\n [zMaskClosable]=\"true\"\n zOverlay=\"blur\"\n (zCancel)=\"closeImagePreview()\">\n <div class=\"z-chat-preview-modal-body\">\n <img [src]=\"previewImage()?.url\" [alt]=\"previewImage()?.name\" class=\"z-chat-preview-image\" />\n </div>\n </z-modal>\n </section>\n\n <button\n type=\"button\"\n class=\"pointer-events-auto cursor-pointer\"\n [class]=\"fabClasses()\"\n (click)=\"onToggle()\"\n [attr.aria-expanded]=\"zOpen()\"\n [attr.aria-label]=\"'i18n_z_ui_chat_ai_toggle' | translate\"\n title=\"{{ 'i18n_z_ui_chat_ai_toggle' | translate }}\">\n @if (zShowPulse() && !zOpen()) {\n <span class=\"bg-primary/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n @if (showRipple()) {\n <span class=\"bg-primary-foreground/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n <z-icon\n [zType]=\"zOpen() ? 'lucideX' : zFabIcon()\"\n zSize=\"24\"\n class=\"relative z-1 transition-transform duration-200\"\n [class.rotate-180]=\"zOpen()\" />\n </button>\n </div>\n</div>\n", styles: [".z-chat textarea{scrollbar-width:thin}.z-chat-panel-shell{box-shadow:0 0 28px #00000024!important}:is(.dark,[data-theme=dark]) .z-chat-panel-shell{box-shadow:0 0 28px #00000057!important}.z-chat-composer__field--overflow:after{position:absolute;right:0;bottom:0;left:0;height:1rem;pointer-events:none;content:\"\";background:linear-gradient(to top,var(--background) 22%,transparent)}.z-chat-composer-shell{position:relative;display:flex;width:100%;flex-direction:column;border:1px solid color-mix(in oklab,var(--border) 82%,transparent);border-radius:1rem;background:linear-gradient(180deg,color-mix(in oklab,var(--card) 98%,var(--background) 2%),color-mix(in oklab,var(--card) 92%,var(--background) 8%));box-shadow:0 6px 18px #00000014;transition:box-shadow .2s ease}.z-chat-composer-shell:focus-within{box-shadow:0 6px 18px #00000014}:is(.dark,[data-theme=dark]) .z-chat-composer-shell{border-color:var(--border);background:linear-gradient(180deg,color-mix(in oklab,var(--card) 96%,var(--background) 4%),color-mix(in oklab,var(--card) 88%,var(--background) 12%));box-shadow:0 10px 24px #00000057}.z-chat-composer-main{position:relative;overflow:hidden;padding:.7rem .35rem .45rem .85rem}.z-chat-composer-textarea{width:100%;height:4.125rem;max-height:4.125rem;border:none;background:transparent;resize:none;overflow-x:hidden;font-size:.875rem;line-height:1.4rem;color:var(--foreground);outline:none;padding-right:.2rem;scrollbar-width:thin;overflow-wrap:anywhere;word-break:break-word}.z-chat-composer-textarea::placeholder{color:var(--muted-foreground)}.z-chat-composer-textarea::-webkit-scrollbar{width:.375rem}.z-chat-composer-textarea::-webkit-scrollbar-track{background:transparent}.z-chat-composer-textarea::-webkit-scrollbar-thumb{border-radius:62.4375rem;background:#64748b73}:is(.dark,[data-theme=dark]) .z-chat-composer-textarea::-webkit-scrollbar-thumb{background:#94a3b88c}.z-chat-composer-toolbar{display:flex;align-items:center;justify-content:space-between;border-top:.0625rem solid rgba(219,219,219,.6);padding:.55rem .7rem .65rem}:is(.dark,[data-theme=dark]) .z-chat-composer-toolbar{border-top-color:#00000038}.z-chat-composer-toolbar-left,.z-chat-composer-toolbar-right{display:flex;align-items:center}.z-chat-composer-tool-btn{display:inline-flex;height:1.75rem;width:1.75rem;cursor:pointer;align-items:center;justify-content:center;border-radius:.6rem;border:none;background:transparent;color:#374151;transition:background-color .2s ease}.z-chat-composer-tool-btn:hover{background:#0000000f}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn{color:#e5e7ebe6}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn:hover{background:#ffffff1a}.z-chat-composer-tool-btn:disabled{cursor:not-allowed;opacity:.5}.z-chat-composer-send-btn{display:inline-flex;height:2rem;width:2rem;cursor:pointer;align-items:center;justify-content:center;border-radius:62.4375rem;border:1px solid rgba(0,0,0,.1);background:var(--primary);color:var(--primary-foreground);box-shadow:0 2px 8px #0000002e,inset 0 1px #ffffff2e;transition:transform .15s ease,opacity .2s ease,box-shadow .2s ease}.z-chat-composer-send-btn:hover{transform:translateY(-.0625rem);box-shadow:0 4px 12px #00000038,inset 0 1px #ffffff38}.z-chat-composer-send-btn:disabled{cursor:not-allowed;opacity:.45;box-shadow:none}.z-chat-scrollbar{contain:layout}.z-chat-content-surface{background:var(--card)}.z-chat-lower-shell{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;background:var(--card);color:var(--card-foreground);border:1px solid transparent;border-top:0;border-bottom-right-radius:1rem;border-bottom-left-radius:1rem}:is(.dark,[data-theme=dark]) .z-chat-lower-shell{border-color:var(--border);border-top:0}.z-chat-footer-surface{background:transparent;border-top:.0625rem solid color-mix(in oklab,var(--border) 70%,transparent)}:is(.dark,[data-theme=dark]) .z-chat-footer-surface{background:transparent;border-top-color:var(--border)}.z-chat-messages{min-height:100%;overflow-x:hidden;padding:.875rem .75rem;contain:content}.z-chat-messages-sizer{position:relative;width:100%}.z-chat-virtual-row{position:absolute;top:0;left:0;width:100%;padding:.5rem 0}.z-chat-virtual-row>.z-chat-message{margin:0}.z-chat-message{display:flex;margin:.5rem 0;padding:0 2px;transform-origin:bottom center;contain:layout style}.z-chat-message-bubble{transform:translateZ(0);backface-visibility:hidden}.z-chat-message--enter{animation:z-chat-message-push-in .3s ease-out;will-change:transform,opacity}.z-chat-suggestion-item{animation:z-chat-suggestion-reveal .28s ease-out both;will-change:transform,opacity}.z-chat-preview-modal-body{display:flex;align-items:center;justify-content:center;max-height:min(70vh,40rem);padding:.25rem}.z-chat-preview-image{max-width:100%;max-height:min(66vh,37.5rem);border-radius:.5rem;object-fit:contain}@keyframes z-chat-message-push-in{0%{opacity:0;transform:translateY(.9375rem) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes z-chat-suggestion-reveal{0%{opacity:0;transform:translateY(.5rem) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ZIconComponent, selector: "z-icon, [z-icon]", inputs: ["class", "zType", "zAnimatedType", "zAnimate", "zAnimationTrigger", "zSize", "zStrokeWidth", "zSvg"] }, { kind: "component", type: ZModalComponent, selector: "z-modal", inputs: ["class", "zBodyClass", "zVisible", "zTitle", "zDescription", "zWidth", "zClosable", "zMaskClosable", "zDisableShadow", "zHeaderBorder", "zFooterBorder", "zHideHeader", "zHideFooter", "zOkText", "zCancelText", "zOkDestructive", "zOkDisabled", "zLoading", "zContentLoading", "zSkeletonRows", "zOverlay"], outputs: ["zOk", "zCancel", "zAfterClose", "zScrollbar", "zVisibleChange"], exportAs: ["zModal"] }, { kind: "component", type: NgScrollbar, selector: "ng-scrollbar:not([externalViewport]), [ngScrollbar]", exportAs: ["ngScrollbar"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
464
519
  }
465
520
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ZChatComponent, decorators: [{
466
521
  type: Component,
467
522
  args: [{ selector: 'z-chat', imports: [FormsModule, TranslatePipe, ZIconComponent, ZModalComponent, NgScrollbar], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
468
523
  class: 'z-chat block',
469
524
  '(document:keydown.escape)': 'onEscapeKey()',
470
- }, exportAs: 'zChat', template: "<div\n [class]=\"wrapperClasses()\"\n [style.top]=\"wrapperTop()\"\n [style.right]=\"wrapperRight()\"\n [style.bottom]=\"wrapperBottom()\"\n [style.left]=\"wrapperLeft()\">\n <div class=\"relative\">\n <section class=\"z-chat-panel-shell\" [class]=\"panelClasses()\">\n <header class=\"from-primary/95 to-primary/75 text-primary-foreground bg-gradient-to-r px-4 py-3.5\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"bg-primary-foreground/15 relative inline-flex size-8 shrink-0 items-center justify-center rounded-full\">\n <z-icon [zType]=\"zBotIcon()\" zSize=\"16\" class=\"text-primary-foreground\" />\n <span\n class=\"ring-primary inline-block size-2.5 rounded-full bg-emerald-300 ring-2\"\n style=\"position: absolute; right: -0.0625rem; bottom: -0.0625rem\"></span>\n </span>\n <h3 class=\"truncate text-sm font-semibold\">{{ zTitle() | translate }}</h3>\n </div>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"clearMessages()\"\n title=\"{{ 'i18n_z_ui_chat_ai_new_chat' | translate }}\">\n <z-icon zType=\"lucideRefreshCcw\" zSize=\"17\" />\n </button>\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"close()\"\n title=\"{{ 'i18n_z_ui_chat_ai_close' | translate }}\">\n <z-icon zType=\"lucideChevronDown\" zSize=\"18\" />\n </button>\n </div>\n </div>\n </header>\n\n <div class=\"z-chat-lower-shell\">\n <ng-scrollbar\n #messagesScrollbar\n (afterInit)=\"onMessagesScrollbarInit()\"\n class=\"z-chat-scrollbar z-chat-content-surface flex-1 overflow-x-hidden\"\n track=\"vertical\"\n orientation=\"vertical\"\n appearance=\"compact\"\n visibility=\"hover\">\n <div class=\"z-chat-messages\">\n @if (hasMessages()) {\n @for (item of zMessages(); track item.id) {\n <div\n class=\"z-chat-message z-chat-message--enter\"\n [class.justify-end]=\"item.role === 'user'\"\n [class.justify-start]=\"item.role === 'assistant'\">\n <div\n class=\"z-chat-message-bubble max-w-[88%] min-w-0 rounded-2xl px-3 py-2.5 text-sm leading-5 [overflow-wrap:anywhere] break-all shadow-xs\"\n [class.bg-primary]=\"item.role === 'user'\"\n [class.text-primary-foreground]=\"item.role === 'user'\"\n [class.rounded-br-md]=\"item.role === 'user'\"\n [class.bg-background]=\"item.role === 'assistant'\"\n [class.border-border/60]=\"item.role === 'assistant'\"\n [class.text-foreground]=\"item.role === 'assistant'\"\n [class.rounded-bl-md]=\"item.role === 'assistant'\"\n [class.border]=\"item.role === 'assistant'\">\n @if (item.files && item.files.length > 0) {\n <div class=\"mb-2 flex flex-col gap-1.5\">\n @for (file of item.files; track file.id) {\n <div\n class=\"flex items-center gap-2 rounded-[0.5rem] px-2 py-1.5 text-xs\"\n [class.bg-white/15]=\"item.role === 'user'\"\n [class.bg-muted/40]=\"item.role === 'assistant'\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background/80 text-muted-foreground inline-flex size-8 items-center justify-center rounded-[0.5rem]\"\n [class.bg-white/20]=\"item.role === 'user'\"\n [class.text-white]=\"item.role === 'user'\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"truncate text-[0.625rem] opacity-80\">{{ formatFileSize(file.size) }}</p>\n </div>\n </div>\n }\n </div>\n }\n {{ item.content }}\n </div>\n </div>\n }\n } @else if (showEmptyState()) {\n <div class=\"flex h-full min-h-60 flex-col items-center justify-center px-2 text-center\">\n <div\n class=\"from-primary/20 to-primary/5 mb-3 inline-flex size-14 items-center justify-center rounded-full bg-gradient-to-br\">\n <z-icon zType=\"lucideBot\" zSize=\"28\" class=\"text-primary\" />\n </div>\n <h4 class=\"text-foreground text-sm font-semibold\">{{ 'i18n_z_ui_chat_ai_empty_title' | translate }}</h4>\n <p class=\"text-muted-foreground mt-1 text-xs\">{{ 'i18n_z_ui_chat_ai_empty_desc' | translate }}</p>\n\n @if (zSuggestions().length > 0) {\n <div class=\"mt-4 flex w-full flex-col gap-2\">\n @for (item of zSuggestions(); track item.label) {\n <button\n type=\"button\"\n class=\"z-chat-suggestion-item bg-background/95 hover:bg-background border-border/60 text-foreground flex w-full cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-left text-xs transition hover:shadow-sm\"\n [style.animation-delay.ms]=\"$index * 70\"\n (click)=\"onSuggestionSelect(item)\">\n @if (item.icon) {\n <z-icon [zType]=\"item.icon!\" zSize=\"14\" class=\"text-primary shrink-0\" />\n }\n <span class=\"line-clamp-2\">{{ item.label | translate }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (isTyping()) {\n <div class=\"z-chat-message z-chat-message--enter justify-start\">\n <div class=\"bg-background border-border/60 rounded-2xl rounded-bl-md border px-3 py-2\">\n <div class=\"flex items-center gap-1\">\n <span class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.15s\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.3s\"></span>\n </div>\n </div>\n </div>\n }\n </div>\n </ng-scrollbar>\n\n <footer class=\"z-chat-footer-surface p-3\">\n @if (attachments().length > 0) {\n <div class=\"mb-2 flex max-h-32 flex-col gap-2 overflow-y-auto pr-1\">\n @for (file of attachments(); track file.id) {\n <div\n class=\"bg-muted/45 border-border/55 relative flex w-full items-center gap-2 rounded-[0.5rem] border px-2 py-1.5\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 shrink-0 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background text-muted-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-[0.5rem]\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"text-muted-foreground truncate text-[0.625rem]\">{{ formatFileSize(file.size) }}</p>\n </div>\n <button\n type=\"button\"\n class=\"bg-background/80 hover:bg-background text-muted-foreground inline-flex size-5 cursor-pointer items-center justify-center rounded-full transition\"\n (click)=\"removeAttachment(file.id)\">\n <z-icon zType=\"lucideX\" zSize=\"12\" />\n </button>\n </div>\n }\n </div>\n }\n\n <div class=\"z-chat-composer-shell\">\n <div class=\"z-chat-composer-main\" [class.z-chat-composer__field--overflow]=\"isComposerOverflowing()\">\n <textarea\n #chatInput\n class=\"z-chat-composer-textarea\"\n [placeholder]=\"zPlaceholder() | translate\"\n [disabled]=\"zDisabled()\"\n [ngModel]=\"draftMessage()\"\n (ngModelChange)=\"draftMessage.set($event)\"\n (input)=\"onTextareaInput($event)\"\n (paste)=\"onTextareaPaste($event)\"\n (keydown)=\"onInputKeydown($event)\"\n rows=\"3\"></textarea>\n </div>\n\n <div class=\"z-chat-composer-toolbar\">\n <div class=\"z-chat-composer-toolbar-left\">\n @if (zAllowAttachments()) {\n <input\n #fileInput\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"zAcceptedFiles()\"\n multiple\n (change)=\"onFileChange($event)\" />\n <button\n type=\"button\"\n class=\"z-chat-composer-tool-btn\"\n [disabled]=\"zDisabled()\"\n (click)=\"onClickAttach()\"\n title=\"{{ 'i18n_z_ui_chat_ai_attach' | translate }}\">\n <z-icon zType=\"lucidePlus\" zSize=\"18\" />\n </button>\n }\n </div>\n\n <div class=\"z-chat-composer-toolbar-right\">\n <button\n type=\"button\"\n class=\"z-chat-composer-send-btn\"\n [disabled]=\"!canSend()\"\n (click)=\"onSendMessage()\"\n title=\"{{ 'i18n_z_ui_chat_ai_send' | translate }}\">\n <z-icon\n [zType]=\"isTyping() ? 'lucideLoaderCircle' : zSendIcon()\"\n zSize=\"16\"\n [class]=\"isTyping() ? 'animate-spin' : ''\" />\n </button>\n </div>\n </div>\n </div>\n </footer>\n </div>\n\n <z-modal\n [zVisible]=\"isPreviewVisible()\"\n [zTitle]=\"previewImage()?.name\"\n [zDescription]=\"previewImageSizeLabel()\"\n zWidth=\"min(92vw, 720px)\"\n [zHideFooter]=\"true\"\n [zMaskClosable]=\"true\"\n zOverlay=\"blur\"\n (zCancel)=\"closeImagePreview()\">\n <div class=\"z-chat-preview-modal-body\">\n <img [src]=\"previewImage()?.url\" [alt]=\"previewImage()?.name\" class=\"z-chat-preview-image\" />\n </div>\n </z-modal>\n </section>\n\n <button\n type=\"button\"\n class=\"pointer-events-auto cursor-pointer\"\n [class]=\"fabClasses()\"\n (click)=\"onToggle()\"\n [attr.aria-expanded]=\"zOpen()\"\n [attr.aria-label]=\"'i18n_z_ui_chat_ai_toggle' | translate\"\n title=\"{{ 'i18n_z_ui_chat_ai_toggle' | translate }}\">\n @if (zShowPulse() && !zOpen()) {\n <span class=\"bg-primary/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n @if (showRipple()) {\n <span class=\"bg-primary-foreground/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n <z-icon\n [zType]=\"zOpen() ? 'lucideX' : zFabIcon()\"\n zSize=\"24\"\n class=\"relative z-1 transition-transform duration-200\"\n [class.rotate-180]=\"zOpen()\" />\n </button>\n </div>\n</div>\n", styles: [".z-chat textarea{scrollbar-width:thin}.z-chat-panel-shell{box-shadow:0 0 28px #00000024!important}:is(.dark,[data-theme=dark]) .z-chat-panel-shell{box-shadow:0 0 28px #00000057!important}.z-chat-composer__field--overflow:after{position:absolute;right:0;bottom:0;left:0;height:1rem;pointer-events:none;content:\"\";background:linear-gradient(to top,var(--background) 22%,transparent)}.z-chat-composer-shell{position:relative;display:flex;width:100%;flex-direction:column;border:1px solid color-mix(in oklab,var(--border) 82%,transparent);border-radius:1rem;background:linear-gradient(180deg,color-mix(in oklab,var(--card) 98%,var(--background) 2%),color-mix(in oklab,var(--card) 92%,var(--background) 8%));box-shadow:0 6px 18px #00000014;transition:box-shadow .2s ease}.z-chat-composer-shell:focus-within{box-shadow:0 6px 18px #00000014}:is(.dark,[data-theme=dark]) .z-chat-composer-shell{border-color:var(--border);background:linear-gradient(180deg,color-mix(in oklab,var(--card) 96%,var(--background) 4%),color-mix(in oklab,var(--card) 88%,var(--background) 12%));box-shadow:0 10px 24px #00000057}.z-chat-composer-main{position:relative;overflow:hidden;padding:.7rem .35rem .45rem .85rem}.z-chat-composer-textarea{width:100%;height:4.125rem;max-height:4.125rem;border:none;background:transparent;resize:none;overflow-x:hidden;font-size:.875rem;line-height:1.4rem;color:var(--foreground);outline:none;padding-right:.2rem;scrollbar-width:thin;overflow-wrap:anywhere;word-break:break-word}.z-chat-composer-textarea::placeholder{color:var(--muted-foreground)}.z-chat-composer-textarea::-webkit-scrollbar{width:.375rem}.z-chat-composer-textarea::-webkit-scrollbar-track{background:transparent}.z-chat-composer-textarea::-webkit-scrollbar-thumb{border-radius:62.4375rem;background:#64748b73}:is(.dark,[data-theme=dark]) .z-chat-composer-textarea::-webkit-scrollbar-thumb{background:#94a3b88c}.z-chat-composer-toolbar{display:flex;align-items:center;justify-content:space-between;border-top:.0625rem solid rgba(219,219,219,.6);padding:.55rem .7rem .65rem}:is(.dark,[data-theme=dark]) .z-chat-composer-toolbar{border-top-color:#00000038}.z-chat-composer-toolbar-left,.z-chat-composer-toolbar-right{display:flex;align-items:center}.z-chat-composer-tool-btn{display:inline-flex;height:1.75rem;width:1.75rem;cursor:pointer;align-items:center;justify-content:center;border-radius:.6rem;border:none;background:transparent;color:#374151;transition:background-color .2s ease}.z-chat-composer-tool-btn:hover{background:#0000000f}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn{color:#e5e7ebe6}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn:hover{background:#ffffff1a}.z-chat-composer-tool-btn:disabled{cursor:not-allowed;opacity:.5}.z-chat-composer-send-btn{display:inline-flex;height:2rem;width:2rem;cursor:pointer;align-items:center;justify-content:center;border-radius:62.4375rem;border:1px solid rgba(0,0,0,.1);background:var(--primary);color:var(--primary-foreground);box-shadow:0 2px 8px #0000002e,inset 0 1px #ffffff2e;transition:transform .15s ease,opacity .2s ease,box-shadow .2s ease}.z-chat-composer-send-btn:hover{transform:translateY(-.0625rem);box-shadow:0 4px 12px #00000038,inset 0 1px #ffffff38}.z-chat-composer-send-btn:disabled{cursor:not-allowed;opacity:.45;box-shadow:none}.z-chat-scrollbar{contain:layout}.z-chat-content-surface{background:var(--card)}.z-chat-lower-shell{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;background:var(--card);color:var(--card-foreground);border:1px solid transparent;border-top:0;border-bottom-right-radius:1rem;border-bottom-left-radius:1rem}:is(.dark,[data-theme=dark]) .z-chat-lower-shell{border-color:var(--border);border-top:0}.z-chat-footer-surface{background:transparent;border-top:.0625rem solid color-mix(in oklab,var(--border) 70%,transparent)}:is(.dark,[data-theme=dark]) .z-chat-footer-surface{background:transparent;border-top-color:var(--border)}.z-chat-messages{min-height:100%;overflow-x:hidden;padding:.875rem .75rem;contain:content}.z-chat-message{display:flex;margin:.5rem 0;padding:0 2px;transform-origin:bottom center;contain:layout style}.z-chat-message-bubble{transform:translateZ(0);backface-visibility:hidden}.z-chat-message--enter{animation:z-chat-message-push-in .3s ease-out;will-change:transform,opacity}.z-chat-suggestion-item{animation:z-chat-suggestion-reveal .28s ease-out both;will-change:transform,opacity}.z-chat-preview-modal-body{display:flex;align-items:center;justify-content:center;max-height:min(70vh,40rem);padding:.25rem}.z-chat-preview-image{max-width:100%;max-height:min(66vh,37.5rem);border-radius:.5rem;object-fit:contain}@keyframes z-chat-message-push-in{0%{opacity:0;transform:translateY(.9375rem) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes z-chat-suggestion-reveal{0%{opacity:0;transform:translateY(.5rem) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] }]
471
- }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], zOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "zOpen", required: false }] }, { type: i0.Output, args: ["zOpenChange"] }], zMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "zMessages", required: false }] }, { type: i0.Output, args: ["zMessagesChange"] }], zTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "zTitle", required: false }] }], zSubtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSubtitle", required: false }] }], zPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "zPlaceholder", required: false }] }], zPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "zPosition", required: false }] }], zOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "zOffset", required: false }] }], zSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSize", required: false }] }], zSuggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSuggestions", required: false }] }], zShowSuggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "zShowSuggestions", required: false }] }], zShowPulse: [{ type: i0.Input, args: [{ isSignal: true, alias: "zShowPulse", required: false }] }], zDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "zDisabled", required: false }] }], zLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "zLoading", required: false }] }], zAllowAttachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAllowAttachments", required: false }] }], zAcceptedFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAcceptedFiles", required: false }] }], zAutoReply: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAutoReply", required: false }] }], zAutoReplyDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAutoReplyDelay", required: false }] }], zFabIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "zFabIcon", required: false }] }], zBotIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "zBotIcon", required: false }] }], zSendIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSendIcon", required: false }] }], zSend: [{ type: i0.Output, args: ["zSend"] }], zResponse: [{ type: i0.Output, args: ["zResponse"] }], zToggle: [{ type: i0.Output, args: ["zToggle"] }], zClear: [{ type: i0.Output, args: ["zClear"] }], _messagesScrollbarRef: [{ type: i0.ViewChild, args: ['messagesScrollbar', { isSignal: true }] }], _chatInputRef: [{ type: i0.ViewChild, args: ['chatInput', { isSignal: true }] }], _fileInputRef: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
525
+ }, exportAs: 'zChat', template: "<div\n [class]=\"wrapperClasses()\"\n [style.top]=\"wrapperTop()\"\n [style.right]=\"wrapperRight()\"\n [style.bottom]=\"wrapperBottom()\"\n [style.left]=\"wrapperLeft()\">\n <div class=\"relative\">\n <section class=\"z-chat-panel-shell\" [class]=\"panelClasses()\">\n <header class=\"from-primary/95 to-primary/75 text-primary-foreground bg-gradient-to-r px-4 py-3.5\">\n <div class=\"flex items-start justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"flex items-center gap-2\">\n <span\n class=\"bg-primary-foreground/15 relative inline-flex size-8 shrink-0 items-center justify-center rounded-full\">\n <z-icon [zType]=\"zBotIcon()\" zSize=\"16\" class=\"text-primary-foreground\" />\n <span\n class=\"ring-primary inline-block size-2.5 rounded-full bg-emerald-300 ring-2\"\n style=\"position: absolute; right: -0.0625rem; bottom: -0.0625rem\"></span>\n </span>\n <h3 class=\"truncate text-sm font-semibold\">{{ zTitle() | translate }}</h3>\n </div>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"clearMessages()\"\n title=\"{{ 'i18n_z_ui_chat_ai_new_chat' | translate }}\">\n <z-icon zType=\"lucideRefreshCcw\" zSize=\"17\" />\n </button>\n <button\n type=\"button\"\n class=\"text-primary-foreground/90 hover:bg-primary-foreground/15 inline-flex size-8 cursor-pointer items-center justify-center rounded-md transition\"\n (click)=\"close()\"\n title=\"{{ 'i18n_z_ui_chat_ai_close' | translate }}\">\n <z-icon zType=\"lucideChevronDown\" zSize=\"18\" />\n </button>\n </div>\n </div>\n </header>\n\n <div class=\"z-chat-lower-shell\">\n <ng-scrollbar\n #messagesScrollbar\n (afterInit)=\"onMessagesScrollbarInit()\"\n class=\"z-chat-scrollbar z-chat-content-surface flex-1 overflow-x-hidden\"\n track=\"vertical\"\n orientation=\"vertical\"\n appearance=\"compact\"\n visibility=\"hover\">\n <div class=\"z-chat-messages\">\n @if (hasMessages()) {\n <div class=\"z-chat-messages-sizer\" [style.height.px]=\"messageVirtualizer.getTotalSize()\">\n @for (row of messageVirtualizer.getVirtualItems(); track row.key) {\n @let item = zMessages()[row.index];\n <div\n #virtualMessage\n class=\"z-chat-virtual-row\"\n [attr.data-index]=\"row.index\"\n [style.transform]=\"'translateY(' + row.start + 'px)'\">\n <div\n class=\"z-chat-message\"\n [class.z-chat-message--enter]=\"item.id === newestMessageId()\"\n [class.justify-end]=\"item.role === 'user'\"\n [class.justify-start]=\"item.role === 'assistant'\">\n <div\n class=\"z-chat-message-bubble max-w-[88%] min-w-0 rounded-2xl px-3 py-2.5 text-sm leading-5 [overflow-wrap:anywhere] break-all shadow-xs\"\n [class.bg-primary]=\"item.role === 'user'\"\n [class.text-primary-foreground]=\"item.role === 'user'\"\n [class.rounded-br-md]=\"item.role === 'user'\"\n [class.bg-background]=\"item.role === 'assistant'\"\n [class.border-border/60]=\"item.role === 'assistant'\"\n [class.text-foreground]=\"item.role === 'assistant'\"\n [class.rounded-bl-md]=\"item.role === 'assistant'\"\n [class.border]=\"item.role === 'assistant'\">\n @if (item.files && item.files.length > 0) {\n <div class=\"mb-2 flex flex-col gap-1.5\">\n @for (file of item.files; track file.id) {\n <div\n class=\"flex items-center gap-2 rounded-[0.5rem] px-2 py-1.5 text-xs\"\n [class.bg-white/15]=\"item.role === 'user'\"\n [class.bg-muted/40]=\"item.role === 'assistant'\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background/80 text-muted-foreground inline-flex size-8 items-center justify-center rounded-[0.5rem]\"\n [class.bg-white/20]=\"item.role === 'user'\"\n [class.text-white]=\"item.role === 'user'\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"truncate text-[0.625rem] opacity-80\">{{ formatFileSize(file.size) }}</p>\n </div>\n </div>\n }\n </div>\n }\n {{ item.content }}\n </div>\n </div>\n </div>\n }\n </div>\n } @else if (showEmptyState()) {\n <div class=\"flex h-full min-h-60 flex-col items-center justify-center px-2 text-center\">\n <div\n class=\"from-primary/20 to-primary/5 mb-3 inline-flex size-14 items-center justify-center rounded-full bg-gradient-to-br\">\n <z-icon zType=\"lucideBot\" zSize=\"28\" class=\"text-primary\" />\n </div>\n <h4 class=\"text-foreground text-sm font-semibold\">{{ 'i18n_z_ui_chat_ai_empty_title' | translate }}</h4>\n <p class=\"text-muted-foreground mt-1 text-xs\">{{ 'i18n_z_ui_chat_ai_empty_desc' | translate }}</p>\n\n @if (zSuggestions().length > 0) {\n <div class=\"mt-4 flex w-full flex-col gap-2\">\n @for (item of zSuggestions(); track item.label) {\n <button\n type=\"button\"\n class=\"z-chat-suggestion-item bg-background/95 hover:bg-background border-border/60 text-foreground flex w-full cursor-pointer items-center gap-2 rounded-xl border px-3 py-2 text-left text-xs transition hover:shadow-sm\"\n [style.animation-delay.ms]=\"$index * 70\"\n (click)=\"onSuggestionSelect(item)\">\n @if (item.icon) {\n <z-icon [zType]=\"item.icon!\" zSize=\"14\" class=\"text-primary shrink-0\" />\n }\n <span class=\"line-clamp-2\">{{ item.label | translate }}</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (isTyping()) {\n <div class=\"z-chat-message z-chat-message--enter justify-start\">\n <div class=\"bg-background border-border/60 rounded-2xl rounded-bl-md border px-3 py-2\">\n <div class=\"flex items-center gap-1\">\n <span class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.15s\"></span>\n <span\n class=\"bg-muted-foreground/60 inline-block size-1.5 animate-bounce rounded-full\"\n style=\"animation-delay: 0.3s\"></span>\n </div>\n </div>\n </div>\n }\n </div>\n </ng-scrollbar>\n\n <footer class=\"z-chat-footer-surface p-3\">\n @if (attachments().length > 0) {\n <div class=\"mb-2 flex max-h-32 flex-col gap-2 overflow-y-auto pr-1\">\n @for (file of attachments(); track file.id) {\n <div\n class=\"bg-muted/45 border-border/55 relative flex w-full items-center gap-2 rounded-[0.5rem] border px-2 py-1.5\">\n @if (isImageAttachment(file) && file.previewUrl) {\n <img\n [src]=\"file.previewUrl\"\n [alt]=\"file.name\"\n class=\"size-8 shrink-0 cursor-pointer rounded-[0.5rem] object-cover\"\n (click)=\"openImagePreview(file)\"\n title=\"Click to preview\" />\n } @else {\n <span\n class=\"bg-background text-muted-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-[0.5rem]\">\n <z-icon zType=\"lucideFileText\" zSize=\"14\" />\n </span>\n }\n <div class=\"min-w-0 flex-1\">\n <p class=\"truncate text-[0.6875rem] font-medium\">{{ file.name }}</p>\n <p class=\"text-muted-foreground truncate text-[0.625rem]\">{{ formatFileSize(file.size) }}</p>\n </div>\n <button\n type=\"button\"\n class=\"bg-background/80 hover:bg-background text-muted-foreground inline-flex size-5 cursor-pointer items-center justify-center rounded-full transition\"\n (click)=\"removeAttachment(file.id)\">\n <z-icon zType=\"lucideX\" zSize=\"12\" />\n </button>\n </div>\n }\n </div>\n }\n\n <div class=\"z-chat-composer-shell\">\n <div class=\"z-chat-composer-main\" [class.z-chat-composer__field--overflow]=\"isComposerOverflowing()\">\n <textarea\n #chatInput\n class=\"z-chat-composer-textarea\"\n [placeholder]=\"zPlaceholder() | translate\"\n [disabled]=\"zDisabled()\"\n [ngModel]=\"draftMessage()\"\n (ngModelChange)=\"draftMessage.set($event)\"\n (input)=\"onTextareaInput($event)\"\n (paste)=\"onTextareaPaste($event)\"\n (keydown)=\"onInputKeydown($event)\"\n rows=\"3\"></textarea>\n </div>\n\n <div class=\"z-chat-composer-toolbar\">\n <div class=\"z-chat-composer-toolbar-left\">\n @if (zAllowAttachments()) {\n <input\n #fileInput\n type=\"file\"\n class=\"hidden\"\n [attr.accept]=\"zAcceptedFiles()\"\n multiple\n (change)=\"onFileChange($event)\" />\n <button\n type=\"button\"\n class=\"z-chat-composer-tool-btn\"\n [disabled]=\"zDisabled()\"\n (click)=\"onClickAttach()\"\n title=\"{{ 'i18n_z_ui_chat_ai_attach' | translate }}\">\n <z-icon zType=\"lucidePlus\" zSize=\"18\" />\n </button>\n }\n </div>\n\n <div class=\"z-chat-composer-toolbar-right\">\n <button\n type=\"button\"\n class=\"z-chat-composer-send-btn\"\n [disabled]=\"!canSend()\"\n (click)=\"onSendMessage()\"\n title=\"{{ 'i18n_z_ui_chat_ai_send' | translate }}\">\n <z-icon\n [zType]=\"isTyping() ? 'lucideLoaderCircle' : zSendIcon()\"\n zSize=\"16\"\n [class]=\"isTyping() ? 'animate-spin' : ''\" />\n </button>\n </div>\n </div>\n </div>\n </footer>\n </div>\n\n <z-modal\n [zVisible]=\"isPreviewVisible()\"\n [zTitle]=\"previewImage()?.name\"\n [zDescription]=\"previewImageSizeLabel()\"\n zWidth=\"min(92vw, 720px)\"\n [zHideFooter]=\"true\"\n [zMaskClosable]=\"true\"\n zOverlay=\"blur\"\n (zCancel)=\"closeImagePreview()\">\n <div class=\"z-chat-preview-modal-body\">\n <img [src]=\"previewImage()?.url\" [alt]=\"previewImage()?.name\" class=\"z-chat-preview-image\" />\n </div>\n </z-modal>\n </section>\n\n <button\n type=\"button\"\n class=\"pointer-events-auto cursor-pointer\"\n [class]=\"fabClasses()\"\n (click)=\"onToggle()\"\n [attr.aria-expanded]=\"zOpen()\"\n [attr.aria-label]=\"'i18n_z_ui_chat_ai_toggle' | translate\"\n title=\"{{ 'i18n_z_ui_chat_ai_toggle' | translate }}\">\n @if (zShowPulse() && !zOpen()) {\n <span class=\"bg-primary/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n @if (showRipple()) {\n <span class=\"bg-primary-foreground/30 absolute inset-0 animate-ping rounded-full\"></span>\n }\n\n <z-icon\n [zType]=\"zOpen() ? 'lucideX' : zFabIcon()\"\n zSize=\"24\"\n class=\"relative z-1 transition-transform duration-200\"\n [class.rotate-180]=\"zOpen()\" />\n </button>\n </div>\n</div>\n", styles: [".z-chat textarea{scrollbar-width:thin}.z-chat-panel-shell{box-shadow:0 0 28px #00000024!important}:is(.dark,[data-theme=dark]) .z-chat-panel-shell{box-shadow:0 0 28px #00000057!important}.z-chat-composer__field--overflow:after{position:absolute;right:0;bottom:0;left:0;height:1rem;pointer-events:none;content:\"\";background:linear-gradient(to top,var(--background) 22%,transparent)}.z-chat-composer-shell{position:relative;display:flex;width:100%;flex-direction:column;border:1px solid color-mix(in oklab,var(--border) 82%,transparent);border-radius:1rem;background:linear-gradient(180deg,color-mix(in oklab,var(--card) 98%,var(--background) 2%),color-mix(in oklab,var(--card) 92%,var(--background) 8%));box-shadow:0 6px 18px #00000014;transition:box-shadow .2s ease}.z-chat-composer-shell:focus-within{box-shadow:0 6px 18px #00000014}:is(.dark,[data-theme=dark]) .z-chat-composer-shell{border-color:var(--border);background:linear-gradient(180deg,color-mix(in oklab,var(--card) 96%,var(--background) 4%),color-mix(in oklab,var(--card) 88%,var(--background) 12%));box-shadow:0 10px 24px #00000057}.z-chat-composer-main{position:relative;overflow:hidden;padding:.7rem .35rem .45rem .85rem}.z-chat-composer-textarea{width:100%;height:4.125rem;max-height:4.125rem;border:none;background:transparent;resize:none;overflow-x:hidden;font-size:.875rem;line-height:1.4rem;color:var(--foreground);outline:none;padding-right:.2rem;scrollbar-width:thin;overflow-wrap:anywhere;word-break:break-word}.z-chat-composer-textarea::placeholder{color:var(--muted-foreground)}.z-chat-composer-textarea::-webkit-scrollbar{width:.375rem}.z-chat-composer-textarea::-webkit-scrollbar-track{background:transparent}.z-chat-composer-textarea::-webkit-scrollbar-thumb{border-radius:62.4375rem;background:#64748b73}:is(.dark,[data-theme=dark]) .z-chat-composer-textarea::-webkit-scrollbar-thumb{background:#94a3b88c}.z-chat-composer-toolbar{display:flex;align-items:center;justify-content:space-between;border-top:.0625rem solid rgba(219,219,219,.6);padding:.55rem .7rem .65rem}:is(.dark,[data-theme=dark]) .z-chat-composer-toolbar{border-top-color:#00000038}.z-chat-composer-toolbar-left,.z-chat-composer-toolbar-right{display:flex;align-items:center}.z-chat-composer-tool-btn{display:inline-flex;height:1.75rem;width:1.75rem;cursor:pointer;align-items:center;justify-content:center;border-radius:.6rem;border:none;background:transparent;color:#374151;transition:background-color .2s ease}.z-chat-composer-tool-btn:hover{background:#0000000f}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn{color:#e5e7ebe6}:is(.dark,[data-theme=dark]) .z-chat-composer-tool-btn:hover{background:#ffffff1a}.z-chat-composer-tool-btn:disabled{cursor:not-allowed;opacity:.5}.z-chat-composer-send-btn{display:inline-flex;height:2rem;width:2rem;cursor:pointer;align-items:center;justify-content:center;border-radius:62.4375rem;border:1px solid rgba(0,0,0,.1);background:var(--primary);color:var(--primary-foreground);box-shadow:0 2px 8px #0000002e,inset 0 1px #ffffff2e;transition:transform .15s ease,opacity .2s ease,box-shadow .2s ease}.z-chat-composer-send-btn:hover{transform:translateY(-.0625rem);box-shadow:0 4px 12px #00000038,inset 0 1px #ffffff38}.z-chat-composer-send-btn:disabled{cursor:not-allowed;opacity:.45;box-shadow:none}.z-chat-scrollbar{contain:layout}.z-chat-content-surface{background:var(--card)}.z-chat-lower-shell{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;background:var(--card);color:var(--card-foreground);border:1px solid transparent;border-top:0;border-bottom-right-radius:1rem;border-bottom-left-radius:1rem}:is(.dark,[data-theme=dark]) .z-chat-lower-shell{border-color:var(--border);border-top:0}.z-chat-footer-surface{background:transparent;border-top:.0625rem solid color-mix(in oklab,var(--border) 70%,transparent)}:is(.dark,[data-theme=dark]) .z-chat-footer-surface{background:transparent;border-top-color:var(--border)}.z-chat-messages{min-height:100%;overflow-x:hidden;padding:.875rem .75rem;contain:content}.z-chat-messages-sizer{position:relative;width:100%}.z-chat-virtual-row{position:absolute;top:0;left:0;width:100%;padding:.5rem 0}.z-chat-virtual-row>.z-chat-message{margin:0}.z-chat-message{display:flex;margin:.5rem 0;padding:0 2px;transform-origin:bottom center;contain:layout style}.z-chat-message-bubble{transform:translateZ(0);backface-visibility:hidden}.z-chat-message--enter{animation:z-chat-message-push-in .3s ease-out;will-change:transform,opacity}.z-chat-suggestion-item{animation:z-chat-suggestion-reveal .28s ease-out both;will-change:transform,opacity}.z-chat-preview-modal-body{display:flex;align-items:center;justify-content:center;max-height:min(70vh,40rem);padding:.25rem}.z-chat-preview-image{max-width:100%;max-height:min(66vh,37.5rem);border-radius:.5rem;object-fit:contain}@keyframes z-chat-message-push-in{0%{opacity:0;transform:translateY(.9375rem) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes z-chat-suggestion-reveal{0%{opacity:0;transform:translateY(.5rem) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}\n"] }]
526
+ }], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], zOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "zOpen", required: false }] }, { type: i0.Output, args: ["zOpenChange"] }], zMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "zMessages", required: false }] }, { type: i0.Output, args: ["zMessagesChange"] }], zTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "zTitle", required: false }] }], zSubtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSubtitle", required: false }] }], zPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "zPlaceholder", required: false }] }], zPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "zPosition", required: false }] }], zOffset: [{ type: i0.Input, args: [{ isSignal: true, alias: "zOffset", required: false }] }], zSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSize", required: false }] }], zSuggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSuggestions", required: false }] }], zShowSuggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "zShowSuggestions", required: false }] }], zShowPulse: [{ type: i0.Input, args: [{ isSignal: true, alias: "zShowPulse", required: false }] }], zDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "zDisabled", required: false }] }], zLoading: [{ type: i0.Input, args: [{ isSignal: true, alias: "zLoading", required: false }] }], zAllowAttachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAllowAttachments", required: false }] }], zAcceptedFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAcceptedFiles", required: false }] }], zAutoReply: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAutoReply", required: false }] }], zAutoReplyDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAutoReplyDelay", required: false }] }], zFabIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "zFabIcon", required: false }] }], zBotIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "zBotIcon", required: false }] }], zSendIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSendIcon", required: false }] }], zSend: [{ type: i0.Output, args: ["zSend"] }], zResponse: [{ type: i0.Output, args: ["zResponse"] }], zToggle: [{ type: i0.Output, args: ["zToggle"] }], zClear: [{ type: i0.Output, args: ["zClear"] }], _messagesScrollbarRef: [{ type: i0.ViewChild, args: ['messagesScrollbar', { isSignal: true }] }], _chatInputRef: [{ type: i0.ViewChild, args: ['chatInput', { isSignal: true }] }], _fileInputRef: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], _virtualMessageRefs: [{ type: i0.ViewChildren, args: ['virtualMessage', { isSignal: true }] }] } });
472
527
 
473
528
  /**
474
529
  * Generated bundle index. Do not edit.