@memberjunction/ng-composer 0.0.0 → 5.45.1

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.
Files changed (26) hide show
  1. package/README.md +81 -28
  2. package/dist/lib/components/mention/mention-dropdown.component.d.ts +53 -0
  3. package/dist/lib/components/mention/mention-dropdown.component.d.ts.map +1 -0
  4. package/dist/lib/components/mention/mention-dropdown.component.js +297 -0
  5. package/dist/lib/components/mention/mention-dropdown.component.js.map +1 -0
  6. package/dist/lib/components/mention/mention-editor.component.d.ts +294 -0
  7. package/dist/lib/components/mention/mention-editor.component.d.ts.map +1 -0
  8. package/dist/lib/components/mention/mention-editor.component.js +1342 -0
  9. package/dist/lib/components/mention/mention-editor.component.js.map +1 -0
  10. package/dist/lib/components/message/message-input-box.component.d.ts +139 -0
  11. package/dist/lib/components/message/message-input-box.component.d.ts.map +1 -0
  12. package/dist/lib/components/message/message-input-box.component.js +340 -0
  13. package/dist/lib/components/message/message-input-box.component.js.map +1 -0
  14. package/dist/lib/composer-trigger-provider.d.ts +123 -0
  15. package/dist/lib/composer-trigger-provider.d.ts.map +1 -0
  16. package/dist/lib/composer-trigger-provider.js +91 -0
  17. package/dist/lib/composer-trigger-provider.js.map +1 -0
  18. package/dist/lib/composer.module.d.ts +32 -0
  19. package/dist/lib/composer.module.d.ts.map +1 -0
  20. package/dist/lib/composer.module.js +62 -0
  21. package/dist/lib/composer.module.js.map +1 -0
  22. package/dist/public-api.d.ts +6 -0
  23. package/dist/public-api.d.ts.map +1 -0
  24. package/dist/public-api.js +15 -0
  25. package/dist/public-api.js.map +1 -0
  26. package/package.json +37 -7
