@cnt-dev/chat-ui 0.1.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.
Files changed (55) hide show
  1. package/AGENTS.md +202 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/index.d.ts +23 -0
  5. package/package.json +41 -0
  6. package/resources/chatui/.library +14 -0
  7. package/resources/chatui/ChatBox-dbg.js +1099 -0
  8. package/resources/chatui/ChatBox-dbg.js.map +1 -0
  9. package/resources/chatui/ChatBox.d.ts +308 -0
  10. package/resources/chatui/ChatBox.d.ts.map +1 -0
  11. package/resources/chatui/ChatBox.gen.d.ts +545 -0
  12. package/resources/chatui/ChatBox.js +5 -0
  13. package/resources/chatui/ChatBox.js.map +1 -0
  14. package/resources/chatui/ChatBox.ts +1153 -0
  15. package/resources/chatui/library-dbg.js +28 -0
  16. package/resources/chatui/library-dbg.js.map +1 -0
  17. package/resources/chatui/library-preload.js +17 -0
  18. package/resources/chatui/library-preload.js.map +1 -0
  19. package/resources/chatui/library.d.ts +3 -0
  20. package/resources/chatui/library.d.ts.map +1 -0
  21. package/resources/chatui/library.js +5 -0
  22. package/resources/chatui/library.js.map +1 -0
  23. package/resources/chatui/library.ts +30 -0
  24. package/resources/chatui/manifest.json +52 -0
  25. package/resources/chatui/markdown-dbg.js +116 -0
  26. package/resources/chatui/markdown-dbg.js.map +1 -0
  27. package/resources/chatui/markdown.d.ts +21 -0
  28. package/resources/chatui/markdown.d.ts.map +1 -0
  29. package/resources/chatui/markdown.js +5 -0
  30. package/resources/chatui/markdown.js.map +1 -0
  31. package/resources/chatui/markdown.ts +140 -0
  32. package/resources/chatui/messagebundle.properties +20 -0
  33. package/resources/chatui/messagebundle_en.properties +20 -0
  34. package/resources/chatui/themes/base/ChatBox.less +702 -0
  35. package/resources/chatui/themes/base/library-RTL.css +3 -0
  36. package/resources/chatui/themes/base/library-parameters.json +1 -0
  37. package/resources/chatui/themes/base/library.css +3 -0
  38. package/resources/chatui/themes/base/library.source.less +4 -0
  39. package/resources/chatui/themes/sap_fiori_3/library-RTL.css +3 -0
  40. package/resources/chatui/themes/sap_fiori_3/library-parameters.json +1 -0
  41. package/resources/chatui/themes/sap_fiori_3/library.css +3 -0
  42. package/resources/chatui/themes/sap_fiori_3/library.source.less +3 -0
  43. package/resources/chatui/themes/sap_fiori_3_dark/library-RTL.css +3 -0
  44. package/resources/chatui/themes/sap_fiori_3_dark/library-parameters.json +1 -0
  45. package/resources/chatui/themes/sap_fiori_3_dark/library.css +3 -0
  46. package/resources/chatui/themes/sap_fiori_3_dark/library.source.less +3 -0
  47. package/resources/chatui/themes/sap_fiori_3_hcb/library-RTL.css +3 -0
  48. package/resources/chatui/themes/sap_fiori_3_hcb/library-parameters.json +1 -0
  49. package/resources/chatui/themes/sap_fiori_3_hcb/library.css +3 -0
  50. package/resources/chatui/themes/sap_fiori_3_hcb/library.source.less +3 -0
  51. package/resources/chatui/themes/sap_fiori_3_hcw/library-RTL.css +3 -0
  52. package/resources/chatui/themes/sap_fiori_3_hcw/library-parameters.json +1 -0
  53. package/resources/chatui/themes/sap_fiori_3_hcw/library.css +3 -0
  54. package/resources/chatui/themes/sap_fiori_3_hcw/library.source.less +3 -0
  55. package/ui5.yaml +15 -0
