mini-chat-bot-widget 0.6.0 → 0.8.0

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