@@ -0,0 +1,1342 @@
1
+ import { Component, Input, Output, EventEmitter, ViewChild, forwardRef } from '@angular/core';
2
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
3
+ import { DiscoverComposerTriggerProviders } from '../../composer-trigger-provider';
4
+ import { UUIDsEqual } from '@memberjunction/global';
5
+ import * as i0 from "@angular/core";
6
+ import * as i1 from "./mention-dropdown.component";
7
+ const _c0 = ["editor"];
8
+ function MentionEditorComponent_Conditional_1_For_2_Conditional_1_Template(rf, ctx) { if (rf & 1) {
9
+ i0.ɵɵelement(0, "img", 8);
10
+ } if (rf & 2) {
11
+ const attachment_r3 = i0.ɵɵnextContext().$implicit;
12
+ i0.ɵɵproperty("src", attachment_r3.thumbnailUrl, i0.ɵɵsanitizeUrl)("alt", attachment_r3.fileName);
13
+ } }
14
+ function MentionEditorComponent_Conditional_1_For_2_Conditional_2_Template(rf, ctx) { if (rf & 1) {
15
+ i0.ɵɵelementStart(0, "div", 9);
16
+ i0.ɵɵelement(1, "i", 12);
17
+ i0.ɵɵelementStart(2, "span", 13);
18
+ i0.ɵɵtext(3);
19
+ i0.ɵɵelementEnd()();
20
+ } if (rf & 2) {
21
+ const attachment_r3 = i0.ɵɵnextContext().$implicit;
22
+ i0.ɵɵadvance(3);
23
+ i0.ɵɵtextInterpolate(attachment_r3.fileName);
24
+ } }
25
+ function MentionEditorComponent_Conditional_1_For_2_Template(rf, ctx) { if (rf & 1) {
26
+ const _r2 = i0.ɵɵgetCurrentView();
27
+ i0.ɵɵelementStart(0, "div", 7);
28
+ i0.ɵɵlistener("click", function MentionEditorComponent_Conditional_1_For_2_Template_div_click_0_listener() { const attachment_r3 = i0.ɵɵrestoreView(_r2).$implicit; const ctx_r3 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r3.onAttachmentClick(attachment_r3)); });
29
+ i0.ɵɵconditionalCreate(1, MentionEditorComponent_Conditional_1_For_2_Conditional_1_Template, 1, 2, "img", 8);
30
+ i0.ɵɵconditionalCreate(2, MentionEditorComponent_Conditional_1_For_2_Conditional_2_Template, 4, 1, "div", 9);
31
+ i0.ɵɵelementStart(3, "button", 10);
32
+ i0.ɵɵlistener("click", function MentionEditorComponent_Conditional_1_For_2_Template_button_click_3_listener($event) { const attachment_r3 = i0.ɵɵrestoreView(_r2).$implicit; const ctx_r3 = i0.ɵɵnextContext(2); ctx_r3.removeAttachment(attachment_r3.id); return i0.ɵɵresetView($event.stopPropagation()); });
33
+ i0.ɵɵelement(4, "i", 11);
34
+ i0.ɵɵelementEnd()();
35
+ } if (rf & 2) {
36
+ const attachment_r3 = ctx.$implicit;
37
+ i0.ɵɵadvance();
38
+ i0.ɵɵconditional(attachment_r3.thumbnailUrl ? 1 : -1);
39
+ i0.ɵɵadvance();
40
+ i0.ɵɵconditional(!attachment_r3.thumbnailUrl ? 2 : -1);
41
+ } }
42
+ function MentionEditorComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
43
+ i0.ɵɵelementStart(0, "div", 2);
44
+ i0.ɵɵrepeaterCreate(1, MentionEditorComponent_Conditional_1_For_2_Template, 5, 2, "div", 6, i0.ɵɵrepeaterTrackByIdentity);
45
+ i0.ɵɵelementEnd();
46
+ } if (rf & 2) {
47
+ const ctx_r3 = i0.ɵɵnextContext();
48
+ i0.ɵɵadvance();
49
+ i0.ɵɵrepeater(ctx_r3.pendingAttachments);
50
+ } }
51
+ function MentionEditorComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
52
+ i0.ɵɵelementStart(0, "div", 4)(1, "div", 14);
53
+ i0.ɵɵelement(2, "i", 15);
54
+ i0.ɵɵelementStart(3, "span");
55
+ i0.ɵɵtext(4, "Drop files here");
56
+ i0.ɵɵelementEnd()()();
57
+ } }
58
+ function MentionEditorComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
59
+ const _r5 = i0.ɵɵgetCurrentView();
60
+ i0.ɵɵelementStart(0, "mj-mention-dropdown", 16);
61
+ i0.ɵɵlistener("suggestionSelected", function MentionEditorComponent_Conditional_5_Template_mj_mention_dropdown_suggestionSelected_0_listener($event) { i0.ɵɵrestoreView(_r5); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.onMentionSelected($event)); })("closed", function MentionEditorComponent_Conditional_5_Template_mj_mention_dropdown_closed_0_listener() { i0.ɵɵrestoreView(_r5); const ctx_r3 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r3.closeMentionDropdown()); });
62
+ i0.ɵɵelementEnd();
63
+ } if (rf & 2) {
64
+ const ctx_r3 = i0.ɵɵnextContext();
65
+ i0.ɵɵproperty("suggestions", ctx_r3.mentionSuggestions)("position", ctx_r3.mentionDropdownPosition)("showAbove", ctx_r3.mentionDropdownShowAbove)("useFixedPositioning", true)("visible", true);
66
+ } }
67
+ /**
68
+ * ContentEditable-based mention editor with visual chips/pills
69
+ * Provides Slack/Teams-style mention UX with immutable mention tokens
70
+ */
71
+ export class MentionEditorComponent {
72
+ cdr;
73
+ editorRef;
74
+ placeholder = 'Type @ to mention agents or users, # for entities...';
75
+ disabled = false;
76
+ currentUser;
77
+ /** Master switch for all mention/command triggers. When false no providers are consulted. */
78
+ enableMentions = true;
79
+ /**
80
+ * Explicit trigger-provider list — when bound, EXACTLY these providers are active
81
+ * (explicit list wins over discovery). Leave null to discover providers registered
82
+ * with the ClassFactory via `@RegisterClass(ComposerTriggerProvider, '<Key>')`.
83
+ */
84
+ TriggerProviders = null;
85
+ /**
86
+ * Discovery-mode filter: providers whose stable `Key` appears here are skipped when
87
+ * resolving registered providers. Ignored when `TriggerProviders` is bound.
88
+ */
89
+ ExcludedTriggerKeys = [];
90
+ /** Optional metadata provider scoping this editor (multi-provider hosts); passed through to trigger providers. */
91
+ Provider = null;
92
+ /** Auto-focus the editor after view init (the composer default). Disable when embedded in forms. */
93
+ autoFocus = true;
94
+ // Attachment settings
95
+ enableAttachments = true;
96
+ maxAttachments = 10;
97
+ maxAttachmentSizeBytes = 20 * 1024 * 1024; // 20MB default
98
+ acceptedFileTypes = 'image/*'; // MIME types to accept
99
+ valueChange = new EventEmitter();
100
+ mentionSelected = new EventEmitter();
101
+ enterPressed = new EventEmitter();
102
+ attachmentsChanged = new EventEmitter();
103
+ attachmentError = new EventEmitter();
104
+ attachmentClicked = new EventEmitter();
105
+ // Pending attachments state
106
+ pendingAttachments = [];
107
+ isDragOver = false;
108
+ // Mention dropdown state
109
+ showMentionDropdown = false;
110
+ mentionSuggestions = [];
111
+ mentionDropdownPosition = { top: 0, left: 0 };
112
+ mentionDropdownShowAbove = false;
113
+ mentionStartIndex = -1;
114
+ mentionQuery = '';
115
+ /** The trigger char that opened the current mention dropdown */
116
+ activeTrigger = '@';
117
+ /**
118
+ * Monotonic sequence guarding the async suggestion fetch — a response only applies
119
+ * when no newer keystroke (or dropdown close) has superseded it.
120
+ */
121
+ suggestionRequestSeq = 0;
122
+ /** Cap on the suggestions displayed; passed to providers as `MaxResults`. */
123
+ static MaxSuggestions = 50;
124
+ /**
125
+ * The trigger providers this editor consults: the explicit `TriggerProviders` list
126
+ * when bound, otherwise ClassFactory discovery filtered by `ExcludedTriggerKeys`.
127
+ * Empty when `enableMentions` is off (or nothing is registered) — the editor then
128
+ * behaves as a plain text editor.
129
+ */
130
+ get activeProviders() {
131
+ if (!this.enableMentions) {
132
+ return [];
133
+ }
134
+ return this.TriggerProviders ?? DiscoverComposerTriggerProviders(this.ExcludedTriggerKeys);
135
+ }
136
+ /** Unique trigger characters across the active providers (priority order preserved). */
137
+ get mentionTriggers() {
138
+ const triggers = [];
139
+ for (const provider of this.activeProviders) {
140
+ if (!triggers.includes(provider.TriggerChar)) {
141
+ triggers.push(provider.TriggerChar);
142
+ }
143
+ }
144
+ return triggers;
145
+ }
146
+ onChange = () => { };
147
+ onTouched = () => { };
148
+ /** Value written via writeValue() before the view (editorRef) exists — applied in ngAfterViewInit. */
149
+ pendingWriteValue = null;
150
+ constructor(cdr) {
151
+ this.cdr = cdr;
152
+ }
153
+ async ngOnInit() {
154
+ // Warm up the active providers (best-effort — a provider that fails to warm
155
+ // simply serves its first real request cold).
156
+ if (this.enableMentions) {
157
+ const user = this.currentUser ?? null;
158
+ await Promise.all(this.activeProviders.map((p) => p.Initialize(user).catch(() => undefined)));
159
+ }
160
+ }
161
+ ngAfterViewInit() {
162
+ // Apply any value written through the ControlValueAccessor before the view existed
163
+ // (NgModel calls writeValue during directive init, before this component's view is ready)
164
+ if (this.pendingWriteValue !== null) {
165
+ this.setEditorContent(this.pendingWriteValue);
166
+ this.pendingWriteValue = null;
167
+ }
168
+ // Auto-focus the editor
169
+ if (this.autoFocus) {
170
+ setTimeout(() => {
171
+ this.editorRef?.nativeElement?.focus();
172
+ }, 100);
173
+ }
174
+ }
175
+ /**
176
+ * Handle clicks on the container - focus the editor if clicking outside the contentEditable
177
+ */
178
+ onContainerClick(event) {
179
+ const target = event.target;
180
+ const editor = this.editorRef?.nativeElement;
181
+ // Don't handle clicks on the dropdown or its children
182
+ if (target.closest('mj-mention-dropdown')) {
183
+ return;
184
+ }
185
+ // If clicking on container or any element that's not the editor itself, focus the editor
186
+ if (target !== editor && !editor?.contains(target)) {
187
+ editor?.focus();
188
+ // Move cursor to end of content
189
+ const selection = window.getSelection();
190
+ const range = document.createRange();
191
+ if (editor && selection) {
192
+ range.selectNodeContents(editor);
193
+ range.collapse(false); // Collapse to end
194
+ selection.removeAllRanges();
195
+ selection.addRange(range);
196
+ }
197
+ }
198
+ }
199
+ /**
200
+ * Handle input changes in contentEditable
201
+ */
202
+ onInput() {
203
+ const plainText = this.getPlainText();
204
+ this.onChange(plainText);
205
+ this.valueChange.emit(plainText);
206
+ // Handle mention/command autocomplete via the active trigger providers
207
+ if (this.enableMentions) {
208
+ this.handleMentionInput();
209
+ }
210
+ }
211
+ /**
212
+ * Handle blur event - close dropdown when editor loses focus
213
+ */
214
+ onBlur() {
215
+ // Call form control touched callback
216
+ this.onTouched();
217
+ // Close dropdown when editor loses focus
218
+ // Use setTimeout to allow mousedown events on dropdown to fire first
219
+ setTimeout(() => {
220
+ if (this.showMentionDropdown) {
221
+ this.closeMentionDropdown();
222
+ }
223
+ }, 200);
224
+ }
225
+ /**
226
+ * Handle paste event - images or plain text
227
+ */
228
+ onPaste(event) {
229
+ // Check for image data in clipboard
230
+ if (this.enableAttachments && event.clipboardData?.items) {
231
+ const items = Array.from(event.clipboardData.items);
232
+ const imageItems = items.filter(item => item.type.startsWith('image/'));
233
+ if (imageItems.length > 0) {
234
+ event.preventDefault();
235
+ // Process each image
236
+ for (const item of imageItems) {
237
+ const file = item.getAsFile();
238
+ if (file) {
239
+ this.processFile(file);
240
+ }
241
+ }
242
+ return;
243
+ }
244
+ }
245
+ // No images - handle as plain text
246
+ event.preventDefault();
247
+ // Get plain text from clipboard
248
+ const text = event.clipboardData?.getData('text/plain') || '';
249
+ if (!text)
250
+ return;
251
+ // Insert plain text at cursor position
252
+ const selection = window.getSelection();
253
+ if (!selection || selection.rangeCount === 0)
254
+ return;
255
+ const range = selection.getRangeAt(0);
256
+ range.deleteContents();
257
+ // Insert text as text node (not HTML)
258
+ const textNode = document.createTextNode(text);
259
+ range.insertNode(textNode);
260
+ // Move cursor to end of inserted text
261
+ range.setStartAfter(textNode);
262
+ range.collapse(true);
263
+ selection.removeAllRanges();
264
+ selection.addRange(range);
265
+ // Trigger input event to update model
266
+ this.onInput();
267
+ }
268
+ /**
269
+ * Handle keydown events
270
+ */
271
+ onKeyDown(event) {
272
+ // Enter alone: Send message (if dropdown not showing)
273
+ if (event.key === 'Enter' && !event.shiftKey && !this.showMentionDropdown) {
274
+ event.preventDefault();
275
+ const plainText = this.getPlainText();
276
+ this.enterPressed.emit(plainText);
277
+ return;
278
+ }
279
+ // Backspace: Check if deleting a mention chip
280
+ if (event.key === 'Backspace') {
281
+ this.handleBackspace(event);
282
+ }
283
+ // Handle mention dropdown navigation
284
+ if (this.showMentionDropdown) {
285
+ // Let the dropdown handle arrow keys, enter, escape
286
+ // (We'll pass these through to mention-dropdown component)
287
+ }
288
+ }
289
+ /**
290
+ * Handle mention/command trigger detection — finds the nearest active trigger char
291
+ * before the cursor and kicks off the (async) provider suggestion fetch.
292
+ */
293
+ handleMentionInput() {
294
+ const selection = window.getSelection();
295
+ if (!selection || selection.rangeCount === 0)
296
+ return;
297
+ const activeTriggers = this.mentionTriggers;
298
+ if (activeTriggers.length === 0) {
299
+ this.closeMentionDropdown();
300
+ return;
301
+ }
302
+ const range = selection.getRangeAt(0);
303
+ const textBeforeCursor = this.getTextBeforeCursor(range);
304
+ // Find the nearest active trigger char before the cursor — whichever is closest wins
305
+ let triggerIndex = -1;
306
+ let triggerChar = '@';
307
+ for (const t of activeTriggers) {
308
+ const idx = textBeforeCursor.lastIndexOf(t);
309
+ if (idx > triggerIndex) {
310
+ triggerIndex = idx;
311
+ triggerChar = t;
312
+ }
313
+ }
314
+ if (triggerIndex === -1) {
315
+ this.closeMentionDropdown();
316
+ return;
317
+ }
318
+ // Check if there's a space between the trigger and cursor (means mention was completed)
319
+ const textAfterTrigger = textBeforeCursor.substring(triggerIndex + 1);
320
+ if (textAfterTrigger.includes(' ')) {
321
+ this.closeMentionDropdown();
322
+ return;
323
+ }
324
+ // Extract query
325
+ this.mentionQuery = textAfterTrigger;
326
+ this.mentionStartIndex = triggerIndex;
327
+ this.activeTrigger = triggerChar;
328
+ // Fetch suggestions from the provider(s) owning this trigger (async, race-guarded)
329
+ void this.fetchSuggestions(triggerChar, textAfterTrigger);
330
+ }
331
+ /**
332
+ * Routes the suggestion request to every active provider whose TriggerChar matches,
333
+ * concatenates their results in priority order, caps the merged list, and opens the
334
+ * dropdown. A monotonic sequence drops stale responses when the user keeps typing.
335
+ */
336
+ async fetchSuggestions(triggerChar, query) {
337
+ const seq = ++this.suggestionRequestSeq;
338
+ const providers = this.activeProviders.filter((p) => p.TriggerChar === triggerChar);
339
+ const request = {
340
+ Query: query,
341
+ MaxResults: MentionEditorComponent.MaxSuggestions,
342
+ ContextUser: this.currentUser ?? null,
343
+ Provider: this.Provider
344
+ };
345
+ const resultSets = await Promise.all(providers.map((p) => p.GetSuggestions(request).catch(() => [])));
346
+ if (seq !== this.suggestionRequestSeq) {
347
+ return; // superseded by a newer keystroke or an explicit close
348
+ }
349
+ const merged = resultSets.flat().slice(0, MentionEditorComponent.MaxSuggestions);
350
+ this.mentionSuggestions = merged;
351
+ if (merged.length > 0) {
352
+ this.showMentionDropdown = true;
353
+ this.positionMentionDropdown();
354
+ }
355
+ else {
356
+ this.closeMentionDropdown();
357
+ }
358
+ this.cdr.detectChanges();
359
+ }
360
+ /**
361
+ * Get text before cursor position
362
+ */
363
+ getTextBeforeCursor(range) {
364
+ const tempRange = range.cloneRange();
365
+ tempRange.selectNodeContents(this.editorRef.nativeElement);
366
+ tempRange.setEnd(range.startContainer, range.startOffset);
367
+ return tempRange.toString();
368
+ }
369
+ /**
370
+ * Position the mention dropdown
371
+ */
372
+ positionMentionDropdown() {
373
+ const editor = this.editorRef?.nativeElement;
374
+ if (!editor)
375
+ return;
376
+ // Get the parent container (message-input-box-container) for alignment
377
+ const container = editor.closest('.message-input-box-container');
378
+ const containerRect = container?.getBoundingClientRect();
379
+ const selection = window.getSelection();
380
+ if (!selection || selection.rangeCount === 0)
381
+ return;
382
+ const range = selection.getRangeAt(0);
383
+ const cursorRect = range.getBoundingClientRect();
384
+ // Check space below vs above
385
+ const spaceBelow = window.innerHeight - cursorRect.bottom;
386
+ const spaceAbove = cursorRect.top;
387
+ const dropdownHeight = Math.min(this.mentionSuggestions.length * 56, 300);
388
+ this.mentionDropdownShowAbove = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
389
+ if (this.mentionDropdownShowAbove) {
390
+ // Position above, aligning with container top if possible
391
+ this.mentionDropdownPosition = {
392
+ top: containerRect ? containerRect.top + window.scrollY : cursorRect.top + window.scrollY - 4,
393
+ left: cursorRect.left + window.scrollX
394
+ };
395
+ }
396
+ else {
397
+ // Position below cursor, but align bottom edge with container top
398
+ this.mentionDropdownPosition = {
399
+ top: containerRect ? containerRect.top + window.scrollY : cursorRect.bottom + window.scrollY + 4,
400
+ left: cursorRect.left + window.scrollX
401
+ };
402
+ }
403
+ }
404
+ /**
405
+ * Handle mention selection from dropdown
406
+ */
407
+ onMentionSelected(suggestion) {
408
+ this.insertMentionChip(suggestion);
409
+ this.closeMentionDropdown();
410
+ this.mentionSelected.emit(suggestion);
411
+ // Refocus the editor after selection
412
+ setTimeout(() => {
413
+ this.editorRef?.nativeElement?.focus();
414
+ }, 50);
415
+ }
416
+ /**
417
+ * Insert a mention chip at the current cursor position
418
+ */
419
+ insertMentionChip(suggestion) {
420
+ const selection = window.getSelection();
421
+ if (!selection || selection.rangeCount === 0)
422
+ return;
423
+ const range = selection.getRangeAt(0);
424
+ // Delete the trigger + query text (use the active trigger char)
425
+ const textBeforeCursor = this.getTextBeforeCursor(range);
426
+ const lastAtIndex = textBeforeCursor.lastIndexOf(this.activeTrigger);
427
+ const deleteLength = textBeforeCursor.length - lastAtIndex;
428
+ range.setStart(range.startContainer, range.startOffset - deleteLength);
429
+ range.deleteContents();
430
+ // Create mention chip element
431
+ const chip = this.createMentionChip(suggestion);
432
+ // Insert chip
433
+ range.insertNode(chip);
434
+ // Add space after chip
435
+ const space = document.createTextNode(' ');
436
+ range.collapse(false);
437
+ range.insertNode(space);
438
+ // Move cursor after the space
439
+ range.setStartAfter(space);
440
+ range.collapse(true);
441
+ selection.removeAllRanges();
442
+ selection.addRange(range);
443
+ // Trigger change detection
444
+ this.onInput();
445
+ }
446
+ /**
447
+ * Create a mention chip DOM element
448
+ */
449
+ createMentionChip(suggestion) {
450
+ const chip = document.createElement('span');
451
+ chip.className = 'mention-chip';
452
+ chip.contentEditable = 'false'; // Make chip non-editable
453
+ chip.setAttribute('data-mention-id', suggestion.id);
454
+ chip.setAttribute('data-mention-type', suggestion.type);
455
+ chip.setAttribute('data-mention-name', suggestion.name);
456
+ // Configuration presets ride along on the suggestion itself (supplied by the trigger
457
+ // provider — e.g. the agent-mentions provider maps AI Agent Configuration presets in).
458
+ // The composer has no knowledge of what a preset IS; it just stores the default's
459
+ // ID/Name on the chip and offers a picker when there are 2+.
460
+ const presets = suggestion.presets ?? [];
461
+ if (presets.length > 0) {
462
+ const defaultPreset = presets.find(p => p.IsDefault) || presets[0];
463
+ if (defaultPreset) {
464
+ chip.setAttribute('data-preset-id', defaultPreset.ID || '');
465
+ chip.setAttribute('data-preset-name', defaultPreset.Name || '');
466
+ }
467
+ }
468
+ // Apply inline styles directly — color by mention type. Skills carry their own accent Color
469
+ // (AISkill.Color UX metadata); when present it drives the chip, otherwise the standard skill
470
+ // green (--mj-status-success). Same color logic as the saved-message badge — keep in sync with
471
+ // message-item's renderMentionHTML.
472
+ const skillPalette = suggestion.type === 'skill'
473
+ ? { bg: suggestion.color || 'var(--mj-status-success)', border: 'rgba(255, 255, 255, 0.35)' }
474
+ : null;
475
+ const palette = skillPalette
476
+ ? skillPalette
477
+ : suggestion.type === 'user'
478
+ ? { bg: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', border: 'rgba(240, 147, 251, 0.4)' }
479
+ : suggestion.type === 'entity'
480
+ ? { bg: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)', border: 'rgba(17, 153, 142, 0.4)' }
481
+ : suggestion.type === 'query'
482
+ ? { bg: 'linear-gradient(135deg, #FF6A00 0%, #FF9E2C 100%)', border: 'rgba(255, 106, 0, 0.4)' }
483
+ : { bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', border: 'rgba(102, 126, 234, 0.4)' };
484
+ // Skills render as a sharp-cornered rectangle (vs. the rounded pill for agents/users/entities)
485
+ // — the shape is the at-a-glance differentiator. Same shape in the saved-message badge.
486
+ const chipRadius = suggestion.type === 'skill' ? '4px' : '16px';
487
+ chip.style.cssText = `
488
+ display: inline-flex;
489
+ align-items: center;
490
+ gap: 5px;
491
+ padding: 4px 12px;
492
+ margin: 0 3px;
493
+ border-radius: ${chipRadius};
494
+ font-size: 13px;
495
+ font-weight: 600;
496
+ cursor: default;
497
+ user-select: none;
498
+ vertical-align: middle;
499
+ white-space: nowrap;
500
+ pointer-events: all;
501
+ background: ${palette.bg};
502
+ color: white;
503
+ border: 2px solid ${palette.border};
504
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2), 0 1px 2px rgba(0, 0, 0, 0.1);
505
+ `;
506
+ // Add icon or image
507
+ if (suggestion.type === 'agent' && suggestion.imageUrl) {
508
+ // Use image if available (LogoURL from agent)
509
+ const img = document.createElement('img');
510
+ img.src = suggestion.imageUrl;
511
+ img.alt = suggestion.displayName;
512
+ img.style.cssText = 'width: 16px; height: 16px; border-radius: 50%; object-fit: cover;';
513
+ chip.appendChild(img);
514
+ }
515
+ else if (suggestion.type === 'agent' && suggestion.icon === 'mj-icon-skip') {
516
+ // Special handling for mj-icon-skip: use the SVG data URI as an image
517
+ const img = document.createElement('img');
518
+ img.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 101.89918457031249 96.83947368421053'%3E%3Cg transform='translate(-0.1288232421875,-0.0)'%3E%3Cpath d='M93.85,41.56c-.84,0-1.62.2-2.37.55-3-4.35-7.49-8.12-13.04-11.04l.04-7.18v-14.44h-10.24v17.6c-1.52-.43-3.07-.8-4.67-1.11V0h-10.24v24.72s-.09,0-.14,0h-4.38s-.1,0-.14,0V7.3h-10.24v18.62c-1.6.32-3.15.69-4.67,1.11v-11.67h-10.24v6.09l.04,9.6c-5.55,2.92-10.04,6.7-13.04,11.04-.75-.35-1.53-.55-2.37-.55-4.5,0-8.14,5.61-8.14,12.51s3.64,12.53,8.14,12.53c.58,0,1.14-.12,1.67-.29,4.1,6.62,11.54,12.06,20.98,15.28l.79.13v7.05c0,2.97,1.45,5.58,3.87,6.99,1.18.69,2.5,1.04,3.85,1.03,1.4,0,2.83-.37,4.15-1.12l7.54-4.29,7.56,4.3c1.31.74,2.73,1.12,4.13,1.12s2.67-.35,3.85-1.04c2.42-1.41,3.86-4.02,3.86-6.98v-7.05l.79-.13c9.44-3.22,16.89-8.66,20.98-15.28.54.17,1.09.29,1.68.29,4.5,0,8.14-5.61,8.14-12.53s-3.63-12.51-8.14-12.51' fill='%23AAAAAA'/%3E%3Cpath d='M86.69,50.87c0-12.22-13.6-19.1-28.94-20.66-4.48-.47-9.19-.54-13.52,0-15.34,1.53-28.93,8.41-28.93,20.66,0,8.55,5.7,15.55,12.68,15.55h7.94c3.05,2.5,6.93,4.1,11.08,4.71,2.65.4,5.44.46,8.01,0,4.15-.6,8.05-2.2,11.1-4.71h7.92c6.97,0,12.68-7,12.68-15.55' fill='white' opacity='0.9'/%3E%3Cpath d='M57.83,55.82c-1.19,2.58-3.8,4.35-6.84,4.35s-5.65-1.77-6.84-4.35h13.68Z' fill='%23AAAAAA'/%3E%3Cpath d='M32.52,41.14c1.74,0,3.18,2.13,3.18,4.76s-1.44,4.74-3.18,4.74-3.16-2.13-3.16-4.74,1.41-4.76,3.16-4.76' fill='%23AAAAAA'/%3E%3Cpath d='M69.46,41.14c1.74,0,3.16,2.13,3.16,4.76s-1.41,4.74-3.16,4.74-3.18-2.13-3.18-4.74,1.41-4.76,3.18-4.76' fill='%23AAAAAA'/%3E%3Cpath d='M63.91,76.15c-.82-.48-1.84-.43-2.8.12l-10.13,5.75-10.11-5.75c-.96-.55-1.98-.59-2.8-.12-.82.47-1.29,1.38-1.29,2.49v10.12c0,1.11.47,2.02,1.28,2.49.38.22.8.33,1.24.33.51,0,1.05-.15,1.57-.44l10.12-5.75,10.11,5.75c.52.29,1.05.44,1.56.44.44,0,.86-.11,1.24-.33.81-.48,1.28-1.38,1.28-2.49v-10.12c0-1.11-.47-2.02-1.28-2.49' fill='white' opacity='0.9'/%3E%3C/g%3E%3C/svg%3E";
519
+ img.alt = suggestion.displayName;
520
+ img.style.cssText = 'width: 16px; height: 16px; border-radius: 50%; object-fit: cover;';
521
+ chip.appendChild(img);
522
+ }
523
+ else {
524
+ // Use icon for users or agents without images
525
+ const icon = document.createElement('i');
526
+ icon.style.cssText = 'font-size: 12px; opacity: 0.95;';
527
+ if (suggestion.type === 'agent' && suggestion.icon) {
528
+ icon.className = this.getIconClasses(suggestion.icon);
529
+ }
530
+ else if (suggestion.type === 'user') {
531
+ icon.className = 'fa-solid fa-user';
532
+ }
533
+ else if (suggestion.type === 'entity') {
534
+ icon.className = this.getIconClasses(suggestion.icon || 'fa-solid fa-table');
535
+ }
536
+ else if (suggestion.type === 'query') {
537
+ icon.className = this.getIconClasses(suggestion.icon || 'fa-solid fa-database');
538
+ }
539
+ else if (suggestion.type === 'skill') {
540
+ icon.className = this.getIconClasses(suggestion.icon || 'fa-solid fa-wand-magic-sparkles');
541
+ }
542
+ else {
543
+ icon.className = 'fa-solid fa-robot';
544
+ }
545
+ chip.appendChild(icon);
546
+ }
547
+ // Add space between icon/image and text
548
+ const space = document.createTextNode(' ');
549
+ chip.appendChild(space);
550
+ // Add text
551
+ const text = document.createTextNode(suggestion.displayName);
552
+ chip.appendChild(text);
553
+ // Add dropdown when the suggestion carries 2+ presets
554
+ if (presets.length >= 2) {
555
+ this.addConfigurationDropdown(chip, presets);
556
+ }
557
+ return chip;
558
+ }
559
+ /**
560
+ * Add configuration preset dropdown to a mention chip
561
+ */
562
+ addConfigurationDropdown(chip, presets) {
563
+ // Store default preset for comparison
564
+ const defaultPreset = presets.find(p => p.IsDefault) || presets[0];
565
+ // Add preset indicator text (only shown when non-default selected)
566
+ const presetIndicator = document.createElement('span');
567
+ presetIndicator.className = 'preset-indicator';
568
+ presetIndicator.style.cssText = `
569
+ display: none;
570
+ font-size: 10px;
571
+ font-weight: 600;
572
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
573
+ background: rgba(255, 255, 255, 0.25);
574
+ padding: 2px 6px;
575
+ border-radius: 4px;
576
+ margin-left: 4px;
577
+ letter-spacing: 0.3px;
578
+ text-transform: uppercase;
579
+ border: 1px solid rgba(255, 255, 255, 0.15);
580
+ `;
581
+ chip.appendChild(presetIndicator);
582
+ // Add dropdown chevron button
583
+ const chevron = document.createElement('i');
584
+ chevron.className = 'fa-solid fa-chevron-down';
585
+ chevron.style.cssText = `
586
+ font-size: 10px;
587
+ opacity: 0.9;
588
+ cursor: pointer;
589
+ padding: 2px;
590
+ `;
591
+ chip.appendChild(chevron);
592
+ // Create dropdown menu (initially hidden) - append to body for global positioning
593
+ const dropdown = document.createElement('div');
594
+ dropdown.className = 'preset-dropdown';
595
+ dropdown.style.cssText = `
596
+ display: none;
597
+ position: fixed;
598
+ background: white;
599
+ border: 1px solid #ddd;
600
+ border-radius: 8px;
601
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
602
+ min-width: 200px;
603
+ max-width: 300px;
604
+ z-index: 10000;
605
+ overflow: hidden;
606
+ `;
607
+ // Add preset options to dropdown
608
+ presets.forEach(preset => {
609
+ const option = document.createElement('div');
610
+ option.className = 'preset-option';
611
+ option.style.cssText = `
612
+ padding: 10px 12px;
613
+ cursor: pointer;
614
+ display: flex;
615
+ align-items: center;
616
+ gap: 8px;
617
+ transition: background 0.2s;
618
+ `;
619
+ // Check if this is the selected preset
620
+ const currentPresetId = chip.getAttribute('data-preset-id');
621
+ const isSelected = UUIDsEqual(preset.ID, currentPresetId);
622
+ // Add checkmark for selected option
623
+ const checkmark = document.createElement('i');
624
+ checkmark.className = 'fa-solid fa-check';
625
+ checkmark.style.cssText = `
626
+ font-size: 12px;
627
+ color: #667eea;
628
+ opacity: ${isSelected ? '1' : '0'};
629
+ width: 14px;
630
+ `;
631
+ option.appendChild(checkmark);
632
+ // Add preset text content
633
+ const textContainer = document.createElement('div');
634
+ textContainer.style.cssText = 'flex: 1;';
635
+ const displayName = document.createElement('div');
636
+ displayName.textContent = preset.DisplayName || preset.Name;
637
+ displayName.style.cssText = `
638
+ font-weight: 600;
639
+ color: #333;
640
+ font-size: 13px;
641
+ `;
642
+ textContainer.appendChild(displayName);
643
+ if (preset.Description) {
644
+ const description = document.createElement('div');
645
+ description.textContent = preset.Description;
646
+ description.style.cssText = `
647
+ font-size: 11px;
648
+ color: #666;
649
+ margin-top: 2px;
650
+ `;
651
+ textContainer.appendChild(description);
652
+ }
653
+ option.appendChild(textContainer);
654
+ // Hover effects
655
+ option.addEventListener('mouseenter', () => {
656
+ option.style.background = '#f5f5f5';
657
+ });
658
+ option.addEventListener('mouseleave', () => {
659
+ option.style.background = 'white';
660
+ });
661
+ // Click handler to select preset
662
+ option.addEventListener('click', (e) => {
663
+ e.stopPropagation();
664
+ // Update chip data attributes
665
+ // IMPORTANT: Store preset.ID (AIAgentConfiguration.ID), not AIConfigurationID
666
+ chip.setAttribute('data-preset-id', preset.ID || '');
667
+ chip.setAttribute('data-preset-name', preset.Name || '');
668
+ // Update preset indicator visibility and text
669
+ const isDefault = UUIDsEqual(preset.ID, defaultPreset.ID);
670
+ if (isDefault) {
671
+ presetIndicator.style.display = 'none';
672
+ }
673
+ else {
674
+ presetIndicator.style.display = 'inline';
675
+ presetIndicator.textContent = preset.DisplayName || preset.Name;
676
+ }
677
+ // Update checkmarks
678
+ dropdown.querySelectorAll('.preset-option').forEach((opt, idx) => {
679
+ const check = opt.querySelector('i.fa-check');
680
+ if (check) {
681
+ check.style.opacity = idx === presets.indexOf(preset) ? '1' : '0';
682
+ }
683
+ });
684
+ // Close dropdown
685
+ dropdown.style.display = 'none';
686
+ });
687
+ dropdown.appendChild(option);
688
+ });
689
+ // Append dropdown to document body for global positioning
690
+ document.body.appendChild(dropdown);
691
+ // Helper function to position dropdown relative to chip
692
+ const positionDropdown = () => {
693
+ const chipRect = chip.getBoundingClientRect();
694
+ const viewportHeight = window.innerHeight;
695
+ const spaceBelow = viewportHeight - chipRect.bottom;
696
+ // Set left position (same for both above/below)
697
+ dropdown.style.left = `${chipRect.left}px`;
698
+ // First, show dropdown to measure its actual height
699
+ const wasHidden = dropdown.style.display === 'none';
700
+ if (wasHidden) {
701
+ dropdown.style.visibility = 'hidden';
702
+ dropdown.style.display = 'block';
703
+ }
704
+ const dropdownHeight = dropdown.offsetHeight;
705
+ const spaceAbove = chipRect.top;
706
+ // Position dropdown above chip if not enough space below
707
+ if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
708
+ // Show above - position so bottom of dropdown is 4px above top of chip
709
+ dropdown.style.top = `${chipRect.top - dropdownHeight - 4}px`;
710
+ }
711
+ else {
712
+ // Show below (default) - position 4px below bottom of chip
713
+ dropdown.style.top = `${chipRect.bottom + 4}px`;
714
+ }
715
+ // Restore visibility
716
+ if (wasHidden) {
717
+ dropdown.style.visibility = 'visible';
718
+ }
719
+ };
720
+ // Toggle dropdown on chevron click
721
+ chevron.addEventListener('click', (e) => {
722
+ e.stopPropagation();
723
+ const isVisible = dropdown.style.display === 'block';
724
+ if (isVisible) {
725
+ dropdown.style.display = 'none';
726
+ }
727
+ else {
728
+ positionDropdown();
729
+ dropdown.style.display = 'block';
730
+ }
731
+ });
732
+ // Close dropdown when clicking outside
733
+ const closeDropdown = (e) => {
734
+ if (!dropdown.contains(e.target) && !chip.contains(e.target)) {
735
+ dropdown.style.display = 'none';
736
+ }
737
+ };
738
+ document.addEventListener('click', closeDropdown);
739
+ // Cleanup: remove dropdown from body and event listeners when chip is removed
740
+ const observer = new MutationObserver((mutations) => {
741
+ mutations.forEach((mutation) => {
742
+ mutation.removedNodes.forEach((node) => {
743
+ if (node === chip) {
744
+ document.removeEventListener('click', closeDropdown);
745
+ if (dropdown.parentNode) {
746
+ dropdown.parentNode.removeChild(dropdown);
747
+ }
748
+ observer.disconnect();
749
+ }
750
+ });
751
+ });
752
+ });
753
+ if (chip.parentNode) {
754
+ observer.observe(chip.parentNode, { childList: true });
755
+ }
756
+ }
757
+ /**
758
+ * Get icon classes with proper FA prefix
759
+ */
760
+ getIconClasses(iconClass) {
761
+ if (!iconClass)
762
+ return 'fa-solid fa-robot';
763
+ if (iconClass.includes('fa-')) {
764
+ if (iconClass.match(/\b(fa-solid|fa-regular|fa-light|fa-brands)\b/)) {
765
+ return iconClass;
766
+ }
767
+ return `fa-solid ${iconClass}`;
768
+ }
769
+ return iconClass;
770
+ }
771
+ /**
772
+ * Handle backspace key - delete entire chip if cursor is right after one
773
+ */
774
+ handleBackspace(event) {
775
+ const selection = window.getSelection();
776
+ if (!selection || selection.rangeCount === 0)
777
+ return;
778
+ const range = selection.getRangeAt(0);
779
+ // Check if cursor is right after a mention chip
780
+ if (range.collapsed && range.startOffset > 0) {
781
+ const prevNode = range.startContainer.childNodes[range.startOffset - 1];
782
+ if (prevNode && prevNode.classList?.contains('mention-chip')) {
783
+ event.preventDefault();
784
+ prevNode.remove();
785
+ this.onInput();
786
+ }
787
+ }
788
+ }
789
+ /**
790
+ * Close mention dropdown
791
+ */
792
+ closeMentionDropdown() {
793
+ // Invalidate any in-flight suggestion fetch so a late response can't reopen the dropdown
794
+ this.suggestionRequestSeq++;
795
+ this.showMentionDropdown = false;
796
+ this.mentionSuggestions = [];
797
+ this.mentionStartIndex = -1;
798
+ this.mentionQuery = '';
799
+ this.activeTrigger = '@';
800
+ }
801
+ /**
802
+ * Convert editor HTML to plain text with @mentions
803
+ */
804
+ getPlainText() {
805
+ const editor = this.editorRef?.nativeElement;
806
+ if (!editor)
807
+ return '';
808
+ let text = '';
809
+ const nodes = editor.childNodes;
810
+ for (let i = 0; i < nodes.length; i++) {
811
+ const node = nodes[i];
812
+ if (node.nodeType === Node.TEXT_NODE) {
813
+ text += node.textContent || '';
814
+ }
815
+ else if (node.nodeType === Node.ELEMENT_NODE) {
816
+ const element = node;
817
+ if (element.classList.contains('mention-chip')) {
818
+ const name = element.getAttribute('data-mention-name') || '';
819
+ const mentionType = element.getAttribute('data-mention-type');
820
+ const prefix = mentionType === 'entity' || mentionType === 'query' ? '#' : mentionType === 'skill' ? '/' : '@';
821
+ // Use quoted format if name has spaces
822
+ text += name.includes(' ') ? `${prefix}"${name}"` : `${prefix}${name}`;
823
+ }
824
+ else if (element.tagName === 'BR') {
825
+ text += '\n';
826
+ }
827
+ else if (element.tagName === 'DIV') {
828
+ // Handle line breaks from contentEditable
829
+ if (i > 0)
830
+ text += '\n';
831
+ text += this.getNodeText(element);
832
+ }
833
+ else {
834
+ text += element.textContent || '';
835
+ }
836
+ }
837
+ }
838
+ return text;
839
+ }
840
+ /**
841
+ * Get text from a node recursively
842
+ */
843
+ getNodeText(node) {
844
+ let text = '';
845
+ const children = node.childNodes;
846
+ for (let i = 0; i < children.length; i++) {
847
+ const child = children[i];
848
+ if (child.nodeType === Node.TEXT_NODE) {
849
+ text += child.textContent || '';
850
+ }
851
+ else if (child.nodeType === Node.ELEMENT_NODE) {
852
+ const element = child;
853
+ if (element.classList.contains('mention-chip')) {
854
+ const name = element.getAttribute('data-mention-name') || '';
855
+ const mentionType = element.getAttribute('data-mention-type');
856
+ const prefix = mentionType === 'entity' || mentionType === 'query' ? '#' : mentionType === 'skill' ? '/' : '@';
857
+ text += name.includes(' ') ? `${prefix}"${name}"` : `${prefix}${name}`;
858
+ }
859
+ else {
860
+ text += this.getNodeText(element);
861
+ }
862
+ }
863
+ }
864
+ return text;
865
+ }
866
+ /**
867
+ * Set editor content from plain text (for programmatic updates)
868
+ */
869
+ setEditorContent(text) {
870
+ const editor = this.editorRef?.nativeElement;
871
+ if (!editor)
872
+ return;
873
+ // For now, just set as plain text
874
+ // TODO: Parse @mentions and render as chips
875
+ editor.textContent = text;
876
+ }
877
+ // ControlValueAccessor implementation
878
+ writeValue(value) {
879
+ const editor = this.editorRef?.nativeElement;
880
+ if (!editor) {
881
+ // View not created yet — buffer the value; ngAfterViewInit applies it
882
+ this.pendingWriteValue = value ?? '';
883
+ return;
884
+ }
885
+ if (value) {
886
+ this.setEditorContent(value);
887
+ }
888
+ else {
889
+ editor.textContent = '';
890
+ }
891
+ }
892
+ registerOnChange(fn) {
893
+ this.onChange = fn;
894
+ }
895
+ registerOnTouched(fn) {
896
+ this.onTouched = fn;
897
+ }
898
+ setDisabledState(isDisabled) {
899
+ this.disabled = isDisabled;
900
+ if (this.editorRef?.nativeElement) {
901
+ this.editorRef.nativeElement.contentEditable = (!isDisabled).toString();
902
+ }
903
+ }
904
+ /**
905
+ * Focus the editor
906
+ */
907
+ focus() {
908
+ this.editorRef?.nativeElement?.focus();
909
+ }
910
+ /**
911
+ * Clear the editor content and pending attachments
912
+ */
913
+ clear() {
914
+ if (this.editorRef?.nativeElement) {
915
+ this.editorRef.nativeElement.textContent = '';
916
+ this.onInput();
917
+ }
918
+ this.clearPendingAttachments();
919
+ }
920
+ /**
921
+ * Extract mention chips with their configuration data
922
+ * Returns array of objects containing mention info and preset configuration
923
+ */
924
+ getMentionChipsData() {
925
+ const editor = this.editorRef?.nativeElement;
926
+ if (!editor)
927
+ return [];
928
+ const chips = [];
929
+ const mentionElements = editor.querySelectorAll('.mention-chip');
930
+ mentionElements.forEach(chip => {
931
+ const element = chip;
932
+ const id = element.getAttribute('data-mention-id') || '';
933
+ const type = element.getAttribute('data-mention-type') || '';
934
+ const name = element.getAttribute('data-mention-name') || '';
935
+ const presetId = element.getAttribute('data-preset-id') || undefined;
936
+ const presetName = element.getAttribute('data-preset-name') || undefined;
937
+ chips.push({
938
+ id,
939
+ type,
940
+ name,
941
+ ...(presetId ? { presetId } : {}),
942
+ ...(presetName ? { presetName } : {})
943
+ });
944
+ });
945
+ return chips;
946
+ }
947
+ /**
948
+ * Get the plain text value with mentions encoded as JSON
949
+ * This format preserves configuration information when messages are saved
950
+ * Format: @{type:"agent",id:"uuid",name:"Agent Name",configId:"uuid",config:"High"}
951
+ */
952
+ getPlainTextWithJsonMentions() {
953
+ const editor = this.editorRef?.nativeElement;
954
+ if (!editor)
955
+ return '';
956
+ let plainText = '';
957
+ const childNodes = Array.from(editor.childNodes);
958
+ for (const node of childNodes) {
959
+ if (node.nodeType === Node.TEXT_NODE) {
960
+ // Regular text node
961
+ plainText += node.textContent || '';
962
+ }
963
+ else if (node.nodeType === Node.ELEMENT_NODE) {
964
+ const element = node;
965
+ if (element.classList.contains('mention-chip')) {
966
+ // Extract mention data attributes
967
+ const type = element.getAttribute('data-mention-type') || '';
968
+ const id = element.getAttribute('data-mention-id') || '';
969
+ const name = element.getAttribute('data-mention-name') || '';
970
+ const presetId = element.getAttribute('data-preset-id');
971
+ const presetName = element.getAttribute('data-preset-name');
972
+ // Build JSON mention object (compact format - no spaces)
973
+ const mentionObj = {
974
+ type,
975
+ id,
976
+ name
977
+ };
978
+ // Add configuration fields if present (for agents with non-default presets)
979
+ if (presetId) {
980
+ mentionObj.configId = presetId;
981
+ }
982
+ if (presetName) {
983
+ mentionObj.config = presetName;
984
+ }
985
+ // Encode as JSON mention
986
+ // Don't remove spaces from the JSON string - spaces in values (like agent names) need to be preserved
987
+ plainText += `@${JSON.stringify(mentionObj)}`;
988
+ }
989
+ else {
990
+ // Other HTML element (e.g., <br>)
991
+ if (element.tagName === 'BR') {
992
+ plainText += '\n';
993
+ }
994
+ else {
995
+ plainText += element.textContent || '';
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ return plainText;
1001
+ }
1002
+ // ==================== Attachment Handling Methods ====================
1003
+ /**
1004
+ * Handle drag over event
1005
+ */
1006
+ onDragOver(event) {
1007
+ if (!this.enableAttachments)
1008
+ return;
1009
+ event.preventDefault();
1010
+ event.stopPropagation();
1011
+ this.isDragOver = true;
1012
+ }
1013
+ /**
1014
+ * Handle drag leave event
1015
+ */
1016
+ onDragLeave(event) {
1017
+ event.preventDefault();
1018
+ event.stopPropagation();
1019
+ this.isDragOver = false;
1020
+ }
1021
+ /**
1022
+ * Handle drop event
1023
+ */
1024
+ onDrop(event) {
1025
+ if (!this.enableAttachments)
1026
+ return;
1027
+ event.preventDefault();
1028
+ event.stopPropagation();
1029
+ this.isDragOver = false;
1030
+ if (event.dataTransfer?.files) {
1031
+ const files = Array.from(event.dataTransfer.files);
1032
+ for (const file of files) {
1033
+ if (this.isAcceptedFile(file)) {
1034
+ this.processFile(file);
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Handle file input change (from file picker)
1041
+ */
1042
+ onFileSelected(event) {
1043
+ const input = event.target;
1044
+ if (input.files) {
1045
+ const files = Array.from(input.files);
1046
+ for (const file of files) {
1047
+ this.processFile(file);
1048
+ }
1049
+ // Reset input so same file can be selected again
1050
+ input.value = '';
1051
+ }
1052
+ }
1053
+ /**
1054
+ * Process a file for attachment
1055
+ */
1056
+ async processFile(file) {
1057
+ // Validate file
1058
+ const validation = this.validateFile(file);
1059
+ if (!validation.valid) {
1060
+ this.attachmentError.emit(validation.error);
1061
+ return;
1062
+ }
1063
+ // Read file as data URL
1064
+ const dataUrl = await this.readFileAsDataUrl(file);
1065
+ if (!dataUrl) {
1066
+ this.attachmentError.emit('Failed to read file');
1067
+ return;
1068
+ }
1069
+ // Get image dimensions if applicable
1070
+ let width;
1071
+ let height;
1072
+ if (file.type.startsWith('image/')) {
1073
+ const dimensions = await this.getImageDimensions(dataUrl);
1074
+ width = dimensions.width;
1075
+ height = dimensions.height;
1076
+ }
1077
+ // Create pending attachment
1078
+ const attachment = {
1079
+ id: this.generateAttachmentId(),
1080
+ file,
1081
+ dataUrl,
1082
+ mimeType: file.type,
1083
+ fileName: file.name,
1084
+ sizeBytes: file.size,
1085
+ width,
1086
+ height,
1087
+ thumbnailUrl: file.type.startsWith('image/') ? dataUrl : undefined
1088
+ };
1089
+ // Add to pending attachments
1090
+ this.pendingAttachments.push(attachment);
1091
+ this.attachmentsChanged.emit([...this.pendingAttachments]);
1092
+ this.cdr.detectChanges();
1093
+ }
1094
+ /**
1095
+ * Validate a file against settings
1096
+ */
1097
+ validateFile(file) {
1098
+ // Check count limit
1099
+ if (this.pendingAttachments.length >= this.maxAttachments) {
1100
+ return {
1101
+ valid: false,
1102
+ error: `Maximum ${this.maxAttachments} attachments allowed`
1103
+ };
1104
+ }
1105
+ // Check size limit
1106
+ if (file.size > this.maxAttachmentSizeBytes) {
1107
+ const maxMB = (this.maxAttachmentSizeBytes / (1024 * 1024)).toFixed(1);
1108
+ const fileMB = (file.size / (1024 * 1024)).toFixed(1);
1109
+ return {
1110
+ valid: false,
1111
+ error: `File size (${fileMB}MB) exceeds maximum (${maxMB}MB)`
1112
+ };
1113
+ }
1114
+ // Check file type
1115
+ if (!this.isAcceptedFile(file)) {
1116
+ return {
1117
+ valid: false,
1118
+ error: `File type ${file.type || 'unknown'} not accepted`
1119
+ };
1120
+ }
1121
+ return { valid: true };
1122
+ }
1123
+ /**
1124
+ * Check if file type is accepted
1125
+ */
1126
+ isAcceptedFile(file) {
1127
+ if (this.acceptedFileTypes === '*' || this.acceptedFileTypes === '*/*') {
1128
+ return true;
1129
+ }
1130
+ const acceptedTypes = this.acceptedFileTypes.split(',').map(t => t.trim());
1131
+ for (const accepted of acceptedTypes) {
1132
+ // Handle universal wildcard */* anywhere in the list
1133
+ if (accepted === '*/*') {
1134
+ return true;
1135
+ }
1136
+ // Handle wildcard MIME types like "image/*"
1137
+ if (accepted.endsWith('/*')) {
1138
+ const category = accepted.slice(0, -2);
1139
+ if (file.type.startsWith(category + '/')) {
1140
+ return true;
1141
+ }
1142
+ }
1143
+ else if (accepted.startsWith('.')) {
1144
+ // Handle file extensions like ".png"
1145
+ if (file.name.toLowerCase().endsWith(accepted.toLowerCase())) {
1146
+ return true;
1147
+ }
1148
+ }
1149
+ else if (file.type === accepted) {
1150
+ // Exact MIME type match
1151
+ return true;
1152
+ }
1153
+ }
1154
+ return false;
1155
+ }
1156
+ /**
1157
+ * Read file as data URL
1158
+ */
1159
+ readFileAsDataUrl(file) {
1160
+ return new Promise((resolve) => {
1161
+ const reader = new FileReader();
1162
+ reader.onload = () => resolve(reader.result);
1163
+ reader.onerror = () => resolve(null);
1164
+ reader.readAsDataURL(file);
1165
+ });
1166
+ }
1167
+ /**
1168
+ * Get image dimensions
1169
+ */
1170
+ getImageDimensions(dataUrl) {
1171
+ return new Promise((resolve) => {
1172
+ const img = new Image();
1173
+ img.onload = () => {
1174
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
1175
+ };
1176
+ img.onerror = () => {
1177
+ resolve({ width: 0, height: 0 });
1178
+ };
1179
+ img.src = dataUrl;
1180
+ });
1181
+ }
1182
+ /**
1183
+ * Generate unique attachment ID
1184
+ */
1185
+ generateAttachmentId() {
1186
+ return `att_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1187
+ }
1188
+ /**
1189
+ * Remove a pending attachment by ID
1190
+ */
1191
+ removeAttachment(id) {
1192
+ const index = this.pendingAttachments.findIndex(a => a.id === id);
1193
+ if (index !== -1) {
1194
+ this.pendingAttachments.splice(index, 1);
1195
+ this.attachmentsChanged.emit([...this.pendingAttachments]);
1196
+ this.cdr.detectChanges();
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Get all pending attachments
1201
+ */
1202
+ getPendingAttachments() {
1203
+ return [...this.pendingAttachments];
1204
+ }
1205
+ /**
1206
+ * Add an artifact as a pending attachment programmatically.
1207
+ * Used by the artifact picker to attach artifacts as conversation inputs.
1208
+ */
1209
+ AddArtifactAttachment(artifact) {
1210
+ const attachment = {
1211
+ id: crypto.randomUUID(),
1212
+ file: null,
1213
+ dataUrl: '',
1214
+ mimeType: artifact.mimeType,
1215
+ fileName: artifact.fileName,
1216
+ sizeBytes: artifact.sizeBytes,
1217
+ fileID: artifact.fileID,
1218
+ source: 'artifact',
1219
+ artifactVersionId: artifact.artifactVersionId
1220
+ };
1221
+ this.pendingAttachments.push(attachment);
1222
+ this.attachmentsChanged.emit([...this.pendingAttachments]);
1223
+ return attachment;
1224
+ }
1225
+ /**
1226
+ * Clear all pending attachments
1227
+ */
1228
+ clearPendingAttachments() {
1229
+ this.pendingAttachments = [];
1230
+ this.attachmentsChanged.emit([]);
1231
+ this.cdr.detectChanges();
1232
+ }
1233
+ /**
1234
+ * Handle click on an attachment thumbnail
1235
+ */
1236
+ onAttachmentClick(attachment) {
1237
+ this.attachmentClicked.emit(attachment);
1238
+ }
1239
+ /**
1240
+ * Check if there are any pending attachments
1241
+ */
1242
+ hasAttachments() {
1243
+ return this.pendingAttachments.length > 0;
1244
+ }
1245
+ /**
1246
+ * Trigger file picker programmatically
1247
+ */
1248
+ openFilePicker() {
1249
+ const input = document.createElement('input');
1250
+ input.type = 'file';
1251
+ // If */* is in the list, accept all files (some browsers don't handle */* correctly)
1252
+ input.accept = this.acceptedFileTypes.includes('*/*') ? '' : this.acceptedFileTypes;
1253
+ input.multiple = true;
1254
+ input.onchange = (e) => this.onFileSelected(e);
1255
+ input.click();
1256
+ }
1257
+ static ɵfac = function MentionEditorComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MentionEditorComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
1258
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MentionEditorComponent, selectors: [["mj-mention-editor"]], viewQuery: function MentionEditorComponent_Query(rf, ctx) { if (rf & 1) {
1259
+ i0.ɵɵviewQuery(_c0, 5);
1260
+ } if (rf & 2) {
1261
+ let _t;
1262
+ i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.editorRef = _t.first);
1263
+ } }, inputs: { placeholder: "placeholder", disabled: "disabled", currentUser: "currentUser", enableMentions: "enableMentions", TriggerProviders: "TriggerProviders", ExcludedTriggerKeys: "ExcludedTriggerKeys", Provider: "Provider", autoFocus: "autoFocus", enableAttachments: "enableAttachments", maxAttachments: "maxAttachments", maxAttachmentSizeBytes: "maxAttachmentSizeBytes", acceptedFileTypes: "acceptedFileTypes" }, outputs: { valueChange: "valueChange", mentionSelected: "mentionSelected", enterPressed: "enterPressed", attachmentsChanged: "attachmentsChanged", attachmentError: "attachmentError", attachmentClicked: "attachmentClicked" }, standalone: false, features: [i0.ɵɵProvidersFeature([
1264
+ {
1265
+ provide: NG_VALUE_ACCESSOR,
1266
+ useExisting: forwardRef(() => MentionEditorComponent),
1267
+ multi: true
1268
+ }
1269
+ ])], decls: 6, vars: 7, consts: [["editor", ""], [1, "mention-editor-container", 3, "click", "dragover", "dragleave", "drop"], [1, "pending-attachments"], ["contenteditable", "true", 1, "mention-editor", 3, "input", "keydown", "blur", "paste"], [1, "drop-overlay"], [3, "suggestions", "position", "showAbove", "useFixedPositioning", "visible"], [1, "attachment-thumbnail"], [1, "attachment-thumbnail", 3, "click"], [1, "thumbnail-image", 3, "src", "alt"], [1, "thumbnail-placeholder"], ["title", "Remove attachment", 1, "remove-attachment", 3, "click"], [1, "fa-solid", "fa-times"], [1, "fa-solid", "fa-file"], [1, "file-name"], [1, "drop-message"], [1, "fa-solid", "fa-cloud-arrow-up"], [3, "suggestionSelected", "closed", "suggestions", "position", "showAbove", "useFixedPositioning", "visible"]], template: function MentionEditorComponent_Template(rf, ctx) { if (rf & 1) {
1270
+ const _r1 = i0.ɵɵgetCurrentView();
1271
+ i0.ɵɵelementStart(0, "div", 1);
1272
+ i0.ɵɵlistener("click", function MentionEditorComponent_Template_div_click_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onContainerClick($event)); })("dragover", function MentionEditorComponent_Template_div_dragover_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDragOver($event)); })("dragleave", function MentionEditorComponent_Template_div_dragleave_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDragLeave($event)); })("drop", function MentionEditorComponent_Template_div_drop_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDrop($event)); });
1273
+ i0.ɵɵconditionalCreate(1, MentionEditorComponent_Conditional_1_Template, 3, 0, "div", 2);
1274
+ i0.ɵɵelementStart(2, "div", 3, 0);
1275
+ i0.ɵɵlistener("input", function MentionEditorComponent_Template_div_input_2_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onInput()); })("keydown", function MentionEditorComponent_Template_div_keydown_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onKeyDown($event)); })("blur", function MentionEditorComponent_Template_div_blur_2_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onBlur()); })("paste", function MentionEditorComponent_Template_div_paste_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onPaste($event)); });
1276
+ i0.ɵɵelementEnd();
1277
+ i0.ɵɵconditionalCreate(4, MentionEditorComponent_Conditional_4_Template, 5, 0, "div", 4);
1278
+ i0.ɵɵconditionalCreate(5, MentionEditorComponent_Conditional_5_Template, 1, 5, "mj-mention-dropdown", 5);
1279
+ i0.ɵɵelementEnd();
1280
+ } if (rf & 2) {
1281
+ i0.ɵɵclassProp("drag-over", ctx.isDragOver);
1282
+ i0.ɵɵadvance();
1283
+ i0.ɵɵconditional(ctx.pendingAttachments.length > 0 ? 1 : -1);
1284
+ i0.ɵɵadvance();
1285
+ i0.ɵɵattribute("contenteditable", !ctx.disabled)("data-placeholder", ctx.placeholder);
1286
+ i0.ɵɵadvance(2);
1287
+ i0.ɵɵconditional(ctx.isDragOver ? 4 : -1);
1288
+ i0.ɵɵadvance();
1289
+ i0.ɵɵconditional(ctx.showMentionDropdown && ctx.enableMentions ? 5 : -1);
1290
+ } }, dependencies: [i1.MentionDropdownComponent], styles: [".mention-editor-container[_ngcontent-%COMP%] {\n position: relative;\n width: 100%;\n display: block;\n}\n\n.mention-editor-container.drag-over[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-color: var(--mj-brand-primary);\n}\n\n.mention-editor[_ngcontent-%COMP%] {\n display: block;\n width: 100%;\n min-height: 100px;\n max-height: 300px;\n padding: 7px 3.5rem 7px 12px;\n outline: none;\n border: none;\n background: transparent;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1.5;\n overflow-y: auto;\n overflow-x: hidden;\n word-wrap: break-word;\n white-space: pre-wrap;\n cursor: text;\n user-select: text;\n box-sizing: border-box;\n}\n.mention-editor[_ngcontent-%COMP%]:empty:before {\n content: attr(data-placeholder);\n color: var(--mj-text-muted);\n pointer-events: none;\n position: absolute;\n}\n.mention-editor[_ngcontent-%COMP%]:focus {\n outline: none;\n}\n.mention-editor[contenteditable=false][_ngcontent-%COMP%] {\n cursor: not-allowed;\n opacity: 0.6;\n background: var(--mj-bg-surface-sunken);\n}\n@media (max-width: 768px) {\n .mention-editor[_ngcontent-%COMP%] {\n font-size: 16px;\n padding: 5px;\n padding-right: 3rem;\n }\n}\n\n .mention-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 12px;\n margin: 0 3px;\n border-radius: 16px;\n font-size: 13px;\n font-weight: 600;\n cursor: default;\n user-select: none;\n vertical-align: middle;\n white-space: nowrap;\n pointer-events: all;\n -webkit-user-modify: read-only;\n -moz-user-modify: read-only;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n box-shadow: var(--mj-shadow-md);\n animation: _ngcontent-%COMP%_chipInsert 0.15s ease-out;\n}\n .mention-chip i {\n font-size: 12px;\n opacity: 0.95;\n}\n .mention-chip[data-mention-type=user] {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n}\n .mention-chip:hover {\n transform: translateY(-1px);\n box-shadow: var(--mj-shadow-lg);\n transition: all 0.2s ease;\n}\n .mention-chip:focus, .mention-chip.selected {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n@keyframes _ngcontent-%COMP%_chipInsert {\n 0% {\n transform: scale(0.8);\n opacity: 0;\n }\n 100% {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n.mention-editor[_ngcontent-%COMP%] .mention-chip[_ngcontent-%COMP%] + .mention-chip[_ngcontent-%COMP%] {\n margin-left: 4px;\n}\n.mention-editor[_ngcontent-%COMP%] br[_ngcontent-%COMP%] {\n display: block;\n content: \"\";\n margin: 0;\n}\n\n.mention-editor[_ngcontent-%COMP%]::-webkit-scrollbar {\n width: 6px;\n}\n.mention-editor[_ngcontent-%COMP%]::-webkit-scrollbar-track {\n background: transparent;\n}\n.mention-editor[_ngcontent-%COMP%]::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-text-primary) 20%, transparent);\n border-radius: 3px;\n}\n.mention-editor[_ngcontent-%COMP%]::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-text-primary) 30%, transparent);\n}\n\n\n\n\n.pending-attachments[_ngcontent-%COMP%] {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 1px solid color-mix(in srgb, var(--mj-text-primary) 8%, transparent);\n background: color-mix(in srgb, var(--mj-text-primary) 2%, transparent);\n}\n\n.attachment-thumbnail[_ngcontent-%COMP%] {\n position: relative;\n width: 80px;\n height: 80px;\n border-radius: 8px;\n overflow: hidden;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid color-mix(in srgb, var(--mj-text-primary) 10%, transparent);\n}\n\n.attachment-thumbnail[_ngcontent-%COMP%]:hover {\n transform: scale(1.05);\n box-shadow: var(--mj-shadow-lg);\n}\n\n.thumbnail-image[_ngcontent-%COMP%] {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.thumbnail-placeholder[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 8px;\n color: var(--mj-text-secondary);\n}\n\n.thumbnail-placeholder[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 24px;\n margin-bottom: 4px;\n color: var(--mj-text-muted);\n}\n\n.thumbnail-placeholder[_ngcontent-%COMP%] .file-name[_ngcontent-%COMP%] {\n font-size: 10px;\n text-align: center;\n word-break: break-all;\n line-height: 1.2;\n max-height: 36px;\n overflow: hidden;\n}\n\n.remove-attachment[_ngcontent-%COMP%] {\n position: absolute;\n top: 4px;\n right: 4px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-text-primary) 60%, transparent);\n color: var(--mj-text-inverse);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s, background 0.2s;\n}\n\n.attachment-thumbnail[_ngcontent-%COMP%]:hover .remove-attachment[_ngcontent-%COMP%] {\n opacity: 1;\n}\n\n.remove-attachment[_ngcontent-%COMP%]:hover {\n background: color-mix(in srgb, var(--mj-status-error) 90%, transparent);\n}\n\n.remove-attachment[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 10px;\n}\n\n\n\n.drop-overlay[_ngcontent-%COMP%] {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n backdrop-filter: blur(2px);\n border: 2px dashed var(--mj-brand-primary);\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n pointer-events: none;\n}\n\n.drop-message[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n.drop-message[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 32px;\n}\n\n.drop-message[_ngcontent-%COMP%] span[_ngcontent-%COMP%] {\n font-size: 14px;\n}"] });
1291
+ }
1292
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MentionEditorComponent, [{
1293
+ type: Component,
1294
+ args: [{ standalone: false, selector: 'mj-mention-editor', providers: [
1295
+ {
1296
+ provide: NG_VALUE_ACCESSOR,
1297
+ useExisting: forwardRef(() => MentionEditorComponent),
1298
+ multi: true
1299
+ }
1300
+ ], template: "<div class=\"mention-editor-container\"\n [class.drag-over]=\"isDragOver\"\n (click)=\"onContainerClick($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n\n <!-- Pending Attachments Preview -->\n @if (pendingAttachments.length > 0) {\n <div class=\"pending-attachments\">\n @for (attachment of pendingAttachments; track attachment) {\n <div class=\"attachment-thumbnail\"\n (click)=\"onAttachmentClick(attachment)\">\n @if (attachment.thumbnailUrl) {\n <img\n [src]=\"attachment.thumbnailUrl\"\n [alt]=\"attachment.fileName\"\n class=\"thumbnail-image\">\n }\n @if (!attachment.thumbnailUrl) {\n <div class=\"thumbnail-placeholder\">\n <i class=\"fa-solid fa-file\"></i>\n <span class=\"file-name\">{{ attachment.fileName }}</span>\n </div>\n }\n <button class=\"remove-attachment\"\n (click)=\"removeAttachment(attachment.id); $event.stopPropagation()\"\n title=\"Remove attachment\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <div\n #editor\n class=\"mention-editor\"\n contenteditable=\"true\"\n [attr.contenteditable]=\"!disabled\"\n [attr.data-placeholder]=\"placeholder\"\n (input)=\"onInput()\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"onBlur()\"\n (paste)=\"onPaste($event)\"\n ></div>\n\n <!-- Drag & Drop Overlay -->\n @if (isDragOver) {\n <div class=\"drop-overlay\">\n <div class=\"drop-message\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Drop files here</span>\n </div>\n </div>\n }\n\n <!-- Mention Dropdown -->\n @if (showMentionDropdown && enableMentions) {\n <mj-mention-dropdown\n [suggestions]=\"mentionSuggestions\"\n [position]=\"mentionDropdownPosition\"\n [showAbove]=\"mentionDropdownShowAbove\"\n [useFixedPositioning]=\"true\"\n [visible]=\"true\"\n (suggestionSelected)=\"onMentionSelected($event)\"\n (closed)=\"closeMentionDropdown()\">\n </mj-mention-dropdown>\n }\n</div>\n", styles: [".mention-editor-container {\n position: relative;\n width: 100%;\n display: block;\n}\n\n.mention-editor-container.drag-over {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-color: var(--mj-brand-primary);\n}\n\n.mention-editor {\n display: block;\n width: 100%;\n min-height: 100px;\n max-height: 300px;\n padding: 7px 3.5rem 7px 12px;\n outline: none;\n border: none;\n background: transparent;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1.5;\n overflow-y: auto;\n overflow-x: hidden;\n word-wrap: break-word;\n white-space: pre-wrap;\n cursor: text;\n user-select: text;\n box-sizing: border-box;\n}\n.mention-editor:empty:before {\n content: attr(data-placeholder);\n color: var(--mj-text-muted);\n pointer-events: none;\n position: absolute;\n}\n.mention-editor:focus {\n outline: none;\n}\n.mention-editor[contenteditable=false] {\n cursor: not-allowed;\n opacity: 0.6;\n background: var(--mj-bg-surface-sunken);\n}\n@media (max-width: 768px) {\n .mention-editor {\n font-size: 16px;\n padding: 5px;\n padding-right: 3rem;\n }\n}\n\n::ng-deep .mention-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 12px;\n margin: 0 3px;\n border-radius: 16px;\n font-size: 13px;\n font-weight: 600;\n cursor: default;\n user-select: none;\n vertical-align: middle;\n white-space: nowrap;\n pointer-events: all;\n -webkit-user-modify: read-only;\n -moz-user-modify: read-only;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n box-shadow: var(--mj-shadow-md);\n animation: chipInsert 0.15s ease-out;\n}\n::ng-deep .mention-chip i {\n font-size: 12px;\n opacity: 0.95;\n}\n::ng-deep .mention-chip[data-mention-type=user] {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n}\n::ng-deep .mention-chip:hover {\n transform: translateY(-1px);\n box-shadow: var(--mj-shadow-lg);\n transition: all 0.2s ease;\n}\n::ng-deep .mention-chip:focus, ::ng-deep .mention-chip.selected {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n@keyframes chipInsert {\n 0% {\n transform: scale(0.8);\n opacity: 0;\n }\n 100% {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n.mention-editor .mention-chip + .mention-chip {\n margin-left: 4px;\n}\n.mention-editor br {\n display: block;\n content: \"\";\n margin: 0;\n}\n\n.mention-editor::-webkit-scrollbar {\n width: 6px;\n}\n.mention-editor::-webkit-scrollbar-track {\n background: transparent;\n}\n.mention-editor::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-text-primary) 20%, transparent);\n border-radius: 3px;\n}\n.mention-editor::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-text-primary) 30%, transparent);\n}\n\n/* ==================== Attachment Styles ==================== */\n\n.pending-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 1px solid color-mix(in srgb, var(--mj-text-primary) 8%, transparent);\n background: color-mix(in srgb, var(--mj-text-primary) 2%, transparent);\n}\n\n.attachment-thumbnail {\n position: relative;\n width: 80px;\n height: 80px;\n border-radius: 8px;\n overflow: hidden;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid color-mix(in srgb, var(--mj-text-primary) 10%, transparent);\n}\n\n.attachment-thumbnail:hover {\n transform: scale(1.05);\n box-shadow: var(--mj-shadow-lg);\n}\n\n.thumbnail-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.thumbnail-placeholder {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 8px;\n color: var(--mj-text-secondary);\n}\n\n.thumbnail-placeholder i {\n font-size: 24px;\n margin-bottom: 4px;\n color: var(--mj-text-muted);\n}\n\n.thumbnail-placeholder .file-name {\n font-size: 10px;\n text-align: center;\n word-break: break-all;\n line-height: 1.2;\n max-height: 36px;\n overflow: hidden;\n}\n\n.remove-attachment {\n position: absolute;\n top: 4px;\n right: 4px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-text-primary) 60%, transparent);\n color: var(--mj-text-inverse);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s, background 0.2s;\n}\n\n.attachment-thumbnail:hover .remove-attachment {\n opacity: 1;\n}\n\n.remove-attachment:hover {\n background: color-mix(in srgb, var(--mj-status-error) 90%, transparent);\n}\n\n.remove-attachment i {\n font-size: 10px;\n}\n\n/* Drop Overlay */\n.drop-overlay {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n backdrop-filter: blur(2px);\n border: 2px dashed var(--mj-brand-primary);\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n pointer-events: none;\n}\n\n.drop-message {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n.drop-message i {\n font-size: 32px;\n}\n\n.drop-message span {\n font-size: 14px;\n}\n"] }]
1301
+ }], () => [{ type: i0.ChangeDetectorRef }], { editorRef: [{
1302
+ type: ViewChild,
1303
+ args: ['editor', { static: false }]
1304
+ }], placeholder: [{
1305
+ type: Input
1306
+ }], disabled: [{
1307
+ type: Input
1308
+ }], currentUser: [{
1309
+ type: Input
1310
+ }], enableMentions: [{
1311
+ type: Input
1312
+ }], TriggerProviders: [{
1313
+ type: Input
1314
+ }], ExcludedTriggerKeys: [{
1315
+ type: Input
1316
+ }], Provider: [{
1317
+ type: Input
1318
+ }], autoFocus: [{
1319
+ type: Input
1320
+ }], enableAttachments: [{
1321
+ type: Input
1322
+ }], maxAttachments: [{
1323
+ type: Input
1324
+ }], maxAttachmentSizeBytes: [{
1325
+ type: Input
1326
+ }], acceptedFileTypes: [{
1327
+ type: Input
1328
+ }], valueChange: [{
1329
+ type: Output
1330
+ }], mentionSelected: [{
1331
+ type: Output
1332
+ }], enterPressed: [{
1333
+ type: Output
1334
+ }], attachmentsChanged: [{
1335
+ type: Output
1336
+ }], attachmentError: [{
1337
+ type: Output
1338
+ }], attachmentClicked: [{
1339
+ type: Output
1340
+ }] }); })();
1341
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MentionEditorComponent, { className: "MentionEditorComponent", filePath: "src/lib/components/mention/mention-editor.component.ts", lineNumber: 72 }); })();
1342
+ //# sourceMappingURL=mention-editor.component.js.map