@@ -0,0 +1,1099 @@
1
+ /*!
2
+ * ${copyright}
3
+ */
4
+ sap.ui.define(["sap/ui/core/Control", "sap/ui/core/IconPool", "sap/ui/core/Lib", "sap/base/i18n/Localization", "./markdown"], function (Control, IconPool, Lib, Localization, __markdownToHtml) {
5
+ "use strict";
6
+
7
+ function _interopRequireDefault(obj) {
8
+ return obj && obj.__esModule && typeof obj.default !== "undefined" ? obj.default : obj;
9
+ }
10
+ const markdownToHtml = _interopRequireDefault(__markdownToHtml);
11
+ /**
12
+ * Constructor for a new <code>chatui.ChatBox</code> control.
13
+ *
14
+ * A flexible chat UI block that can be embedded into any UI5 application.
15
+ * It renders a conversation sidebar, a scrollable message list, an
16
+ * auto-growing input textarea and a send button. The control is
17
+ * intentionally transport-agnostic: it fires a <code>send</code> event with
18
+ * the user's message and expects the embedding application to feed the
19
+ * assistant's reply back via {@link #addAssistantMessage} /
20
+ * {@link #appendToLastAssistantMessage}.
21
+ *
22
+ * The control fills the space provided by its parent. For standalone use
23
+ * a sensible default size is applied via CSS.
24
+ *
25
+ * @extends sap.ui.core.Control
26
+ *
27
+ * @author CONET
28
+ * @version 0.1.0
29
+ *
30
+ * @constructor
31
+ * @public
32
+ * @name chatui.ChatBox
33
+ */
34
+ const ChatBox = Control.extend("chatui.ChatBox", {
35
+ renderer: {
36
+ apiVersion: 2,
37
+ render(rm, control) {
38
+ const sendIcon = IconPool.getIconInfo("sap-icon://paper-plane");
39
+ const newChatIcon = IconPool.getIconInfo("sap-icon://add");
40
+ const attachIcon = IconPool.getIconInfo("sap-icon://attachment");
41
+ const i18n = control._i18n();
42
+ const placeholder = control.getPlaceholder() || i18n.getText("chatbox.placeholder") || "";
43
+ const allowAttachments = control.getAllowAttachments();
44
+ rm.openStart("div", control);
45
+ rm.class("cnetChatBox");
46
+ rm.style("width", control.getWidth());
47
+ rm.style("height", control.getHeight());
48
+ rm.openEnd();
49
+
50
+ // ---- Sidebar ----
51
+ rm.openStart("aside");
52
+ rm.class("cnetChatBox-sidebar");
53
+ rm.openEnd();
54
+ rm.openStart("div");
55
+ rm.class("cnetChatBox-sidebar-header");
56
+ rm.openEnd();
57
+ rm.openStart("span");
58
+ rm.class("cnetChatBox-sidebar-title");
59
+ rm.openEnd();
60
+ rm.text(i18n.getText("chatbox.conversations") ?? "");
61
+ rm.close("span");
62
+ rm.close("div");
63
+ rm.openStart("div", control.getId() + "-conversations");
64
+ rm.class("cnetChatBox-conversations");
65
+ rm.openEnd();
66
+ rm.close("div");
67
+ rm.close("aside");
68
+
69
+ // ---- Main column ----
70
+ rm.openStart("div");
71
+ rm.class("cnetChatBox-main");
72
+ rm.openEnd();
73
+
74
+ // Header
75
+ rm.openStart("div");
76
+ rm.class("cnetChatBox-header");
77
+ rm.openEnd();
78
+ rm.openStart("div", control.getId() + "-headerTitle");
79
+ rm.class("cnetChatBox-header-title");
80
+ rm.openEnd();
81
+ rm.close("div");
82
+ rm.openStart("button", control.getId() + "-newChat");
83
+ rm.class("cnetChatBox-newChatButton");
84
+ rm.attr("type", "button");
85
+ rm.attr("title", i18n.getText("chatbox.newConversation") ?? "");
86
+ rm.openEnd();
87
+ rm.openStart("span");
88
+ rm.class("cnetChatBox-icon");
89
+ if (newChatIcon) rm.style("font-family", newChatIcon.fontFamily);
90
+ rm.openEnd();
91
+ rm.text(newChatIcon?.content ?? "+");
92
+ rm.close("span");
93
+ rm.openStart("span");
94
+ rm.class("cnetChatBox-newChatButton-label");
95
+ rm.openEnd();
96
+ rm.text(i18n.getText("chatbox.newConversation") ?? "");
97
+ rm.close("span");
98
+ rm.close("button");
99
+ rm.close("div");
100
+
101
+ // Messages
102
+ rm.openStart("div", control.getId() + "-messages");
103
+ rm.class("cnetChatBox-messages");
104
+ rm.openEnd();
105
+ rm.openStart("div");
106
+ rm.class("cnetChatBox-empty");
107
+ rm.openEnd();
108
+ rm.text(i18n.getText("chatbox.emptyHint") ?? "");
109
+ rm.close("div");
110
+ rm.close("div");
111
+
112
+ // Input area
113
+ rm.openStart("div");
114
+ rm.class("cnetChatBox-inputArea");
115
+ rm.openEnd();
116
+ rm.openStart("div");
117
+ rm.class("cnetChatBox-composer");
118
+ rm.openEnd();
119
+ rm.openStart("div", control.getId() + "-pending");
120
+ rm.class("cnetChatBox-attachmentsPending");
121
+ rm.openEnd();
122
+ rm.close("div");
123
+ rm.openStart("textarea", control.getId() + "-input");
124
+ rm.class("cnetChatBox-textarea");
125
+ rm.attr("placeholder", placeholder);
126
+ rm.attr("rows", "1");
127
+ if (control.getBusy()) rm.attr("disabled", "disabled");
128
+ rm.openEnd();
129
+ rm.close("textarea");
130
+ rm.openStart("div");
131
+ rm.class("cnetChatBox-composerBar");
132
+ rm.openEnd();
133
+ rm.openStart("div");
134
+ rm.class("cnetChatBox-composerBar-left");
135
+ rm.openEnd();
136
+ if (allowAttachments) {
137
+ rm.openStart("button", control.getId() + "-attach");
138
+ rm.class("cnetChatBox-toolButton");
139
+ rm.class("cnetChatBox-attach");
140
+ rm.attr("type", "button");
141
+ rm.attr("title", i18n.getText("chatbox.attach") ?? "");
142
+ rm.attr("aria-label", i18n.getText("chatbox.attach") ?? "");
143
+ rm.openEnd();
144
+ rm.openStart("span");
145
+ rm.class("cnetChatBox-icon");
146
+ if (attachIcon) rm.style("font-family", attachIcon.fontFamily);
147
+ rm.openEnd();
148
+ rm.text(attachIcon?.content ?? "📎");
149
+ rm.close("span");
150
+ rm.close("button");
151
+ rm.voidStart("input", control.getId() + "-fileinput");
152
+ rm.class("cnetChatBox-fileinput");
153
+ rm.attr("type", "file");
154
+ rm.attr("multiple", "multiple");
155
+ if (control.getAttachmentAccept()) rm.attr("accept", control.getAttachmentAccept());
156
+ rm.voidEnd();
157
+ }
158
+ rm.close("div");
159
+ rm.openStart("button", control.getId() + "-send");
160
+ rm.class("cnetChatBox-send");
161
+ rm.attr("type", "button");
162
+ rm.attr("title", i18n.getText("chatbox.send") ?? "");
163
+ rm.attr("aria-label", i18n.getText("chatbox.send") ?? "");
164
+ rm.openEnd();
165
+ rm.openStart("span");
166
+ rm.class("cnetChatBox-icon");
167
+ if (sendIcon) rm.style("font-family", sendIcon.fontFamily);
168
+ rm.openEnd();
169
+ rm.text(sendIcon?.content ?? "➤");
170
+ rm.close("span");
171
+ rm.close("button");
172
+ rm.close("div");
173
+ rm.close("div");
174
+ rm.close("div");
175
+ rm.close("div"); // main
176
+
177
+ if (allowAttachments) {
178
+ rm.openStart("div", control.getId() + "-drop");
179
+ rm.class("cnetChatBox-dropOverlay");
180
+ rm.openEnd();
181
+ rm.openStart("span");
182
+ rm.class("cnetChatBox-dropLabel");
183
+ rm.openEnd();
184
+ rm.text(i18n.getText("chatbox.dropHere") ?? "");
185
+ rm.close("span");
186
+ rm.close("div");
187
+ }
188
+ rm.close("div");
189
+ }
190
+ },
191
+ metadata: {
192
+ library: "chatui",
193
+ properties: {
194
+ /**
195
+ * Placeholder shown in the empty input field. If empty, a
196
+ * localised default is used.
197
+ */
198
+ placeholder: {
199
+ type: "string",
200
+ group: "Appearance",
201
+ defaultValue: ""
202
+ },
203
+ /**
204
+ * When true, the input and buttons are disabled — typically set
205
+ * while the assistant is producing a reply.
206
+ */
207
+ busy: {
208
+ type: "boolean",
209
+ group: "Behavior",
210
+ defaultValue: false
211
+ },
212
+ /**
213
+ * CSS width of the control. Defaults to <code>100%</code> so the
214
+ * chat adapts to its parent; override for a fixed size.
215
+ */
216
+ width: {
217
+ type: "sap.ui.core.CSSSize",
218
+ group: "Dimension",
219
+ defaultValue: "100%"
220
+ },
221
+ /**
222
+ * CSS height of the control. Defaults to <code>100%</code>; falls
223
+ * back to a sensible min-height when the parent has no explicit
224
+ * height.
225
+ */
226
+ height: {
227
+ type: "sap.ui.core.CSSSize",
228
+ group: "Dimension",
229
+ defaultValue: "100%"
230
+ },
231
+ /**
232
+ * When true (default), the user can attach files via the paperclip
233
+ * button and drag-and-drop. Uploaded <code>File</code> objects are
234
+ * handed to the consumer via the <code>files</code> parameter of
235
+ * the <code>send</code> event — the library never performs any
236
+ * network transfer itself.
237
+ */
238
+ allowAttachments: {
239
+ type: "boolean",
240
+ group: "Behavior",
241
+ defaultValue: true
242
+ },
243
+ /**
244
+ * Value passed to the underlying <code>&lt;input type="file"&gt;</code>
245
+ * as its <code>accept</code> attribute (e.g. <code>"image/*"</code>
246
+ * or <code>".pdf,.docx"</code>). Empty string means "any file".
247
+ */
248
+ attachmentAccept: {
249
+ type: "string",
250
+ group: "Behavior",
251
+ defaultValue: ""
252
+ },
253
+ /**
254
+ * The list of conversations shown in the sidebar. Each entry has
255
+ * an <code>id</code>, an optional <code>title</code>, an optional
256
+ * ordered array of <code>messages</code>, and an optional
257
+ * <code>updatedAt</code> timestamp (number, ISO string or Date).
258
+ * Passed in by the embedding application and never mutated by
259
+ * the library. When set for the first time, the library
260
+ * automatically selects and displays the first conversation. If
261
+ * the property is empty or unset, an empty new-chat view is
262
+ * shown.
263
+ */
264
+ conversations: {
265
+ type: "object",
266
+ group: "Data",
267
+ defaultValue: []
268
+ },
269
+ /**
270
+ * The id of the currently displayed conversation. Empty string
271
+ * means "new chat draft" — no history is shown, and the next
272
+ * <code>send</code> event carries an empty <code>conversationId</code>
273
+ * so the consumer can allocate a fresh id. When set to a value
274
+ * that matches an entry in {@link #getConversations}, that
275
+ * conversation's messages are loaded into the transcript.
276
+ */
277
+ activeConversationId: {
278
+ type: "string",
279
+ group: "Data",
280
+ defaultValue: ""
281
+ }
282
+ },
283
+ events: {
284
+ /**
285
+ * Fired when the user submits a prompt. The embedding
286
+ * application should produce an assistant response and feed it
287
+ * back via {@link #addAssistantMessage} or
288
+ * {@link #appendToLastAssistantMessage}.
289
+ */
290
+ send: {
291
+ parameters: {
292
+ /** The prompt the user submitted (trimmed; empty when only files are sent). */
293
+ message: {
294
+ type: "string"
295
+ },
296
+ /** File objects the user attached; empty array if none. Not transferred by the library — the consumer is responsible for upload/transport. */
297
+ files: {
298
+ type: "object"
299
+ },
300
+ /** Id of the conversation the message belongs to. Empty string when the user is composing in a new-chat draft — the consumer typically allocates a fresh id and pushes an updated conversation list back. */
301
+ conversationId: {
302
+ type: "string"
303
+ }
304
+ }
305
+ },
306
+ /**
307
+ * Fired when the user clicks the "Neue Konversation" button.
308
+ * The transcript view has already been cleared and
309
+ * <code>activeConversationId</code> reset to <code>""</code> by
310
+ * the time the event fires — listeners typically use this to
311
+ * reset any server-side session state.
312
+ */
313
+ newChat: {},
314
+ /**
315
+ * Fired when the user picks a conversation from the sidebar.
316
+ * The library has already switched the transcript view to that
317
+ * conversation and updated <code>activeConversationId</code>.
318
+ */
319
+ conversationSelect: {
320
+ parameters: {
321
+ /** Id of the conversation the user selected. */
322
+ id: {
323
+ type: "string"
324
+ }
325
+ }
326
+ },
327
+ /**
328
+ * Fired when the user clicks the × on a conversation in the
329
+ * sidebar. The library does not mutate the conversations
330
+ * property itself — the consumer is expected to drop the entry
331
+ * from its store and push an updated list back via
332
+ * {@link #setConversations}.
333
+ */
334
+ conversationDelete: {
335
+ parameters: {
336
+ /** Id of the conversation the user asked to delete. */
337
+ id: {
338
+ type: "string"
339
+ }
340
+ }
341
+ }
342
+ }
343
+ },
344
+ constructor: function _constructor(id, settings) {
345
+ Control.prototype.constructor.call(this, id, settings);
346
+ this._messages = [];
347
+ this._pendingAttachments = [];
348
+ this._conversations = [];
349
+ this._initialised = false;
350
+ this._onLocaleChange = () => {
351
+ this.invalidate();
352
+ };
353
+ },
354
+ init: function _init() {
355
+ Localization.attachChange(this._onLocaleChange);
356
+ },
357
+ exit: function _exit() {
358
+ Localization.detachChange(this._onLocaleChange);
359
+ },
360
+ onAfterRendering: function _onAfterRendering() {
361
+ const textarea = this._dom("input");
362
+ const sendBtn = this._dom("send");
363
+ const newChatBtn = this._dom("newChat");
364
+ textarea?.addEventListener("input", () => {
365
+ textarea.style.height = "auto";
366
+ textarea.style.height = Math.min(textarea.scrollHeight, 160) + "px";
367
+ });
368
+ textarea?.addEventListener("keydown", e => {
369
+ if (e.key === "Enter" && !e.shiftKey) {
370
+ e.preventDefault();
371
+ this._submit();
372
+ }
373
+ });
374
+ sendBtn?.addEventListener("click", () => this._submit());
375
+ newChatBtn?.addEventListener("click", () => this._startNewChat());
376
+ if (this.getAllowAttachments()) {
377
+ this._wireAttachments();
378
+ }
379
+ this._renderSidebar();
380
+ this._renderHeaderTitle();
381
+ this._restoreMessages();
382
+ this._renderPending();
383
+ },
384
+ /**
385
+ * Append an assistant message to the transcript. Pass an empty string
386
+ * to open a "pending" bubble that can then be filled incrementally via
387
+ * {@link #appendToLastAssistantMessage}.
388
+ *
389
+ * @public
390
+ */
391
+ addAssistantMessage: function _addAssistantMessage(text) {
392
+ this._messages.push({
393
+ role: "assistant",
394
+ content: text ?? ""
395
+ });
396
+ this._appendMessageDom(this._messages.length - 1);
397
+ return this;
398
+ },
399
+ /**
400
+ * Append a chunk to the last assistant message. Useful when streaming
401
+ * tokens from a language model. If the last message is not a plain
402
+ * assistant text message, a new one is started.
403
+ *
404
+ * @public
405
+ */
406
+ appendToLastAssistantMessage: function _appendToLastAssistantMessage(chunk) {
407
+ if (!chunk) return this;
408
+ const last = this._messages[this._messages.length - 1];
409
+ if (!last || last.role !== "assistant" || last.control) {
410
+ return this.addAssistantMessage(chunk);
411
+ }
412
+ last.content += chunk;
413
+ this._updateBubble(this._messages.length - 1);
414
+ return this;
415
+ },
416
+ /**
417
+ * Attach an arbitrary UI5 control as a stand-alone assistant bubble
418
+ * (e.g. a <code>sap.viz.ui5.controls.VizFrame</code>, an
419
+ * <code>sap.ui.integration.widgets.Card</code>, or any custom
420
+ * control). The library owns the control's lifecycle from here on —
421
+ * it is destroyed together with the ChatBox as well as on
422
+ * {@link #clear}, {@link #setMessages}, or the user-driven new-chat
423
+ * action. Do not add the same control twice.
424
+ *
425
+ * @public
426
+ */
427
+ addAssistantControl: function _addAssistantControl(control) {
428
+ this.addDependent(control);
429
+ this._messages.push({
430
+ role: "assistant",
431
+ content: "",
432
+ control
433
+ });
434
+ this._appendMessageDom(this._messages.length - 1);
435
+ return this;
436
+ },
437
+ /**
438
+ * Remove all messages from the transcript. Does not fire
439
+ * <code>newChat</code> — that event is reserved for the user-driven
440
+ * clear action. Does not touch the conversations list.
441
+ *
442
+ * @public
443
+ */
444
+ clear: function _clear() {
445
+ this._messages = [];
446
+ this._pendingAttachments = [];
447
+ this.destroyDependents();
448
+ this._restoreMessages();
449
+ this._renderPending();
450
+ return this;
451
+ },
452
+ /**
453
+ * Read-only snapshot of the current transcript. Intended for
454
+ * persistence — do not mutate.
455
+ *
456
+ * @public
457
+ */
458
+ getMessages: function _getMessages() {
459
+ return this._messages.slice();
460
+ },
461
+ /**
462
+ * Replace the transcript with the given messages. Intended as a
463
+ * restore path — the consumer stores {@link #getMessages} somewhere
464
+ * (localStorage, backend, …) and hands it back after a reload. Does
465
+ * not fire <code>newChat</code> or <code>send</code>. Pending
466
+ * attachments (a possibly ongoing draft) are left untouched. Invalid
467
+ * entries (missing/unknown <code>role</code> or non-string
468
+ * <code>content</code>) are silently skipped.
469
+ *
470
+ * @public
471
+ */
472
+ setMessages: function _setMessages(messages) {
473
+ this._messages = this._sanitiseMessages(messages);
474
+ this.destroyDependents();
475
+ this._restoreMessages();
476
+ return this;
477
+ },
478
+ /**
479
+ * Replace the conversations list shown in the sidebar. On the first
480
+ * call with a non-empty list, the first entry is automatically
481
+ * selected and its messages are loaded into the transcript. Later
482
+ * calls only update the sidebar — call {@link #setActiveConversationId}
483
+ * to switch the visible transcript.
484
+ *
485
+ * @public
486
+ */
487
+ setConversations: function _setConversations(conversations) {
488
+ const list = this._sanitiseConversations(conversations);
489
+ this._conversations = list;
490
+ this.setProperty("conversations", list, true);
491
+ if (!this._initialised && list.length > 0 && !this.getActiveConversationId()) {
492
+ const first = list[0];
493
+ this.setProperty("activeConversationId", first.id, true);
494
+ this._messages = this._sanitiseMessages(first.messages ?? []);
495
+ this.destroyDependents();
496
+ this._restoreMessages();
497
+ this._renderHeaderTitle();
498
+ }
499
+ this._initialised = true;
500
+ this._renderSidebar();
501
+ return this;
502
+ },
503
+ /**
504
+ * Read-only snapshot of the conversations currently shown in the
505
+ * sidebar.
506
+ *
507
+ * @public
508
+ */
509
+ getConversations: function _getConversations() {
510
+ return this._conversations.slice();
511
+ },
512
+ /**
513
+ * Switch the transcript view to the given conversation. If the id
514
+ * does not match any conversation (including the empty string), the
515
+ * transcript is cleared — that is the "new chat draft" state.
516
+ *
517
+ * @public
518
+ */
519
+ setActiveConversationId: function _setActiveConversationId(id) {
520
+ const next = typeof id === "string" ? id : "";
521
+ if (next === this.getActiveConversationId()) return this;
522
+ this.setProperty("activeConversationId", next, true);
523
+ const list = this._conversations ?? [];
524
+ const conv = list.find(c => c.id === next);
525
+ this._messages = conv ? this._sanitiseMessages(conv.messages ?? []) : [];
526
+ this._pendingAttachments = [];
527
+ this.destroyDependents();
528
+ this._renderSidebar();
529
+ this._renderHeaderTitle();
530
+ this._restoreMessages();
531
+ this._renderPending();
532
+ return this;
533
+ },
534
+ _i18n: function _i18n() {
535
+ return Lib.getResourceBundleFor("chatui");
536
+ },
537
+ _dom: function _dom(suffix) {
538
+ return document.getElementById(this.getId() + "-" + suffix);
539
+ },
540
+ _messagesContainer: function _messagesContainer() {
541
+ return this._dom("messages");
542
+ },
543
+ _sanitiseMessages: function _sanitiseMessages(messages) {
544
+ const clean = [];
545
+ if (!Array.isArray(messages)) return clean;
546
+ for (const raw of messages) {
547
+ if (!raw || raw.role !== "user" && raw.role !== "assistant") continue;
548
+ if (typeof raw.content !== "string") continue;
549
+ if (raw.control) continue;
550
+ const msg = {
551
+ role: raw.role,
552
+ content: raw.content
553
+ };
554
+ if (Array.isArray(raw.files) && raw.files.length > 0) {
555
+ msg.files = raw.files.slice();
556
+ }
557
+ clean.push(msg);
558
+ }
559
+ return clean;
560
+ },
561
+ _sanitiseConversations: function _sanitiseConversations(conversations) {
562
+ const clean = [];
563
+ if (!Array.isArray(conversations)) return clean;
564
+ for (const raw of conversations) {
565
+ if (!raw || typeof raw.id !== "string" || !raw.id) continue;
566
+ const conv = {
567
+ id: raw.id
568
+ };
569
+ if (typeof raw.title === "string") conv.title = raw.title;
570
+ if (Array.isArray(raw.messages)) conv.messages = this._sanitiseMessages(raw.messages);
571
+ if (raw.updatedAt !== undefined && raw.updatedAt !== null) conv.updatedAt = raw.updatedAt;
572
+ clean.push(conv);
573
+ }
574
+ return clean;
575
+ },
576
+ _submit: function _submit() {
577
+ if (this.getBusy()) return;
578
+ const textarea = this._dom("input");
579
+ const text = textarea?.value.trim() ?? "";
580
+ const files = this._pendingAttachments.slice();
581
+ if (!text && files.length === 0) return;
582
+ if (textarea) {
583
+ textarea.value = "";
584
+ textarea.style.height = "auto";
585
+ }
586
+ const message = {
587
+ role: "user",
588
+ content: text
589
+ };
590
+ if (files.length > 0) message.files = files;
591
+ this._messages.push(message);
592
+ this._pendingAttachments = [];
593
+ this._appendMessageDom(this._messages.length - 1);
594
+ this._renderPending();
595
+ this.fireEvent("send", {
596
+ message: text,
597
+ files,
598
+ conversationId: this.getActiveConversationId()
599
+ });
600
+ },
601
+ _startNewChat: function _startNewChat() {
602
+ if (this.getBusy()) return;
603
+ this._messages = [];
604
+ this._pendingAttachments = [];
605
+ this.destroyDependents();
606
+ this.setProperty("activeConversationId", "", true);
607
+ this._renderSidebar();
608
+ this._renderHeaderTitle();
609
+ this._restoreMessages();
610
+ this._renderPending();
611
+ this.fireEvent("newChat");
612
+ },
613
+ _selectConversation: function _selectConversation(id) {
614
+ if (this.getBusy()) return;
615
+ if (id === this.getActiveConversationId()) return;
616
+ this.setActiveConversationId(id);
617
+ this.fireEvent("conversationSelect", {
618
+ id
619
+ });
620
+ },
621
+ _requestDeleteConversation: function _requestDeleteConversation(id) {
622
+ if (this.getBusy()) return;
623
+ this.fireEvent("conversationDelete", {
624
+ id
625
+ });
626
+ },
627
+ _buildMessageDom: function _buildMessageDom(index) {
628
+ const msg = this._messages[index];
629
+ const div = document.createElement("div");
630
+ const isAssistant = msg.role === "assistant";
631
+ div.className = `cnetChatBox-message cnetChatBox-message--${isAssistant ? "assistant" : "user"}`;
632
+ const bubble = document.createElement("div");
633
+ bubble.className = "cnetChatBox-bubble";
634
+ this._renderBubble(bubble, msg);
635
+ div.appendChild(bubble);
636
+ if (!msg.control) {
637
+ const actions = this._buildActions(index, msg);
638
+ if (actions) div.appendChild(actions);
639
+ }
640
+ return div;
641
+ },
642
+ _buildActions: function _buildActions(index, msg) {
643
+ const actions = document.createElement("div");
644
+ actions.className = "cnetChatBox-actions";
645
+ if (msg.role === "assistant") {
646
+ actions.appendChild(this._buildCopyButton(index));
647
+ } else {
648
+ actions.appendChild(this._buildEditButton(index));
649
+ }
650
+ return actions;
651
+ },
652
+ _buildIconButton: function _buildIconButton(iconName, className, label, fallback) {
653
+ const icon = IconPool.getIconInfo(iconName);
654
+ const btn = document.createElement("button");
655
+ btn.type = "button";
656
+ btn.className = `cnetChatBox-action ${className}`;
657
+ btn.title = label;
658
+ btn.setAttribute("aria-label", label);
659
+ const iconSpan = document.createElement("span");
660
+ iconSpan.className = "cnetChatBox-icon";
661
+ if (icon) iconSpan.style.fontFamily = icon.fontFamily;
662
+ iconSpan.textContent = icon?.content ?? fallback;
663
+ btn.appendChild(iconSpan);
664
+ return btn;
665
+ },
666
+ _buildCopyButton: function _buildCopyButton(index) {
667
+ const label = this._i18n().getText("chatbox.copy") ?? "Copy";
668
+ const btn = this._buildIconButton("sap-icon://copy", "cnetChatBox-copy", label, "⎘");
669
+ btn.addEventListener("click", () => {
670
+ const msg = this._messages[index];
671
+ if (!msg || !msg.content) return;
672
+ void this._copyMessage(msg.content, btn);
673
+ });
674
+ return btn;
675
+ },
676
+ _buildEditButton: function _buildEditButton(index) {
677
+ const label = this._i18n().getText("chatbox.edit") ?? "Edit";
678
+ const btn = this._buildIconButton("sap-icon://edit", "cnetChatBox-edit", label, "✎");
679
+ btn.addEventListener("click", () => this._enterEditMode(index));
680
+ return btn;
681
+ },
682
+ _copyMessage: async function _copyMessage(text, btn) {
683
+ const ok = await this._writeClipboard(text);
684
+ if (!ok) return;
685
+ const i18n = this._i18n();
686
+ const original = btn.getAttribute("aria-label") ?? "";
687
+ const done = i18n.getText("chatbox.copied") ?? "Copied";
688
+ btn.classList.add("cnetChatBox-action--done");
689
+ btn.title = done;
690
+ window.setTimeout(() => {
691
+ btn.classList.remove("cnetChatBox-action--done");
692
+ btn.title = original;
693
+ }, 1200);
694
+ },
695
+ _enterEditMode: function _enterEditMode(index) {
696
+ if (this.getBusy()) return;
697
+ const wrapper = this._messagesContainer()?.children[index];
698
+ const msg = this._messages[index];
699
+ if (!wrapper || !msg || msg.role !== "user") return;
700
+ const bubble = wrapper.querySelector(".cnetChatBox-bubble");
701
+ if (!bubble) return;
702
+ const actionsRow = wrapper.querySelector(".cnetChatBox-actions");
703
+ if (actionsRow) actionsRow.style.display = "none";
704
+ bubble.textContent = "";
705
+ bubble.classList.add("cnetChatBox-bubble--editing");
706
+ if (msg.files && msg.files.length > 0) {
707
+ bubble.appendChild(this._buildAttachmentList(msg.files, "sent"));
708
+ }
709
+ const editor = document.createElement("textarea");
710
+ editor.className = "cnetChatBox-editTextarea";
711
+ editor.value = msg.content;
712
+ editor.rows = 2;
713
+ bubble.appendChild(editor);
714
+ const editBar = document.createElement("div");
715
+ editBar.className = "cnetChatBox-editActions";
716
+ const i18n = this._i18n();
717
+ const cancelBtn = document.createElement("button");
718
+ cancelBtn.type = "button";
719
+ cancelBtn.className = "cnetChatBox-editCancel";
720
+ cancelBtn.textContent = i18n.getText("chatbox.cancel") ?? "Cancel";
721
+ const saveBtn = document.createElement("button");
722
+ saveBtn.type = "button";
723
+ saveBtn.className = "cnetChatBox-editSave";
724
+ saveBtn.textContent = i18n.getText("chatbox.save") ?? "Save";
725
+ cancelBtn.addEventListener("click", () => this._cancelEdit(index));
726
+ saveBtn.addEventListener("click", () => this._commitEdit(index, editor.value));
727
+ editor.addEventListener("keydown", e => {
728
+ if (e.key === "Enter" && !e.shiftKey) {
729
+ e.preventDefault();
730
+ this._commitEdit(index, editor.value);
731
+ } else if (e.key === "Escape") {
732
+ e.preventDefault();
733
+ this._cancelEdit(index);
734
+ }
735
+ });
736
+ editBar.appendChild(cancelBtn);
737
+ editBar.appendChild(saveBtn);
738
+ bubble.appendChild(editBar);
739
+ editor.focus();
740
+ editor.setSelectionRange(editor.value.length, editor.value.length);
741
+ },
742
+ _cancelEdit: function _cancelEdit(index) {
743
+ const wrapper = this._messagesContainer()?.children[index];
744
+ const msg = this._messages[index];
745
+ if (!wrapper || !msg) return;
746
+ const bubble = wrapper.querySelector(".cnetChatBox-bubble");
747
+ if (!bubble) return;
748
+ bubble.classList.remove("cnetChatBox-bubble--editing");
749
+ this._renderBubble(bubble, msg);
750
+ const actionsRow = wrapper.querySelector(".cnetChatBox-actions");
751
+ if (actionsRow) actionsRow.style.display = "";
752
+ },
753
+ _commitEdit: function _commitEdit(index, newText) {
754
+ if (this.getBusy()) return;
755
+ const msg = this._messages[index];
756
+ if (!msg || msg.role !== "user") return;
757
+ const trimmed = newText.trim();
758
+ const files = msg.files ? msg.files.slice() : [];
759
+ const unchanged = trimmed === msg.content;
760
+ const empty = !trimmed && files.length === 0;
761
+ if (unchanged || empty) {
762
+ this._cancelEdit(index);
763
+ return;
764
+ }
765
+ for (let i = index + 1; i < this._messages.length; i++) {
766
+ const later = this._messages[i];
767
+ if (later.control) later.control.destroy();
768
+ }
769
+ this._messages.length = index + 1;
770
+ msg.content = trimmed;
771
+ this._restoreMessages();
772
+ this.fireEvent("send", {
773
+ message: trimmed,
774
+ files,
775
+ conversationId: this.getActiveConversationId()
776
+ });
777
+ },
778
+ _writeClipboard: async function _writeClipboard(text) {
779
+ try {
780
+ if (navigator.clipboard?.writeText) {
781
+ await navigator.clipboard.writeText(text);
782
+ return true;
783
+ }
784
+ } catch {
785
+ // fall through to legacy path
786
+ }
787
+ try {
788
+ const ta = document.createElement("textarea");
789
+ ta.value = text;
790
+ ta.setAttribute("readonly", "");
791
+ ta.style.position = "fixed";
792
+ ta.style.opacity = "0";
793
+ document.body.appendChild(ta);
794
+ ta.select();
795
+ const ok = document.execCommand("copy");
796
+ document.body.removeChild(ta);
797
+ return ok;
798
+ } catch {
799
+ return false;
800
+ }
801
+ },
802
+ _renderBubble: function _renderBubble(bubble, msg) {
803
+ if (msg.role === "assistant") {
804
+ if (msg.control) {
805
+ bubble.classList.remove("cnetChatBox-bubble--pending");
806
+ bubble.classList.add("cnetChatBox-bubble--rich");
807
+ bubble.innerHTML = "";
808
+ msg.control.placeAt(bubble);
809
+ return;
810
+ }
811
+ bubble.classList.remove("cnetChatBox-bubble--rich");
812
+ if (!msg.content) {
813
+ bubble.classList.add("cnetChatBox-bubble--pending");
814
+ bubble.innerHTML = '<span class="cnetChatBox-dots"><span></span><span></span><span></span></span>';
815
+ return;
816
+ }
817
+ bubble.classList.remove("cnetChatBox-bubble--pending");
818
+ bubble.innerHTML = markdownToHtml(msg.content);
819
+ return;
820
+ }
821
+ bubble.textContent = "";
822
+ if (msg.files && msg.files.length > 0) {
823
+ bubble.appendChild(this._buildAttachmentList(msg.files, "sent"));
824
+ }
825
+ if (msg.content) {
826
+ const text = document.createElement("div");
827
+ text.className = "cnetChatBox-bubble-text";
828
+ text.textContent = msg.content;
829
+ bubble.appendChild(text);
830
+ }
831
+ },
832
+ _buildAttachmentList: function _buildAttachmentList(files, variant) {
833
+ const wrap = document.createElement("div");
834
+ wrap.className = `cnetChatBox-attachments cnetChatBox-attachments--${variant}`;
835
+ for (let i = 0; i < files.length; i++) {
836
+ wrap.appendChild(this._buildAttachmentItem(files[i], variant === "pending" ? i : -1));
837
+ }
838
+ return wrap;
839
+ },
840
+ _buildAttachmentItem: function _buildAttachmentItem(file, pendingIndex) {
841
+ const isImage = file.type.startsWith("image/");
842
+ const item = document.createElement("div");
843
+ item.className = `cnetChatBox-attachment cnetChatBox-attachment--${isImage ? "image" : "file"}`;
844
+ if (isImage) {
845
+ const img = document.createElement("img");
846
+ img.className = "cnetChatBox-attachment-thumb";
847
+ img.alt = file.name;
848
+ img.src = URL.createObjectURL(file);
849
+ item.appendChild(img);
850
+ } else {
851
+ const iconInfo = IconPool.getIconInfo("sap-icon://document");
852
+ const iconSpan = document.createElement("span");
853
+ iconSpan.className = "cnetChatBox-icon cnetChatBox-attachment-icon";
854
+ if (iconInfo) iconSpan.style.fontFamily = iconInfo.fontFamily;
855
+ iconSpan.textContent = iconInfo?.content ?? "📄";
856
+ item.appendChild(iconSpan);
857
+ }
858
+ const name = document.createElement("span");
859
+ name.className = "cnetChatBox-attachment-name";
860
+ name.textContent = file.name;
861
+ name.title = file.name;
862
+ item.appendChild(name);
863
+ if (pendingIndex >= 0) {
864
+ const i18n = this._i18n();
865
+ const removeLabel = i18n.getText("chatbox.removeAttachment") ?? "Remove";
866
+ const btn = document.createElement("button");
867
+ btn.type = "button";
868
+ btn.className = "cnetChatBox-attachment-remove";
869
+ btn.title = removeLabel;
870
+ btn.setAttribute("aria-label", removeLabel);
871
+ btn.textContent = "×";
872
+ btn.addEventListener("click", () => this._removePending(pendingIndex));
873
+ item.appendChild(btn);
874
+ }
875
+ return item;
876
+ },
877
+ _appendMessageDom: function _appendMessageDom(index) {
878
+ const container = this._messagesContainer();
879
+ if (!container) return;
880
+ container.querySelector(".cnetChatBox-empty")?.remove();
881
+ container.appendChild(this._buildMessageDom(index));
882
+ container.scrollTop = container.scrollHeight;
883
+ },
884
+ _updateBubble: function _updateBubble(index) {
885
+ const container = this._messagesContainer();
886
+ const wrapper = container?.children[index];
887
+ const bubble = wrapper?.querySelector(".cnetChatBox-bubble");
888
+ if (!bubble || !container) return;
889
+ this._renderBubble(bubble, this._messages[index]);
890
+ const atBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 80;
891
+ if (atBottom) container.scrollTop = container.scrollHeight;
892
+ },
893
+ _restoreMessages: function _restoreMessages() {
894
+ const container = this._messagesContainer();
895
+ if (!container) return;
896
+ container.innerHTML = "";
897
+ if (this._messages.length === 0) {
898
+ const hint = document.createElement("div");
899
+ hint.className = "cnetChatBox-empty";
900
+ hint.textContent = this._i18n().getText("chatbox.emptyHint") ?? "";
901
+ container.appendChild(hint);
902
+ return;
903
+ }
904
+ for (let i = 0; i < this._messages.length; i++) {
905
+ container.appendChild(this._buildMessageDom(i));
906
+ }
907
+ container.scrollTop = container.scrollHeight;
908
+ },
909
+ _renderSidebar: function _renderSidebar() {
910
+ const container = this._dom("conversations");
911
+ if (!container) return;
912
+ container.innerHTML = "";
913
+ if (this._conversations.length === 0) {
914
+ const empty = document.createElement("div");
915
+ empty.className = "cnetChatBox-conversations-empty";
916
+ empty.textContent = this._i18n().getText("chatbox.noConversations") ?? "";
917
+ container.appendChild(empty);
918
+ return;
919
+ }
920
+ const active = this.getActiveConversationId();
921
+ for (const conv of this._conversations) {
922
+ container.appendChild(this._buildConversationItem(conv, conv.id === active));
923
+ }
924
+ },
925
+ _buildConversationItem: function _buildConversationItem(conv, isActive) {
926
+ const i18n = this._i18n();
927
+ const item = document.createElement("div");
928
+ item.className = "cnetChatBox-conversation" + (isActive ? " cnetChatBox-conversation--active" : "");
929
+ item.setAttribute("role", "button");
930
+ item.tabIndex = 0;
931
+ const body = document.createElement("div");
932
+ body.className = "cnetChatBox-conversation-body";
933
+ const title = document.createElement("div");
934
+ title.className = "cnetChatBox-conversation-title";
935
+ title.textContent = this._conversationTitle(conv);
936
+ body.appendChild(title);
937
+ const snippet = document.createElement("div");
938
+ snippet.className = "cnetChatBox-conversation-snippet";
939
+ snippet.textContent = this._conversationSnippet(conv);
940
+ body.appendChild(snippet);
941
+ const time = document.createElement("div");
942
+ time.className = "cnetChatBox-conversation-time";
943
+ time.textContent = this._formatTimestamp(conv.updatedAt);
944
+ body.appendChild(time);
945
+ item.appendChild(body);
946
+ const removeLabel = i18n.getText("chatbox.deleteConversation") ?? "Delete conversation";
947
+ const del = document.createElement("button");
948
+ del.type = "button";
949
+ del.className = "cnetChatBox-conversation-remove";
950
+ del.title = removeLabel;
951
+ del.setAttribute("aria-label", removeLabel);
952
+ del.textContent = "×";
953
+ del.addEventListener("click", e => {
954
+ e.stopPropagation();
955
+ this._requestDeleteConversation(conv.id);
956
+ });
957
+ item.appendChild(del);
958
+ item.addEventListener("click", () => this._selectConversation(conv.id));
959
+ item.addEventListener("keydown", e => {
960
+ if (e.key === "Enter" || e.key === " ") {
961
+ e.preventDefault();
962
+ this._selectConversation(conv.id);
963
+ }
964
+ });
965
+ return item;
966
+ },
967
+ _conversationTitle: function _conversationTitle(conv) {
968
+ if (conv.title && conv.title.trim()) return conv.title.trim();
969
+ const msgs = conv.messages ?? [];
970
+ for (const m of msgs) {
971
+ if (m.role === "user" && m.content && m.content.trim()) {
972
+ return this._truncate(m.content.trim(), 40);
973
+ }
974
+ }
975
+ return this._i18n().getText("chatbox.untitledConversation") ?? "Neue Konversation";
976
+ },
977
+ _conversationSnippet: function _conversationSnippet(conv) {
978
+ const msgs = conv.messages ?? [];
979
+ for (let i = msgs.length - 1; i >= 0; i--) {
980
+ const m = msgs[i];
981
+ if (m.content && m.content.trim()) {
982
+ const prefix = m.role === "user" ? (this._i18n().getText("chatbox.youPrefix") ?? "Du:") + " " : "";
983
+ return this._truncate(prefix + m.content.trim().replace(/\s+/g, " "), 60);
984
+ }
985
+ }
986
+ return "";
987
+ },
988
+ _truncate: function _truncate(text, max) {
989
+ return text.length > max ? text.slice(0, max - 1).trimEnd() + "…" : text;
990
+ },
991
+ _formatTimestamp: function _formatTimestamp(raw) {
992
+ if (raw === undefined || raw === null) return "";
993
+ const d = raw instanceof Date ? raw : new Date(raw);
994
+ if (isNaN(d.getTime())) return "";
995
+ const now = new Date();
996
+ const sameDay = d.toDateString() === now.toDateString();
997
+ const yesterday = new Date(now);
998
+ yesterday.setDate(now.getDate() - 1);
999
+ const wasYesterday = d.toDateString() === yesterday.toDateString();
1000
+ const locale = Localization.getLanguage() || undefined;
1001
+ if (sameDay) {
1002
+ return d.toLocaleTimeString(locale, {
1003
+ hour: "2-digit",
1004
+ minute: "2-digit"
1005
+ });
1006
+ }
1007
+ if (wasYesterday) {
1008
+ return this._i18n().getText("chatbox.yesterday") ?? "Gestern";
1009
+ }
1010
+ const weekDelta = (now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24);
1011
+ if (weekDelta < 7) {
1012
+ return d.toLocaleDateString(locale, {
1013
+ weekday: "short"
1014
+ });
1015
+ }
1016
+ return d.toLocaleDateString(locale, {
1017
+ day: "2-digit",
1018
+ month: "2-digit",
1019
+ year: "2-digit"
1020
+ });
1021
+ },
1022
+ _renderHeaderTitle: function _renderHeaderTitle() {
1023
+ const target = this._dom("headerTitle");
1024
+ if (!target) return;
1025
+ const active = this.getActiveConversationId();
1026
+ const conv = active ? this._conversations.find(c => c.id === active) : undefined;
1027
+ target.textContent = conv ? this._conversationTitle(conv) : "";
1028
+ },
1029
+ _wireAttachments: function _wireAttachments() {
1030
+ const attachBtn = this._dom("attach");
1031
+ const fileInput = this._dom("fileinput");
1032
+ const root = this.getDomRef();
1033
+ attachBtn?.addEventListener("click", () => fileInput?.click());
1034
+ fileInput?.addEventListener("change", () => {
1035
+ if (!fileInput.files) return;
1036
+ this._addPending(Array.from(fileInput.files));
1037
+ fileInput.value = "";
1038
+ });
1039
+ if (!root) return;
1040
+ let dragDepth = 0;
1041
+ root.addEventListener("dragenter", e => {
1042
+ if (!this._hasFiles(e)) return;
1043
+ e.preventDefault();
1044
+ dragDepth++;
1045
+ root.classList.add("cnetChatBox--dragOver");
1046
+ });
1047
+ root.addEventListener("dragover", e => {
1048
+ if (!this._hasFiles(e)) return;
1049
+ e.preventDefault();
1050
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
1051
+ });
1052
+ root.addEventListener("dragleave", () => {
1053
+ dragDepth = Math.max(0, dragDepth - 1);
1054
+ if (dragDepth === 0) root.classList.remove("cnetChatBox--dragOver");
1055
+ });
1056
+ root.addEventListener("drop", e => {
1057
+ if (!this._hasFiles(e)) return;
1058
+ e.preventDefault();
1059
+ dragDepth = 0;
1060
+ root.classList.remove("cnetChatBox--dragOver");
1061
+ const files = e.dataTransfer?.files;
1062
+ if (!files || files.length === 0) return;
1063
+ this._addPending(Array.from(files));
1064
+ });
1065
+ },
1066
+ _hasFiles: function _hasFiles(e) {
1067
+ const types = e.dataTransfer?.types;
1068
+ if (!types) return false;
1069
+ for (let i = 0; i < types.length; i++) {
1070
+ if (types[i] === "Files") return true;
1071
+ }
1072
+ return false;
1073
+ },
1074
+ _addPending: function _addPending(files) {
1075
+ if (this.getBusy()) return;
1076
+ if (files.length === 0) return;
1077
+ this._pendingAttachments = this._pendingAttachments.concat(files);
1078
+ this._renderPending();
1079
+ },
1080
+ _removePending: function _removePending(index) {
1081
+ if (index < 0 || index >= this._pendingAttachments.length) return;
1082
+ this._pendingAttachments.splice(index, 1);
1083
+ this._renderPending();
1084
+ },
1085
+ _renderPending: function _renderPending() {
1086
+ const container = this._dom("pending");
1087
+ if (!container) return;
1088
+ container.innerHTML = "";
1089
+ if (this._pendingAttachments.length === 0) {
1090
+ container.classList.remove("cnetChatBox-attachmentsPending--hasItems");
1091
+ return;
1092
+ }
1093
+ container.classList.add("cnetChatBox-attachmentsPending--hasItems");
1094
+ container.appendChild(this._buildAttachmentList(this._pendingAttachments, "pending"));
1095
+ }
1096
+ });
1097
+ return ChatBox;
1098
+ });
1099
+ //# sourceMappingURL=ChatBox-dbg.js.map