mini-chat-bot-widget 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1286 @@
1
+ // text-chat-screen.js - Text chat screen component
2
+
3
+ class TextChatScreen {
4
+ constructor(options = {}) {
5
+ this.placeholder = options.placeholder || "Type your message...";
6
+ this.primaryColor = options.primaryColor || "#1a5c4b";
7
+ this.title = options.title || "Chat Assistant";
8
+ this.onMessage = options.onMessage || (() => { });
9
+ this.onBack = options.onBack || (() => { });
10
+ this.onOpenDrawer = options.onOpenDrawer || (() => { });
11
+ this.onClose = options.onClose || (() => { });
12
+ this.container = null;
13
+ this.messages = options.messages
14
+ this.sendMessage = options.sendMessage || null;
15
+ this.contentBlocks = options.contentBlocks || [];
16
+ this.onContentBlocksChange = options.onContentBlocksChange || (() => { });
17
+ this.navigateToAudioScreen = options.navigateToAudioScreen
18
+ // Supported file types
19
+ this.SUPPORTED_FILE_TYPES = [
20
+ "image/jpeg",
21
+ "image/png",
22
+ "image/gif",
23
+ "image/webp",
24
+ ];
25
+ }
26
+
27
+ render(container) {
28
+ this.container = container;
29
+ this._applyStyles();
30
+ container.innerHTML = `
31
+ <div class="text-chat-screen">
32
+ <div class="chat-header">
33
+ <div class="chat-header-content">
34
+ <button class="chat-back" id="text-chat-menu">
35
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu-icon lucide-menu"><path d="M4 5h16"/><path d="M4 12h16"/><path d="M4 19h16"/></svg>
36
+ </button>
37
+
38
+ <div class="chat-header-text">
39
+ <div class="chat-title">${this.title}</div>
40
+ </div>
41
+ </div>
42
+ <button class="chat-close" id="navigate-to-audio-screen">
43
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic-icon lucide-mic"><path d="M12 19v3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><rect x="9" y="2" width="6" height="13" rx="3"/></svg>
44
+ </button>
45
+ &#8203; &#8203; &#8203;
46
+ <button class="chat-close" id="text-chat-close">
47
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
48
+ <line x1="18" y1="6" x2="6" y2="18"></line>
49
+ <line x1="6" y1="6" x2="18" y2="18"></line>
50
+ </svg>
51
+ </button>
52
+ </div>
53
+ <div class="chat-messages" id="chat-messages">
54
+ <div class="chat-welcome">
55
+ <div class="welcome-text">👋 Hello! I'm your AI assistant. How can I help you today?</div>
56
+ </div>
57
+ </div>
58
+ <div class="file-attachments-container" id="file-attachments-container" style="display: none;"></div>
59
+ <div class="chat-input-wrapper">
60
+ <div class="file-upload-controls">
61
+ <input
62
+ type="file"
63
+ id="file-upload-input"
64
+ accept="image/jpeg,image/png,image/gif,image/webp"
65
+ style="display: none;"
66
+ />
67
+ <button id="file-upload-button" class="file-upload-button" title="Attach files">
68
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
69
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
70
+ </svg>
71
+ </button>
72
+ </div>
73
+ <input type="text" id="chat-input" placeholder="${this.placeholder}" autocomplete="off" />
74
+ <button id="chat-send" disabled>
75
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
76
+ <line x1="22" y1="2" x2="11" y2="13"></line>
77
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
78
+ </svg>
79
+ </button>
80
+ </div>
81
+ </div>
82
+ `;
83
+
84
+ // Bind header events
85
+ const menuBtn = container.querySelector("#text-chat-menu");
86
+ const closeBtn = container.querySelector("#text-chat-close");
87
+ const navigateToAudioBtn = container.querySelector("#navigate-to-audio-screen")
88
+
89
+ if (menuBtn) {
90
+ menuBtn.addEventListener("click", () => {
91
+ if (this.onOpenDrawer) {
92
+ this.onOpenDrawer();
93
+ } else if (this.onBack) {
94
+ this.onBack();
95
+ }
96
+ });
97
+ }
98
+
99
+ if (closeBtn) {
100
+ closeBtn.addEventListener("click", () => {
101
+ if (this.onClose) this.onClose();
102
+ });
103
+ }
104
+
105
+ if (navigateToAudioBtn) {
106
+ navigateToAudioBtn.addEventListener("click", () => {
107
+ if (this.navigateToAudioScreen) this.navigateToAudioScreen()
108
+ })
109
+ }
110
+
111
+ // Bind text chat events
112
+ const input = container.querySelector("#chat-input");
113
+ const sendBtn = container.querySelector("#chat-send");
114
+ const fileInput = container.querySelector("#file-upload-input");
115
+ const fileUploadBtn = container.querySelector("#file-upload-button");
116
+
117
+ // File upload handler
118
+ fileUploadBtn.addEventListener("click", () => {
119
+ fileInput.click();
120
+ });
121
+
122
+ fileInput.addEventListener("change", (e) => {
123
+ this.handleFileUpload(e);
124
+ });
125
+
126
+ // Update send button state based on input and contentBlocks
127
+ const updateSendButton = () => {
128
+ const hasText = input.value.trim().length > 0;
129
+ const hasFiles = this.contentBlocks.length > 0;
130
+ sendBtn.disabled = !(hasText || hasFiles);
131
+ };
132
+
133
+ input.addEventListener("input", () => {
134
+ updateSendButton();
135
+ });
136
+
137
+ const send = async () => {
138
+ const text = input.value.trim();
139
+ if (!text && this.contentBlocks.length === 0) return;
140
+
141
+ // Add user message to UI immediately
142
+ this.addMessage(text || "📎 File attachments", true);
143
+ input.value = "";
144
+ sendBtn.disabled = true;
145
+
146
+ this.hideTypingIndicator();
147
+
148
+ // Use the sendMessage function from widget if available
149
+ if (this.sendMessage) {
150
+ try {
151
+ await this.sendMessage(text, this.contentBlocks);
152
+ // Content blocks are cleared in widget's sendMessage, but ensure UI is updated
153
+ this.contentBlocks = [];
154
+ this.onContentBlocksChange(this.contentBlocks);
155
+ this.renderFilePreview(); // Update preview
156
+ this.updateSendButton();
157
+ } catch (error) {
158
+ console.error("Error sending message:", error);
159
+ this.addMessage("Sorry, I encountered an error. Please try again.", false);
160
+ } finally {
161
+ this.hideTypingIndicator();
162
+ }
163
+ } else {
164
+ // Fallback to old behavior
165
+ setTimeout(() => {
166
+ this.hideTypingIndicator();
167
+ this.handleUserMessage(text);
168
+ }, 1000 + Math.random() * 1000);
169
+ }
170
+ };
171
+
172
+ sendBtn.addEventListener("click", send);
173
+ input.addEventListener("keypress", (e) => {
174
+ if (e.key === "Enter" && !sendBtn.disabled) send();
175
+ });
176
+
177
+ // Focus input when screen loads
178
+ setTimeout(() => input.focus(), 100);
179
+
180
+ // Render file preview if contentBlocks exist
181
+ this.renderFilePreview();
182
+
183
+ // Sync existing messages on render
184
+ if (this.messages && this.messages.length > 0) {
185
+ setTimeout(() => {
186
+ this._syncMessages(this.messages);
187
+ }, 50);
188
+ }
189
+ }
190
+
191
+ // Convert file to content block
192
+ fileToContentBlock(file) {
193
+ return new Promise((resolve, reject) => {
194
+ const reader = new FileReader();
195
+
196
+ reader.onload = () => {
197
+ const base64Data = reader.result.split(",")[1]; // Remove data:mime;base64, prefix
198
+
199
+ if (this.SUPPORTED_FILE_TYPES.includes(file.type)) {
200
+ resolve({
201
+ type: "image",
202
+ source_type: "base64",
203
+ mime_type: file.type,
204
+ data: base64Data,
205
+ metadata: {
206
+ name: file.name,
207
+ size: file.size,
208
+ },
209
+ });
210
+ } else {
211
+ reject(new Error(`Unsupported file type: ${file.type}`));
212
+ }
213
+ };
214
+
215
+ reader.onerror = () => {
216
+ reject(new Error(`Failed to read file: ${file.name}`));
217
+ };
218
+
219
+ reader.readAsDataURL(file);
220
+ });
221
+ }
222
+
223
+ // Check if file is duplicate
224
+ isDuplicateFile(file, existingBlocks) {
225
+ if (this.SUPPORTED_FILE_TYPES.includes(file.type)) {
226
+ return existingBlocks.some(
227
+ (block) =>
228
+ block.type === "image" &&
229
+ block.metadata?.name === file.name &&
230
+ block.mime_type === file.type
231
+ );
232
+ }
233
+
234
+ return false;
235
+ }
236
+
237
+ // Format file size
238
+ formatFileSize(bytes) {
239
+ if (bytes === 0) return "0 Bytes";
240
+
241
+ const k = 1024;
242
+ const sizes = ["Bytes", "KB", "MB", "GB"];
243
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
244
+
245
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
246
+ }
247
+
248
+ // Get file type display name
249
+ getFileTypeDisplay(mimeType) {
250
+ switch (mimeType) {
251
+ case "image/jpeg":
252
+ case "image/jpg":
253
+ return "JPEG Image";
254
+ case "image/png":
255
+ return "PNG Image";
256
+ case "image/gif":
257
+ return "GIF Image";
258
+ case "image/webp":
259
+ return "WebP Image";
260
+ default:
261
+ return "Unknown File";
262
+ }
263
+ }
264
+
265
+ // Handle file upload
266
+ async handleFileUpload(e) {
267
+ const files = e.target.files;
268
+ if (!files) return;
269
+
270
+ const fileArray = Array.from(files);
271
+ const validFiles = fileArray.filter((file) =>
272
+ this.SUPPORTED_FILE_TYPES.includes(file.type)
273
+ );
274
+ const invalidFiles = fileArray.filter(
275
+ (file) => !this.SUPPORTED_FILE_TYPES.includes(file.type)
276
+ );
277
+
278
+ if (invalidFiles.length > 0) {
279
+ console.warn(
280
+ "Invalid file type. Please upload a JPEG, PNG, GIF, or WEBP image."
281
+ );
282
+ // Could show a toast/alert here
283
+ }
284
+
285
+ // Since we only allow 1 file, we take the first valid one and replace existing
286
+ const fileToProcess = validFiles[0];
287
+
288
+ if (fileToProcess) {
289
+ try {
290
+ const newBlocks = await this.fileToContentBlock(fileToProcess);
291
+ // Replace existing blocks with new one
292
+ this.contentBlocks = [newBlocks];
293
+ this.onContentBlocksChange(this.contentBlocks);
294
+ this.renderFilePreview();
295
+ this.updateSendButton();
296
+ } catch (error) {
297
+ console.error(`Failed to process files: ${error.message}`);
298
+ }
299
+ }
300
+
301
+ // Clear the input
302
+ e.target.value = "";
303
+ }
304
+
305
+ // Remove content block
306
+ removeBlock(index) {
307
+ this.contentBlocks = this.contentBlocks.filter((_, i) => i !== index);
308
+ this.onContentBlocksChange(this.contentBlocks);
309
+ this.renderFilePreview();
310
+ this.updateSendButton();
311
+ }
312
+
313
+ // Update send button state
314
+ updateSendButton() {
315
+ if (!this.container) return;
316
+ const input = this.container.querySelector("#chat-input");
317
+ const sendBtn = this.container.querySelector("#chat-send");
318
+ if (!input || !sendBtn) return;
319
+
320
+ const hasText = input.value.trim().length > 0;
321
+ const hasFiles = this.contentBlocks.length > 0;
322
+ sendBtn.disabled = !(hasText || hasFiles);
323
+ }
324
+
325
+ // Render file preview
326
+ renderFilePreview() {
327
+ if (!this.container) return;
328
+
329
+ const container = this.container.querySelector("#file-attachments-container");
330
+ if (!container) return;
331
+
332
+ if (!this.contentBlocks || this.contentBlocks.length === 0) {
333
+ container.innerHTML = "";
334
+ container.style.display = "none";
335
+ return;
336
+ }
337
+
338
+ container.style.display = "flex";
339
+
340
+ container.innerHTML = this.contentBlocks
341
+ .map((block, index) => {
342
+ const displayName =
343
+ block.metadata?.filename || block.metadata?.name || "file";
344
+ const size = this.formatFileSize(block.metadata?.size || 0);
345
+ const isImage = block.type === "image";
346
+ const src = isImage
347
+ ? `data:${block.mime_type};base64,${block.data}`
348
+ : null;
349
+
350
+ return `
351
+ <div class="file-attachment ${isImage ? "has-thumbnail" : ""}" data-index="${index}">
352
+ ${isImage
353
+ ? `
354
+ <div class="file-thumbnail">
355
+ <img src="${src}" alt="${displayName}" class="file-thumbnail-image" />
356
+ </div>
357
+ `
358
+ : '<div class="file-attachment-icon">📄</div>'
359
+ }
360
+ <div class="file-attachment-info">
361
+ <div class="file-attachment-name" title="${displayName}">
362
+ ${displayName}
363
+ </div>
364
+ <div class="file-attachment-size">${size}</div>
365
+ </div>
366
+ <button
367
+ class="file-attachment-remove"
368
+ data-index="${index}"
369
+ title="Remove file"
370
+ >
371
+
372
+ </button>
373
+ </div>
374
+ `;
375
+ })
376
+ .join("");
377
+
378
+ // Bind remove buttons
379
+ container.querySelectorAll(".file-attachment-remove").forEach((btn) => {
380
+ btn.addEventListener("click", (e) => {
381
+ e.stopPropagation();
382
+ const index = parseInt(btn.getAttribute("data-index"));
383
+ this.removeBlock(index);
384
+ });
385
+ });
386
+
387
+ // Bind image click for expansion (optional - can add modal later)
388
+ container.querySelectorAll(".file-thumbnail-image").forEach((img) => {
389
+ img.addEventListener("click", (e) => {
390
+ e.stopPropagation();
391
+ // Could add image expansion modal here
392
+ });
393
+ });
394
+ }
395
+
396
+ _formatTime(date) {
397
+ let hours = date.getHours();
398
+ const minutes = date.getMinutes();
399
+ const ampm = hours >= 12 ? 'PM' : 'AM';
400
+ hours = hours % 12;
401
+ hours = hours ? hours : 12;
402
+ const strTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ' ' + ampm;
403
+ return strTime;
404
+ }
405
+
406
+ addMessage(text, isUser, messageData = null) {
407
+ console.log("Is user", isUser)
408
+ if (!this.container) return;
409
+
410
+ const messagesContainer = this.container.querySelector("#chat-messages");
411
+ if (!messagesContainer) return;
412
+
413
+ const welcome = messagesContainer.querySelector(".chat-welcome");
414
+ if (welcome) welcome.remove();
415
+
416
+ // Check if message already exists (for syncing)
417
+ const messageId = messageData?.id || Date.now();
418
+ console.log("Message ID", messageId)
419
+ const existingMessage = messagesContainer.querySelector(`[data-message-id="${messageId}"]`);
420
+ if (existingMessage && messageData) {
421
+ // Update existing message
422
+ const bubble = existingMessage.querySelector(".message-bubble");
423
+ if (bubble) {
424
+ bubble.innerHTML = this._escapeHtml(messageData.content || text);
425
+ }
426
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
427
+ return;
428
+ }
429
+
430
+ const messageEl = document.createElement("div");
431
+ messageEl.className = `chat-message ${isUser ? "user" : "bot"}`;
432
+ messageEl.setAttribute("data-message-id", messageId);
433
+
434
+ // Handle different message types
435
+ let content = text;
436
+ if (messageData) {
437
+ if (messageData.isStreaming) {
438
+ content = messageData.content || "Thinking...";
439
+ } else if (messageData.isError) {
440
+ content = messageData.content || "Error occurred";
441
+ messageEl.classList.add("error");
442
+ } else {
443
+ content = messageData.content || text;
444
+ }
445
+ }
446
+
447
+ // Build attachments HTML if present
448
+ let attachmentsHTML = "";
449
+ if (messageData && messageData.attachments && messageData.attachments.length > 0) {
450
+ attachmentsHTML = `
451
+ <div class="message-attachments">
452
+ ${messageData.attachments.map((attachment, index) => {
453
+ const displayName = attachment.metadata?.name || attachment.metadata?.filename || "Unknown file";
454
+ const isImage = attachment.type === "image";
455
+ const imageSrc = isImage && attachment.data
456
+ ? `data:${attachment.mime_type};base64,${attachment.data}`
457
+ : null;
458
+
459
+ return `
460
+ <div class="message-attachment ${isImage ? "image" : "pdf"}" data-attachment-index="${index}">
461
+ <span class="message-attachment-icon">
462
+ ${isImage ? "🖼️" : "📄"}
463
+ </span>
464
+ <div class="message-attachment-info">
465
+ <div
466
+ class="message-attachment-name ${isImage ? "clickable" : ""}"
467
+ ${isImage && imageSrc ? `data-image-src="${imageSrc}" data-image-alt="${displayName}"` : ""}
468
+ style="cursor: ${isImage ? "pointer" : "default"};"
469
+ >
470
+ ${this._escapeHtml(displayName)}
471
+ </div>
472
+ <div class="message-attachment-size">
473
+ ${this.formatFileSize(attachment.metadata?.size || 0)} • ${this.getFileTypeDisplay(attachment.mime_type)}
474
+ </div>
475
+ </div>
476
+ </div>
477
+ `;
478
+ }).join("")}
479
+ </div>
480
+ `;
481
+ }
482
+
483
+ const timestamp = messageData?.timestamp ? new Date(messageData.timestamp) : new Date();
484
+ const formattedTime = this._formatTime(timestamp);
485
+
486
+ messageEl.innerHTML = `
487
+ <div class="message-content">
488
+ ${attachmentsHTML}
489
+ <div class="message-bubble">
490
+ ${this._escapeHtml(content)}
491
+ </div>
492
+ <div class="message-time">${formattedTime}</div>
493
+ </div>
494
+ `;
495
+
496
+ // Bind image click handlers for expansion
497
+ if (messageData && messageData.attachments) {
498
+ messageEl.querySelectorAll(".message-attachment-name.clickable").forEach((el) => {
499
+ el.addEventListener("click", (e) => {
500
+ e.stopPropagation();
501
+ const imageSrc = el.getAttribute("data-image-src");
502
+ const imageAlt = el.getAttribute("data-image-alt");
503
+ if (imageSrc) {
504
+ this.showExpandedImage(imageSrc, imageAlt);
505
+ }
506
+ });
507
+ });
508
+ }
509
+
510
+ messagesContainer.appendChild(messageEl);
511
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
512
+ }
513
+
514
+ // Sync messages from shared array
515
+ _syncMessages(messages) {
516
+ if (!this.container) return;
517
+
518
+ const messagesContainer = this.container.querySelector("#chat-messages");
519
+ if (!messagesContainer) return;
520
+
521
+ // Remove welcome message if there are any messages
522
+ if (messages.length > 0) {
523
+ const welcome = messagesContainer.querySelector(".chat-welcome");
524
+ if (welcome) welcome.remove();
525
+ }
526
+
527
+ // Get existing message elements
528
+ const existingMessages = messagesContainer.querySelectorAll("[data-message-id]");
529
+ const existingIds = new Set(Array.from(existingMessages).map(el => el.getAttribute("data-message-id")));
530
+
531
+ // Add or update messages
532
+ messages.forEach((msg) => {
533
+ const msgId = String(msg.id);
534
+ if (existingIds.has(msgId)) {
535
+ // Update existing message
536
+ const existingEl = messagesContainer.querySelector(`[data-message-id="${msgId}"]`);
537
+ if (existingEl) {
538
+ const bubble = existingEl.querySelector(".message-bubble");
539
+ if (bubble) {
540
+ bubble.innerHTML = this._escapeHtml(msg.content || "");
541
+ }
542
+
543
+ // Update attachments if they exist
544
+ const messageContent = existingEl.querySelector(".message-content");
545
+ if (messageContent && msg.attachments && msg.attachments.length > 0) {
546
+ let attachmentsHTML = `
547
+ <div class="message-attachments">
548
+ ${msg.attachments.map((attachment, index) => {
549
+ const displayName = attachment.metadata?.name || attachment.metadata?.filename || "Unknown file";
550
+ const isImage = attachment.type === "image";
551
+ const imageSrc = isImage && attachment.data
552
+ ? `data:${attachment.mime_type};base64,${attachment.data}`
553
+ : null;
554
+
555
+ return `
556
+ <div class="message-attachment ${isImage ? "image" : "pdf"}" data-attachment-index="${index}">
557
+ <span class="message-attachment-icon">
558
+ ${isImage ? "🖼️" : "📄"}
559
+ </span>
560
+ <div class="message-attachment-info">
561
+ <div
562
+ class="message-attachment-name ${isImage ? "clickable" : ""}"
563
+ ${isImage && imageSrc ? `data-image-src="${imageSrc}" data-image-alt="${displayName}"` : ""}
564
+ style="cursor: ${isImage ? "pointer" : "default"};"
565
+ >
566
+ ${this._escapeHtml(displayName)}
567
+ </div>
568
+ <div class="message-attachment-size">
569
+ ${this.formatFileSize(attachment.metadata?.size || 0)} • ${this.getFileTypeDisplay(attachment.mime_type)}
570
+ </div>
571
+ </div>
572
+ </div>
573
+ `;
574
+ }).join("")}
575
+ </div>
576
+ `;
577
+
578
+ // Insert or update attachments
579
+ const existingAttachments = messageContent.querySelector(".message-attachments");
580
+ if (existingAttachments) {
581
+ existingAttachments.outerHTML = attachmentsHTML;
582
+ } else {
583
+ messageContent.insertAdjacentHTML("afterbegin", attachmentsHTML);
584
+ }
585
+
586
+ // Re-bind image click handlers
587
+ messageContent.querySelectorAll(".message-attachment-name.clickable").forEach((el) => {
588
+ // Remove existing listeners by cloning
589
+ const newEl = el.cloneNode(true);
590
+ el.parentNode.replaceChild(newEl, el);
591
+
592
+ newEl.addEventListener("click", (e) => {
593
+ e.stopPropagation();
594
+ const imageSrc = newEl.getAttribute("data-image-src");
595
+ const imageAlt = newEl.getAttribute("data-image-alt");
596
+ if (imageSrc) {
597
+ this.showExpandedImage(imageSrc, imageAlt);
598
+ }
599
+ });
600
+ });
601
+ }
602
+
603
+ // Update classes based on message state
604
+ if (msg.isStreaming) {
605
+ existingEl.classList.add("streaming");
606
+ } else {
607
+ existingEl.classList.remove("streaming");
608
+ }
609
+ if (msg.isError) {
610
+ existingEl.classList.add("error");
611
+ }
612
+ }
613
+ } else {
614
+ // Add new message
615
+ this.addMessage(
616
+ msg.content || "",
617
+ msg.sender === "user",
618
+ msg
619
+ );
620
+ }
621
+ });
622
+
623
+ // Remove messages that are no longer in the array
624
+ existingMessages.forEach((el) => {
625
+ const msgId = el.getAttribute("data-message-id");
626
+ const exists = messages.some(msg => String(msg.id) === msgId);
627
+ if (!exists) {
628
+ el.remove();
629
+ }
630
+ });
631
+
632
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
633
+ }
634
+
635
+ showTypingIndicator() {
636
+ if (!this.container) return;
637
+
638
+ const messagesContainer = this.container.querySelector("#chat-messages");
639
+ if (!messagesContainer) return;
640
+
641
+ const indicator = document.createElement("div");
642
+ indicator.className = "chat-message bot";
643
+ indicator.id = "typing-indicator";
644
+ indicator.innerHTML = `
645
+ <div class="typing-indicator">
646
+ <div class="typing-dot"></div>
647
+ <div class="typing-dot"></div>
648
+ <div class="typing-dot"></div>
649
+ </div>
650
+ `;
651
+ messagesContainer.appendChild(indicator);
652
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
653
+ }
654
+
655
+ hideTypingIndicator() {
656
+ if (!this.container) return;
657
+ const indicator = this.container.querySelector("#typing-indicator");
658
+ if (indicator) indicator.remove();
659
+ }
660
+
661
+ handleUserMessage(text) {
662
+ // Call the callback if provided, otherwise use default echo
663
+ if (this.onMessage) {
664
+ this.onMessage(text, (response) => {
665
+ this.addMessage(response, false);
666
+ });
667
+ } else {
668
+ // Default echo response
669
+ this.addMessage(`You said: "${text}"`, false);
670
+ }
671
+ }
672
+
673
+ _escapeHtml(text) {
674
+ const div = document.createElement("div");
675
+ div.textContent = text;
676
+ return div.innerHTML;
677
+ }
678
+
679
+ // Show expanded image modal
680
+ showExpandedImage(src, alt) {
681
+ // Remove existing modal if any
682
+ const existingModal = document.querySelector(".expanded-image-modal");
683
+ if (existingModal) {
684
+ existingModal.remove();
685
+ }
686
+
687
+ const modal = document.createElement("div");
688
+ modal.className = "expanded-image-modal";
689
+ modal.innerHTML = `
690
+ <div class="expanded-image-container">
691
+ <button class="expanded-image-close" title="Close">✕</button>
692
+ <img src="${src}" alt="${alt || "Image"}" class="expanded-image" />
693
+ <div class="expanded-image-caption">${this._escapeHtml(alt || "Image preview")}</div>
694
+ </div>
695
+ `;
696
+
697
+ // Close handlers
698
+ const closeModal = (e) => {
699
+ e?.stopPropagation();
700
+ modal.remove();
701
+ document.removeEventListener("mousedown", handleClickOutside);
702
+ };
703
+
704
+ const handleClickOutside = (e) => {
705
+ if (!e.target.closest(".expanded-image-container")) {
706
+ closeModal(e);
707
+ }
708
+ };
709
+
710
+ modal.querySelector(".expanded-image-close").addEventListener("click", closeModal);
711
+ modal.addEventListener("click", (e) => {
712
+ if (e.target === modal) {
713
+ closeModal(e);
714
+ }
715
+ });
716
+ document.addEventListener("mousedown", handleClickOutside);
717
+
718
+ document.body.appendChild(modal);
719
+ }
720
+
721
+ _applyStyles() {
722
+ // Check if styles already applied
723
+ if (document.getElementById('text-chat-screen-styles')) return;
724
+
725
+ const style = document.createElement("style");
726
+ style.id = 'text-chat-screen-styles';
727
+ style.textContent = `
728
+ .text-chat-screen {
729
+ flex: 1;
730
+ display: flex;
731
+ flex-direction: column;
732
+ overflow: hidden;
733
+
734
+ }
735
+
736
+ .text-chat-screen .chat-header {
737
+ background: transparent;
738
+ color: ${this.primaryColor};
739
+ padding: 20px;
740
+ display: flex;
741
+ align-items: center;
742
+ justify-content: space-between;
743
+ }
744
+
745
+ .text-chat-screen .chat-header-content {
746
+ display: flex;
747
+ align-items: center;
748
+ gap: 12px;
749
+ flex: 1;
750
+ }
751
+
752
+ .text-chat-screen .chat-back {
753
+ background: ${this.primaryColor};
754
+ border: none;
755
+ color: white;
756
+ cursor: pointer;
757
+ padding: 8px;
758
+ border-radius: 50%;
759
+ display: flex;
760
+ align-items: center;
761
+ justify-content: center;
762
+ transition: all 0.2s;
763
+ margin-right: 8px;
764
+ }
765
+
766
+
767
+
768
+ .text-chat-screen .chat-avatar {
769
+ width: 40px;
770
+ height: 40px;
771
+ border-radius: 50%;
772
+ background: rgba(255, 255, 255, 0.2);
773
+ backdrop-filter: blur(10px);
774
+ display: flex;
775
+ align-items: center;
776
+ justify-content: center;
777
+ }
778
+
779
+ .text-chat-screen .chat-header-text {
780
+ display: flex;
781
+ flex-direction: column;
782
+ gap: 2px;
783
+ }
784
+
785
+ .text-chat-screen .chat-title {
786
+ font-weight: 600;
787
+ font-size: 20px;
788
+ }
789
+
790
+ .text-chat-screen .chat-status {
791
+ font-size: 12px;
792
+ opacity: 0.9;
793
+ display: flex;
794
+ align-items: center;
795
+ gap: 4px;
796
+ }
797
+
798
+ .text-chat-screen .chat-close {
799
+ background: ${this.primaryColor};
800
+ border: none;
801
+ color: white;
802
+ cursor: pointer;
803
+ padding: 8px;
804
+ border-radius: 50%;
805
+ display: flex;
806
+ align-items: center;
807
+ justify-content: center;
808
+ transition: all 0.2s;
809
+ }
810
+
811
+
812
+
813
+ .text-chat-screen .chat-messages {
814
+ flex: 1;
815
+ padding: 20px;
816
+ overflow-y: auto;
817
+ background: linear-gradient(180deg, white 10%, #E1EFCC );
818
+ display: flex;
819
+ flex-direction: column;
820
+ gap: 12px;
821
+ }
822
+
823
+ .text-chat-screen .chat-messages::-webkit-scrollbar {
824
+ width: 6px;
825
+ }
826
+
827
+ .text-chat-screen .chat-messages::-webkit-scrollbar-track {
828
+ background: transparent;
829
+ }
830
+
831
+ .text-chat-screen .chat-messages::-webkit-scrollbar-thumb {
832
+ background: #cbd5e1;
833
+ border-radius: 3px;
834
+ }
835
+
836
+ .text-chat-screen .chat-welcome {
837
+ text-align: center;
838
+ padding: 100px 20px;
839
+ color: #64748b;
840
+
841
+ }
842
+
843
+ .text-chat-screen .chat-welcome .welcome-icon {
844
+ font-size: 48px;
845
+ margin-bottom: 12px;
846
+ }
847
+
848
+ .text-chat-screen .welcome-text {
849
+ font-size: 15px;
850
+ font-weight: 500;
851
+ color: #475569;
852
+ }
853
+
854
+ .text-chat-screen .chat-message {
855
+ display: flex;
856
+ gap: 8px;
857
+ animation: messageSlide 0.3s ease-out;
858
+ }
859
+
860
+ @keyframes messageSlide {
861
+ from {
862
+ opacity: 0;
863
+ transform: translateY(10px);
864
+ }
865
+ to {
866
+ opacity: 1;
867
+ transform: translateY(0);
868
+ }
869
+ }
870
+
871
+ .text-chat-screen .chat-message.user {
872
+ flex-direction: row-reverse;
873
+ }
874
+
875
+ .text-chat-screen .message-content {
876
+ max-width: 75%;
877
+ }
878
+
879
+ .text-chat-screen .message-bubble {
880
+ padding: 12px 16px;
881
+ border-radius: 16px;
882
+ font-size: 14px;
883
+ line-height: 1.5;
884
+ word-wrap: break-word;
885
+ }
886
+
887
+ .text-chat-screen .chat-message.user .message-bubble {
888
+ background: #e9f5d7;
889
+ color: ${this.primaryColor};
890
+ border-bottom-right-radius: 4px;
891
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
892
+ }
893
+
894
+ .text-chat-screen .chat-message.bot .message-bubble {
895
+ background: white;
896
+ color: #1e293b;
897
+ border-bottom-left-radius: 4px;
898
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
899
+ }
900
+
901
+ .text-chat-screen .message-time {
902
+ font-size: 10px;
903
+ color: #94a3b8;
904
+ margin-top: 4px;
905
+ text-align: right;
906
+ }
907
+
908
+ .text-chat-screen .typing-indicator {
909
+ display: flex;
910
+ gap: 4px;
911
+ padding: 12px 16px;
912
+ background: white;
913
+ border-radius: 16px;
914
+ border-bottom-left-radius: 4px;
915
+ max-width: 60px;
916
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
917
+ }
918
+
919
+ .text-chat-screen .typing-dot {
920
+ width: 8px;
921
+ height: 8px;
922
+ border-radius: 50%;
923
+ background: #94a3b8;
924
+ animation: typing 1.4s infinite;
925
+ }
926
+
927
+ .text-chat-screen .typing-dot:nth-child(2) {
928
+ animation-delay: 0.2s;
929
+ }
930
+
931
+ .text-chat-screen .typing-dot:nth-child(3) {
932
+ animation-delay: 0.4s;
933
+ }
934
+
935
+ @keyframes typing {
936
+ 0%, 60%, 100% {
937
+ transform: translateY(0);
938
+ opacity: 0.7;
939
+ }
940
+ 30% {
941
+ transform: translateY(-10px);
942
+ opacity: 1;
943
+ }
944
+ }
945
+
946
+ .text-chat-screen .file-attachments-container {
947
+ padding: 8px 12px;
948
+ background: white;
949
+ border-top: 1px solid #e2e8f0;
950
+ display: flex;
951
+ flex-wrap: wrap;
952
+ gap: 8px;
953
+ max-height: 120px;
954
+ overflow-y: auto;
955
+ }
956
+
957
+ .text-chat-screen .file-attachments-container::-webkit-scrollbar {
958
+ width: 4px;
959
+ height: 4px;
960
+ }
961
+
962
+ .text-chat-screen .file-attachments-container::-webkit-scrollbar-track {
963
+ background: transparent;
964
+ }
965
+
966
+ .text-chat-screen .file-attachments-container::-webkit-scrollbar-thumb {
967
+ background: #cbd5e1;
968
+ border-radius: 2px;
969
+ }
970
+
971
+ .text-chat-screen .file-attachment {
972
+ display: flex;
973
+ align-items: center;
974
+ gap: 8px;
975
+ padding: 6px 10px;
976
+ background: #f1f5f9;
977
+ border: 1px solid #e2e8f0;
978
+ border-radius: 8px;
979
+ font-size: 12px;
980
+ max-width: 200px;
981
+ position: relative;
982
+ }
983
+
984
+ .text-chat-screen .file-attachment.has-thumbnail {
985
+ padding: 4px;
986
+ }
987
+
988
+ .text-chat-screen .file-thumbnail {
989
+ width: 40px;
990
+ height: 40px;
991
+ border-radius: 6px;
992
+ overflow: hidden;
993
+ flex-shrink: 0;
994
+ background: #e2e8f0;
995
+ display: flex;
996
+ align-items: center;
997
+ justify-content: center;
998
+ }
999
+
1000
+ .text-chat-screen .file-thumbnail-image {
1001
+ width: 100%;
1002
+ height: 100%;
1003
+ object-fit: cover;
1004
+ cursor: pointer;
1005
+ }
1006
+
1007
+ .text-chat-screen .file-attachment-icon {
1008
+ font-size: 24px;
1009
+ flex-shrink: 0;
1010
+ }
1011
+
1012
+ .text-chat-screen .file-attachment-info {
1013
+ flex: 1;
1014
+ min-width: 0;
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ gap: 2px;
1018
+ }
1019
+
1020
+ .text-chat-screen .file-attachment-name {
1021
+ font-weight: 500;
1022
+ color: #1e293b;
1023
+ white-space: nowrap;
1024
+ overflow: hidden;
1025
+ text-overflow: ellipsis;
1026
+ max-width: 120px;
1027
+ }
1028
+
1029
+ .text-chat-screen .file-attachment-size {
1030
+ font-size: 11px;
1031
+ color: #64748b;
1032
+ }
1033
+
1034
+ .text-chat-screen .file-attachment-remove {
1035
+ background: transparent;
1036
+ border: none;
1037
+ color: #64748b;
1038
+ cursor: pointer;
1039
+ padding: 4px;
1040
+ border-radius: 4px;
1041
+ font-size: 16px;
1042
+ line-height: 1;
1043
+ display: flex;
1044
+ align-items: center;
1045
+ justify-content: center;
1046
+ transition: all 0.2s;
1047
+ flex-shrink: 0;
1048
+ }
1049
+
1050
+ .text-chat-screen .file-attachment-remove:hover {
1051
+ background: #fee2e2;
1052
+ color: #dc2626;
1053
+ }
1054
+
1055
+ .text-chat-screen .chat-input-wrapper {
1056
+ display: flex;
1057
+ padding: 10px;
1058
+ gap: 8px;
1059
+ background: white;
1060
+ border-top: 1px solid #e2e8f0;
1061
+ align-items: center;
1062
+ }
1063
+
1064
+ .text-chat-screen .file-upload-controls {
1065
+ display: flex;
1066
+ align-items: center;
1067
+ }
1068
+
1069
+ .text-chat-screen .file-upload-button {
1070
+ background: transparent;
1071
+ border: none;
1072
+ color: ${this.primaryColor};
1073
+ cursor: pointer;
1074
+ padding: 8px;
1075
+ border-radius: 8px;
1076
+ display: flex;
1077
+ align-items: center;
1078
+ justify-content: center;
1079
+ transition: all 0.2s;
1080
+ }
1081
+
1082
+ .text-chat-screen .file-upload-button:hover {
1083
+ background: rgba(26, 92, 75, 0.1);
1084
+ }
1085
+
1086
+ .text-chat-screen .file-upload-button:active {
1087
+ transform: scale(0.95);
1088
+ }
1089
+
1090
+ .text-chat-screen #chat-input {
1091
+ flex: 1;
1092
+ border: 2px solid #e2e8f0;
1093
+ border-radius: 12px;
1094
+ padding: 12px 16px;
1095
+ font-size: 14px;
1096
+ outline: none;
1097
+ transition: all 0.2s;
1098
+ font-family: inherit;
1099
+ }
1100
+
1101
+ .text-chat-screen #chat-input:focus {
1102
+ border-color: ${this.primaryColor};
1103
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
1104
+ }
1105
+
1106
+ .text-chat-screen #chat-send {
1107
+ background: ${this.primaryColor};
1108
+ color: white;
1109
+ border: none;
1110
+ border-radius: 12px;
1111
+ padding: 12px 16px;
1112
+ cursor: pointer;
1113
+ display: flex;
1114
+ align-items: center;
1115
+ justify-content: center;
1116
+ transition: all 0.2s;
1117
+ }
1118
+
1119
+ .text-chat-screen #chat-send:disabled {
1120
+ opacity: 0.5;
1121
+ cursor: not-allowed;
1122
+ }
1123
+
1124
+ .text-chat-screen #chat-send:not(:disabled):hover {
1125
+ transform: scale(1.05);
1126
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
1127
+ }
1128
+
1129
+ .text-chat-screen #chat-send:not(:disabled):active {
1130
+ transform: scale(0.95);
1131
+ }
1132
+
1133
+ /* Message Attachments Styles */
1134
+ .text-chat-screen .message-attachments {
1135
+ display: flex;
1136
+ flex-direction: column;
1137
+ gap: 8px;
1138
+ margin-bottom: 8px;
1139
+ }
1140
+
1141
+ .text-chat-screen .message-attachment {
1142
+ display: flex;
1143
+ align-items: center;
1144
+ gap: 8px;
1145
+ padding: 8px 12px;
1146
+ background: #f1f5f9;
1147
+ border: 1px solid #e2e8f0;
1148
+ border-radius: 8px;
1149
+ font-size: 12px;
1150
+ max-width: 100%;
1151
+ }
1152
+
1153
+ .text-chat-screen .message-attachment.image {
1154
+ background: #f8fafc;
1155
+ }
1156
+
1157
+ .text-chat-screen .message-attachment.pdf {
1158
+ background: #fef2f2;
1159
+ }
1160
+
1161
+ .text-chat-screen .message-attachment-icon {
1162
+ font-size: 20px;
1163
+ flex-shrink: 0;
1164
+ }
1165
+
1166
+ .text-chat-screen .message-attachment-info {
1167
+ flex: 1;
1168
+ min-width: 0;
1169
+ display: flex;
1170
+ flex-direction: column;
1171
+ gap: 2px;
1172
+ }
1173
+
1174
+ .text-chat-screen .message-attachment-name {
1175
+ font-weight: 500;
1176
+ color: #1e293b;
1177
+ white-space: nowrap;
1178
+ overflow: hidden;
1179
+ text-overflow: ellipsis;
1180
+ }
1181
+
1182
+ .text-chat-screen .message-attachment-name.clickable:hover {
1183
+ color: ${this.primaryColor};
1184
+ text-decoration: underline;
1185
+ }
1186
+
1187
+ .text-chat-screen .message-attachment-size {
1188
+ font-size: 11px;
1189
+ color: #64748b;
1190
+ }
1191
+
1192
+ /* Expanded Image Modal Styles */
1193
+ .expanded-image-modal {
1194
+ position: fixed;
1195
+ top: 0;
1196
+ left: 0;
1197
+ right: 0;
1198
+ bottom: 0;
1199
+ background: rgba(0, 0, 0, 0.8);
1200
+ display: flex;
1201
+ align-items: center;
1202
+ justify-content: center;
1203
+ z-index: 10000;
1204
+ animation: fadeIn 0.2s ease;
1205
+ }
1206
+
1207
+ @keyframes fadeIn {
1208
+ from {
1209
+ opacity: 0;
1210
+ }
1211
+ to {
1212
+ opacity: 1;
1213
+ }
1214
+ }
1215
+
1216
+ .expanded-image-container {
1217
+ position: relative;
1218
+ max-width: 90%;
1219
+ max-height: 90vh;
1220
+ background: #fff;
1221
+ border-radius: 8px;
1222
+ overflow: hidden;
1223
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1224
+ animation: scaleIn 0.2s ease;
1225
+ display: flex;
1226
+ flex-direction: column;
1227
+ }
1228
+
1229
+ @keyframes scaleIn {
1230
+ from {
1231
+ transform: scale(0.9);
1232
+ opacity: 0;
1233
+ }
1234
+ to {
1235
+ transform: scale(1);
1236
+ opacity: 1;
1237
+ }
1238
+ }
1239
+
1240
+ .expanded-image-close {
1241
+ position: absolute;
1242
+ top: 12px;
1243
+ right: 12px;
1244
+ background: rgba(0, 0, 0, 0.6);
1245
+ border: none;
1246
+ color: white;
1247
+ width: 32px;
1248
+ height: 32px;
1249
+ border-radius: 50%;
1250
+ cursor: pointer;
1251
+ display: flex;
1252
+ align-items: center;
1253
+ justify-content: center;
1254
+ font-size: 18px;
1255
+ z-index: 1;
1256
+ transition: all 0.2s;
1257
+ }
1258
+
1259
+ .expanded-image-close:hover {
1260
+ background: rgba(0, 0, 0, 0.8);
1261
+ transform: scale(1.1);
1262
+ }
1263
+
1264
+ .expanded-image {
1265
+ max-width: 100%;
1266
+ max-height: calc(90vh - 60px);
1267
+ object-fit: contain;
1268
+ display: block;
1269
+ }
1270
+
1271
+ .expanded-image-caption {
1272
+ padding: 12px 16px;
1273
+ background: #fff;
1274
+ color: #1e293b;
1275
+ font-size: 14px;
1276
+ text-align: center;
1277
+ border-top: 1px solid #e2e8f0;
1278
+ }
1279
+ `;
1280
+ document.head.appendChild(style);
1281
+ }
1282
+ }
1283
+
1284
+ // Export as ESM (Rollup will handle UMD conversion)
1285
+ export default TextChatScreen;
1286
+