@mk-kit/ui 0.36.0 → 0.38.0

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.
@@ -0,0 +1,490 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, model, input, booleanAttribute, numberAttribute, output, signal, viewChild, computed, afterRenderEffect, ChangeDetectionStrategy, Component, TemplateRef, Directive, contentChild, untracked } from '@angular/core';
3
+ import { MK_I18N, mkUniqueId } from '@mk-kit/ui/core';
4
+ import { MkButton } from '@mk-kit/ui/button';
5
+ import { MkChip } from '@mk-kit/ui/chip';
6
+ import { MkIcon } from '@mk-kit/ui/icon';
7
+ import { MkAvatar, MkMarkdown } from '@mk-kit/ui/data';
8
+ import { MkCopyToClipboard } from '@mk-kit/ui/directives';
9
+ import { NgTemplateOutlet } from '@angular/common';
10
+
11
+ /**
12
+ * PromptBox — the composer of a chat: an auto-growing textarea, attachments
13
+ * (button, drop, paste), quick-reply suggestions and a send button that turns
14
+ * into *stop* while a reply is being generated.
15
+ *
16
+ * Enter sends, Shift+Enter breaks the line (`sendOnEnter="false"` to swap).
17
+ *
18
+ * ```html
19
+ * <mk-prompt-box
20
+ * [(value)]="draft"
21
+ * [busy]="generating()"
22
+ * attachments
23
+ * [suggestions]="['Summarise this page', 'Draft a reply']"
24
+ * (send)="ask($event)"
25
+ * (stop)="abort()"
26
+ * />
27
+ * ```
28
+ */
29
+ class MkPromptBox {
30
+ i18n = inject(MK_I18N);
31
+ id = mkUniqueId('mk-prompt');
32
+ /** The draft text (two-way). */
33
+ value = model('', /* @ts-ignore */
34
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
35
+ /** Placeholder of the textarea. */
36
+ placeholder = input(this.i18n.chatPlaceholder, /* @ts-ignore */
37
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
38
+ /** Disable everything (e.g. while offline). */
39
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
40
+ /** A reply is being generated: the send button becomes *stop*, sending is paused. */
41
+ busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
42
+ /** Allow attaching files (button, drag & drop, paste). */
43
+ attachments = input(false, { ...(ngDevMode ? { debugName: "attachments" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
44
+ /** Accepted file types for the picker (`accept` attribute syntax). */
45
+ accept = input('', /* @ts-ignore */
46
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
47
+ /** Maximum number of attached files (0 = unlimited). */
48
+ maxFiles = input(0, { ...(ngDevMode ? { debugName: "maxFiles" } : /* istanbul ignore next */ {}), transform: numberAttribute });
49
+ /** Maximum draft length; shows a counter when set. */
50
+ maxLength = input(0, { ...(ngDevMode ? { debugName: "maxLength" } : /* istanbul ignore next */ {}), transform: numberAttribute });
51
+ /** Minimum visible rows. */
52
+ rows = input(1, { ...(ngDevMode ? { debugName: "rows" } : /* istanbul ignore next */ {}), transform: numberAttribute });
53
+ /** Rows the textarea grows to before scrolling. */
54
+ maxRows = input(8, { ...(ngDevMode ? { debugName: "maxRows" } : /* istanbul ignore next */ {}), transform: numberAttribute });
55
+ /** Enter sends and Shift+Enter breaks the line (default); `false` swaps them. */
56
+ sendOnEnter = input(true, { ...(ngDevMode ? { debugName: "sendOnEnter" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
57
+ /** Quick replies shown above the box; clicking one sends it. */
58
+ suggestions = input([], /* @ts-ignore */
59
+ ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
60
+ /** Focus the textarea on render. */
61
+ autofocus = input(false, { ...(ngDevMode ? { debugName: "autofocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
62
+ /** The user submitted the draft (and any files). */
63
+ send = output();
64
+ /** The user pressed *stop* while `busy`. */
65
+ stop = output();
66
+ /** Files waiting to be sent. */
67
+ files = signal([], /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
69
+ dragging = signal(false, /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
71
+ textarea = viewChild.required('textarea', /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "textarea" }] : /* istanbul ignore next */ []));
73
+ fileInput = viewChild('fileInput', /* @ts-ignore */
74
+ ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
75
+ canSend = computed(() => !this.disabled() && !this.busy() && (this.value().trim().length > 0 || this.files().length > 0), /* @ts-ignore */
76
+ ...(ngDevMode ? [{ debugName: "canSend" }] : /* istanbul ignore next */ []));
77
+ remaining = computed(() => (this.maxLength() > 0 ? this.maxLength() - this.value().length : null), /* @ts-ignore */
78
+ ...(ngDevMode ? [{ debugName: "remaining" }] : /* istanbul ignore next */ []));
79
+ constructor() {
80
+ afterRenderEffect(() => {
81
+ this.value();
82
+ this.rows();
83
+ this.maxRows();
84
+ this.autosize();
85
+ });
86
+ afterRenderEffect(() => {
87
+ if (this.autofocus())
88
+ this.textarea().nativeElement.focus();
89
+ });
90
+ }
91
+ /** Focus the textarea. */
92
+ focus() {
93
+ this.textarea().nativeElement.focus();
94
+ }
95
+ /** Empty the draft and the attachment list. */
96
+ clear() {
97
+ this.value.set('');
98
+ this.files.set([]);
99
+ }
100
+ /** Submit the current draft (no-op when there is nothing to send or while busy). */
101
+ submit() {
102
+ if (!this.canSend())
103
+ return;
104
+ this.send.emit({ text: this.value().trim(), files: this.files() });
105
+ this.clear();
106
+ }
107
+ /** Add files, honouring `maxFiles` and `accept`. */
108
+ addFiles(list) {
109
+ if (!list || !this.attachments() || this.disabled())
110
+ return;
111
+ const accepted = Array.from(list).filter((f) => this.accepts(f));
112
+ if (!accepted.length)
113
+ return;
114
+ this.files.update((current) => {
115
+ const next = [...current, ...accepted];
116
+ const max = this.maxFiles();
117
+ return max > 0 ? next.slice(0, max) : next;
118
+ });
119
+ }
120
+ removeFile(index) {
121
+ this.files.update((current) => current.filter((_, i) => i !== index));
122
+ this.focus();
123
+ }
124
+ onInput(event) {
125
+ this.value.set(event.target.value);
126
+ }
127
+ onKeydown(event) {
128
+ if (event.key !== 'Enter' || event.isComposing)
129
+ return;
130
+ const plain = !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;
131
+ const sends = this.sendOnEnter() ? plain : event.ctrlKey || event.metaKey;
132
+ if (!sends)
133
+ return;
134
+ event.preventDefault();
135
+ if (this.busy())
136
+ return;
137
+ this.submit();
138
+ }
139
+ onPaste(event) {
140
+ const files = event.clipboardData?.files;
141
+ if (files?.length && this.attachments()) {
142
+ event.preventDefault();
143
+ this.addFiles(files);
144
+ }
145
+ }
146
+ onDragOver(event) {
147
+ if (!this.attachments() || this.disabled())
148
+ return;
149
+ event.preventDefault();
150
+ this.dragging.set(true);
151
+ }
152
+ onDrop(event) {
153
+ this.dragging.set(false);
154
+ if (!this.attachments() || this.disabled())
155
+ return;
156
+ event.preventDefault();
157
+ this.addFiles(event.dataTransfer?.files);
158
+ }
159
+ openPicker() {
160
+ this.fileInput()?.nativeElement.click();
161
+ }
162
+ onFilesPicked(event) {
163
+ const el = event.target;
164
+ this.addFiles(el.files);
165
+ el.value = '';
166
+ }
167
+ onSuggestion(text) {
168
+ if (this.disabled() || this.busy())
169
+ return;
170
+ this.send.emit({ text, files: [] });
171
+ }
172
+ onPrimary() {
173
+ if (this.busy())
174
+ this.stop.emit();
175
+ else
176
+ this.submit();
177
+ }
178
+ /** Human-readable size, e.g. `1.2 MB`. */
179
+ formatSize(bytes) {
180
+ if (bytes < 1024)
181
+ return `${bytes} B`;
182
+ if (bytes < 1024 * 1024)
183
+ return `${(bytes / 1024).toFixed(0)} kB`;
184
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
185
+ }
186
+ accepts(file) {
187
+ const accept = this.accept().trim();
188
+ if (!accept)
189
+ return true;
190
+ const ext = file.name.includes('.') ? '.' + file.name.split('.').pop().toLowerCase() : '';
191
+ return accept.split(',').some((rule) => {
192
+ const r = rule.trim().toLowerCase();
193
+ if (!r)
194
+ return false;
195
+ if (r.startsWith('.'))
196
+ return ext === r;
197
+ if (r.endsWith('/*'))
198
+ return file.type.toLowerCase().startsWith(r.slice(0, -1));
199
+ return file.type.toLowerCase() === r;
200
+ });
201
+ }
202
+ autosize() {
203
+ const el = this.textarea().nativeElement;
204
+ const view = el.ownerDocument.defaultView;
205
+ if (!view)
206
+ return;
207
+ const style = view.getComputedStyle(el);
208
+ const line = parseFloat(style.lineHeight) || 20;
209
+ const pad = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
210
+ const min = this.rows() * line + pad;
211
+ const max = this.maxRows() * line + pad;
212
+ el.style.height = 'auto';
213
+ const next = Math.min(Math.max(el.scrollHeight, min), max);
214
+ el.style.height = `${next}px`;
215
+ el.style.overflowY = el.scrollHeight > max ? 'auto' : 'hidden';
216
+ }
217
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPromptBox, deps: [], target: i0.ɵɵFactoryTarget.Component });
218
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkPromptBox, isStandalone: true, selector: "mk-prompt-box", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null }, sendOnEnter: { classPropertyName: "sendOnEnter", publicName: "sendOnEnter", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, autofocus: { classPropertyName: "autofocus", publicName: "autofocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", send: "send", stop: "stop" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "dragging.set(false)", "drop": "onDrop($event)" }, properties: { "class.mk-prompt-box--disabled": "disabled()", "class.mk-prompt-box--dragging": "dragging()" }, classAttribute: "mk-prompt-box" }, viewQueries: [{ propertyName: "textarea", first: true, predicate: ["textarea"], descendants: true, isSignal: true }, { propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (suggestions().length) {\n <div class=\"mk-prompt-box__suggestions\" role=\"group\" [attr.aria-label]=\"i18n.chatSuggestions\">\n @for (s of suggestions(); track s) {\n <button type=\"button\" class=\"mk-prompt-box__suggestion\" [disabled]=\"disabled() || busy()\" (click)=\"onSuggestion(s)\">\n {{ s }}\n </button>\n }\n </div>\n}\n\n<div class=\"mk-prompt-box__frame\">\n @if (files().length) {\n <ul class=\"mk-prompt-box__files\" [attr.aria-label]=\"i18n.chatAttachments\">\n @for (file of files(); track $index) {\n <li>\n <mk-chip size=\"sm\" removable [removeLabel]=\"i18n.chatRemoveAttachment\" (removed)=\"removeFile($index)\">\n <mk-icon [name]=\"file.type.startsWith('image/') ? 'image' : 'file'\" size=\"sm\" />\n {{ file.name }}\n <span class=\"mk-prompt-box__size\">{{ formatSize(file.size) }}</span>\n </mk-chip>\n </li>\n }\n </ul>\n }\n\n <textarea\n #textarea\n class=\"mk-prompt-box__input\"\n [id]=\"id\"\n [value]=\"value()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [attr.maxlength]=\"maxLength() > 0 ? maxLength() : null\"\n [attr.aria-label]=\"i18n.chatComposerLabel\"\n rows=\"1\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (paste)=\"onPaste($event)\"\n ></textarea>\n\n <div class=\"mk-prompt-box__actions\">\n @if (attachments()) {\n <input #fileInput type=\"file\" class=\"mk-prompt-box__file-input\" multiple [accept]=\"accept()\" tabindex=\"-1\" aria-hidden=\"true\" (change)=\"onFilesPicked($event)\" />\n <button\n mkButton\n type=\"button\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n class=\"mk-prompt-box__attach\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"i18n.chatAttach\"\n (click)=\"openPicker()\"\n >\n <mk-icon name=\"paperclip\" />\n </button>\n }\n @if (remaining() !== null) {\n <span class=\"mk-prompt-box__counter\" [class.mk-prompt-box__counter--over]=\"remaining()! < 0\" aria-live=\"polite\">{{ remaining() }}</span>\n }\n <button\n mkButton\n type=\"button\"\n size=\"sm\"\n class=\"mk-prompt-box__send\"\n [tone]=\"busy() ? 'neutral' : 'primary'\"\n [disabled]=\"disabled() || (!busy() && !canSend())\"\n [attr.aria-label]=\"busy() ? i18n.chatStop : i18n.chatSend\"\n (click)=\"onPrimary()\"\n >\n <mk-icon [name]=\"busy() ? 'square' : 'send'\" />\n </button>\n </div>\n</div>\n", styles: [":host{display:block;color:var(--mk-text)}.mk-prompt-box__suggestions{display:flex;flex-wrap:wrap;gap:var(--mk-space-2);margin-bottom:var(--mk-space-2)}.mk-prompt-box__suggestion{padding:var(--mk-space-1) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-full, 999px);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),border-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-prompt-box__suggestion:hover:not(:disabled){background:var(--mk-surface-2);border-color:var(--mk-border-strong, var(--mk-border))}.mk-prompt-box__suggestion:disabled{opacity:.5;cursor:default}.mk-prompt-box__suggestion:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-prompt-box__frame{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:\"files files\" \"input actions\";align-items:end;gap:var(--mk-space-2);padding:var(--mk-space-2) var(--mk-space-2) var(--mk-space-2) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}.mk-prompt-box__frame:focus-within{border-color:var(--mk-primary);box-shadow:0 0 0 var(--mk-focus-ring-width) var(--mk-focus-ring, var(--mk-primary-subtle))}:host(.mk-prompt-box--dragging) .mk-prompt-box__frame{border-style:dashed;border-color:var(--mk-primary);background:var(--mk-primary-subtle, var(--mk-surface-2))}:host(.mk-prompt-box--disabled) .mk-prompt-box__frame{opacity:.6}.mk-prompt-box__files{grid-area:files;display:flex;flex-wrap:wrap;gap:var(--mk-space-2);margin:0;padding:0;list-style:none}.mk-prompt-box__size{margin-inline-start:var(--mk-space-1);color:var(--mk-text-muted)}.mk-prompt-box__input{grid-area:input;width:100%;min-height:2.25rem;padding:var(--mk-space-2) 0;border:0;background:transparent;color:inherit;font:inherit;line-height:1.5;resize:none;outline:none}.mk-prompt-box__input::placeholder{color:var(--mk-text-muted)}.mk-prompt-box__actions{grid-area:actions;display:flex;align-items:center;gap:var(--mk-space-1)}.mk-prompt-box__file-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.mk-prompt-box__counter{font-size:var(--mk-font-size-xs);color:var(--mk-text-muted);font-variant-numeric:tabular-nums}.mk-prompt-box__counter--over{color:var(--mk-danger)}@media(pointer:coarse){.mk-prompt-box__input{font-size:max(var(--mk-font-size-md),16px)}}\n"], dependencies: [{ kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }, { kind: "component", type: MkChip, selector: "mk-chip", inputs: ["tone", "variant", "size", "selectable", "removable", "disabled", "removeLabel", "selected"], outputs: ["selectedChange", "removed"] }, { kind: "component", type: MkIcon, selector: "mk-icon", inputs: ["name", "size", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
219
+ }
220
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkPromptBox, decorators: [{
221
+ type: Component,
222
+ args: [{ selector: 'mk-prompt-box', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkButton, MkChip, MkIcon], host: {
223
+ class: 'mk-prompt-box',
224
+ '[class.mk-prompt-box--disabled]': 'disabled()',
225
+ '[class.mk-prompt-box--dragging]': 'dragging()',
226
+ '(dragover)': 'onDragOver($event)',
227
+ '(dragleave)': 'dragging.set(false)',
228
+ '(drop)': 'onDrop($event)',
229
+ }, template: "@if (suggestions().length) {\n <div class=\"mk-prompt-box__suggestions\" role=\"group\" [attr.aria-label]=\"i18n.chatSuggestions\">\n @for (s of suggestions(); track s) {\n <button type=\"button\" class=\"mk-prompt-box__suggestion\" [disabled]=\"disabled() || busy()\" (click)=\"onSuggestion(s)\">\n {{ s }}\n </button>\n }\n </div>\n}\n\n<div class=\"mk-prompt-box__frame\">\n @if (files().length) {\n <ul class=\"mk-prompt-box__files\" [attr.aria-label]=\"i18n.chatAttachments\">\n @for (file of files(); track $index) {\n <li>\n <mk-chip size=\"sm\" removable [removeLabel]=\"i18n.chatRemoveAttachment\" (removed)=\"removeFile($index)\">\n <mk-icon [name]=\"file.type.startsWith('image/') ? 'image' : 'file'\" size=\"sm\" />\n {{ file.name }}\n <span class=\"mk-prompt-box__size\">{{ formatSize(file.size) }}</span>\n </mk-chip>\n </li>\n }\n </ul>\n }\n\n <textarea\n #textarea\n class=\"mk-prompt-box__input\"\n [id]=\"id\"\n [value]=\"value()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [attr.maxlength]=\"maxLength() > 0 ? maxLength() : null\"\n [attr.aria-label]=\"i18n.chatComposerLabel\"\n rows=\"1\"\n (input)=\"onInput($event)\"\n (keydown)=\"onKeydown($event)\"\n (paste)=\"onPaste($event)\"\n ></textarea>\n\n <div class=\"mk-prompt-box__actions\">\n @if (attachments()) {\n <input #fileInput type=\"file\" class=\"mk-prompt-box__file-input\" multiple [accept]=\"accept()\" tabindex=\"-1\" aria-hidden=\"true\" (change)=\"onFilesPicked($event)\" />\n <button\n mkButton\n type=\"button\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n class=\"mk-prompt-box__attach\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"i18n.chatAttach\"\n (click)=\"openPicker()\"\n >\n <mk-icon name=\"paperclip\" />\n </button>\n }\n @if (remaining() !== null) {\n <span class=\"mk-prompt-box__counter\" [class.mk-prompt-box__counter--over]=\"remaining()! < 0\" aria-live=\"polite\">{{ remaining() }}</span>\n }\n <button\n mkButton\n type=\"button\"\n size=\"sm\"\n class=\"mk-prompt-box__send\"\n [tone]=\"busy() ? 'neutral' : 'primary'\"\n [disabled]=\"disabled() || (!busy() && !canSend())\"\n [attr.aria-label]=\"busy() ? i18n.chatStop : i18n.chatSend\"\n (click)=\"onPrimary()\"\n >\n <mk-icon [name]=\"busy() ? 'square' : 'send'\" />\n </button>\n </div>\n</div>\n", styles: [":host{display:block;color:var(--mk-text)}.mk-prompt-box__suggestions{display:flex;flex-wrap:wrap;gap:var(--mk-space-2);margin-bottom:var(--mk-space-2)}.mk-prompt-box__suggestion{padding:var(--mk-space-1) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-full, 999px);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard),border-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-prompt-box__suggestion:hover:not(:disabled){background:var(--mk-surface-2);border-color:var(--mk-border-strong, var(--mk-border))}.mk-prompt-box__suggestion:disabled{opacity:.5;cursor:default}.mk-prompt-box__suggestion:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-prompt-box__frame{display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:\"files files\" \"input actions\";align-items:end;gap:var(--mk-space-2);padding:var(--mk-space-2) var(--mk-space-2) var(--mk-space-2) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard)}.mk-prompt-box__frame:focus-within{border-color:var(--mk-primary);box-shadow:0 0 0 var(--mk-focus-ring-width) var(--mk-focus-ring, var(--mk-primary-subtle))}:host(.mk-prompt-box--dragging) .mk-prompt-box__frame{border-style:dashed;border-color:var(--mk-primary);background:var(--mk-primary-subtle, var(--mk-surface-2))}:host(.mk-prompt-box--disabled) .mk-prompt-box__frame{opacity:.6}.mk-prompt-box__files{grid-area:files;display:flex;flex-wrap:wrap;gap:var(--mk-space-2);margin:0;padding:0;list-style:none}.mk-prompt-box__size{margin-inline-start:var(--mk-space-1);color:var(--mk-text-muted)}.mk-prompt-box__input{grid-area:input;width:100%;min-height:2.25rem;padding:var(--mk-space-2) 0;border:0;background:transparent;color:inherit;font:inherit;line-height:1.5;resize:none;outline:none}.mk-prompt-box__input::placeholder{color:var(--mk-text-muted)}.mk-prompt-box__actions{grid-area:actions;display:flex;align-items:center;gap:var(--mk-space-1)}.mk-prompt-box__file-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}.mk-prompt-box__counter{font-size:var(--mk-font-size-xs);color:var(--mk-text-muted);font-variant-numeric:tabular-nums}.mk-prompt-box__counter--over{color:var(--mk-danger)}@media(pointer:coarse){.mk-prompt-box__input{font-size:max(var(--mk-font-size-md),16px)}}\n"] }]
230
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }], sendOnEnter: [{ type: i0.Input, args: [{ isSignal: true, alias: "sendOnEnter", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], autofocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autofocus", required: false }] }], send: [{ type: i0.Output, args: ["send"] }], stop: [{ type: i0.Output, args: ["stop"] }], textarea: [{ type: i0.ViewChild, args: ['textarea', { isSignal: true }] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
231
+
232
+ /**
233
+ * ChatMessage — one bubble: avatar, author and time, attachment previews,
234
+ * tool-call cards, the text (Markdown for assistants) with a streaming
235
+ * cursor, and copy / retry actions. `mk-chat` renders these for you; use it
236
+ * directly to build your own list.
237
+ */
238
+ class MkChatMessageComponent {
239
+ i18n = inject(MK_I18N);
240
+ /** The message to render. */
241
+ message = input.required(/* @ts-ignore */
242
+ ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
243
+ /** Render on the "own" (end) side — `mk-chat` sets this from `ownRole`. */
244
+ own = input(false, { ...(ngDevMode ? { debugName: "own" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
245
+ /** Render `text` as Markdown (assistants only; user text is always plain). */
246
+ markdown = input(true, { ...(ngDevMode ? { debugName: "markdown" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
247
+ showAvatar = input(true, { ...(ngDevMode ? { debugName: "showAvatar" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
248
+ showTimestamp = input(true, { ...(ngDevMode ? { debugName: "showTimestamp" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
249
+ /** Offer a copy button on hover / focus. */
250
+ copyable = input(true, { ...(ngDevMode ? { debugName: "copyable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
251
+ /** The retry button of a failed message was pressed. */
252
+ retry = output();
253
+ authorName = computed(() => {
254
+ const m = this.message();
255
+ if (m.author?.name)
256
+ return m.author.name;
257
+ return m.role === 'user' ? this.i18n.chatYou : m.role === 'assistant' ? this.i18n.chatAssistant : '';
258
+ }, /* @ts-ignore */
259
+ ...(ngDevMode ? [{ debugName: "authorName" }] : /* istanbul ignore next */ []));
260
+ time = computed(() => {
261
+ const ts = this.message().timestamp;
262
+ if (ts == null)
263
+ return '';
264
+ const d = ts instanceof Date ? ts : new Date(ts);
265
+ if (Number.isNaN(d.getTime()))
266
+ return '';
267
+ return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(d);
268
+ }, /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "time" }] : /* istanbul ignore next */ []));
270
+ ariaLabel = computed(() => {
271
+ const parts = [this.authorName(), this.time()].filter(Boolean);
272
+ return parts.join(', ') || null;
273
+ }, /* @ts-ignore */
274
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
275
+ useMarkdown = computed(() => this.markdown() && this.message().role === 'assistant', /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "useMarkdown" }] : /* istanbul ignore next */ []));
277
+ isImage(a) {
278
+ return !!a.url && !!a.type?.startsWith('image/');
279
+ }
280
+ formatSize(bytes) {
281
+ if (bytes == null)
282
+ return '';
283
+ if (bytes < 1024)
284
+ return `${bytes} B`;
285
+ if (bytes < 1024 * 1024)
286
+ return `${(bytes / 1024).toFixed(0)} kB`;
287
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
288
+ }
289
+ toolIcon(tool) {
290
+ return tool.status === 'done' ? 'check' : tool.status === 'error' ? 'circle-alert' : 'loader';
291
+ }
292
+ toolStatus(tool) {
293
+ return tool.status === 'done'
294
+ ? this.i18n.chatToolDone
295
+ : tool.status === 'error'
296
+ ? this.i18n.chatToolError
297
+ : this.i18n.chatToolRunning;
298
+ }
299
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkChatMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
300
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkChatMessageComponent, isStandalone: true, selector: "mk-chat-message", inputs: { message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: true, transformFunction: null }, own: { classPropertyName: "own", publicName: "own", isSignal: true, isRequired: false, transformFunction: null }, markdown: { classPropertyName: "markdown", publicName: "markdown", isSignal: true, isRequired: false, transformFunction: null }, showAvatar: { classPropertyName: "showAvatar", publicName: "showAvatar", isSignal: true, isRequired: false, transformFunction: null }, showTimestamp: { classPropertyName: "showTimestamp", publicName: "showTimestamp", isSignal: true, isRequired: false, transformFunction: null }, copyable: { classPropertyName: "copyable", publicName: "copyable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry" }, host: { attributes: { "role": "article" }, properties: { "class.mk-chat-message--own": "own()", "class.mk-chat-message--system": "message().role === \"system\"", "class.mk-chat-message--streaming": "message().streaming", "class.mk-chat-message--error": "!!message().error", "attr.aria-label": "ariaLabel()" }, classAttribute: "mk-chat-message" }, ngImport: i0, template: "@let m = message();\n@if (m.role === 'system') {\n <div class=\"mk-chat-message__system\">{{ m.text }}</div>\n} @else {\n @if (showAvatar()) {\n <mk-avatar class=\"mk-chat-message__avatar\" [name]=\"authorName()\" [src]=\"m.author?.avatar\" size=\"sm\" />\n }\n <div class=\"mk-chat-message__body\">\n <div class=\"mk-chat-message__meta\">\n <span class=\"mk-chat-message__author\">{{ authorName() }}</span>\n @if (showTimestamp() && time()) {\n <span class=\"mk-chat-message__time\">{{ time() }}</span>\n }\n </div>\n\n <div class=\"mk-chat-message__bubble\">\n @if (m.tools?.length) {\n <div class=\"mk-chat-message__tools\">\n @for (tool of m.tools; track tool.id ?? $index) {\n <details class=\"mk-chat-message__tool\" [class.mk-chat-message__tool--error]=\"tool.status === 'error'\">\n <summary>\n <mk-icon [name]=\"toolIcon(tool)\" size=\"sm\" [class.mk-chat-message__spin]=\"tool.status === 'running'\" />\n <code>{{ tool.name }}</code>\n @if (tool.summary) { <span class=\"mk-chat-message__tool-summary\">{{ tool.summary }}</span> }\n <span class=\"mk-visually-hidden\">{{ toolStatus(tool) }}</span>\n </summary>\n @if (tool.input) { <pre class=\"mk-chat-message__tool-io\"><code>{{ tool.input }}</code></pre> }\n @if (tool.output) { <pre class=\"mk-chat-message__tool-io\"><code>{{ tool.output }}</code></pre> }\n </details>\n }\n </div>\n }\n\n @if (m.attachments?.length) {\n <ul class=\"mk-chat-message__attachments\" [attr.aria-label]=\"i18n.chatAttachments\">\n @for (a of m.attachments; track $index) {\n <li>\n @if (isImage(a)) {\n <a [href]=\"a.url\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"mk-chat-message__image\">\n <img [src]=\"a.url\" [alt]=\"a.name\" loading=\"lazy\" />\n </a>\n } @else {\n <a [href]=\"a.url || null\" [attr.download]=\"a.url ? a.name : null\" class=\"mk-chat-message__file\">\n <mk-icon name=\"file\" size=\"sm\" />\n <span>{{ a.name }}</span>\n @if (a.size != null) { <span class=\"mk-chat-message__file-size\">{{ formatSize(a.size) }}</span> }\n </a>\n }\n </li>\n }\n </ul>\n }\n\n @if (m.text || m.streaming) {\n <div class=\"mk-chat-message__text\">\n @if (useMarkdown()) {\n <mk-markdown [source]=\"m.text\" linkTarget=\"_blank\" />\n } @else {\n <p class=\"mk-chat-message__plain\">{{ m.text }}</p>\n }\n @if (m.streaming) {\n <span class=\"mk-chat-message__cursor\" aria-hidden=\"true\"></span>\n <span class=\"mk-visually-hidden\">{{ i18n.chatStreaming }}</span>\n }\n </div>\n }\n </div>\n\n @if (m.error) {\n <div class=\"mk-chat-message__error\" role=\"alert\">\n <mk-icon name=\"circle-alert\" size=\"sm\" />\n <span>{{ m.error }}</span>\n <button mkButton type=\"button\" variant=\"ghost\" tone=\"danger\" size=\"sm\" (click)=\"retry.emit(m)\">\n <mk-icon name=\"refresh-cw\" size=\"sm\" /> {{ i18n.chatRetry }}\n </button>\n </div>\n }\n\n @if (copyable() && m.text && !m.streaming) {\n <div class=\"mk-chat-message__actions\">\n <button mkButton type=\"button\" variant=\"ghost\" tone=\"neutral\" size=\"sm\" [mkCopyToClipboard]=\"m.text\" [attr.aria-label]=\"i18n.chatCopy\">\n <mk-icon name=\"copy\" size=\"sm\" />\n </button>\n </div>\n }\n </div>\n}\n", styles: [":host{display:flex;gap:var(--mk-space-3);max-width:100%;color:var(--mk-text)}:host(.mk-chat-message--own){flex-direction:row-reverse}:host(.mk-chat-message--system){justify-content:center}.mk-chat-message__system{padding:var(--mk-space-1) var(--mk-space-3);border-radius:var(--mk-radius-full, 999px);background:var(--mk-surface-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs);text-align:center}.mk-chat-message__avatar{flex:none;margin-top:var(--mk-space-5)}.mk-chat-message__body{display:flex;flex-direction:column;gap:var(--mk-space-1);min-width:0;max-width:min(100%,44rem)}:host(.mk-chat-message--own) .mk-chat-message__body{align-items:flex-end}.mk-chat-message__meta{display:flex;gap:var(--mk-space-2);align-items:baseline;padding:0 var(--mk-space-1);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-chat-message__author{font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}.mk-chat-message__bubble{display:flex;flex-direction:column;gap:var(--mk-space-2);padding:var(--mk-space-3) var(--mk-space-4);border-radius:var(--mk-radius-lg);border-start-start-radius:var(--mk-radius-sm);background:var(--mk-surface-2);font-size:var(--mk-font-size-md);line-height:1.55;overflow-wrap:break-word}:host(.mk-chat-message--own) .mk-chat-message__bubble{border-start-start-radius:var(--mk-radius-lg);border-start-end-radius:var(--mk-radius-sm);background:var(--mk-primary);color:var(--mk-primary-contrast, #fff)}.mk-chat-message__plain{margin:0;color:inherit;white-space:pre-wrap}.mk-chat-message__text{position:relative}.mk-chat-message__text ::ng-deep .mk-markdown,.mk-chat-message__text ::ng-deep .mk-markdown p{color:inherit}.mk-chat-message__text ::ng-deep .mk-markdown>:first-child{margin-top:0}.mk-chat-message__text ::ng-deep .mk-markdown>:last-child{margin-bottom:0}.mk-chat-message__cursor{display:inline-block;width:.55em;height:1.1em;margin-inline-start:.15em;vertical-align:text-bottom;background:currentColor;border-radius:1px;animation:mk-chat-blink 1s steps(2,start) infinite}@keyframes mk-chat-blink{to{visibility:hidden}}.mk-chat-message__tools{display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-chat-message__tool{border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);font-size:var(--mk-font-size-sm)}.mk-chat-message__tool summary{display:flex;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-1) var(--mk-space-2);cursor:pointer;list-style:none}.mk-chat-message__tool summary::-webkit-details-marker{display:none}.mk-chat-message__tool code{font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs)}.mk-chat-message__tool--error{border-color:var(--mk-danger);color:var(--mk-danger)}.mk-chat-message__tool-summary{color:var(--mk-text-muted)}.mk-chat-message__tool-io{margin:0;padding:var(--mk-space-2);border-top:var(--mk-border-width) solid var(--mk-border);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs);white-space:pre-wrap;overflow-wrap:break-word;max-height:16rem;overflow:auto}.mk-chat-message__spin{animation:mk-chat-spin 1s linear infinite}@keyframes mk-chat-spin{to{transform:rotate(360deg)}}.mk-chat-message__attachments{display:flex;flex-wrap:wrap;gap:var(--mk-space-2);margin:0;padding:0;list-style:none}.mk-chat-message__image{display:block;max-width:16rem;border-radius:var(--mk-radius-md);overflow:hidden}.mk-chat-message__image img{display:block;max-width:100%;height:auto}.mk-chat-message__file{display:inline-flex;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-1) var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font-size:var(--mk-font-size-sm);text-decoration:none}.mk-chat-message__file-size{color:var(--mk-text-muted)}.mk-chat-message__error{display:flex;align-items:center;gap:var(--mk-space-2);padding:0 var(--mk-space-1);color:var(--mk-danger);font-size:var(--mk-font-size-sm)}.mk-chat-message__actions{display:flex;gap:var(--mk-space-1);opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}:host(:hover) .mk-chat-message__actions,:host(:focus-within) .mk-chat-message__actions{opacity:1}@media(prefers-reduced-motion:reduce){.mk-chat-message__cursor,.mk-chat-message__spin{animation:none}}@media(hover:none){.mk-chat-message__actions{opacity:1}}\n"], dependencies: [{ kind: "component", type: MkAvatar, selector: "mk-avatar", inputs: ["src", "name", "alt", "size", "shape", "status"] }, { kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }, { kind: "directive", type: MkCopyToClipboard, selector: "[mkCopyToClipboard]", inputs: ["mkCopyToClipboard", "mkCopyFeedbackDuration"], outputs: ["copiedText", "copyFailed"], exportAs: ["mkCopyToClipboard"] }, { kind: "component", type: MkIcon, selector: "mk-icon", inputs: ["name", "size", "label"] }, { kind: "component", type: MkMarkdown, selector: "mk-markdown", inputs: ["source", "autolink", "linkTarget"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
301
+ }
302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkChatMessageComponent, decorators: [{
303
+ type: Component,
304
+ args: [{ selector: 'mk-chat-message', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkAvatar, MkButton, MkCopyToClipboard, MkIcon, MkMarkdown], host: {
305
+ class: 'mk-chat-message',
306
+ role: 'article',
307
+ '[class.mk-chat-message--own]': 'own()',
308
+ '[class.mk-chat-message--system]': 'message().role === "system"',
309
+ '[class.mk-chat-message--streaming]': 'message().streaming',
310
+ '[class.mk-chat-message--error]': '!!message().error',
311
+ '[attr.aria-label]': 'ariaLabel()',
312
+ }, template: "@let m = message();\n@if (m.role === 'system') {\n <div class=\"mk-chat-message__system\">{{ m.text }}</div>\n} @else {\n @if (showAvatar()) {\n <mk-avatar class=\"mk-chat-message__avatar\" [name]=\"authorName()\" [src]=\"m.author?.avatar\" size=\"sm\" />\n }\n <div class=\"mk-chat-message__body\">\n <div class=\"mk-chat-message__meta\">\n <span class=\"mk-chat-message__author\">{{ authorName() }}</span>\n @if (showTimestamp() && time()) {\n <span class=\"mk-chat-message__time\">{{ time() }}</span>\n }\n </div>\n\n <div class=\"mk-chat-message__bubble\">\n @if (m.tools?.length) {\n <div class=\"mk-chat-message__tools\">\n @for (tool of m.tools; track tool.id ?? $index) {\n <details class=\"mk-chat-message__tool\" [class.mk-chat-message__tool--error]=\"tool.status === 'error'\">\n <summary>\n <mk-icon [name]=\"toolIcon(tool)\" size=\"sm\" [class.mk-chat-message__spin]=\"tool.status === 'running'\" />\n <code>{{ tool.name }}</code>\n @if (tool.summary) { <span class=\"mk-chat-message__tool-summary\">{{ tool.summary }}</span> }\n <span class=\"mk-visually-hidden\">{{ toolStatus(tool) }}</span>\n </summary>\n @if (tool.input) { <pre class=\"mk-chat-message__tool-io\"><code>{{ tool.input }}</code></pre> }\n @if (tool.output) { <pre class=\"mk-chat-message__tool-io\"><code>{{ tool.output }}</code></pre> }\n </details>\n }\n </div>\n }\n\n @if (m.attachments?.length) {\n <ul class=\"mk-chat-message__attachments\" [attr.aria-label]=\"i18n.chatAttachments\">\n @for (a of m.attachments; track $index) {\n <li>\n @if (isImage(a)) {\n <a [href]=\"a.url\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"mk-chat-message__image\">\n <img [src]=\"a.url\" [alt]=\"a.name\" loading=\"lazy\" />\n </a>\n } @else {\n <a [href]=\"a.url || null\" [attr.download]=\"a.url ? a.name : null\" class=\"mk-chat-message__file\">\n <mk-icon name=\"file\" size=\"sm\" />\n <span>{{ a.name }}</span>\n @if (a.size != null) { <span class=\"mk-chat-message__file-size\">{{ formatSize(a.size) }}</span> }\n </a>\n }\n </li>\n }\n </ul>\n }\n\n @if (m.text || m.streaming) {\n <div class=\"mk-chat-message__text\">\n @if (useMarkdown()) {\n <mk-markdown [source]=\"m.text\" linkTarget=\"_blank\" />\n } @else {\n <p class=\"mk-chat-message__plain\">{{ m.text }}</p>\n }\n @if (m.streaming) {\n <span class=\"mk-chat-message__cursor\" aria-hidden=\"true\"></span>\n <span class=\"mk-visually-hidden\">{{ i18n.chatStreaming }}</span>\n }\n </div>\n }\n </div>\n\n @if (m.error) {\n <div class=\"mk-chat-message__error\" role=\"alert\">\n <mk-icon name=\"circle-alert\" size=\"sm\" />\n <span>{{ m.error }}</span>\n <button mkButton type=\"button\" variant=\"ghost\" tone=\"danger\" size=\"sm\" (click)=\"retry.emit(m)\">\n <mk-icon name=\"refresh-cw\" size=\"sm\" /> {{ i18n.chatRetry }}\n </button>\n </div>\n }\n\n @if (copyable() && m.text && !m.streaming) {\n <div class=\"mk-chat-message__actions\">\n <button mkButton type=\"button\" variant=\"ghost\" tone=\"neutral\" size=\"sm\" [mkCopyToClipboard]=\"m.text\" [attr.aria-label]=\"i18n.chatCopy\">\n <mk-icon name=\"copy\" size=\"sm\" />\n </button>\n </div>\n }\n </div>\n}\n", styles: [":host{display:flex;gap:var(--mk-space-3);max-width:100%;color:var(--mk-text)}:host(.mk-chat-message--own){flex-direction:row-reverse}:host(.mk-chat-message--system){justify-content:center}.mk-chat-message__system{padding:var(--mk-space-1) var(--mk-space-3);border-radius:var(--mk-radius-full, 999px);background:var(--mk-surface-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs);text-align:center}.mk-chat-message__avatar{flex:none;margin-top:var(--mk-space-5)}.mk-chat-message__body{display:flex;flex-direction:column;gap:var(--mk-space-1);min-width:0;max-width:min(100%,44rem)}:host(.mk-chat-message--own) .mk-chat-message__body{align-items:flex-end}.mk-chat-message__meta{display:flex;gap:var(--mk-space-2);align-items:baseline;padding:0 var(--mk-space-1);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-chat-message__author{font-weight:var(--mk-font-weight-semibold);color:var(--mk-text)}.mk-chat-message__bubble{display:flex;flex-direction:column;gap:var(--mk-space-2);padding:var(--mk-space-3) var(--mk-space-4);border-radius:var(--mk-radius-lg);border-start-start-radius:var(--mk-radius-sm);background:var(--mk-surface-2);font-size:var(--mk-font-size-md);line-height:1.55;overflow-wrap:break-word}:host(.mk-chat-message--own) .mk-chat-message__bubble{border-start-start-radius:var(--mk-radius-lg);border-start-end-radius:var(--mk-radius-sm);background:var(--mk-primary);color:var(--mk-primary-contrast, #fff)}.mk-chat-message__plain{margin:0;color:inherit;white-space:pre-wrap}.mk-chat-message__text{position:relative}.mk-chat-message__text ::ng-deep .mk-markdown,.mk-chat-message__text ::ng-deep .mk-markdown p{color:inherit}.mk-chat-message__text ::ng-deep .mk-markdown>:first-child{margin-top:0}.mk-chat-message__text ::ng-deep .mk-markdown>:last-child{margin-bottom:0}.mk-chat-message__cursor{display:inline-block;width:.55em;height:1.1em;margin-inline-start:.15em;vertical-align:text-bottom;background:currentColor;border-radius:1px;animation:mk-chat-blink 1s steps(2,start) infinite}@keyframes mk-chat-blink{to{visibility:hidden}}.mk-chat-message__tools{display:flex;flex-direction:column;gap:var(--mk-space-1)}.mk-chat-message__tool{border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);font-size:var(--mk-font-size-sm)}.mk-chat-message__tool summary{display:flex;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-1) var(--mk-space-2);cursor:pointer;list-style:none}.mk-chat-message__tool summary::-webkit-details-marker{display:none}.mk-chat-message__tool code{font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs)}.mk-chat-message__tool--error{border-color:var(--mk-danger);color:var(--mk-danger)}.mk-chat-message__tool-summary{color:var(--mk-text-muted)}.mk-chat-message__tool-io{margin:0;padding:var(--mk-space-2);border-top:var(--mk-border-width) solid var(--mk-border);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs);white-space:pre-wrap;overflow-wrap:break-word;max-height:16rem;overflow:auto}.mk-chat-message__spin{animation:mk-chat-spin 1s linear infinite}@keyframes mk-chat-spin{to{transform:rotate(360deg)}}.mk-chat-message__attachments{display:flex;flex-wrap:wrap;gap:var(--mk-space-2);margin:0;padding:0;list-style:none}.mk-chat-message__image{display:block;max-width:16rem;border-radius:var(--mk-radius-md);overflow:hidden}.mk-chat-message__image img{display:block;max-width:100%;height:auto}.mk-chat-message__file{display:inline-flex;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-1) var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font-size:var(--mk-font-size-sm);text-decoration:none}.mk-chat-message__file-size{color:var(--mk-text-muted)}.mk-chat-message__error{display:flex;align-items:center;gap:var(--mk-space-2);padding:0 var(--mk-space-1);color:var(--mk-danger);font-size:var(--mk-font-size-sm)}.mk-chat-message__actions{display:flex;gap:var(--mk-space-1);opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}:host(:hover) .mk-chat-message__actions,:host(:focus-within) .mk-chat-message__actions{opacity:1}@media(prefers-reduced-motion:reduce){.mk-chat-message__cursor,.mk-chat-message__spin{animation:none}}@media(hover:none){.mk-chat-message__actions{opacity:1}}\n"] }]
313
+ }], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: true }] }], own: [{ type: i0.Input, args: [{ isSignal: true, alias: "own", required: false }] }], markdown: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdown", required: false }] }], showAvatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAvatar", required: false }] }], showTimestamp: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTimestamp", required: false }] }], copyable: [{ type: i0.Input, args: [{ isSignal: true, alias: "copyable", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }] } });
314
+
315
+ /**
316
+ * Custom message rendering for `mk-chat`:
317
+ *
318
+ * ```html
319
+ * <mk-chat [messages]="messages()">
320
+ * <ng-template mkChatMessageDef let-message let-own="own">
321
+ * <my-bubble [message]="message" [mine]="own" />
322
+ * </ng-template>
323
+ * </mk-chat>
324
+ * ```
325
+ */
326
+ class MkChatMessageDef {
327
+ template = inject((TemplateRef));
328
+ static ngTemplateContextGuard(_dir, ctx) {
329
+ return true;
330
+ }
331
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkChatMessageDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
332
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: MkChatMessageDef, isStandalone: true, selector: "ng-template[mkChatMessageDef]", ngImport: i0 });
333
+ }
334
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkChatMessageDef, decorators: [{
335
+ type: Directive,
336
+ args: [{ selector: 'ng-template[mkChatMessageDef]' }]
337
+ }] });
338
+ /**
339
+ * Chat — a conversation: a scrolling, screen-reader-announced log of
340
+ * `mk-chat-message` bubbles that stays pinned to the newest message while
341
+ * text streams in (with a *jump to latest* button once the reader scrolls
342
+ * up), a typing indicator, and an `mk-prompt-box` composer.
343
+ *
344
+ * The component is presentational: hand it `messages` (replace the array or
345
+ * a message object to update — signals compare by reference) and react to
346
+ * `(send)` / `(stop)` / `(retry)` with your own transport.
347
+ *
348
+ * ```html
349
+ * <mk-chat
350
+ * [messages]="messages()"
351
+ * [busy]="generating()"
352
+ * [typing]="peerTyping()"
353
+ * attachments
354
+ * [suggestions]="starters"
355
+ * (send)="ask($event)"
356
+ * (stop)="abort()"
357
+ * (retry)="resend($event)"
358
+ * style="height: 32rem"
359
+ * >
360
+ * <div mkChatHeader>…title, model picker…</div>
361
+ * <div mkChatEmpty>…first-run hint…</div>
362
+ * </mk-chat>
363
+ * ```
364
+ *
365
+ * Slots: `[mkChatHeader]`, `[mkChatEmpty]`, `[mkChatFooter]` (under the composer).
366
+ */
367
+ class MkChat {
368
+ i18n = inject(MK_I18N);
369
+ /** The conversation, oldest first. */
370
+ messages = input([], /* @ts-ignore */
371
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
372
+ /** Which role renders on the "own" side (default `user`). */
373
+ ownRole = input('user', /* @ts-ignore */
374
+ ...(ngDevMode ? [{ debugName: "ownRole" }] : /* istanbul ignore next */ []));
375
+ /** Render assistant text as Markdown. */
376
+ markdown = input(true, { ...(ngDevMode ? { debugName: "markdown" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
377
+ showAvatars = input(true, { ...(ngDevMode ? { debugName: "showAvatars" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
378
+ showTimestamps = input(true, { ...(ngDevMode ? { debugName: "showTimestamps" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
379
+ /** Someone else is typing (shows the dots indicator). */
380
+ typing = input(false, { ...(ngDevMode ? { debugName: "typing" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
381
+ /** A reply is being generated: composer shows *stop*. */
382
+ busy = input(false, { ...(ngDevMode ? { debugName: "busy" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
383
+ /** Disable the composer. */
384
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
385
+ /** Hide the composer entirely (read-only transcript). */
386
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
387
+ placeholder = input(undefined, /* @ts-ignore */
388
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
389
+ /** Allow attachments in the composer. */
390
+ attachments = input(false, { ...(ngDevMode ? { debugName: "attachments" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
391
+ accept = input('', /* @ts-ignore */
392
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
393
+ maxFiles = input(0, /* @ts-ignore */
394
+ ...(ngDevMode ? [{ debugName: "maxFiles" }] : /* istanbul ignore next */ []));
395
+ maxLength = input(0, /* @ts-ignore */
396
+ ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
397
+ /** Quick replies above the composer; each click sends its text. */
398
+ suggestions = input([], /* @ts-ignore */
399
+ ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
400
+ /** Text shown when there are no messages (or project `[mkChatEmpty]`). */
401
+ emptyMessage = input(undefined, /* @ts-ignore */
402
+ ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
403
+ /** Keep the log pinned to the newest message while the reader is at the bottom. */
404
+ autoScroll = input(true, { ...(ngDevMode ? { debugName: "autoScroll" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
405
+ /** Accessible name of the log region. */
406
+ ariaLabel = input(undefined, /* @ts-ignore */
407
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
408
+ send = output();
409
+ stop = output();
410
+ retry = output();
411
+ messageDef = contentChild(MkChatMessageDef, /* @ts-ignore */
412
+ ...(ngDevMode ? [{ debugName: "messageDef" }] : /* istanbul ignore next */ []));
413
+ log = viewChild.required('log', /* @ts-ignore */
414
+ ...(ngDevMode ? [{ debugName: "log" }] : /* istanbul ignore next */ []));
415
+ composer = viewChild(MkPromptBox, /* @ts-ignore */
416
+ ...(ngDevMode ? [{ debugName: "composer" }] : /* istanbul ignore next */ []));
417
+ /** The reader is (near) the bottom of the log. */
418
+ atBottom = signal(true, /* @ts-ignore */
419
+ ...(ngDevMode ? [{ debugName: "atBottom" }] : /* istanbul ignore next */ []));
420
+ /** Messages that arrived while scrolled up. */
421
+ unseen = signal(0, /* @ts-ignore */
422
+ ...(ngDevMode ? [{ debugName: "unseen" }] : /* istanbul ignore next */ []));
423
+ lastCount = 0;
424
+ resolvedEmpty = computed(() => this.emptyMessage() ?? this.i18n.chatEmpty, /* @ts-ignore */
425
+ ...(ngDevMode ? [{ debugName: "resolvedEmpty" }] : /* istanbul ignore next */ []));
426
+ resolvedPlaceholder = computed(() => this.placeholder() ?? this.i18n.chatPlaceholder, /* @ts-ignore */
427
+ ...(ngDevMode ? [{ debugName: "resolvedPlaceholder" }] : /* istanbul ignore next */ []));
428
+ constructor() {
429
+ // Pin to the bottom on new content while the reader is there.
430
+ afterRenderEffect(() => {
431
+ const list = this.messages();
432
+ const last = list[list.length - 1];
433
+ // Track text growth of a streaming message as well as new messages.
434
+ void last?.text.length;
435
+ void this.typing();
436
+ const count = list.length;
437
+ untracked(() => {
438
+ const grew = count > this.lastCount;
439
+ this.lastCount = count;
440
+ if (!this.autoScroll())
441
+ return;
442
+ if (this.atBottom())
443
+ this.scrollToBottom();
444
+ else if (grew)
445
+ this.unseen.update((n) => n + 1);
446
+ });
447
+ });
448
+ }
449
+ /** Scroll the log to the newest message. */
450
+ scrollToBottom() {
451
+ const el = this.log().nativeElement;
452
+ el.scrollTop = el.scrollHeight;
453
+ this.atBottom.set(true);
454
+ this.unseen.set(0);
455
+ }
456
+ /** Focus the composer. */
457
+ focus() {
458
+ this.composer()?.focus();
459
+ }
460
+ onScroll() {
461
+ const el = this.log().nativeElement;
462
+ const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
463
+ const near = distance < 48;
464
+ this.atBottom.set(near);
465
+ if (near)
466
+ this.unseen.set(0);
467
+ }
468
+ isOwn(m) {
469
+ return m.role === this.ownRole();
470
+ }
471
+ trackMessage = (_, m) => m.id;
472
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkChat, deps: [], target: i0.ɵɵFactoryTarget.Component });
473
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkChat, isStandalone: true, selector: "mk-chat", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null }, ownRole: { classPropertyName: "ownRole", publicName: "ownRole", isSignal: true, isRequired: false, transformFunction: null }, markdown: { classPropertyName: "markdown", publicName: "markdown", isSignal: true, isRequired: false, transformFunction: null }, showAvatars: { classPropertyName: "showAvatars", publicName: "showAvatars", isSignal: true, isRequired: false, transformFunction: null }, showTimestamps: { classPropertyName: "showTimestamps", publicName: "showTimestamps", isSignal: true, isRequired: false, transformFunction: null }, typing: { classPropertyName: "typing", publicName: "typing", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, attachments: { classPropertyName: "attachments", publicName: "attachments", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, autoScroll: { classPropertyName: "autoScroll", publicName: "autoScroll", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { send: "send", stop: "stop", retry: "retry" }, host: { classAttribute: "mk-chat" }, queries: [{ propertyName: "messageDef", first: true, predicate: MkChatMessageDef, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "log", first: true, predicate: ["log"], descendants: true, isSignal: true }, { propertyName: "composer", first: true, predicate: MkPromptBox, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"mk-chat__header\">\n <ng-content select=\"[mkChatHeader]\" />\n</div>\n\n<div class=\"mk-chat__viewport\">\n <div\n #log\n class=\"mk-chat__log\"\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n [attr.aria-label]=\"ariaLabel() ?? i18n.chatLabel\"\n (scroll)=\"onScroll()\"\n >\n @if (!messages().length) {\n <div class=\"mk-chat__empty\">\n <ng-content select=\"[mkChatEmpty]\">\n <mk-icon name=\"message-circle\" size=\"lg\" />\n <p>{{ resolvedEmpty() }}</p>\n </ng-content>\n </div>\n }\n @for (m of messages(); track m.id; let i = $index) {\n @if (messageDef(); as def) {\n <ng-container *ngTemplateOutlet=\"def.template; context: { $implicit: m, own: isOwn(m), index: i }\" />\n } @else {\n <mk-chat-message\n [message]=\"m\"\n [own]=\"isOwn(m)\"\n [markdown]=\"markdown()\"\n [showAvatar]=\"showAvatars()\"\n [showTimestamp]=\"showTimestamps()\"\n (retry)=\"retry.emit($event)\"\n />\n }\n }\n @if (typing()) {\n <div class=\"mk-chat__typing\" [attr.aria-label]=\"i18n.chatTyping\" role=\"status\">\n <span></span><span></span><span></span>\n </div>\n }\n </div>\n\n @if (!atBottom()) {\n <button mkButton type=\"button\" size=\"sm\" variant=\"outline\" tone=\"neutral\" class=\"mk-chat__jump\" (click)=\"scrollToBottom()\">\n <mk-icon name=\"arrow-down\" size=\"sm\" />\n {{ i18n.chatJumpToLatest }}\n @if (unseen() > 0) { <span class=\"mk-chat__badge\">{{ unseen() }}</span> }\n </button>\n }\n</div>\n\n@if (!readonly()) {\n <div class=\"mk-chat__composer\">\n <mk-prompt-box\n [placeholder]=\"resolvedPlaceholder()\"\n [disabled]=\"disabled()\"\n [busy]=\"busy()\"\n [attachments]=\"attachments()\"\n [accept]=\"accept()\"\n [maxFiles]=\"maxFiles()\"\n [maxLength]=\"maxLength()\"\n [suggestions]=\"suggestions()\"\n (send)=\"send.emit($event)\"\n (stop)=\"stop.emit()\"\n />\n <div class=\"mk-chat__footer\">\n <ng-content select=\"[mkChatFooter]\" />\n </div>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;min-height:0;height:100%;color:var(--mk-text);background:var(--mk-bg)}.mk-chat__header:empty,.mk-chat__footer:empty{display:none}.mk-chat__header{flex:none;padding:var(--mk-space-3) var(--mk-space-4);border-bottom:var(--mk-border-width) solid var(--mk-border)}.mk-chat__viewport{position:relative;flex:1 1 auto;min-height:0;display:flex}.mk-chat__log{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--mk-space-4);padding:var(--mk-space-4);overflow-y:auto;overscroll-behavior:contain;scroll-behavior:smooth}@media(prefers-reduced-motion:reduce){.mk-chat__log{scroll-behavior:auto}}.mk-chat__empty{margin:auto;display:flex;flex-direction:column;align-items:center;gap:var(--mk-space-2);color:var(--mk-text-muted);text-align:center}.mk-chat__empty p{margin:0}.mk-chat__typing{display:inline-flex;gap:4px;align-self:flex-start;padding:var(--mk-space-3) var(--mk-space-4);border-radius:var(--mk-radius-lg);background:var(--mk-surface-2)}.mk-chat__typing span{width:6px;height:6px;border-radius:50%;background:var(--mk-text-muted);animation:mk-chat-dot 1.2s infinite ease-in-out}.mk-chat__typing span:nth-child(2){animation-delay:.15s}.mk-chat__typing span:nth-child(3){animation-delay:.3s}@keyframes mk-chat-dot{0%,60%,to{transform:translateY(0);opacity:.5}30%{transform:translateY(-4px);opacity:1}}@media(prefers-reduced-motion:reduce){.mk-chat__typing span{animation:none}}.mk-chat__jump{position:absolute;bottom:var(--mk-space-3);left:50%;transform:translate(-50%);box-shadow:var(--mk-shadow-md)}.mk-chat__badge{min-width:1.25em;padding:0 .4em;border-radius:var(--mk-radius-full, 999px);background:var(--mk-primary);color:var(--mk-primary-contrast, #fff);font-size:var(--mk-font-size-xs);text-align:center}.mk-chat__composer{flex:none;padding:var(--mk-space-3) var(--mk-space-4) var(--mk-space-4);border-top:var(--mk-border-width) solid var(--mk-border);background:var(--mk-bg)}.mk-chat__footer{margin-top:var(--mk-space-2);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted);text-align:center}\n"], dependencies: [{ kind: "component", type: MkButton, selector: "button[mkButton], a[mkButton]", inputs: ["variant", "tone", "size", "loading", "fullWidth", "iconOnly", "disabled"] }, { kind: "component", type: MkChatMessageComponent, selector: "mk-chat-message", inputs: ["message", "own", "markdown", "showAvatar", "showTimestamp", "copyable"], outputs: ["retry"] }, { kind: "component", type: MkIcon, selector: "mk-icon", inputs: ["name", "size", "label"] }, { kind: "component", type: MkPromptBox, selector: "mk-prompt-box", inputs: ["value", "placeholder", "disabled", "busy", "attachments", "accept", "maxFiles", "maxLength", "rows", "maxRows", "sendOnEnter", "suggestions", "autofocus"], outputs: ["valueChange", "send", "stop"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
474
+ }
475
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkChat, decorators: [{
476
+ type: Component,
477
+ args: [{ selector: 'mk-chat', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MkButton, MkChatMessageComponent, MkIcon, MkPromptBox, NgTemplateOutlet], host: { class: 'mk-chat' }, template: "<div class=\"mk-chat__header\">\n <ng-content select=\"[mkChatHeader]\" />\n</div>\n\n<div class=\"mk-chat__viewport\">\n <div\n #log\n class=\"mk-chat__log\"\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n [attr.aria-label]=\"ariaLabel() ?? i18n.chatLabel\"\n (scroll)=\"onScroll()\"\n >\n @if (!messages().length) {\n <div class=\"mk-chat__empty\">\n <ng-content select=\"[mkChatEmpty]\">\n <mk-icon name=\"message-circle\" size=\"lg\" />\n <p>{{ resolvedEmpty() }}</p>\n </ng-content>\n </div>\n }\n @for (m of messages(); track m.id; let i = $index) {\n @if (messageDef(); as def) {\n <ng-container *ngTemplateOutlet=\"def.template; context: { $implicit: m, own: isOwn(m), index: i }\" />\n } @else {\n <mk-chat-message\n [message]=\"m\"\n [own]=\"isOwn(m)\"\n [markdown]=\"markdown()\"\n [showAvatar]=\"showAvatars()\"\n [showTimestamp]=\"showTimestamps()\"\n (retry)=\"retry.emit($event)\"\n />\n }\n }\n @if (typing()) {\n <div class=\"mk-chat__typing\" [attr.aria-label]=\"i18n.chatTyping\" role=\"status\">\n <span></span><span></span><span></span>\n </div>\n }\n </div>\n\n @if (!atBottom()) {\n <button mkButton type=\"button\" size=\"sm\" variant=\"outline\" tone=\"neutral\" class=\"mk-chat__jump\" (click)=\"scrollToBottom()\">\n <mk-icon name=\"arrow-down\" size=\"sm\" />\n {{ i18n.chatJumpToLatest }}\n @if (unseen() > 0) { <span class=\"mk-chat__badge\">{{ unseen() }}</span> }\n </button>\n }\n</div>\n\n@if (!readonly()) {\n <div class=\"mk-chat__composer\">\n <mk-prompt-box\n [placeholder]=\"resolvedPlaceholder()\"\n [disabled]=\"disabled()\"\n [busy]=\"busy()\"\n [attachments]=\"attachments()\"\n [accept]=\"accept()\"\n [maxFiles]=\"maxFiles()\"\n [maxLength]=\"maxLength()\"\n [suggestions]=\"suggestions()\"\n (send)=\"send.emit($event)\"\n (stop)=\"stop.emit()\"\n />\n <div class=\"mk-chat__footer\">\n <ng-content select=\"[mkChatFooter]\" />\n </div>\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;min-height:0;height:100%;color:var(--mk-text);background:var(--mk-bg)}.mk-chat__header:empty,.mk-chat__footer:empty{display:none}.mk-chat__header{flex:none;padding:var(--mk-space-3) var(--mk-space-4);border-bottom:var(--mk-border-width) solid var(--mk-border)}.mk-chat__viewport{position:relative;flex:1 1 auto;min-height:0;display:flex}.mk-chat__log{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--mk-space-4);padding:var(--mk-space-4);overflow-y:auto;overscroll-behavior:contain;scroll-behavior:smooth}@media(prefers-reduced-motion:reduce){.mk-chat__log{scroll-behavior:auto}}.mk-chat__empty{margin:auto;display:flex;flex-direction:column;align-items:center;gap:var(--mk-space-2);color:var(--mk-text-muted);text-align:center}.mk-chat__empty p{margin:0}.mk-chat__typing{display:inline-flex;gap:4px;align-self:flex-start;padding:var(--mk-space-3) var(--mk-space-4);border-radius:var(--mk-radius-lg);background:var(--mk-surface-2)}.mk-chat__typing span{width:6px;height:6px;border-radius:50%;background:var(--mk-text-muted);animation:mk-chat-dot 1.2s infinite ease-in-out}.mk-chat__typing span:nth-child(2){animation-delay:.15s}.mk-chat__typing span:nth-child(3){animation-delay:.3s}@keyframes mk-chat-dot{0%,60%,to{transform:translateY(0);opacity:.5}30%{transform:translateY(-4px);opacity:1}}@media(prefers-reduced-motion:reduce){.mk-chat__typing span{animation:none}}.mk-chat__jump{position:absolute;bottom:var(--mk-space-3);left:50%;transform:translate(-50%);box-shadow:var(--mk-shadow-md)}.mk-chat__badge{min-width:1.25em;padding:0 .4em;border-radius:var(--mk-radius-full, 999px);background:var(--mk-primary);color:var(--mk-primary-contrast, #fff);font-size:var(--mk-font-size-xs);text-align:center}.mk-chat__composer{flex:none;padding:var(--mk-space-3) var(--mk-space-4) var(--mk-space-4);border-top:var(--mk-border-width) solid var(--mk-border);background:var(--mk-bg)}.mk-chat__footer{margin-top:var(--mk-space-2);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted);text-align:center}\n"] }]
478
+ }], ctorParameters: () => [], propDecorators: { messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], ownRole: [{ type: i0.Input, args: [{ isSignal: true, alias: "ownRole", required: false }] }], markdown: [{ type: i0.Input, args: [{ isSignal: true, alias: "markdown", required: false }] }], showAvatars: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAvatars", required: false }] }], showTimestamps: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTimestamps", required: false }] }], typing: [{ type: i0.Input, args: [{ isSignal: true, alias: "typing", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], attachments: [{ type: i0.Input, args: [{ isSignal: true, alias: "attachments", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], autoScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoScroll", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], send: [{ type: i0.Output, args: ["send"] }], stop: [{ type: i0.Output, args: ["stop"] }], retry: [{ type: i0.Output, args: ["retry"] }], messageDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => MkChatMessageDef), { isSignal: true }] }], log: [{ type: i0.ViewChild, args: ['log', { isSignal: true }] }], composer: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MkPromptBox), { isSignal: true }] }] } });
479
+
480
+ /**
481
+ * @mk-kit/ui/chat — conversational UI: `mk-chat` (log + composer),
482
+ * `mk-chat-message` and `mk-prompt-box`.
483
+ */
484
+
485
+ /**
486
+ * Generated bundle index. Do not edit.
487
+ */
488
+
489
+ export { MkChat, MkChatMessageComponent, MkChatMessageDef, MkPromptBox };
490
+ //# sourceMappingURL=mk-kit-ui-chat.mjs.map