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,1598 @@
1
+ // audio-chat-screen.js - Audio chat screen component
2
+ import { BhashiniFrontend } from "./bhashiniApi.js";
3
+
4
+ class AudioChatScreen {
5
+ constructor(options = {}) {
6
+ this.primaryColor = options.primaryColor || "#1a5c4b";
7
+ this.title = options.title || "Chat Assistant";
8
+ this.onRecordStart = options.onRecordStart || (() => { });
9
+ this.onRecordStop = options.onRecordStop || (() => { });
10
+ this.onBack = options.onBack || (() => { });
11
+ this.onOpenDrawer = options.onOpenDrawer || (() => { });
12
+ this.onClose = options.onClose || (() => { });
13
+ this.navigateToTextScreen = options.navigateToTextScreen;
14
+ this.sendMessage = options.sendMessage || null;
15
+ this.selectedLanguage = options.selectedLanguage || "en";
16
+ this.container = null;
17
+ this.messages = options.messages || [];
18
+
19
+ // Bhashini Integration
20
+ this.bhashini = new BhashiniFrontend();
21
+ this.mediaRecorder = null;
22
+ this.audioChunks = [];
23
+ this.isRecording = false;
24
+
25
+ // TTS tracking
26
+ this.playedMessageIds = new Set();
27
+ this.audioQueue = [];
28
+ this.isPlaying = false;
29
+
30
+ // File attachments
31
+ this.contentBlocks = [];
32
+ this.SUPPORTED_FILE_TYPES = [
33
+ "image/jpeg",
34
+ "image/png",
35
+ "image/gif",
36
+ "image/webp",
37
+ ];
38
+ }
39
+
40
+ render(container) {
41
+ this.container = container;
42
+ this._applyStyles();
43
+ container.innerHTML = `
44
+ <div class="audio-chat-screen">
45
+ <div class="chat-header">
46
+ <div class="chat-header-content">
47
+ <button class="chat-back" id="audio-chat-menu">
48
+ <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>
49
+ </button>
50
+
51
+ <div class="chat-header-text">
52
+ <div class="chat-title">${this.title}</div>
53
+ </div>
54
+ </div>
55
+ <button class="chat-close" id="navigate-to-text-screen">
56
+ <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-message-square-text-icon lucide-message-square-text"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/><path d="M7 11h10"/><path d="M7 15h6"/><path d="M7 7h8"/></svg>
57
+ </button>
58
+ &#8203; &#8203; &#8203;
59
+ <button class="chat-close" id="audio-chat-close">
60
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
61
+ <line x1="18" y1="6" x2="6" y2="18"></line>
62
+ <line x1="6" y1="6" x2="18" y2="18"></line>
63
+ </svg>
64
+ </button>
65
+ </div>
66
+
67
+
68
+ <div class="chat-messages" id="chat-messages">
69
+ <div class="chat-welcome">
70
+ <div class="welcome-text">👋 Hello! I'm your AI assistant. How can I help you today?</div>
71
+ </div>
72
+ </div>
73
+
74
+
75
+ <div class="file-attachments-container" id="file-attachments-container" style="display: none;"></div>
76
+ <div class="bottom-inputs">
77
+ <button id="record-button">
78
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
79
+ </button>
80
+ &#8203; &#8203; &#8203;&#8203; &#8203; &#8203;
81
+ <input
82
+ type="file"
83
+ id="file-upload-input"
84
+ accept="image/jpeg,image/png,image/gif,image/webp"
85
+ style="display: none;"
86
+ />
87
+ <button id="attachment-button">
88
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paperclip-icon lucide-paperclip"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551"/></svg>
89
+ </button>
90
+ </div>
91
+
92
+
93
+ </div>
94
+ `;
95
+
96
+ // Bind header events
97
+ const menuBtn = container.querySelector("#audio-chat-menu");
98
+ const closeBtn = container.querySelector("#audio-chat-close");
99
+ const navigateToTextScreen = container.querySelector("#navigate-to-text-screen")
100
+
101
+ if (menuBtn) {
102
+ menuBtn.addEventListener("click", () => {
103
+ if (this.onOpenDrawer) {
104
+ this.onOpenDrawer();
105
+ } else if (this.onBack) {
106
+ this.onBack();
107
+ }
108
+ });
109
+ }
110
+
111
+ if (closeBtn) {
112
+ closeBtn.addEventListener("click", () => {
113
+ if (this.onClose) this.onClose();
114
+ });
115
+ }
116
+
117
+ if (navigateToTextScreen) {
118
+ navigateToTextScreen.addEventListener("click", () => {
119
+ if (this.navigateToTextScreen) this.navigateToTextScreen()
120
+ })
121
+ }
122
+
123
+ // Bind audio chat events
124
+ const recordBtn = container.querySelector("#record-button");
125
+ const attachmentBtn = container.querySelector("#attachment-button");
126
+
127
+ if (recordBtn) {
128
+ recordBtn.addEventListener("click", () => {
129
+ this.toggleRecording(recordBtn);
130
+ });
131
+ }
132
+
133
+ const fileInput = container.querySelector("#file-upload-input");
134
+ if (attachmentBtn && fileInput) {
135
+ attachmentBtn.addEventListener("click", () => {
136
+ fileInput.click();
137
+ });
138
+
139
+ fileInput.addEventListener("change", (e) => {
140
+ this.handleFileUpload(e);
141
+ });
142
+ }
143
+
144
+ // Sync existing messages on render
145
+ if (this.messages && this.messages.length > 0) {
146
+ setTimeout(() => {
147
+ this._syncMessages(this.messages);
148
+ }, 50);
149
+ }
150
+ }
151
+
152
+ // Convert file to content block
153
+ fileToContentBlock(file) {
154
+ return new Promise((resolve, reject) => {
155
+ const reader = new FileReader();
156
+
157
+ reader.onload = () => {
158
+ const base64Data = reader.result.split(",")[1];
159
+
160
+ if (this.SUPPORTED_FILE_TYPES.includes(file.type)) {
161
+ resolve({
162
+ type: "image",
163
+ source_type: "base64",
164
+ mime_type: file.type,
165
+ data: base64Data,
166
+ metadata: {
167
+ name: file.name,
168
+ size: file.size,
169
+ },
170
+ });
171
+ } else {
172
+ reject(new Error(`Unsupported file type: ${file.type}`));
173
+ }
174
+ };
175
+
176
+ reader.onerror = () => {
177
+ reject(new Error(`Failed to read file: ${file.name}`));
178
+ };
179
+
180
+ reader.readAsDataURL(file);
181
+ });
182
+ }
183
+
184
+ // Handle file upload
185
+ async handleFileUpload(e) {
186
+ const files = e.target.files;
187
+ if (!files) return;
188
+
189
+ const fileArray = Array.from(files);
190
+ const validFiles = fileArray.filter((file) =>
191
+ this.SUPPORTED_FILE_TYPES.includes(file.type)
192
+ );
193
+
194
+ if (validFiles.length === 0) {
195
+ if (fileArray.length > 0) {
196
+ console.warn("Invalid file type.");
197
+ }
198
+ return;
199
+ }
200
+
201
+ // Replace existing with the new one
202
+ const file = validFiles[0];
203
+ try {
204
+ const block = await this.fileToContentBlock(file);
205
+ this.contentBlocks = [block];
206
+ this.renderFilePreview();
207
+ } catch (error) {
208
+ console.error("Error processing file:", error);
209
+ }
210
+
211
+ // Clear input
212
+ e.target.value = "";
213
+ }
214
+
215
+ removeBlock(index) {
216
+ this.contentBlocks = []; // Since we only have max 1, removing index 0 means clearing all
217
+ this.renderFilePreview();
218
+ }
219
+
220
+ renderFilePreview() {
221
+ if (!this.container) return;
222
+ const container = this.container.querySelector("#file-attachments-container");
223
+ if (!container) return;
224
+
225
+ if (!this.contentBlocks || this.contentBlocks.length === 0) {
226
+ container.innerHTML = "";
227
+ container.style.display = "none";
228
+ return;
229
+ }
230
+
231
+ container.style.display = "flex";
232
+
233
+ container.innerHTML = this.contentBlocks
234
+ .map((block, index) => {
235
+ const displayName = block.metadata?.filename || block.metadata?.name || "file";
236
+ const size = this.formatFileSize(block.metadata?.size || 0);
237
+ const isImage = block.type === "image";
238
+ const src = isImage
239
+ ? `data:${block.mime_type};base64,${block.data}`
240
+ : null;
241
+
242
+ return `
243
+ <div class="file-attachment ${isImage ? "has-thumbnail" : ""}" data-index="${index}">
244
+ <div class="file-thumbnail">
245
+ <img src="${src}" alt="${displayName}" class="file-thumbnail-image" />
246
+ </div>
247
+ <div class="file-attachment-info">
248
+ <div class="file-attachment-name" title="${displayName}">
249
+ ${displayName}
250
+ </div>
251
+ <div class="file-attachment-size">${size}</div>
252
+ </div>
253
+ <button
254
+ class="file-attachment-remove"
255
+ data-index="${index}"
256
+ title="Remove file"
257
+ >
258
+
259
+ </button>
260
+ </div>
261
+ `;
262
+ })
263
+ .join("");
264
+
265
+ // Bind remove buttons
266
+ container.querySelectorAll(".file-attachment-remove").forEach((btn) => {
267
+ btn.addEventListener("click", (e) => {
268
+ e.stopPropagation();
269
+ const index = parseInt(btn.getAttribute("data-index"));
270
+ this.removeBlock(index);
271
+ });
272
+ });
273
+ }
274
+
275
+ async toggleRecording(btn) {
276
+ if (this.isRecording) {
277
+ this.stopRecording(btn);
278
+ } else {
279
+ await this.startRecording(btn);
280
+ }
281
+ }
282
+
283
+ async startRecording(btn) {
284
+ try {
285
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
286
+ this.mediaRecorder = new MediaRecorder(stream);
287
+ this.audioChunks = [];
288
+
289
+ this.mediaRecorder.ondataavailable = (event) => {
290
+ this.audioChunks.push(event.data);
291
+ };
292
+
293
+ this.mediaRecorder.onstop = async () => {
294
+ const audioBlob = new Blob(this.audioChunks, { type: "audio/wav" });
295
+ this.audioChunks = [];
296
+
297
+ // Stop all tracks
298
+ stream.getTracks().forEach(track => track.stop());
299
+
300
+ // Process audio
301
+ await this.processAudioInput(audioBlob);
302
+ };
303
+
304
+ this.mediaRecorder.start();
305
+ this.isRecording = true;
306
+ this.onRecordStart();
307
+
308
+ // Update UI
309
+ btn.classList.add("recording");
310
+ btn.innerHTML = `
311
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-icon"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>
312
+ `;
313
+
314
+ // Stop current TTS if any
315
+ this.stopTTS();
316
+
317
+ } catch (error) {
318
+ console.error("Error accessing microphone:", error);
319
+ alert("Could not access microphone. Please check permissions.");
320
+ }
321
+ }
322
+
323
+ stopRecording(btn) {
324
+ if (this.mediaRecorder && this.isRecording) {
325
+ this.mediaRecorder.stop();
326
+ this.isRecording = false;
327
+ this.onRecordStop();
328
+
329
+ // Update UI
330
+ btn.classList.remove("recording");
331
+ btn.innerHTML = `
332
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
333
+ `;
334
+ }
335
+ }
336
+
337
+ async processAudioInput(audioBlob) {
338
+ try {
339
+ // Show processing state?
340
+
341
+ // 1. Convert to WAV Base64 (using Bhashini helper)
342
+ const base64Audio = await this.bhashini.convertToWav(audioBlob);
343
+
344
+ // 2. STT
345
+ const text = await this.bhashini.speechToText(base64Audio, this.selectedLanguage, this.selectedLanguage);
346
+
347
+ if (text && text.trim()) {
348
+ // 3. Send Message
349
+ if (this.sendMessage) {
350
+ await this.sendMessage(text, this.contentBlocks);
351
+ // Clear attachments after sending
352
+ this.contentBlocks = [];
353
+ this.renderFilePreview(); // Update UI to remove preview
354
+ }
355
+ }
356
+ } catch (error) {
357
+ console.error("Error processing audio:", error);
358
+ this.addMessage("Sorry, I couldn't understand that.", false, { isError: true });
359
+ }
360
+ }
361
+ // 9113770648
362
+ stopTTS() {
363
+ // Cancel any active speech synthesis (if using browser API) or stop audio element
364
+ if (this.currentAudio) {
365
+ this.currentAudio.pause();
366
+ this.currentAudio = null;
367
+ }
368
+ this.audioQueue = [];
369
+ this.isPlaying = false;
370
+ }
371
+
372
+ async playNextInQueue() {
373
+ // Check if audio screen is active
374
+ const isVisible = this.container && this.container.querySelector(".audio-chat-screen");
375
+ if (!isVisible) return;
376
+
377
+ console.log("Playing next in queue", this.audioQueue)
378
+ if (this.isPlaying || this.audioQueue.length === 0) return;
379
+
380
+ this.isPlaying = true;
381
+ const text = this.audioQueue.shift();
382
+
383
+ try {
384
+ const audioContent = await this.bhashini.textToSpeech(text, this.selectedLanguage);
385
+ if (audioContent) {
386
+ const audioSrc = `data:audio/wav;base64,${audioContent}`;
387
+ this.currentAudio = new Audio(audioSrc);
388
+
389
+ this.currentAudio.onended = () => {
390
+ this.isPlaying = false;
391
+ this.playNextInQueue();
392
+ };
393
+
394
+ this.currentAudio.onerror = () => {
395
+ this.isPlaying = false;
396
+ this.playNextInQueue();
397
+ };
398
+
399
+ await this.currentAudio.play();
400
+ } else {
401
+ this.isPlaying = false;
402
+ this.playNextInQueue();
403
+ }
404
+ } catch (error) {
405
+ console.error("TTS Error:", error);
406
+ this.isPlaying = false;
407
+ this.playNextInQueue();
408
+ }
409
+ }
410
+
411
+ // Format file size (mirrors TextChatScreen)
412
+ formatFileSize(bytes) {
413
+ if (bytes === 0) return "0 Bytes";
414
+
415
+ const k = 1024;
416
+ const sizes = ["Bytes", "KB", "MB", "GB"];
417
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
418
+
419
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
420
+ }
421
+
422
+ // Get file type display name (mirrors TextChatScreen)
423
+ getFileTypeDisplay(mimeType) {
424
+ switch (mimeType) {
425
+ case "image/jpeg":
426
+ case "image/jpg":
427
+ return "JPEG Image";
428
+ case "image/png":
429
+ return "PNG Image";
430
+ case "image/gif":
431
+ return "GIF Image";
432
+ case "image/webp":
433
+ return "WebP Image";
434
+ case "application/pdf":
435
+ return "PDF Document";
436
+ default:
437
+ return "Unknown File";
438
+ }
439
+ }
440
+
441
+ _formatTime(date) {
442
+ let hours = date.getHours();
443
+ const minutes = date.getMinutes();
444
+ const ampm = hours >= 12 ? 'PM' : 'AM';
445
+ hours = hours % 12;
446
+ hours = hours ? hours : 12;
447
+ const strTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ' ' + ampm;
448
+ return strTime;
449
+ }
450
+
451
+ addMessage(text, isUser, messageData = null) {
452
+ if (!this.container) return;
453
+
454
+ const messagesContainer = this.container.querySelector("#chat-messages");
455
+ if (!messagesContainer) return;
456
+
457
+ const welcome = messagesContainer.querySelector(".chat-welcome");
458
+ if (welcome) welcome.remove();
459
+
460
+ // Check if message already exists (for syncing)
461
+ const messageId = messageData?.id || Date.now();
462
+ const existingMessage = messagesContainer.querySelector(`[data-message-id="${messageId}"]`);
463
+ if (existingMessage && messageData) {
464
+ // Update existing message
465
+ const bubble = existingMessage.querySelector(".message-bubble");
466
+ if (bubble) {
467
+ // Parse markdown for assistant messages, escape for user messages
468
+ const content = messageData.content || text;
469
+ bubble.innerHTML = !isUser ? this._parseMarkdown(content) : this._escapeHtml(content);
470
+ }
471
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
472
+ return;
473
+ }
474
+
475
+ const messageEl = document.createElement("div");
476
+ messageEl.className = `chat-message ${isUser ? "user" : "bot"}`;
477
+ messageEl.setAttribute("data-message-id", messageId);
478
+
479
+ // Handle different message types
480
+ let content = text;
481
+ if (messageData) {
482
+ if (messageData.isStreaming) {
483
+ content = messageData.content || "Thinking...";
484
+ } else if (messageData.isError) {
485
+ content = messageData.content || "Error occurred";
486
+ messageEl.classList.add("error");
487
+ } else {
488
+ content = messageData.content || text;
489
+ }
490
+ }
491
+
492
+ // Build attachments HTML if present
493
+ let attachmentsHTML = "";
494
+ if (messageData && messageData.attachments && messageData.attachments.length > 0) {
495
+ attachmentsHTML = `
496
+ <div class="message-attachments">
497
+ ${messageData.attachments.map((attachment, index) => {
498
+ const displayName = attachment.metadata?.name || attachment.metadata?.filename || "Unknown file";
499
+ const isImage = attachment.type === "image";
500
+ const imageSrc = isImage && attachment.data
501
+ ? `data:${attachment.mime_type};base64,${attachment.data}`
502
+ : null;
503
+
504
+ return `
505
+ <div class="message-attachment ${isImage ? "image" : "pdf"}" data-attachment-index="${index}">
506
+ <span class="message-attachment-icon">
507
+ ${isImage ? "🖼️" : "📄"}
508
+ </span>
509
+ <div class="message-attachment-info">
510
+ <div
511
+ class="message-attachment-name ${isImage ? "clickable" : ""}"
512
+ ${isImage && imageSrc ? `data-image-src="${imageSrc}" data-image-alt="${displayName}"` : ""}
513
+ style="cursor: ${isImage ? "pointer" : "default"};"
514
+ >
515
+ ${this._escapeHtml(displayName)}
516
+ </div>
517
+ <div class="message-attachment-size">
518
+ ${this.formatFileSize(attachment.metadata?.size || 0)} • ${this.getFileTypeDisplay(attachment.mime_type)}
519
+ </div>
520
+ </div>
521
+ </div>
522
+ `;
523
+ }).join("")}
524
+ </div>
525
+ `;
526
+ }
527
+
528
+ const timestamp = messageData?.timestamp ? new Date(messageData.timestamp) : new Date();
529
+ const formattedTime = this._formatTime(timestamp);
530
+
531
+ // Parse markdown for assistant messages, escape for user messages
532
+ const renderedContent = !isUser ? this._parseMarkdown(content) : this._escapeHtml(content);
533
+
534
+ messageEl.innerHTML = `
535
+ <div class="message-content">
536
+ ${attachmentsHTML}
537
+ <div class="message-bubble">${renderedContent}</div>
538
+ <div class="message-time">${formattedTime}</div>
539
+ </div>
540
+ `;
541
+
542
+ // Bind image click handlers for expansion
543
+ if (messageData && messageData.attachments) {
544
+ messageEl.querySelectorAll(".message-attachment-name.clickable").forEach((el) => {
545
+ el.addEventListener("click", (e) => {
546
+ e.stopPropagation();
547
+ const imageSrc = el.getAttribute("data-image-src");
548
+ const imageAlt = el.getAttribute("data-image-alt");
549
+ if (imageSrc) {
550
+ this.showExpandedImage(imageSrc, imageAlt);
551
+ }
552
+ });
553
+ });
554
+ }
555
+
556
+ messagesContainer.appendChild(messageEl);
557
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
558
+ }
559
+
560
+ // Sync messages from shared array, using same DOM structure as TextChatScreen
561
+ _syncMessages(messages) {
562
+ if (!this.container) return;
563
+
564
+ const messagesContainer = this.container.querySelector("#chat-messages");
565
+ if (!messagesContainer) return;
566
+
567
+ // Remove welcome message if there are any messages
568
+ if (messages.length > 0) {
569
+ const welcome = messagesContainer.querySelector(".chat-welcome");
570
+ if (welcome) welcome.remove();
571
+ }
572
+
573
+ // Get existing message elements
574
+ const existingMessages = messagesContainer.querySelectorAll("[data-message-id]");
575
+ const existingIds = new Set(Array.from(existingMessages).map(el => el.getAttribute("data-message-id")));
576
+
577
+ // Add or update messages
578
+ messages.forEach((msg) => {
579
+ const msgId = String(msg.id);
580
+
581
+ // Handle TTS for new bot messages
582
+ if (msg.sender !== "user" && !this.playedMessageIds.has(msgId) && !msg.isStreaming && !msg.isProcessing && msg.content) {
583
+ this.playedMessageIds.add(msgId);
584
+ this.audioQueue.push(msg.content);
585
+ this.playNextInQueue();
586
+ }
587
+
588
+ if (existingIds.has(msgId)) {
589
+ // Update existing message
590
+ const existingEl = messagesContainer.querySelector(`[data-message-id="${msgId}"]`);
591
+ if (existingEl) {
592
+ const bubble = existingEl.querySelector(".message-bubble");
593
+ if (bubble) {
594
+ // Parse markdown for assistant messages, escape for user messages
595
+ const content = msg.content || "";
596
+ const isUserMessage = msg.sender === "user";
597
+ bubble.innerHTML = !isUserMessage ? this._parseMarkdown(content) : this._escapeHtml(content);
598
+ }
599
+
600
+ // Update attachments if they exist
601
+ const messageContent = existingEl.querySelector(".message-content");
602
+ if (messageContent && msg.attachments && msg.attachments.length > 0) {
603
+ let attachmentsHTML = `
604
+ <div class="message-attachments">
605
+ ${msg.attachments.map((attachment, index) => {
606
+ const displayName = attachment.metadata?.name || attachment.metadata?.filename || "Unknown file";
607
+ const isImage = attachment.type === "image";
608
+ const imageSrc = isImage && attachment.data
609
+ ? `data:${attachment.mime_type};base64,${attachment.data}`
610
+ : null;
611
+
612
+ return `
613
+ <div class="message-attachment ${isImage ? "image" : "pdf"}" data-attachment-index="${index}">
614
+ <span class="message-attachment-icon">
615
+ ${isImage ? "🖼️" : "📄"}
616
+ </span>
617
+ <div class="message-attachment-info">
618
+ <div
619
+ class="message-attachment-name ${isImage ? "clickable" : ""}"
620
+ ${isImage && imageSrc ? `data-image-src="${imageSrc}" data-image-alt="${displayName}"` : ""}
621
+ style="cursor: ${isImage ? "pointer" : "default"};"
622
+ >
623
+ ${this._escapeHtml(displayName)}
624
+ </div>
625
+ <div class="message-attachment-size">
626
+ ${this.formatFileSize(attachment.metadata?.size || 0)} • ${this.getFileTypeDisplay(attachment.mime_type)}
627
+ </div>
628
+ </div>
629
+ </div>
630
+ `;
631
+ }).join("")}
632
+ </div>
633
+ `;
634
+
635
+ // Insert or update attachments
636
+ const existingAttachments = messageContent.querySelector(".message-attachments");
637
+ if (existingAttachments) {
638
+ existingAttachments.outerHTML = attachmentsHTML;
639
+ } else {
640
+ messageContent.insertAdjacentHTML("afterbegin", attachmentsHTML);
641
+ }
642
+
643
+ // Re-bind image click handlers
644
+ messageContent.querySelectorAll(".message-attachment-name.clickable").forEach((el) => {
645
+ // Remove existing listeners by cloning
646
+ const newEl = el.cloneNode(true);
647
+ el.parentNode.replaceChild(newEl, el);
648
+
649
+ newEl.addEventListener("click", (e) => {
650
+ e.stopPropagation();
651
+ const imageSrc = newEl.getAttribute("data-image-src");
652
+ const imageAlt = newEl.getAttribute("data-image-alt");
653
+ if (imageSrc) {
654
+ this.showExpandedImage(imageSrc, imageAlt);
655
+ }
656
+ });
657
+ });
658
+ }
659
+
660
+ // Update classes based on message state
661
+ if (msg.isStreaming) {
662
+ existingEl.classList.add("streaming");
663
+ } else {
664
+ existingEl.classList.remove("streaming");
665
+ }
666
+ if (msg.isError) {
667
+ existingEl.classList.add("error");
668
+ }
669
+ }
670
+ } else {
671
+ // Add new message
672
+ this.addMessage(
673
+ msg.content || "",
674
+ msg.sender === "user",
675
+ msg
676
+ );
677
+ }
678
+ });
679
+
680
+ // Remove messages that are no longer in the array
681
+ existingMessages.forEach((el) => {
682
+ const msgId = el.getAttribute("data-message-id");
683
+ const exists = messages.some(msg => String(msg.id) === msgId);
684
+ if (!exists) {
685
+ el.remove();
686
+ }
687
+ });
688
+
689
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
690
+ }
691
+
692
+ _escapeHtml(text) {
693
+ const div = document.createElement("div");
694
+ div.textContent = text;
695
+ return div.innerHTML;
696
+ }
697
+
698
+ // Parse markdown to HTML
699
+ _parseMarkdown(text) {
700
+ if (!text || typeof text !== 'string') return '';
701
+
702
+ let html = text;
703
+
704
+ // Escape HTML first to prevent XSS
705
+ html = html
706
+ .replace(/&/g, '&amp;')
707
+ .replace(/</g, '&lt;')
708
+ .replace(/>/g, '&gt;');
709
+
710
+ // Split into lines for processing
711
+ const lines = html.split('\n');
712
+ const processedLines = [];
713
+ let inList = false;
714
+
715
+ for (let i = 0; i < lines.length; i++) {
716
+ let line = lines[i];
717
+
718
+ // Check for headers first (#, ##, ###, etc.)
719
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
720
+ if (headerMatch) {
721
+ if (inList) {
722
+ processedLines.push('</ul>');
723
+ inList = false;
724
+ }
725
+ const level = headerMatch[1].length;
726
+ const headerText = headerMatch[2];
727
+ // Process markdown inside header
728
+ let processedHeader = headerText;
729
+ processedHeader = processedHeader.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
730
+ processedHeader = processedHeader.replace(/__([^_]+)__/g, '<strong>$1</strong>');
731
+ processedHeader = processedHeader.replace(/(^|[^*])\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
732
+ processedHeader = processedHeader.replace(/(^|[^_])_([^_]+)_([^_]|$)/g, '$1<em>$2</em>$3');
733
+ processedLines.push(`<h${level}>${processedHeader}</h${level}>`);
734
+ continue;
735
+ }
736
+
737
+ // Check if this is a list item (before processing bold/italic)
738
+ const listMatch = line.match(/^[\s]*[-*]\s+(.+)$/);
739
+
740
+ if (listMatch) {
741
+ // Process markdown inside list item
742
+ let listContent = listMatch[1];
743
+
744
+ // Bold: **text** (must be processed before italic)
745
+ listContent = listContent.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
746
+ listContent = listContent.replace(/__([^_]+)__/g, '<strong>$1</strong>');
747
+
748
+ // Italic: *text* (single asterisk, avoid matching **text**)
749
+ listContent = listContent.replace(/(^|[^*])\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
750
+ listContent = listContent.replace(/(^|[^_])_([^_]+)_([^_]|$)/g, '$1<em>$2</em>$3');
751
+
752
+ if (!inList) {
753
+ processedLines.push('<ul>');
754
+ inList = true;
755
+ }
756
+ processedLines.push(`<li>${listContent}</li>`);
757
+ } else {
758
+ if (inList) {
759
+ processedLines.push('</ul>');
760
+ inList = false;
761
+ }
762
+
763
+ // Skip empty lines (they'll become <br> later)
764
+ if (line.trim() === '') {
765
+ processedLines.push('');
766
+ continue;
767
+ }
768
+
769
+ // Process markdown in non-list lines
770
+ // Bold: **text** (must be processed before italic)
771
+ line = line.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
772
+ line = line.replace(/__([^_]+)__/g, '<strong>$1</strong>');
773
+
774
+ // Italic: *text* (single asterisk, avoid matching **text**)
775
+ line = line.replace(/(^|[^*])\*([^*]+)\*([^*]|$)/g, '$1<em>$2</em>$3');
776
+ line = line.replace(/(^|[^_])_([^_]+)_([^_]|$)/g, '$1<em>$2</em>$3');
777
+
778
+ // Inline code: `code`
779
+ line = line.replace(/`([^`]+)`/g, '<code>$1</code>');
780
+
781
+ // Links: [text](url)
782
+ line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
783
+
784
+ processedLines.push(line);
785
+ }
786
+ }
787
+
788
+ if (inList) {
789
+ processedLines.push('</ul>');
790
+ }
791
+
792
+ // Join lines with <br> and return
793
+ html = processedLines.join('<br>');
794
+
795
+ return html;
796
+ }
797
+
798
+ // Show expanded image modal (mirrors TextChatScreen)
799
+ showExpandedImage(src, alt) {
800
+ const existingModal = document.querySelector(".expanded-image-modal");
801
+ if (existingModal) {
802
+ existingModal.remove();
803
+ }
804
+
805
+ const modal = document.createElement("div");
806
+ modal.className = "expanded-image-modal";
807
+ modal.innerHTML = `
808
+ <div class="expanded-image-container">
809
+ <button class="expanded-image-close" title="Close">✕</button>
810
+ <img src="${src}" alt="${alt || "Image"}" class="expanded-image" />
811
+ <div class="expanded-image-caption">${this._escapeHtml(alt || "Image preview")}</div>
812
+ </div>
813
+ `;
814
+
815
+ const closeModal = (e) => {
816
+ if (e) e.stopPropagation();
817
+ modal.remove();
818
+ document.removeEventListener("mousedown", handleClickOutside);
819
+ };
820
+
821
+ const handleClickOutside = (e) => {
822
+ if (!e.target.closest(".expanded-image-container")) {
823
+ closeModal(e);
824
+ }
825
+ };
826
+
827
+ modal
828
+ .querySelector(".expanded-image-close")
829
+ .addEventListener("click", closeModal);
830
+ modal.addEventListener("click", (e) => {
831
+ if (e.target === modal) {
832
+ closeModal(e);
833
+ }
834
+ });
835
+ document.addEventListener("mousedown", handleClickOutside);
836
+
837
+ document.body.appendChild(modal);
838
+ }
839
+
840
+ _applyStyles() {
841
+ // Check if styles already applied
842
+ if (document.getElementById('audio-chat-screen-styles')) return;
843
+
844
+ const style = document.createElement("style");
845
+ style.id = 'audio-chat-screen-styles';
846
+ style.textContent = `
847
+ .audio-chat-screen {
848
+ flex: 1;
849
+ height: 100%;
850
+ display: flex;
851
+ flex-direction: column;
852
+ position: relative;
853
+ overflow: hidden;
854
+ background: linear-gradient(180deg, white 10%, #E1EFCC );
855
+ }
856
+
857
+ .audio-chat-screen .chat-header {
858
+ color: ${this.primaryColor};
859
+ padding: 20px;
860
+ display: flex;
861
+ align-items: center;
862
+ justify-content: space-between;
863
+ flex-shrink: 0;
864
+ background: transparent;
865
+ z-index: 20;
866
+ }
867
+
868
+ .audio-chat-screen .chat-header-content {
869
+ display: flex;
870
+ align-items: center;
871
+ gap: 12px;
872
+ flex: 1;
873
+ }
874
+
875
+ .audio-chat-screen .chat-back {
876
+ background: ${this.primaryColor};
877
+ border: none;
878
+ color: white;
879
+ cursor: pointer;
880
+ padding: 8px;
881
+ border-radius: 50%;
882
+ display: flex;
883
+ align-items: center;
884
+ justify-content: center;
885
+ transition: all 0.2s;
886
+ margin-right: 8px;
887
+ }
888
+
889
+ .bottom-inputs {
890
+ width: 100%;
891
+ padding: 20px;
892
+ display: flex;
893
+ justify-content: center;
894
+ align-items: center;
895
+ gap: 5px;
896
+ z-index: 10;
897
+ flex-shrink: 0;
898
+ }
899
+
900
+ .bottom-inputs button {
901
+ border: none;
902
+ border-radius: 9999px;
903
+ display: flex;
904
+ align-items: center;
905
+ justify-content: center;
906
+ cursor: pointer;
907
+ transition: all 0.2s ease;
908
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
909
+ }
910
+
911
+ #record-button {
912
+ width: 50px;
913
+ height: 50px;
914
+ background: ${this.primaryColor};
915
+ color: #ffffff;
916
+ }
917
+
918
+ #record-button.recording {
919
+ background: #ef4444;
920
+ animation: pulse-ring 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
921
+ }
922
+
923
+ @keyframes pulse-ring {
924
+ 0% {
925
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);
926
+ }
927
+ 70% {
928
+ box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
929
+ }
930
+ 100% {
931
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
932
+ }
933
+ }
934
+
935
+ #record-button:hover {
936
+ transform: translateY(-2px) scale(1.03);
937
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
938
+ }
939
+
940
+ #record-button:active {
941
+ transform: translateY(0) scale(0.97);
942
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
943
+ }
944
+
945
+ #attachment-button {
946
+ width: 50px;
947
+ height: 50px;
948
+ background: #ffffff;
949
+ color: ${this.primaryColor};
950
+ border: 1px solid rgba(148, 163, 184, 0.5);
951
+ }
952
+
953
+ #attachment-button:hover {
954
+ transform: translateY(-2px) scale(1.03);
955
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
956
+ }
957
+
958
+ #attachment-button:active {
959
+ transform: translateY(1px);
960
+ box-shadow: 0 3px 8px rgba(15, 23, 42, 0.15);
961
+ }
962
+ .audio-chat-screen .chat-avatar {
963
+ width: 40px;
964
+ height: 40px;
965
+ border-radius: 50%;
966
+ background: rgba(255, 255, 255, 0.2);
967
+ backdrop-filter: blur(10px);
968
+ display: flex;
969
+ align-items: center;
970
+ justify-content: center;
971
+ }
972
+
973
+ .audio-chat-screen .chat-header-text {
974
+ display: flex;
975
+ flex-direction: column;
976
+ gap: 2px;
977
+ }
978
+
979
+ .audio-chat-screen .chat-title {
980
+ font-weight: 600;
981
+ font-size: 20px;
982
+ }
983
+
984
+ .audio-chat-screen .chat-status {
985
+ font-size: 12px;
986
+ opacity: 0.9;
987
+ display: flex;
988
+ align-items: center;
989
+ gap: 4px;
990
+ }
991
+
992
+ .audio-chat-screen .chat-close {
993
+ background: ${this.primaryColor};
994
+ border: none;
995
+ color: white;
996
+ cursor: pointer;
997
+ padding: 8px;
998
+ border-radius: 50%;
999
+ display: flex;
1000
+ align-items: center;
1001
+ justify-content: center;
1002
+ transition: all 0.2s;
1003
+ }
1004
+
1005
+
1006
+ .audio-chat-screen .audio-chat-content {
1007
+ flex: 1;
1008
+ height:100%;
1009
+ display: flex;
1010
+ flex-direction: column;
1011
+ padding: 20px;
1012
+ gap: 20px;
1013
+ }
1014
+
1015
+ .audio-chat-screen .audio-status {
1016
+ display: flex;
1017
+ flex-direction: column;
1018
+ align-items: center;
1019
+ justify-content: center;
1020
+ padding: 40px 20px;
1021
+ background: white;
1022
+ border-radius: 16px;
1023
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1024
+ }
1025
+
1026
+ .audio-chat-screen .audio-status-icon {
1027
+ font-size: 48px;
1028
+ margin-bottom: 12px;
1029
+ transition: transform 0.3s;
1030
+ }
1031
+
1032
+ .audio-chat-screen .audio-status-text {
1033
+ font-size: 16px;
1034
+ font-weight: 500;
1035
+ color: #475569;
1036
+ }
1037
+
1038
+ /* Message list styles mirrored from TextChatScreen */
1039
+ .audio-chat-screen .chat-messages {
1040
+ flex: 1;
1041
+ padding: 20px;
1042
+ overflow-y: auto;
1043
+ overflow-x: hidden; /* Prevent horizontal scroll */
1044
+ min-height: 0; /* Crucial for nested flex scrolling */
1045
+ background: transparent;
1046
+ display: flex;
1047
+ flex-direction: column;
1048
+ gap: 12px;
1049
+ }
1050
+
1051
+ .audio-chat-screen .chat-messages::-webkit-scrollbar {
1052
+ width: 6px;
1053
+ }
1054
+
1055
+ .audio-chat-screen .chat-messages::-webkit-scrollbar-track {
1056
+ background: transparent;
1057
+ }
1058
+
1059
+ .audio-chat-screen .chat-messages::-webkit-scrollbar-thumb {
1060
+ background: #cbd5e1;
1061
+ border-radius: 3px;
1062
+ }
1063
+
1064
+ .audio-chat-screen .file-attachments-container {
1065
+ padding: 8px 12px;
1066
+ background: transparent;
1067
+ display: flex;
1068
+ flex-wrap: wrap;
1069
+ gap: 8px;
1070
+ max-height: 120px;
1071
+ overflow-y: auto;
1072
+ position: relative;
1073
+ z-index: 5;
1074
+ }
1075
+
1076
+ .audio-chat-screen .file-attachment {
1077
+ display: flex;
1078
+ align-items: center;
1079
+ gap: 8px;
1080
+ padding: 6px 10px;
1081
+ background: #f1f5f9;
1082
+ border: 1px solid #e2e8f0;
1083
+ border-radius: 8px;
1084
+ font-size: 12px;
1085
+ max-width: 200px;
1086
+ }
1087
+
1088
+ .audio-chat-screen .file-thumbnail {
1089
+ width: 40px;
1090
+ height: 40px;
1091
+ border-radius: 6px;
1092
+ overflow: hidden;
1093
+ flex-shrink: 0;
1094
+ background: #e2e8f0;
1095
+ display: flex;
1096
+ align-items: center;
1097
+ justify-content: center;
1098
+ }
1099
+
1100
+ .audio-chat-screen .file-thumbnail-image {
1101
+ width: 100%;
1102
+ height: 100%;
1103
+ object-fit: cover;
1104
+ }
1105
+
1106
+ .audio-chat-screen .file-attachment-info {
1107
+ flex: 1;
1108
+ min-width: 0;
1109
+ display: flex;
1110
+ flex-direction: column;
1111
+ gap: 2px;
1112
+ }
1113
+
1114
+ .audio-chat-screen .file-attachment-name {
1115
+ font-weight: 500;
1116
+ color: #1e293b;
1117
+ white-space: nowrap;
1118
+ overflow: hidden;
1119
+ text-overflow: ellipsis;
1120
+ max-width: 120px;
1121
+ }
1122
+
1123
+ .audio-chat-screen .file-attachment-size {
1124
+ font-size: 11px;
1125
+ color: #64748b;
1126
+ }
1127
+
1128
+ .audio-chat-screen .file-attachment-remove {
1129
+ background: transparent;
1130
+ border: none;
1131
+ color: #64748b;
1132
+ cursor: pointer;
1133
+ padding: 4px;
1134
+ border-radius: 4px;
1135
+ font-size: 16px;
1136
+ line-height: 1;
1137
+ display: flex;
1138
+ align-items: center;
1139
+ justify-content: center;
1140
+ transition: all 0.2s;
1141
+ flex-shrink: 0;
1142
+ }
1143
+
1144
+ .audio-chat-screen .file-attachment-remove:hover {
1145
+ background: #fee2e2;
1146
+ color: #dc2626;
1147
+ }
1148
+
1149
+ .audio-chat-screen .chat-welcome {
1150
+ text-align: center;
1151
+ padding: 100px 20px;
1152
+ color: #64748b;
1153
+ }
1154
+
1155
+ .audio-chat-screen .chat-welcome .welcome-icon {
1156
+ font-size: 48px;
1157
+ margin-bottom: 12px;
1158
+ }
1159
+
1160
+ .audio-chat-screen .welcome-text {
1161
+ font-size: 15px;
1162
+ font-weight: 500;
1163
+ color: #475569;
1164
+ }
1165
+
1166
+ .audio-chat-screen .chat-message {
1167
+ display: flex;
1168
+ gap: 8px;
1169
+ animation: messageSlide 0.3s ease-out;
1170
+ }
1171
+
1172
+ @keyframes messageSlide {
1173
+ from {
1174
+ opacity: 0;
1175
+ transform: translateY(10px);
1176
+ }
1177
+ to {
1178
+ opacity: 1;
1179
+ transform: translateY(0);
1180
+ }
1181
+ }
1182
+
1183
+ .audio-chat-screen .chat-message.user {
1184
+ flex-direction: row-reverse;
1185
+ }
1186
+
1187
+ .audio-chat-screen .message-content {
1188
+ max-width: 75%;
1189
+ }
1190
+
1191
+ .audio-chat-screen .message-bubble {
1192
+ padding: 12px 16px;
1193
+ border-radius: 16px;
1194
+ font-size: 14px;
1195
+ line-height: 1.5;
1196
+ word-wrap: break-word;
1197
+ }
1198
+
1199
+ .audio-chat-screen .chat-message.user .message-bubble {
1200
+ background: #e9f5d7;
1201
+ color: ${this.primaryColor};
1202
+ border-bottom-right-radius: 4px;
1203
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1204
+ }
1205
+
1206
+ .audio-chat-screen .chat-message.bot .message-bubble {
1207
+ background: white;
1208
+ color: #1e293b;
1209
+ border-bottom-left-radius: 4px;
1210
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1211
+ }
1212
+
1213
+ /* Markdown styles */
1214
+ .audio-chat-screen .message-bubble strong {
1215
+ font-weight: 600;
1216
+ color: inherit;
1217
+ }
1218
+
1219
+ .audio-chat-screen .message-bubble em {
1220
+ font-style: italic;
1221
+ }
1222
+
1223
+ .audio-chat-screen .message-bubble ul {
1224
+ margin: 0px 0;
1225
+ padding-left: 20px;
1226
+ list-style-type: disc;
1227
+ margin-bottom:0px;
1228
+ margin-top:5px;
1229
+ }
1230
+
1231
+ .audio-chat-screen .message-bubble li {
1232
+ margin: 0;
1233
+ line-height: 1.1;
1234
+ padding-left: 3px;
1235
+ }
1236
+
1237
+ .audio-chat-screen .message-bubble h1,
1238
+ .audio-chat-screen .message-bubble h2,
1239
+ .audio-chat-screen .message-bubble h3,
1240
+ .audio-chat-screen .message-bubble h4,
1241
+ .audio-chat-screen .message-bubble h5,
1242
+ .audio-chat-screen .message-bubble h6 {
1243
+ margin: 0px 0 0px 0;
1244
+ font-weight: 600;
1245
+ color: inherit;
1246
+ line-height: 1.3;
1247
+ }
1248
+
1249
+ .audio-chat-screen .message-bubble h1 {
1250
+ font-size: 1.5em;
1251
+ }
1252
+
1253
+ .audio-chat-screen .message-bubble h2 {
1254
+ font-size: 1.3em;
1255
+ }
1256
+
1257
+ .audio-chat-screen .message-bubble h3 {
1258
+ font-size: 1.15em;
1259
+ }
1260
+
1261
+ .audio-chat-screen .message-bubble h4 {
1262
+ font-size: 1.05em;
1263
+ }
1264
+
1265
+ .audio-chat-screen .message-bubble h5 {
1266
+ font-size: 1em;
1267
+ }
1268
+
1269
+ .audio-chat-screen .message-bubble h6 {
1270
+ font-size: 0.95em;
1271
+ }
1272
+
1273
+ .audio-chat-screen .message-bubble code {
1274
+ background: rgba(0, 0, 0, 0.05);
1275
+ padding: 2px 6px;
1276
+ border-radius: 4px;
1277
+ font-family: 'Courier New', Courier, monospace;
1278
+ font-size: 0.9em;
1279
+ }
1280
+
1281
+ .audio-chat-screen .message-bubble a {
1282
+ color: ${this.primaryColor};
1283
+ text-decoration: underline;
1284
+ }
1285
+
1286
+ .audio-chat-screen .message-bubble a:hover {
1287
+ opacity: 0.8;
1288
+ }
1289
+
1290
+ .audio-chat-screen .message-bubble br {
1291
+ line-height: 0.1; /* Adjust between 0 and 1 */
1292
+ }
1293
+
1294
+
1295
+ .audio-chat-screen .message-time {
1296
+ font-size: 10px;
1297
+ color: #94a3b8;
1298
+ margin-top: 4px;
1299
+ text-align: right;
1300
+ }
1301
+
1302
+ /* Message Attachments Styles (mirrored from TextChatScreen) */
1303
+ .audio-chat-screen .message-attachments {
1304
+ display: flex;
1305
+ flex-direction: column;
1306
+ gap: 8px;
1307
+ margin-bottom: 8px;
1308
+ }
1309
+
1310
+ .audio-chat-screen .message-attachment {
1311
+ display: flex;
1312
+ align-items: center;
1313
+ gap: 8px;
1314
+ padding: 8px 12px;
1315
+ background: #f1f5f9;
1316
+ border: 1px solid #e2e8f0;
1317
+ border-radius: 8px;
1318
+ font-size: 12px;
1319
+ max-width: 100%;
1320
+ }
1321
+
1322
+ .audio-chat-screen .message-attachment.image {
1323
+ background: #f8fafc;
1324
+ }
1325
+
1326
+ .audio-chat-screen .message-attachment.pdf {
1327
+ background: #fef2f2;
1328
+ }
1329
+
1330
+ .audio-chat-screen .message-attachment-icon {
1331
+ font-size: 20px;
1332
+ flex-shrink: 0;
1333
+ }
1334
+
1335
+ .audio-chat-screen .message-attachment-info {
1336
+ flex: 1;
1337
+ min-width: 0;
1338
+ display: flex;
1339
+ flex-direction: column;
1340
+ gap: 2px;
1341
+ }
1342
+
1343
+ .audio-chat-screen .message-attachment-name {
1344
+ font-weight: 500;
1345
+ color: #1e293b;
1346
+ white-space: nowrap;
1347
+ overflow: hidden;
1348
+ text-overflow: ellipsis;
1349
+ }
1350
+
1351
+ .audio-chat-screen .message-attachment-name.clickable:hover {
1352
+ color: ${this.primaryColor};
1353
+ text-decoration: underline;
1354
+ }
1355
+
1356
+ .audio-chat-screen .message-attachment-size {
1357
+ font-size: 11px;
1358
+ color: #64748b;
1359
+ }
1360
+
1361
+ .audio-chat-screen .audio-messages {
1362
+ flex: 1;
1363
+ overflow-y: auto;
1364
+ display: flex;
1365
+ flex-direction: column;
1366
+ gap: 12px;
1367
+ min-height: 200px;
1368
+ }
1369
+
1370
+ .audio-chat-screen .audio-message {
1371
+ display: flex;
1372
+ gap: 8px;
1373
+ animation: messageSlide 0.3s ease-out;
1374
+ }
1375
+
1376
+ .audio-chat-screen .audio-message.user {
1377
+ flex-direction: row-reverse;
1378
+ }
1379
+
1380
+ .audio-chat-screen .audio-message-content {
1381
+ max-width: 75%;
1382
+ }
1383
+
1384
+ .audio-chat-screen .audio-message-bubble {
1385
+ padding: 12px 16px;
1386
+ border-radius: 16px;
1387
+ font-size: 14px;
1388
+ line-height: 1.5;
1389
+ word-wrap: break-word;
1390
+ }
1391
+
1392
+ .audio-chat-screen .audio-message.user .audio-message-bubble {
1393
+ background: ${this.primaryColor};
1394
+ color: white;
1395
+ border-bottom-right-radius: 4px;
1396
+ }
1397
+
1398
+ .audio-chat-screen .audio-message.assistant .audio-message-bubble {
1399
+ background: white;
1400
+ color: #1e293b;
1401
+ border-bottom-left-radius: 4px;
1402
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
1403
+ }
1404
+
1405
+ @keyframes messageSlide {
1406
+ from {
1407
+ opacity: 0;
1408
+ transform: translateY(10px);
1409
+ }
1410
+ to {
1411
+ opacity: 1;
1412
+ transform: translateY(0);
1413
+ }
1414
+ }
1415
+
1416
+ .audio-chat-screen .audio-messages::-webkit-scrollbar {
1417
+ width: 6px;
1418
+ }
1419
+
1420
+ .audio-chat-screen .audio-messages::-webkit-scrollbar-track {
1421
+ background: transparent;
1422
+ }
1423
+
1424
+ .audio-chat-screen .audio-messages::-webkit-scrollbar-thumb {
1425
+ background: #cbd5e1;
1426
+ border-radius: 3px;
1427
+ }
1428
+
1429
+ .audio-chat-screen .audio-controls {
1430
+ position: absolute;
1431
+ left: 50%;
1432
+ bottom: 20px;
1433
+ transform: translateX(-50%);
1434
+ display: flex;
1435
+ justify-content: center;
1436
+ align-items: center;
1437
+ pointer-events: none; /* let only the button receive events */
1438
+ }
1439
+
1440
+ .audio-chat-screen .audio-controls .audio-record-btn {
1441
+ pointer-events: auto;
1442
+ }
1443
+
1444
+ .audio-chat-screen .audio-record-btn {
1445
+ width: 80px;
1446
+ height: 80px;
1447
+ border-radius: 50%;
1448
+ background: ${this.primaryColor};
1449
+ color: white;
1450
+ border: none;
1451
+ cursor: pointer;
1452
+ display: flex;
1453
+ flex-direction: column;
1454
+ align-items: center;
1455
+ justify-content: center;
1456
+ gap: 8px;
1457
+ transition: all 0.3s;
1458
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1459
+ }
1460
+
1461
+ .audio-chat-screen .audio-record-btn:hover {
1462
+ transform: scale(1.05);
1463
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
1464
+ }
1465
+
1466
+ .audio-chat-screen .audio-record-btn:active {
1467
+ transform: scale(0.95);
1468
+ }
1469
+
1470
+ .audio-chat-screen .audio-record-btn.recording {
1471
+ background: #ef4444;
1472
+ animation: pulse-record 1.5s infinite;
1473
+ }
1474
+
1475
+ @keyframes pulse-record {
1476
+ 0%, 100% {
1477
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
1478
+ }
1479
+ 50% {
1480
+ box-shadow: 0 4px 24px rgba(239, 68, 68, 0.6);
1481
+ }
1482
+ }
1483
+
1484
+ .audio-chat-screen .audio-record-btn span {
1485
+ font-size: 11px;
1486
+ font-weight: 500;
1487
+ margin-top: 4px;
1488
+ }
1489
+
1490
+ .audio-chat-screen .audio-record-btn svg {
1491
+ width: 32px;
1492
+ height: 32px;
1493
+ }
1494
+
1495
+ @media (max-width: 768px) {
1496
+ .audio-chat-screen .audio-record-btn {
1497
+ width: 100px;
1498
+ height: 100px;
1499
+ }
1500
+ }
1501
+
1502
+ /* Expanded Image Modal Styles */
1503
+ .expanded-image-modal {
1504
+ position: fixed;
1505
+ top: 0;
1506
+ left: 0;
1507
+ right: 0;
1508
+ bottom: 0;
1509
+ background: rgba(0, 0, 0, 0.8);
1510
+ display: flex;
1511
+ align-items: center;
1512
+ justify-content: center;
1513
+ z-index: 10000;
1514
+ animation: fadeIn 0.2s ease;
1515
+ }
1516
+
1517
+ @keyframes fadeIn {
1518
+ from {
1519
+ opacity: 0;
1520
+ }
1521
+ to {
1522
+ opacity: 1;
1523
+ }
1524
+ }
1525
+
1526
+ .expanded-image-container {
1527
+ position: relative;
1528
+ max-width: 90%;
1529
+ max-height: 90vh;
1530
+ background: #fff;
1531
+ border-radius: 8px;
1532
+ overflow: hidden;
1533
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1534
+ animation: scaleIn 0.2s ease;
1535
+ display: flex;
1536
+ flex-direction: column;
1537
+ }
1538
+
1539
+ @keyframes scaleIn {
1540
+ from {
1541
+ transform: scale(0.9);
1542
+ opacity: 0;
1543
+ }
1544
+ to {
1545
+ transform: scale(1);
1546
+ opacity: 1;
1547
+ }
1548
+ }
1549
+
1550
+ .expanded-image-close {
1551
+ position: absolute;
1552
+ top: 12px;
1553
+ right: 12px;
1554
+ background: rgba(0, 0, 0, 0.6);
1555
+ border: none;
1556
+ color: white;
1557
+ width: 32px;
1558
+ height: 32px;
1559
+ border-radius: 50%;
1560
+ cursor: pointer;
1561
+ display: flex;
1562
+ align-items: center;
1563
+ justify-content: center;
1564
+ font-size: 18px;
1565
+ z-index: 1;
1566
+ transition: all 0.2s;
1567
+ }
1568
+
1569
+ .expanded-image-close:hover {
1570
+ background: rgba(0, 0, 0, 0.8);
1571
+ transform: scale(1.1);
1572
+ }
1573
+
1574
+ .expanded-image {
1575
+ max-width: 100%;
1576
+ max-height: calc(90vh - 60px);
1577
+ object-fit: contain;
1578
+ display: block;
1579
+ }
1580
+
1581
+ .expanded-image-caption {
1582
+ padding: 12px 16px;
1583
+ background: #fff;
1584
+ color: #1e293b;
1585
+ font-size: 14px;
1586
+ text-align: center;
1587
+ border-top: 1px solid #e2e8f0;
1588
+ }
1589
+ `;
1590
+ document.head.appendChild(style);
1591
+ }
1592
+ }
1593
+
1594
+ // Export as ESM (Rollup will handle UMD conversion)
1595
+ export default AudioChatScreen;
1596
+
1597
+
1598
+