@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.
- package/README.md +11 -6
- package/fesm2022/mk-kit-ui-chat.mjs +490 -0
- package/fesm2022/mk-kit-ui-chat.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-core.mjs +310 -1
- package/fesm2022/mk-kit-ui-core.mjs.map +1 -1
- package/fesm2022/mk-kit-ui-dynamic-form.mjs +578 -0
- package/fesm2022/mk-kit-ui-dynamic-form.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-feedback.mjs +400 -8
- package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -1
- package/fesm2022/mk-kit-ui-forms.mjs +816 -6
- package/fesm2022/mk-kit-ui-forms.mjs.map +1 -1
- package/fesm2022/mk-kit-ui-query-builder.mjs +276 -0
- package/fesm2022/mk-kit-ui-query-builder.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-table.mjs +20 -1
- package/fesm2022/mk-kit-ui-table.mjs.map +1 -1
- package/fesm2022/mk-kit-ui-testing.mjs +834 -0
- package/fesm2022/mk-kit-ui-testing.mjs.map +1 -0
- package/fesm2022/mk-kit-ui.mjs +3 -0
- package/fesm2022/mk-kit-ui.mjs.map +1 -1
- package/package.json +17 -1
- package/types/mk-kit-ui-chat.d.ts +288 -0
- package/types/mk-kit-ui-core.d.ts +167 -2
- package/types/mk-kit-ui-dynamic-form.d.ts +305 -0
- package/types/mk-kit-ui-feedback.d.ts +111 -2
- package/types/mk-kit-ui-forms.d.ts +264 -3
- package/types/mk-kit-ui-query-builder.d.ts +98 -0
- package/types/mk-kit-ui-table.d.ts +12 -0
- package/types/mk-kit-ui-testing.d.ts +344 -0
- package/types/mk-kit-ui.d.ts +3 -0
|
@@ -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
|