mini-chat-bot-widget 0.2.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.
- package/dist/chat-widget.esm.js +2860 -80
- package/dist/chat-widget.esm.min.js +1 -1
- package/dist/chat-widget.umd.js +2860 -80
- package/dist/chat-widget.umd.min.js +1 -1
- package/index.html +56 -0
- package/managed_context/metadata.json +1 -0
- package/package.json +13 -7
- package/src/audio-chat-screen.js +1408 -0
- package/src/bhashiniApi.js +363 -0
- package/src/chat-widget.js +2490 -86
- package/src/text-chat-screen.js +1286 -0
- package/test_suite_analysis/metadata.json +1 -0
|
@@ -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
|
+
​ ​ ​
|
|
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
|
+
​ ​ ​​ ​ ​
|
|
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
|
+
|