mini-chat-bot-widget 0.6.0 → 0.9.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,1467 @@
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
+ // Parse markdown for assistant messages, escape for user messages
425
+ const content = messageData.content || text;
426
+ bubble.innerHTML = !isUser ? this._parseMarkdown(content) : this._escapeHtml(content);
427
+ }
428
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
429
+ return;
430
+ }
431
+
432
+ const messageEl = document.createElement("div");
433
+ messageEl.className = `chat-message ${isUser ? "user" : "bot"}`;
434
+ messageEl.setAttribute("data-message-id", messageId);
435
+
436
+ // Handle different message types
437
+ let content = text;
438
+ if (messageData) {
439
+ if (messageData.isStreaming) {
440
+ content = messageData.content || "Thinking...";
441
+ } else if (messageData.isError) {
442
+ content = messageData.content || "Error occurred";
443
+ messageEl.classList.add("error");
444
+ } else {
445
+ content = messageData.content || text;
446
+ }
447
+ }
448
+
449
+ // Build attachments HTML if present
450
+ let attachmentsHTML = "";
451
+ if (messageData && messageData.attachments && messageData.attachments.length > 0) {
452
+ attachmentsHTML = `
453
+ <div class="message-attachments">
454
+ ${messageData.attachments.map((attachment, index) => {
455
+ const displayName = attachment.metadata?.name || attachment.metadata?.filename || "Unknown file";
456
+ const isImage = attachment.type === "image";
457
+ const imageSrc = isImage && attachment.data
458
+ ? `data:${attachment.mime_type};base64,${attachment.data}`
459
+ : null;
460
+
461
+ return `
462
+ <div class="message-attachment ${isImage ? "image" : "pdf"}" data-attachment-index="${index}">
463
+ <span class="message-attachment-icon">
464
+ ${isImage ? "🖼️" : "📄"}
465
+ </span>
466
+ <div class="message-attachment-info">
467
+ <div
468
+ class="message-attachment-name ${isImage ? "clickable" : ""}"
469
+ ${isImage && imageSrc ? `data-image-src="${imageSrc}" data-image-alt="${displayName}"` : ""}
470
+ style="cursor: ${isImage ? "pointer" : "default"};"
471
+ >
472
+ ${this._escapeHtml(displayName)}
473
+ </div>
474
+ <div class="message-attachment-size">
475
+ ${this.formatFileSize(attachment.metadata?.size || 0)} • ${this.getFileTypeDisplay(attachment.mime_type)}
476
+ </div>
477
+ </div>
478
+ </div>
479
+ `;
480
+ }).join("")}
481
+ </div>
482
+ `;
483
+ }
484
+
485
+ const timestamp = messageData?.timestamp ? new Date(messageData.timestamp) : new Date();
486
+ const formattedTime = this._formatTime(timestamp);
487
+
488
+ // Parse markdown for assistant messages, escape for user messages
489
+ const renderedContent = !isUser ? this._parseMarkdown(content) : this._escapeHtml(content);
490
+
491
+ messageEl.innerHTML = `
492
+ <div class="message-content">
493
+ ${attachmentsHTML}
494
+ <div class="message-bubble">
495
+ ${renderedContent}
496
+ </div>
497
+ <div class="message-time">${formattedTime}</div>
498
+ </div>
499
+ `;
500
+
501
+ // Bind image click handlers for expansion
502
+ if (messageData && messageData.attachments) {
503
+ messageEl.querySelectorAll(".message-attachment-name.clickable").forEach((el) => {
504
+ el.addEventListener("click", (e) => {
505
+ e.stopPropagation();
506
+ const imageSrc = el.getAttribute("data-image-src");
507
+ const imageAlt = el.getAttribute("data-image-alt");
508
+ if (imageSrc) {
509
+ this.showExpandedImage(imageSrc, imageAlt);
510
+ }
511
+ });
512
+ });
513
+ }
514
+
515
+ messagesContainer.appendChild(messageEl);
516
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
517
+ }
518
+
519
+ // Sync messages from shared array
520
+ _syncMessages(messages) {
521
+ if (!this.container) return;
522
+
523
+ const messagesContainer = this.container.querySelector("#chat-messages");
524
+ if (!messagesContainer) return;
525
+
526
+ // Remove welcome message if there are any messages
527
+ if (messages.length > 0) {
528
+ const welcome = messagesContainer.querySelector(".chat-welcome");
529
+ if (welcome) welcome.remove();
530
+ }
531
+
532
+ // Get existing message elements
533
+ const existingMessages = messagesContainer.querySelectorAll("[data-message-id]");
534
+ const existingIds = new Set(Array.from(existingMessages).map(el => el.getAttribute("data-message-id")));
535
+
536
+ // Add or update messages
537
+ messages.forEach((msg) => {
538
+ const msgId = String(msg.id);
539
+ if (existingIds.has(msgId)) {
540
+ // Update existing message
541
+ const existingEl = messagesContainer.querySelector(`[data-message-id="${msgId}"]`);
542
+ if (existingEl) {
543
+ const bubble = existingEl.querySelector(".message-bubble");
544
+ if (bubble) {
545
+ // Parse markdown for assistant messages, escape for user messages
546
+ const content = msg.content || "";
547
+ const isUserMessage = msg.sender === "user";
548
+ bubble.innerHTML = !isUserMessage ? this._parseMarkdown(content) : this._escapeHtml(content);
549
+ }
550
+
551
+ // Update attachments if they exist
552
+ const messageContent = existingEl.querySelector(".message-content");
553
+ if (messageContent && msg.attachments && msg.attachments.length > 0) {
554
+ let attachmentsHTML = `
555
+ <div class="message-attachments">
556
+ ${msg.attachments.map((attachment, index) => {
557
+ const displayName = attachment.metadata?.name || attachment.metadata?.filename || "Unknown file";
558
+ const isImage = attachment.type === "image";
559
+ const imageSrc = isImage && attachment.data
560
+ ? `data:${attachment.mime_type};base64,${attachment.data}`
561
+ : null;
562
+
563
+ return `
564
+ <div class="message-attachment ${isImage ? "image" : "pdf"}" data-attachment-index="${index}">
565
+ <span class="message-attachment-icon">
566
+ ${isImage ? "🖼️" : "📄"}
567
+ </span>
568
+ <div class="message-attachment-info">
569
+ <div
570
+ class="message-attachment-name ${isImage ? "clickable" : ""}"
571
+ ${isImage && imageSrc ? `data-image-src="${imageSrc}" data-image-alt="${displayName}"` : ""}
572
+ style="cursor: ${isImage ? "pointer" : "default"};"
573
+ >
574
+ ${this._escapeHtml(displayName)}
575
+ </div>
576
+ <div class="message-attachment-size">
577
+ ${this.formatFileSize(attachment.metadata?.size || 0)} • ${this.getFileTypeDisplay(attachment.mime_type)}
578
+ </div>
579
+ </div>
580
+ </div>
581
+ `;
582
+ }).join("")}
583
+ </div>
584
+ `;
585
+
586
+ // Insert or update attachments
587
+ const existingAttachments = messageContent.querySelector(".message-attachments");
588
+ if (existingAttachments) {
589
+ existingAttachments.outerHTML = attachmentsHTML;
590
+ } else {
591
+ messageContent.insertAdjacentHTML("afterbegin", attachmentsHTML);
592
+ }
593
+
594
+ // Re-bind image click handlers
595
+ messageContent.querySelectorAll(".message-attachment-name.clickable").forEach((el) => {
596
+ // Remove existing listeners by cloning
597
+ const newEl = el.cloneNode(true);
598
+ el.parentNode.replaceChild(newEl, el);
599
+
600
+ newEl.addEventListener("click", (e) => {
601
+ e.stopPropagation();
602
+ const imageSrc = newEl.getAttribute("data-image-src");
603
+ const imageAlt = newEl.getAttribute("data-image-alt");
604
+ if (imageSrc) {
605
+ this.showExpandedImage(imageSrc, imageAlt);
606
+ }
607
+ });
608
+ });
609
+ }
610
+
611
+ // Update classes based on message state
612
+ if (msg.isStreaming) {
613
+ existingEl.classList.add("streaming");
614
+ } else {
615
+ existingEl.classList.remove("streaming");
616
+ }
617
+ if (msg.isError) {
618
+ existingEl.classList.add("error");
619
+ }
620
+ }
621
+ } else {
622
+ // Add new message
623
+ this.addMessage(
624
+ msg.content || "",
625
+ msg.sender === "user",
626
+ msg
627
+ );
628
+ }
629
+ });
630
+
631
+ // Remove messages that are no longer in the array
632
+ existingMessages.forEach((el) => {
633
+ const msgId = el.getAttribute("data-message-id");
634
+ const exists = messages.some(msg => String(msg.id) === msgId);
635
+ if (!exists) {
636
+ el.remove();
637
+ }
638
+ });
639
+
640
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
641
+ }
642
+
643
+ showTypingIndicator() {
644
+ if (!this.container) return;
645
+
646
+ const messagesContainer = this.container.querySelector("#chat-messages");
647
+ if (!messagesContainer) return;
648
+
649
+ const indicator = document.createElement("div");
650
+ indicator.className = "chat-message bot";
651
+ indicator.id = "typing-indicator";
652
+ indicator.innerHTML = `
653
+ <div class="typing-indicator">
654
+ <div class="typing-dot"></div>
655
+ <div class="typing-dot"></div>
656
+ <div class="typing-dot"></div>
657
+ </div>
658
+ `;
659
+ messagesContainer.appendChild(indicator);
660
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
661
+ }
662
+
663
+ hideTypingIndicator() {
664
+ if (!this.container) return;
665
+ const indicator = this.container.querySelector("#typing-indicator");
666
+ if (indicator) indicator.remove();
667
+ }
668
+
669
+ handleUserMessage(text) {
670
+ // Call the callback if provided, otherwise use default echo
671
+ if (this.onMessage) {
672
+ this.onMessage(text, (response) => {
673
+ this.addMessage(response, false);
674
+ });
675
+ } else {
676
+ // Default echo response
677
+ this.addMessage(`You said: "${text}"`, false);
678
+ }
679
+ }
680
+
681
+ _escapeHtml(text) {
682
+ const div = document.createElement("div");
683
+ div.textContent = text;
684
+ return div.innerHTML;
685
+ }
686
+
687
+ // Parse markdown to HTML
688
+ _parseMarkdown(text) {
689
+ if (!text || typeof text !== 'string') return '';
690
+
691
+ let html = text;
692
+
693
+ // Escape HTML first to prevent XSS
694
+ html = html
695
+ .replace(/&/g, '&amp;')
696
+ .replace(/</g, '&lt;')
697
+ .replace(/>/g, '&gt;');
698
+
699
+ // Split into lines for processing
700
+ const lines = html.split('\n');
701
+ const processedLines = [];
702
+ let inList = false;
703
+
704
+ for (let i = 0; i < lines.length; i++) {
705
+ let line = lines[i];
706
+
707
+ // Check for headers first (#, ##, ###, etc.)
708
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
709
+ if (headerMatch) {
710
+ if (inList) {
711
+ processedLines.push('</ul>');
712
+ inList = false;
713
+ }
714
+ const level = headerMatch[1].length;
715
+ const headerText = headerMatch[2];
716
+ // Process markdown inside header
717
+ let processedHeader = headerText;
718
+ processedHeader = processedHeader.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
719
+ processedHeader = processedHeader.replace(/__([^_]+)__/g, '<strong>$1</strong>');
720
+ processedHeader = processedHeader.replace(/(^|[^*])\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
721
+ processedHeader = processedHeader.replace(/(^|[^_])_([^_]+)_([^_]|$)/g, '$1<em>$2</em>$3');
722
+ processedLines.push(`<h${level}>${processedHeader}</h${level}>`);
723
+ continue;
724
+ }
725
+
726
+ // Check if this is a list item (before processing bold/italic)
727
+ const listMatch = line.match(/^[\s]*[-*]\s+(.+)$/);
728
+
729
+ if (listMatch) {
730
+ // Process markdown inside list item
731
+ let listContent = listMatch[1];
732
+
733
+ // Bold: **text** (must be processed before italic)
734
+ listContent = listContent.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
735
+ listContent = listContent.replace(/__([^_]+)__/g, '<strong>$1</strong>');
736
+
737
+ // Italic: *text* (single asterisk, avoid matching **text**)
738
+ listContent = listContent.replace(/(^|[^*])\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
739
+ listContent = listContent.replace(/(^|[^_])_([^_]+)_([^_]|$)/g, '$1<em>$2</em>$3');
740
+
741
+ if (!inList) {
742
+ processedLines.push('<ul>');
743
+ inList = true;
744
+ }
745
+ processedLines.push(`<li>${listContent}</li>`);
746
+ } else {
747
+ if (inList) {
748
+ processedLines.push('</ul>');
749
+ inList = false;
750
+ }
751
+
752
+ // Skip empty lines (they'll become <br> later)
753
+ if (line.trim() === '') {
754
+ processedLines.push('');
755
+ continue;
756
+ }
757
+
758
+ // Process markdown in non-list lines
759
+ // Bold: **text** (must be processed before italic)
760
+ line = line.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
761
+ line = line.replace(/__([^_]+)__/g, '<strong>$1</strong>');
762
+
763
+ // Italic: *text* (single asterisk, avoid matching **text**)
764
+ line = line.replace(/(^|[^*])\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
765
+ line = line.replace(/(^|[^_])_([^_]+)_([^_]|$)/g, '$1<em>$2</em>$3');
766
+
767
+ // Inline code: `code`
768
+ line = line.replace(/`([^`]+)`/g, '<code>$1</code>');
769
+
770
+ // Links: [text](url)
771
+ line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
772
+
773
+ processedLines.push(line);
774
+ }
775
+ }
776
+
777
+ if (inList) {
778
+ processedLines.push('</ul>');
779
+ }
780
+
781
+ // Join lines with <br> and return
782
+ html = processedLines.join('<br>');
783
+
784
+ return html;
785
+ }
786
+
787
+ // Show expanded image modal
788
+ showExpandedImage(src, alt) {
789
+ // Remove existing modal if any
790
+ const existingModal = document.querySelector(".expanded-image-modal");
791
+ if (existingModal) {
792
+ existingModal.remove();
793
+ }
794
+
795
+ const modal = document.createElement("div");
796
+ modal.className = "expanded-image-modal";
797
+ modal.innerHTML = `
798
+ <div class="expanded-image-container">
799
+ <button class="expanded-image-close" title="Close">✕</button>
800
+ <img src="${src}" alt="${alt || "Image"}" class="expanded-image" />
801
+ <div class="expanded-image-caption">${this._escapeHtml(alt || "Image preview")}</div>
802
+ </div>
803
+ `;
804
+
805
+ // Close handlers
806
+ const closeModal = (e) => {
807
+ e?.stopPropagation();
808
+ modal.remove();
809
+ document.removeEventListener("mousedown", handleClickOutside);
810
+ };
811
+
812
+ const handleClickOutside = (e) => {
813
+ if (!e.target.closest(".expanded-image-container")) {
814
+ closeModal(e);
815
+ }
816
+ };
817
+
818
+ modal.querySelector(".expanded-image-close").addEventListener("click", closeModal);
819
+ modal.addEventListener("click", (e) => {
820
+ if (e.target === modal) {
821
+ closeModal(e);
822
+ }
823
+ });
824
+ document.addEventListener("mousedown", handleClickOutside);
825
+
826
+ document.body.appendChild(modal);
827
+ }
828
+
829
+ _applyStyles() {
830
+ // Check if styles already applied
831
+ if (document.getElementById('text-chat-screen-styles')) return;
832
+
833
+ const style = document.createElement("style");
834
+ style.id = 'text-chat-screen-styles';
835
+ style.textContent = `
836
+ .text-chat-screen {
837
+ flex: 1;
838
+ display: flex;
839
+ flex-direction: column;
840
+ overflow: hidden;
841
+
842
+ }
843
+
844
+ .text-chat-screen .chat-header {
845
+ background: transparent;
846
+ color: ${this.primaryColor};
847
+ padding: 20px;
848
+ display: flex;
849
+ align-items: center;
850
+ justify-content: space-between;
851
+ }
852
+
853
+ .text-chat-screen .chat-header-content {
854
+ display: flex;
855
+ align-items: center;
856
+ gap: 12px;
857
+ flex: 1;
858
+ }
859
+
860
+ .text-chat-screen .chat-back {
861
+ background: ${this.primaryColor};
862
+ border: none;
863
+ color: white;
864
+ cursor: pointer;
865
+ padding: 8px;
866
+ border-radius: 50%;
867
+ display: flex;
868
+ align-items: center;
869
+ justify-content: center;
870
+ transition: all 0.2s;
871
+ margin-right: 8px;
872
+ }
873
+
874
+
875
+
876
+ .text-chat-screen .chat-avatar {
877
+ width: 40px;
878
+ height: 40px;
879
+ border-radius: 50%;
880
+ background: rgba(255, 255, 255, 0.2);
881
+ backdrop-filter: blur(10px);
882
+ display: flex;
883
+ align-items: center;
884
+ justify-content: center;
885
+ }
886
+
887
+ .text-chat-screen .chat-header-text {
888
+ display: flex;
889
+ flex-direction: column;
890
+ gap: 2px;
891
+ }
892
+
893
+ .text-chat-screen .chat-title {
894
+ font-weight: 600;
895
+ font-size: 20px;
896
+ }
897
+
898
+ .text-chat-screen .chat-status {
899
+ font-size: 12px;
900
+ opacity: 0.9;
901
+ display: flex;
902
+ align-items: center;
903
+ gap: 4px;
904
+ }
905
+
906
+ .text-chat-screen .chat-close {
907
+ background: ${this.primaryColor};
908
+ border: none;
909
+ color: white;
910
+ cursor: pointer;
911
+ padding: 8px;
912
+ border-radius: 50%;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ transition: all 0.2s;
917
+ }
918
+
919
+
920
+
921
+ .text-chat-screen .chat-messages {
922
+ flex: 1;
923
+ padding: 20px;
924
+ overflow-y: auto;
925
+ background: linear-gradient(180deg, white 10%, #E1EFCC );
926
+ display: flex;
927
+ flex-direction: column;
928
+ gap: 12px;
929
+ }
930
+
931
+ .text-chat-screen .chat-messages::-webkit-scrollbar {
932
+ width: 6px;
933
+ }
934
+
935
+ .text-chat-screen .chat-messages::-webkit-scrollbar-track {
936
+ background: transparent;
937
+ }
938
+
939
+ .text-chat-screen .chat-messages::-webkit-scrollbar-thumb {
940
+ background: #cbd5e1;
941
+ border-radius: 3px;
942
+ }
943
+
944
+ .text-chat-screen .chat-welcome {
945
+ text-align: center;
946
+ padding: 100px 20px;
947
+ color: #64748b;
948
+
949
+ }
950
+
951
+ .text-chat-screen .chat-welcome .welcome-icon {
952
+ font-size: 48px;
953
+ margin-bottom: 12px;
954
+ }
955
+
956
+ .text-chat-screen .welcome-text {
957
+ font-size: 15px;
958
+ font-weight: 500;
959
+ color: #475569;
960
+ }
961
+
962
+ .text-chat-screen .chat-message {
963
+ display: flex;
964
+ gap: 8px;
965
+ animation: messageSlide 0.3s ease-out;
966
+ }
967
+
968
+ @keyframes messageSlide {
969
+ from {
970
+ opacity: 0;
971
+ transform: translateY(10px);
972
+ }
973
+ to {
974
+ opacity: 1;
975
+ transform: translateY(0);
976
+ }
977
+ }
978
+
979
+ .text-chat-screen .chat-message.user {
980
+ flex-direction: row-reverse;
981
+ }
982
+
983
+ .text-chat-screen .message-content {
984
+ max-width: 75%;
985
+ }
986
+
987
+ .text-chat-screen .message-bubble {
988
+ padding: 12px 16px;
989
+ border-radius: 16px;
990
+ font-size: 14px;
991
+ line-height: 1.5;
992
+ word-wrap: break-word;
993
+ }
994
+
995
+ .text-chat-screen .chat-message.user .message-bubble {
996
+ background: #e9f5d7;
997
+ color: ${this.primaryColor};
998
+ border-bottom-right-radius: 4px;
999
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1000
+ }
1001
+
1002
+ .text-chat-screen .chat-message.bot .message-bubble {
1003
+ background: white;
1004
+ color: #1e293b;
1005
+ border-bottom-left-radius: 4px;
1006
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1007
+ }
1008
+
1009
+ /* ============================================
1010
+ Text Chat Message Bubble Styles
1011
+ ============================================ */
1012
+
1013
+ /* Typography - Bold */
1014
+ .text-chat-screen .message-bubble strong {
1015
+ font-weight: 600;
1016
+ color: inherit;
1017
+ }
1018
+
1019
+ /* Typography - Italic */
1020
+ .text-chat-screen .message-bubble em {
1021
+ font-style: italic;
1022
+ }
1023
+
1024
+ /* Lists */
1025
+ .text-chat-screen .message-bubble ul {
1026
+ margin: 0px 0;
1027
+ padding-left: 20px;
1028
+ list-style-type: disc;
1029
+ margin-bottom:0px;
1030
+ margin-top:5px;
1031
+ }
1032
+
1033
+ .text-chat-screen .message-bubble li {
1034
+ margin: 0;
1035
+ line-height: 1.1;
1036
+ padding-left: 3px;
1037
+ }
1038
+
1039
+ /* Headings - Shared Styles */
1040
+ .text-chat-screen .message-bubble h1,
1041
+ .text-chat-screen .message-bubble h2,
1042
+ .text-chat-screen .message-bubble h3,
1043
+ .text-chat-screen .message-bubble h4,
1044
+ .text-chat-screen .message-bubble h5,
1045
+ .text-chat-screen .message-bubble h6 {
1046
+ margin: 0px 0 0px 0;
1047
+ font-weight: 600;
1048
+ color: inherit;
1049
+ line-height: 1.3;
1050
+ }
1051
+
1052
+ /* Heading Sizes */
1053
+ .text-chat-screen .message-bubble h1 { font-size: 1.5em; }
1054
+ .text-chat-screen .message-bubble h2 { font-size: 1.3em; }
1055
+ .text-chat-screen .message-bubble h3 { font-size: 1.15em; }
1056
+ .text-chat-screen .message-bubble h4 { font-size: 1.05em; }
1057
+ .text-chat-screen .message-bubble h5 { font-size: 1em; }
1058
+ .text-chat-screen .message-bubble h6 { font-size: 0.95em; }
1059
+
1060
+ /* Inline Code */
1061
+ .text-chat-screen .message-bubble code {
1062
+ background: rgba(0, 0, 0, 0.05);
1063
+ padding: 2px 6px;
1064
+ border-radius: 4px;
1065
+ font-family: 'Courier New', Courier, monospace;
1066
+ font-size: 0.9em;
1067
+ }
1068
+
1069
+ /* Links */
1070
+ .text-chat-screen .message-bubble a {
1071
+ color: ${this.primaryColor};
1072
+ text-decoration: underline;
1073
+ }
1074
+
1075
+ .text-chat-screen .message-bubble a:hover {
1076
+ opacity: 0.8;
1077
+ }
1078
+ .text-chat-screen .message-bubble br {
1079
+ line-height: 0.1; /* Adjust between 0 and 1 */
1080
+ }
1081
+
1082
+ .text-chat-screen .message-time {
1083
+ font-size: 10px;
1084
+ color: #94a3b8;
1085
+ margin-top: 4px;
1086
+ text-align: right;
1087
+ }
1088
+
1089
+ .text-chat-screen .typing-indicator {
1090
+ display: flex;
1091
+ gap: 4px;
1092
+ padding: 12px 16px;
1093
+ background: white;
1094
+ border-radius: 16px;
1095
+ border-bottom-left-radius: 4px;
1096
+ max-width: 60px;
1097
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1098
+ }
1099
+
1100
+ .text-chat-screen .typing-dot {
1101
+ width: 8px;
1102
+ height: 8px;
1103
+ border-radius: 50%;
1104
+ background: #94a3b8;
1105
+ animation: typing 1.4s infinite;
1106
+ }
1107
+
1108
+ .text-chat-screen .typing-dot:nth-child(2) {
1109
+ animation-delay: 0.2s;
1110
+ }
1111
+
1112
+ .text-chat-screen .typing-dot:nth-child(3) {
1113
+ animation-delay: 0.4s;
1114
+ }
1115
+
1116
+ @keyframes typing {
1117
+ 0%, 60%, 100% {
1118
+ transform: translateY(0);
1119
+ opacity: 0.7;
1120
+ }
1121
+ 30% {
1122
+ transform: translateY(-10px);
1123
+ opacity: 1;
1124
+ }
1125
+ }
1126
+
1127
+ .text-chat-screen .file-attachments-container {
1128
+ padding: 8px 12px;
1129
+ background: white;
1130
+ border-top: 1px solid #e2e8f0;
1131
+ display: flex;
1132
+ flex-wrap: wrap;
1133
+ gap: 8px;
1134
+ max-height: 120px;
1135
+ overflow-y: auto;
1136
+ }
1137
+
1138
+ .text-chat-screen .file-attachments-container::-webkit-scrollbar {
1139
+ width: 4px;
1140
+ height: 4px;
1141
+ }
1142
+
1143
+ .text-chat-screen .file-attachments-container::-webkit-scrollbar-track {
1144
+ background: transparent;
1145
+ }
1146
+
1147
+ .text-chat-screen .file-attachments-container::-webkit-scrollbar-thumb {
1148
+ background: #cbd5e1;
1149
+ border-radius: 2px;
1150
+ }
1151
+
1152
+ .text-chat-screen .file-attachment {
1153
+ display: flex;
1154
+ align-items: center;
1155
+ gap: 8px;
1156
+ padding: 6px 10px;
1157
+ background: #f1f5f9;
1158
+ border: 1px solid #e2e8f0;
1159
+ border-radius: 8px;
1160
+ font-size: 12px;
1161
+ max-width: 200px;
1162
+ position: relative;
1163
+ }
1164
+
1165
+ .text-chat-screen .file-attachment.has-thumbnail {
1166
+ padding: 4px;
1167
+ }
1168
+
1169
+ .text-chat-screen .file-thumbnail {
1170
+ width: 40px;
1171
+ height: 40px;
1172
+ border-radius: 6px;
1173
+ overflow: hidden;
1174
+ flex-shrink: 0;
1175
+ background: #e2e8f0;
1176
+ display: flex;
1177
+ align-items: center;
1178
+ justify-content: center;
1179
+ }
1180
+
1181
+ .text-chat-screen .file-thumbnail-image {
1182
+ width: 100%;
1183
+ height: 100%;
1184
+ object-fit: cover;
1185
+ cursor: pointer;
1186
+ }
1187
+
1188
+ .text-chat-screen .file-attachment-icon {
1189
+ font-size: 24px;
1190
+ flex-shrink: 0;
1191
+ }
1192
+
1193
+ .text-chat-screen .file-attachment-info {
1194
+ flex: 1;
1195
+ min-width: 0;
1196
+ display: flex;
1197
+ flex-direction: column;
1198
+ gap: 2px;
1199
+ }
1200
+
1201
+ .text-chat-screen .file-attachment-name {
1202
+ font-weight: 500;
1203
+ color: #1e293b;
1204
+ white-space: nowrap;
1205
+ overflow: hidden;
1206
+ text-overflow: ellipsis;
1207
+ max-width: 120px;
1208
+ }
1209
+
1210
+ .text-chat-screen .file-attachment-size {
1211
+ font-size: 11px;
1212
+ color: #64748b;
1213
+ }
1214
+
1215
+ .text-chat-screen .file-attachment-remove {
1216
+ background: transparent;
1217
+ border: none;
1218
+ color: #64748b;
1219
+ cursor: pointer;
1220
+ padding: 4px;
1221
+ border-radius: 4px;
1222
+ font-size: 16px;
1223
+ line-height: 1;
1224
+ display: flex;
1225
+ align-items: center;
1226
+ justify-content: center;
1227
+ transition: all 0.2s;
1228
+ flex-shrink: 0;
1229
+ }
1230
+
1231
+ .text-chat-screen .file-attachment-remove:hover {
1232
+ background: #fee2e2;
1233
+ color: #dc2626;
1234
+ }
1235
+
1236
+ .text-chat-screen .chat-input-wrapper {
1237
+ display: flex;
1238
+ padding: 10px;
1239
+ gap: 8px;
1240
+ background: white;
1241
+ border-top: 1px solid #e2e8f0;
1242
+ align-items: center;
1243
+ }
1244
+
1245
+ .text-chat-screen .file-upload-controls {
1246
+ display: flex;
1247
+ align-items: center;
1248
+ }
1249
+
1250
+ .text-chat-screen .file-upload-button {
1251
+ background: transparent;
1252
+ border: none;
1253
+ color: ${this.primaryColor};
1254
+ cursor: pointer;
1255
+ padding: 8px;
1256
+ border-radius: 8px;
1257
+ display: flex;
1258
+ align-items: center;
1259
+ justify-content: center;
1260
+ transition: all 0.2s;
1261
+ }
1262
+
1263
+ .text-chat-screen .file-upload-button:hover {
1264
+ background: rgba(26, 92, 75, 0.1);
1265
+ }
1266
+
1267
+ .text-chat-screen .file-upload-button:active {
1268
+ transform: scale(0.95);
1269
+ }
1270
+
1271
+ .text-chat-screen #chat-input {
1272
+ flex: 1;
1273
+ border: 2px solid #e2e8f0;
1274
+ border-radius: 12px;
1275
+ padding: 12px 16px;
1276
+ font-size: 14px;
1277
+ outline: none;
1278
+ transition: all 0.2s;
1279
+ font-family: inherit;
1280
+ }
1281
+
1282
+ .text-chat-screen #chat-input:focus {
1283
+ border-color: ${this.primaryColor};
1284
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
1285
+ }
1286
+
1287
+ .text-chat-screen #chat-send {
1288
+ background: ${this.primaryColor};
1289
+ color: white;
1290
+ border: none;
1291
+ border-radius: 12px;
1292
+ padding: 12px 16px;
1293
+ cursor: pointer;
1294
+ display: flex;
1295
+ align-items: center;
1296
+ justify-content: center;
1297
+ transition: all 0.2s;
1298
+ }
1299
+
1300
+ .text-chat-screen #chat-send:disabled {
1301
+ opacity: 0.5;
1302
+ cursor: not-allowed;
1303
+ }
1304
+
1305
+ .text-chat-screen #chat-send:not(:disabled):hover {
1306
+ transform: scale(1.05);
1307
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
1308
+ }
1309
+
1310
+ .text-chat-screen #chat-send:not(:disabled):active {
1311
+ transform: scale(0.95);
1312
+ }
1313
+
1314
+ /* Message Attachments Styles */
1315
+ .text-chat-screen .message-attachments {
1316
+ display: flex;
1317
+ flex-direction: column;
1318
+ gap: 8px;
1319
+ margin-bottom: 8px;
1320
+ }
1321
+
1322
+ .text-chat-screen .message-attachment {
1323
+ display: flex;
1324
+ align-items: center;
1325
+ gap: 8px;
1326
+ padding: 8px 12px;
1327
+ background: #f1f5f9;
1328
+ border: 1px solid #e2e8f0;
1329
+ border-radius: 8px;
1330
+ font-size: 12px;
1331
+ max-width: 100%;
1332
+ }
1333
+
1334
+ .text-chat-screen .message-attachment.image {
1335
+ background: #f8fafc;
1336
+ }
1337
+
1338
+ .text-chat-screen .message-attachment.pdf {
1339
+ background: #fef2f2;
1340
+ }
1341
+
1342
+ .text-chat-screen .message-attachment-icon {
1343
+ font-size: 20px;
1344
+ flex-shrink: 0;
1345
+ }
1346
+
1347
+ .text-chat-screen .message-attachment-info {
1348
+ flex: 1;
1349
+ min-width: 0;
1350
+ display: flex;
1351
+ flex-direction: column;
1352
+ gap: 2px;
1353
+ }
1354
+
1355
+ .text-chat-screen .message-attachment-name {
1356
+ font-weight: 500;
1357
+ color: #1e293b;
1358
+ white-space: nowrap;
1359
+ overflow: hidden;
1360
+ text-overflow: ellipsis;
1361
+ }
1362
+
1363
+ .text-chat-screen .message-attachment-name.clickable:hover {
1364
+ color: ${this.primaryColor};
1365
+ text-decoration: underline;
1366
+ }
1367
+
1368
+ .text-chat-screen .message-attachment-size {
1369
+ font-size: 11px;
1370
+ color: #64748b;
1371
+ }
1372
+
1373
+ /* Expanded Image Modal Styles */
1374
+ .expanded-image-modal {
1375
+ position: fixed;
1376
+ top: 0;
1377
+ left: 0;
1378
+ right: 0;
1379
+ bottom: 0;
1380
+ background: rgba(0, 0, 0, 0.8);
1381
+ display: flex;
1382
+ align-items: center;
1383
+ justify-content: center;
1384
+ z-index: 10000;
1385
+ animation: fadeIn 0.2s ease;
1386
+ }
1387
+
1388
+ @keyframes fadeIn {
1389
+ from {
1390
+ opacity: 0;
1391
+ }
1392
+ to {
1393
+ opacity: 1;
1394
+ }
1395
+ }
1396
+
1397
+ .expanded-image-container {
1398
+ position: relative;
1399
+ max-width: 90%;
1400
+ max-height: 90vh;
1401
+ background: #fff;
1402
+ border-radius: 8px;
1403
+ overflow: hidden;
1404
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1405
+ animation: scaleIn 0.2s ease;
1406
+ display: flex;
1407
+ flex-direction: column;
1408
+ }
1409
+
1410
+ @keyframes scaleIn {
1411
+ from {
1412
+ transform: scale(0.9);
1413
+ opacity: 0;
1414
+ }
1415
+ to {
1416
+ transform: scale(1);
1417
+ opacity: 1;
1418
+ }
1419
+ }
1420
+
1421
+ .expanded-image-close {
1422
+ position: absolute;
1423
+ top: 12px;
1424
+ right: 12px;
1425
+ background: rgba(0, 0, 0, 0.6);
1426
+ border: none;
1427
+ color: white;
1428
+ width: 32px;
1429
+ height: 32px;
1430
+ border-radius: 50%;
1431
+ cursor: pointer;
1432
+ display: flex;
1433
+ align-items: center;
1434
+ justify-content: center;
1435
+ font-size: 18px;
1436
+ z-index: 1;
1437
+ transition: all 0.2s;
1438
+ }
1439
+
1440
+ .expanded-image-close:hover {
1441
+ background: rgba(0, 0, 0, 0.8);
1442
+ transform: scale(1.1);
1443
+ }
1444
+
1445
+ .expanded-image {
1446
+ max-width: 100%;
1447
+ max-height: calc(90vh - 60px);
1448
+ object-fit: contain;
1449
+ display: block;
1450
+ }
1451
+
1452
+ .expanded-image-caption {
1453
+ padding: 12px 16px;
1454
+ background: #fff;
1455
+ color: #1e293b;
1456
+ font-size: 14px;
1457
+ text-align: center;
1458
+ border-top: 1px solid #e2e8f0;
1459
+ }
1460
+ `;
1461
+ document.head.appendChild(style);
1462
+ }
1463
+ }
1464
+
1465
+ // Export as ESM (Rollup will handle UMD conversion)
1466
+ export default TextChatScreen;
1467
+