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
package/dist/chat-widget.esm.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
1
|
+
function _defineProperty(e, r, t) {
|
|
2
|
+
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
3
|
+
value: t,
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true
|
|
7
|
+
}) : e[r] = t, e;
|
|
3
8
|
}
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
function ownKeys(e, r) {
|
|
10
|
+
var t = Object.keys(e);
|
|
11
|
+
if (Object.getOwnPropertySymbols) {
|
|
12
|
+
var o = Object.getOwnPropertySymbols(e);
|
|
13
|
+
r && (o = o.filter(function (r) {
|
|
14
|
+
return Object.getOwnPropertyDescriptor(e, r).enumerable;
|
|
15
|
+
})), t.push.apply(t, o);
|
|
8
16
|
}
|
|
17
|
+
return t;
|
|
9
18
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
function _objectSpread2(e) {
|
|
20
|
+
for (var r = 1; r < arguments.length; r++) {
|
|
21
|
+
var t = null != arguments[r] ? arguments[r] : {};
|
|
22
|
+
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
|
|
23
|
+
_defineProperty(e, r, t[r]);
|
|
24
|
+
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
|
|
25
|
+
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return e;
|
|
14
29
|
}
|
|
15
30
|
function _toPrimitive(t, r) {
|
|
16
31
|
if ("object" != typeof t || !t) return t;
|
|
@@ -20,87 +35,2852 @@ function _toPrimitive(t, r) {
|
|
|
20
35
|
if ("object" != typeof i) return i;
|
|
21
36
|
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
22
37
|
}
|
|
23
|
-
return (String )(t);
|
|
38
|
+
return ("string" === r ? String : Number)(t);
|
|
24
39
|
}
|
|
25
40
|
function _toPropertyKey(t) {
|
|
26
41
|
var i = _toPrimitive(t, "string");
|
|
27
42
|
return "symbol" == typeof i ? i : i + "";
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
// text-chat-screen.js - Text chat screen component
|
|
46
|
+
|
|
47
|
+
class TextChatScreen {
|
|
48
|
+
constructor() {
|
|
49
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
50
|
+
this.placeholder = options.placeholder || "Type your message...";
|
|
51
|
+
this.primaryColor = options.primaryColor || "#1a5c4b";
|
|
52
|
+
this.title = options.title || "Chat Assistant";
|
|
53
|
+
this.onMessage = options.onMessage || (() => {});
|
|
54
|
+
this.onBack = options.onBack || (() => {});
|
|
55
|
+
this.onOpenDrawer = options.onOpenDrawer || (() => {});
|
|
56
|
+
this.onClose = options.onClose || (() => {});
|
|
57
|
+
this.container = null;
|
|
58
|
+
this.messages = options.messages;
|
|
59
|
+
this.sendMessage = options.sendMessage || null;
|
|
60
|
+
this.contentBlocks = options.contentBlocks || [];
|
|
61
|
+
this.onContentBlocksChange = options.onContentBlocksChange || (() => {});
|
|
62
|
+
this.navigateToAudioScreen = options.navigateToAudioScreen;
|
|
63
|
+
// Supported file types
|
|
64
|
+
this.SUPPORTED_FILE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
65
|
+
}
|
|
66
|
+
render(container) {
|
|
67
|
+
this.container = container;
|
|
68
|
+
this._applyStyles();
|
|
69
|
+
container.innerHTML = "\n <div class=\"text-chat-screen\">\n <div class=\"chat-header\">\n <div class=\"chat-header-content\">\n <button class=\"chat-back\" id=\"text-chat-menu\">\n <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>\n </button>\n \n <div class=\"chat-header-text\">\n <div class=\"chat-title\">".concat(this.title, "</div>\n </div>\n </div>\n <button class=\"chat-close\" id=\"navigate-to-audio-screen\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-mic-icon lucide-mic\"><path d=\"M12 19v3\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><rect x=\"9\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\"/></svg>\n </button>\n ​ ​ ​\n <button class=\"chat-close\" id=\"text-chat-close\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n <div class=\"chat-messages\" id=\"chat-messages\">\n <div class=\"chat-welcome\">\n <div class=\"welcome-text\">\uD83D\uDC4B Hello! I'm your AI assistant. How can I help you today?</div>\n </div>\n </div>\n <div class=\"file-attachments-container\" id=\"file-attachments-container\" style=\"display: none;\"></div>\n <div class=\"chat-input-wrapper\">\n <div class=\"file-upload-controls\">\n <input\n type=\"file\"\n id=\"file-upload-input\"\n accept=\"image/jpeg,image/png,image/gif,image/webp\"\n style=\"display: none;\"\n />\n <button id=\"file-upload-button\" class=\"file-upload-button\" title=\"Attach files\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48\"></path>\n </svg>\n </button>\n </div>\n <input type=\"text\" id=\"chat-input\" placeholder=\"").concat(this.placeholder, "\" autocomplete=\"off\" />\n <button id=\"chat-send\" disabled>\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"></line>\n <polygon points=\"22 2 15 22 11 13 2 9 22 2\"></polygon>\n </svg>\n </button>\n </div>\n </div>\n ");
|
|
70
|
+
|
|
71
|
+
// Bind header events
|
|
72
|
+
const menuBtn = container.querySelector("#text-chat-menu");
|
|
73
|
+
const closeBtn = container.querySelector("#text-chat-close");
|
|
74
|
+
const navigateToAudioBtn = container.querySelector("#navigate-to-audio-screen");
|
|
75
|
+
if (menuBtn) {
|
|
76
|
+
menuBtn.addEventListener("click", () => {
|
|
77
|
+
if (this.onOpenDrawer) {
|
|
78
|
+
this.onOpenDrawer();
|
|
79
|
+
} else if (this.onBack) {
|
|
80
|
+
this.onBack();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (closeBtn) {
|
|
85
|
+
closeBtn.addEventListener("click", () => {
|
|
86
|
+
if (this.onClose) this.onClose();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (navigateToAudioBtn) {
|
|
90
|
+
navigateToAudioBtn.addEventListener("click", () => {
|
|
91
|
+
if (this.navigateToAudioScreen) this.navigateToAudioScreen();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Bind text chat events
|
|
96
|
+
const input = container.querySelector("#chat-input");
|
|
97
|
+
const sendBtn = container.querySelector("#chat-send");
|
|
98
|
+
const fileInput = container.querySelector("#file-upload-input");
|
|
99
|
+
const fileUploadBtn = container.querySelector("#file-upload-button");
|
|
100
|
+
|
|
101
|
+
// File upload handler
|
|
102
|
+
fileUploadBtn.addEventListener("click", () => {
|
|
103
|
+
fileInput.click();
|
|
104
|
+
});
|
|
105
|
+
fileInput.addEventListener("change", e => {
|
|
106
|
+
this.handleFileUpload(e);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Update send button state based on input and contentBlocks
|
|
110
|
+
const updateSendButton = () => {
|
|
111
|
+
const hasText = input.value.trim().length > 0;
|
|
112
|
+
const hasFiles = this.contentBlocks.length > 0;
|
|
113
|
+
sendBtn.disabled = !(hasText || hasFiles);
|
|
114
|
+
};
|
|
115
|
+
input.addEventListener("input", () => {
|
|
116
|
+
updateSendButton();
|
|
117
|
+
});
|
|
118
|
+
const send = async () => {
|
|
119
|
+
const text = input.value.trim();
|
|
120
|
+
if (!text && this.contentBlocks.length === 0) return;
|
|
121
|
+
|
|
122
|
+
// Add user message to UI immediately
|
|
123
|
+
this.addMessage(text || "📎 File attachments", true);
|
|
124
|
+
input.value = "";
|
|
125
|
+
sendBtn.disabled = true;
|
|
126
|
+
this.hideTypingIndicator();
|
|
127
|
+
|
|
128
|
+
// Use the sendMessage function from widget if available
|
|
129
|
+
if (this.sendMessage) {
|
|
130
|
+
try {
|
|
131
|
+
await this.sendMessage(text, this.contentBlocks);
|
|
132
|
+
// Content blocks are cleared in widget's sendMessage, but ensure UI is updated
|
|
133
|
+
this.contentBlocks = [];
|
|
134
|
+
this.onContentBlocksChange(this.contentBlocks);
|
|
135
|
+
this.renderFilePreview(); // Update preview
|
|
136
|
+
this.updateSendButton();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error("Error sending message:", error);
|
|
139
|
+
this.addMessage("Sorry, I encountered an error. Please try again.", false);
|
|
140
|
+
} finally {
|
|
141
|
+
this.hideTypingIndicator();
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
// Fallback to old behavior
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
this.hideTypingIndicator();
|
|
147
|
+
this.handleUserMessage(text);
|
|
148
|
+
}, 1000 + Math.random() * 1000);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
sendBtn.addEventListener("click", send);
|
|
152
|
+
input.addEventListener("keypress", e => {
|
|
153
|
+
if (e.key === "Enter" && !sendBtn.disabled) send();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Focus input when screen loads
|
|
157
|
+
setTimeout(() => input.focus(), 100);
|
|
158
|
+
|
|
159
|
+
// Render file preview if contentBlocks exist
|
|
160
|
+
this.renderFilePreview();
|
|
161
|
+
|
|
162
|
+
// Sync existing messages on render
|
|
163
|
+
if (this.messages && this.messages.length > 0) {
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
this._syncMessages(this.messages);
|
|
166
|
+
}, 50);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Convert file to content block
|
|
171
|
+
fileToContentBlock(file) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const reader = new FileReader();
|
|
174
|
+
reader.onload = () => {
|
|
175
|
+
const base64Data = reader.result.split(",")[1]; // Remove data:mime;base64, prefix
|
|
176
|
+
|
|
177
|
+
if (this.SUPPORTED_FILE_TYPES.includes(file.type)) {
|
|
178
|
+
resolve({
|
|
179
|
+
type: "image",
|
|
180
|
+
source_type: "base64",
|
|
181
|
+
mime_type: file.type,
|
|
182
|
+
data: base64Data,
|
|
183
|
+
metadata: {
|
|
184
|
+
name: file.name,
|
|
185
|
+
size: file.size
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
reject(new Error("Unsupported file type: ".concat(file.type)));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
reader.onerror = () => {
|
|
193
|
+
reject(new Error("Failed to read file: ".concat(file.name)));
|
|
194
|
+
};
|
|
195
|
+
reader.readAsDataURL(file);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check if file is duplicate
|
|
200
|
+
isDuplicateFile(file, existingBlocks) {
|
|
201
|
+
if (this.SUPPORTED_FILE_TYPES.includes(file.type)) {
|
|
202
|
+
return existingBlocks.some(block => {
|
|
203
|
+
var _block$metadata;
|
|
204
|
+
return block.type === "image" && ((_block$metadata = block.metadata) === null || _block$metadata === void 0 ? void 0 : _block$metadata.name) === file.name && block.mime_type === file.type;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Format file size
|
|
211
|
+
formatFileSize(bytes) {
|
|
212
|
+
if (bytes === 0) return "0 Bytes";
|
|
213
|
+
const k = 1024;
|
|
214
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
215
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
216
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Get file type display name
|
|
220
|
+
getFileTypeDisplay(mimeType) {
|
|
221
|
+
switch (mimeType) {
|
|
222
|
+
case "image/jpeg":
|
|
223
|
+
case "image/jpg":
|
|
224
|
+
return "JPEG Image";
|
|
225
|
+
case "image/png":
|
|
226
|
+
return "PNG Image";
|
|
227
|
+
case "image/gif":
|
|
228
|
+
return "GIF Image";
|
|
229
|
+
case "image/webp":
|
|
230
|
+
return "WebP Image";
|
|
231
|
+
default:
|
|
232
|
+
return "Unknown File";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle file upload
|
|
237
|
+
async handleFileUpload(e) {
|
|
238
|
+
const files = e.target.files;
|
|
239
|
+
if (!files) return;
|
|
240
|
+
const fileArray = Array.from(files);
|
|
241
|
+
const validFiles = fileArray.filter(file => this.SUPPORTED_FILE_TYPES.includes(file.type));
|
|
242
|
+
const invalidFiles = fileArray.filter(file => !this.SUPPORTED_FILE_TYPES.includes(file.type));
|
|
243
|
+
if (invalidFiles.length > 0) {
|
|
244
|
+
console.warn("Invalid file type. Please upload a JPEG, PNG, GIF, or WEBP image.");
|
|
245
|
+
// Could show a toast/alert here
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Since we only allow 1 file, we take the first valid one and replace existing
|
|
249
|
+
const fileToProcess = validFiles[0];
|
|
250
|
+
if (fileToProcess) {
|
|
251
|
+
try {
|
|
252
|
+
const newBlocks = await this.fileToContentBlock(fileToProcess);
|
|
253
|
+
// Replace existing blocks with new one
|
|
254
|
+
this.contentBlocks = [newBlocks];
|
|
255
|
+
this.onContentBlocksChange(this.contentBlocks);
|
|
256
|
+
this.renderFilePreview();
|
|
257
|
+
this.updateSendButton();
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error("Failed to process files: ".concat(error.message));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Clear the input
|
|
264
|
+
e.target.value = "";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Remove content block
|
|
268
|
+
removeBlock(index) {
|
|
269
|
+
this.contentBlocks = this.contentBlocks.filter((_, i) => i !== index);
|
|
270
|
+
this.onContentBlocksChange(this.contentBlocks);
|
|
271
|
+
this.renderFilePreview();
|
|
272
|
+
this.updateSendButton();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Update send button state
|
|
276
|
+
updateSendButton() {
|
|
277
|
+
if (!this.container) return;
|
|
278
|
+
const input = this.container.querySelector("#chat-input");
|
|
279
|
+
const sendBtn = this.container.querySelector("#chat-send");
|
|
280
|
+
if (!input || !sendBtn) return;
|
|
281
|
+
const hasText = input.value.trim().length > 0;
|
|
282
|
+
const hasFiles = this.contentBlocks.length > 0;
|
|
283
|
+
sendBtn.disabled = !(hasText || hasFiles);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Render file preview
|
|
287
|
+
renderFilePreview() {
|
|
288
|
+
if (!this.container) return;
|
|
289
|
+
const container = this.container.querySelector("#file-attachments-container");
|
|
290
|
+
if (!container) return;
|
|
291
|
+
if (!this.contentBlocks || this.contentBlocks.length === 0) {
|
|
292
|
+
container.innerHTML = "";
|
|
293
|
+
container.style.display = "none";
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
container.style.display = "flex";
|
|
297
|
+
container.innerHTML = this.contentBlocks.map((block, index) => {
|
|
298
|
+
var _block$metadata2, _block$metadata3, _block$metadata4;
|
|
299
|
+
const displayName = ((_block$metadata2 = block.metadata) === null || _block$metadata2 === void 0 ? void 0 : _block$metadata2.filename) || ((_block$metadata3 = block.metadata) === null || _block$metadata3 === void 0 ? void 0 : _block$metadata3.name) || "file";
|
|
300
|
+
const size = this.formatFileSize(((_block$metadata4 = block.metadata) === null || _block$metadata4 === void 0 ? void 0 : _block$metadata4.size) || 0);
|
|
301
|
+
const isImage = block.type === "image";
|
|
302
|
+
const src = isImage ? "data:".concat(block.mime_type, ";base64,").concat(block.data) : null;
|
|
303
|
+
return "\n <div class=\"file-attachment ".concat(isImage ? "has-thumbnail" : "", "\" data-index=\"").concat(index, "\">\n ").concat(isImage ? "\n <div class=\"file-thumbnail\">\n <img src=\"".concat(src, "\" alt=\"").concat(displayName, "\" class=\"file-thumbnail-image\" />\n </div>\n ") : '<div class="file-attachment-icon">📄</div>', "\n <div class=\"file-attachment-info\">\n <div class=\"file-attachment-name\" title=\"").concat(displayName, "\">\n ").concat(displayName, "\n </div>\n <div class=\"file-attachment-size\">").concat(size, "</div>\n </div>\n <button\n class=\"file-attachment-remove\"\n data-index=\"").concat(index, "\"\n title=\"Remove file\"\n >\n \u2715\n </button>\n </div>\n ");
|
|
304
|
+
}).join("");
|
|
305
|
+
|
|
306
|
+
// Bind remove buttons
|
|
307
|
+
container.querySelectorAll(".file-attachment-remove").forEach(btn => {
|
|
308
|
+
btn.addEventListener("click", e => {
|
|
309
|
+
e.stopPropagation();
|
|
310
|
+
const index = parseInt(btn.getAttribute("data-index"));
|
|
311
|
+
this.removeBlock(index);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Bind image click for expansion (optional - can add modal later)
|
|
316
|
+
container.querySelectorAll(".file-thumbnail-image").forEach(img => {
|
|
317
|
+
img.addEventListener("click", e => {
|
|
318
|
+
e.stopPropagation();
|
|
319
|
+
// Could add image expansion modal here
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
_formatTime(date) {
|
|
324
|
+
let hours = date.getHours();
|
|
325
|
+
const minutes = date.getMinutes();
|
|
326
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
327
|
+
hours = hours % 12;
|
|
328
|
+
hours = hours ? hours : 12;
|
|
329
|
+
const strTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ' ' + ampm;
|
|
330
|
+
return strTime;
|
|
331
|
+
}
|
|
332
|
+
addMessage(text, isUser) {
|
|
333
|
+
let messageData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
334
|
+
console.log("Is user", isUser);
|
|
335
|
+
if (!this.container) return;
|
|
336
|
+
const messagesContainer = this.container.querySelector("#chat-messages");
|
|
337
|
+
if (!messagesContainer) return;
|
|
338
|
+
const welcome = messagesContainer.querySelector(".chat-welcome");
|
|
339
|
+
if (welcome) welcome.remove();
|
|
340
|
+
|
|
341
|
+
// Check if message already exists (for syncing)
|
|
342
|
+
const messageId = (messageData === null || messageData === void 0 ? void 0 : messageData.id) || Date.now();
|
|
343
|
+
console.log("Message ID", messageId);
|
|
344
|
+
const existingMessage = messagesContainer.querySelector("[data-message-id=\"".concat(messageId, "\"]"));
|
|
345
|
+
if (existingMessage && messageData) {
|
|
346
|
+
// Update existing message
|
|
347
|
+
const bubble = existingMessage.querySelector(".message-bubble");
|
|
348
|
+
if (bubble) {
|
|
349
|
+
bubble.innerHTML = this._escapeHtml(messageData.content || text);
|
|
350
|
+
}
|
|
351
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const messageEl = document.createElement("div");
|
|
355
|
+
messageEl.className = "chat-message ".concat(isUser ? "user" : "bot");
|
|
356
|
+
messageEl.setAttribute("data-message-id", messageId);
|
|
357
|
+
|
|
358
|
+
// Handle different message types
|
|
359
|
+
let content = text;
|
|
360
|
+
if (messageData) {
|
|
361
|
+
if (messageData.isStreaming) {
|
|
362
|
+
content = messageData.content || "Thinking...";
|
|
363
|
+
} else if (messageData.isError) {
|
|
364
|
+
content = messageData.content || "Error occurred";
|
|
365
|
+
messageEl.classList.add("error");
|
|
366
|
+
} else {
|
|
367
|
+
content = messageData.content || text;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Build attachments HTML if present
|
|
372
|
+
let attachmentsHTML = "";
|
|
373
|
+
if (messageData && messageData.attachments && messageData.attachments.length > 0) {
|
|
374
|
+
attachmentsHTML = "\n <div class=\"message-attachments\">\n ".concat(messageData.attachments.map((attachment, index) => {
|
|
375
|
+
var _attachment$metadata, _attachment$metadata2, _attachment$metadata3;
|
|
376
|
+
const displayName = ((_attachment$metadata = attachment.metadata) === null || _attachment$metadata === void 0 ? void 0 : _attachment$metadata.name) || ((_attachment$metadata2 = attachment.metadata) === null || _attachment$metadata2 === void 0 ? void 0 : _attachment$metadata2.filename) || "Unknown file";
|
|
377
|
+
const isImage = attachment.type === "image";
|
|
378
|
+
const imageSrc = isImage && attachment.data ? "data:".concat(attachment.mime_type, ";base64,").concat(attachment.data) : null;
|
|
379
|
+
return "\n <div class=\"message-attachment ".concat(isImage ? "image" : "pdf", "\" data-attachment-index=\"").concat(index, "\">\n <span class=\"message-attachment-icon\">\n ").concat(isImage ? "🖼️" : "📄", "\n </span>\n <div class=\"message-attachment-info\">\n <div \n class=\"message-attachment-name ").concat(isImage ? "clickable" : "", "\"\n ").concat(isImage && imageSrc ? "data-image-src=\"".concat(imageSrc, "\" data-image-alt=\"").concat(displayName, "\"") : "", "\n style=\"cursor: ").concat(isImage ? "pointer" : "default", ";\"\n >\n ").concat(this._escapeHtml(displayName), "\n </div>\n <div class=\"message-attachment-size\">\n ").concat(this.formatFileSize(((_attachment$metadata3 = attachment.metadata) === null || _attachment$metadata3 === void 0 ? void 0 : _attachment$metadata3.size) || 0), " \u2022 ").concat(this.getFileTypeDisplay(attachment.mime_type), "\n </div>\n </div>\n </div>\n ");
|
|
380
|
+
}).join(""), "\n </div>\n ");
|
|
381
|
+
}
|
|
382
|
+
const timestamp = messageData !== null && messageData !== void 0 && messageData.timestamp ? new Date(messageData.timestamp) : new Date();
|
|
383
|
+
const formattedTime = this._formatTime(timestamp);
|
|
384
|
+
messageEl.innerHTML = "\n <div class=\"message-content\">\n ".concat(attachmentsHTML, "\n <div class=\"message-bubble\">\n ").concat(this._escapeHtml(content), "\n </div>\n <div class=\"message-time\">").concat(formattedTime, "</div>\n </div>\n ");
|
|
385
|
+
|
|
386
|
+
// Bind image click handlers for expansion
|
|
387
|
+
if (messageData && messageData.attachments) {
|
|
388
|
+
messageEl.querySelectorAll(".message-attachment-name.clickable").forEach(el => {
|
|
389
|
+
el.addEventListener("click", e => {
|
|
390
|
+
e.stopPropagation();
|
|
391
|
+
const imageSrc = el.getAttribute("data-image-src");
|
|
392
|
+
const imageAlt = el.getAttribute("data-image-alt");
|
|
393
|
+
if (imageSrc) {
|
|
394
|
+
this.showExpandedImage(imageSrc, imageAlt);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
messagesContainer.appendChild(messageEl);
|
|
400
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Sync messages from shared array
|
|
404
|
+
_syncMessages(messages) {
|
|
405
|
+
if (!this.container) return;
|
|
406
|
+
const messagesContainer = this.container.querySelector("#chat-messages");
|
|
407
|
+
if (!messagesContainer) return;
|
|
408
|
+
|
|
409
|
+
// Remove welcome message if there are any messages
|
|
410
|
+
if (messages.length > 0) {
|
|
411
|
+
const welcome = messagesContainer.querySelector(".chat-welcome");
|
|
412
|
+
if (welcome) welcome.remove();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Get existing message elements
|
|
416
|
+
const existingMessages = messagesContainer.querySelectorAll("[data-message-id]");
|
|
417
|
+
const existingIds = new Set(Array.from(existingMessages).map(el => el.getAttribute("data-message-id")));
|
|
418
|
+
|
|
419
|
+
// Add or update messages
|
|
420
|
+
messages.forEach(msg => {
|
|
421
|
+
const msgId = String(msg.id);
|
|
422
|
+
if (existingIds.has(msgId)) {
|
|
423
|
+
// Update existing message
|
|
424
|
+
const existingEl = messagesContainer.querySelector("[data-message-id=\"".concat(msgId, "\"]"));
|
|
425
|
+
if (existingEl) {
|
|
426
|
+
const bubble = existingEl.querySelector(".message-bubble");
|
|
427
|
+
if (bubble) {
|
|
428
|
+
bubble.innerHTML = this._escapeHtml(msg.content || "");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Update attachments if they exist
|
|
432
|
+
const messageContent = existingEl.querySelector(".message-content");
|
|
433
|
+
if (messageContent && msg.attachments && msg.attachments.length > 0) {
|
|
434
|
+
let attachmentsHTML = "\n <div class=\"message-attachments\">\n ".concat(msg.attachments.map((attachment, index) => {
|
|
435
|
+
var _attachment$metadata4, _attachment$metadata5, _attachment$metadata6;
|
|
436
|
+
const displayName = ((_attachment$metadata4 = attachment.metadata) === null || _attachment$metadata4 === void 0 ? void 0 : _attachment$metadata4.name) || ((_attachment$metadata5 = attachment.metadata) === null || _attachment$metadata5 === void 0 ? void 0 : _attachment$metadata5.filename) || "Unknown file";
|
|
437
|
+
const isImage = attachment.type === "image";
|
|
438
|
+
const imageSrc = isImage && attachment.data ? "data:".concat(attachment.mime_type, ";base64,").concat(attachment.data) : null;
|
|
439
|
+
return "\n <div class=\"message-attachment ".concat(isImage ? "image" : "pdf", "\" data-attachment-index=\"").concat(index, "\">\n <span class=\"message-attachment-icon\">\n ").concat(isImage ? "🖼️" : "📄", "\n </span>\n <div class=\"message-attachment-info\">\n <div \n class=\"message-attachment-name ").concat(isImage ? "clickable" : "", "\"\n ").concat(isImage && imageSrc ? "data-image-src=\"".concat(imageSrc, "\" data-image-alt=\"").concat(displayName, "\"") : "", "\n style=\"cursor: ").concat(isImage ? "pointer" : "default", ";\"\n >\n ").concat(this._escapeHtml(displayName), "\n </div>\n <div class=\"message-attachment-size\">\n ").concat(this.formatFileSize(((_attachment$metadata6 = attachment.metadata) === null || _attachment$metadata6 === void 0 ? void 0 : _attachment$metadata6.size) || 0), " \u2022 ").concat(this.getFileTypeDisplay(attachment.mime_type), "\n </div>\n </div>\n </div>\n ");
|
|
440
|
+
}).join(""), "\n </div>\n ");
|
|
441
|
+
|
|
442
|
+
// Insert or update attachments
|
|
443
|
+
const existingAttachments = messageContent.querySelector(".message-attachments");
|
|
444
|
+
if (existingAttachments) {
|
|
445
|
+
existingAttachments.outerHTML = attachmentsHTML;
|
|
446
|
+
} else {
|
|
447
|
+
messageContent.insertAdjacentHTML("afterbegin", attachmentsHTML);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Re-bind image click handlers
|
|
451
|
+
messageContent.querySelectorAll(".message-attachment-name.clickable").forEach(el => {
|
|
452
|
+
// Remove existing listeners by cloning
|
|
453
|
+
const newEl = el.cloneNode(true);
|
|
454
|
+
el.parentNode.replaceChild(newEl, el);
|
|
455
|
+
newEl.addEventListener("click", e => {
|
|
456
|
+
e.stopPropagation();
|
|
457
|
+
const imageSrc = newEl.getAttribute("data-image-src");
|
|
458
|
+
const imageAlt = newEl.getAttribute("data-image-alt");
|
|
459
|
+
if (imageSrc) {
|
|
460
|
+
this.showExpandedImage(imageSrc, imageAlt);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Update classes based on message state
|
|
467
|
+
if (msg.isStreaming) {
|
|
468
|
+
existingEl.classList.add("streaming");
|
|
469
|
+
} else {
|
|
470
|
+
existingEl.classList.remove("streaming");
|
|
471
|
+
}
|
|
472
|
+
if (msg.isError) {
|
|
473
|
+
existingEl.classList.add("error");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// Add new message
|
|
478
|
+
this.addMessage(msg.content || "", msg.sender === "user", msg);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// Remove messages that are no longer in the array
|
|
483
|
+
existingMessages.forEach(el => {
|
|
484
|
+
const msgId = el.getAttribute("data-message-id");
|
|
485
|
+
const exists = messages.some(msg => String(msg.id) === msgId);
|
|
486
|
+
if (!exists) {
|
|
487
|
+
el.remove();
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
491
|
+
}
|
|
492
|
+
showTypingIndicator() {
|
|
493
|
+
if (!this.container) return;
|
|
494
|
+
const messagesContainer = this.container.querySelector("#chat-messages");
|
|
495
|
+
if (!messagesContainer) return;
|
|
496
|
+
const indicator = document.createElement("div");
|
|
497
|
+
indicator.className = "chat-message bot";
|
|
498
|
+
indicator.id = "typing-indicator";
|
|
499
|
+
indicator.innerHTML = "\n <div class=\"typing-indicator\">\n <div class=\"typing-dot\"></div>\n <div class=\"typing-dot\"></div>\n <div class=\"typing-dot\"></div>\n </div>\n ";
|
|
500
|
+
messagesContainer.appendChild(indicator);
|
|
501
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
502
|
+
}
|
|
503
|
+
hideTypingIndicator() {
|
|
504
|
+
if (!this.container) return;
|
|
505
|
+
const indicator = this.container.querySelector("#typing-indicator");
|
|
506
|
+
if (indicator) indicator.remove();
|
|
507
|
+
}
|
|
508
|
+
handleUserMessage(text) {
|
|
509
|
+
// Call the callback if provided, otherwise use default echo
|
|
510
|
+
if (this.onMessage) {
|
|
511
|
+
this.onMessage(text, response => {
|
|
512
|
+
this.addMessage(response, false);
|
|
513
|
+
});
|
|
514
|
+
} else {
|
|
515
|
+
// Default echo response
|
|
516
|
+
this.addMessage("You said: \"".concat(text, "\""), false);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
_escapeHtml(text) {
|
|
520
|
+
const div = document.createElement("div");
|
|
521
|
+
div.textContent = text;
|
|
522
|
+
return div.innerHTML;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Show expanded image modal
|
|
526
|
+
showExpandedImage(src, alt) {
|
|
527
|
+
// Remove existing modal if any
|
|
528
|
+
const existingModal = document.querySelector(".expanded-image-modal");
|
|
529
|
+
if (existingModal) {
|
|
530
|
+
existingModal.remove();
|
|
531
|
+
}
|
|
532
|
+
const modal = document.createElement("div");
|
|
533
|
+
modal.className = "expanded-image-modal";
|
|
534
|
+
modal.innerHTML = "\n <div class=\"expanded-image-container\">\n <button class=\"expanded-image-close\" title=\"Close\">\u2715</button>\n <img src=\"".concat(src, "\" alt=\"").concat(alt || "Image", "\" class=\"expanded-image\" />\n <div class=\"expanded-image-caption\">").concat(this._escapeHtml(alt || "Image preview"), "</div>\n </div>\n ");
|
|
535
|
+
|
|
536
|
+
// Close handlers
|
|
537
|
+
const closeModal = e => {
|
|
538
|
+
e === null || e === void 0 || e.stopPropagation();
|
|
539
|
+
modal.remove();
|
|
540
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
541
|
+
};
|
|
542
|
+
const handleClickOutside = e => {
|
|
543
|
+
if (!e.target.closest(".expanded-image-container")) {
|
|
544
|
+
closeModal(e);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
modal.querySelector(".expanded-image-close").addEventListener("click", closeModal);
|
|
548
|
+
modal.addEventListener("click", e => {
|
|
549
|
+
if (e.target === modal) {
|
|
550
|
+
closeModal(e);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
554
|
+
document.body.appendChild(modal);
|
|
555
|
+
}
|
|
556
|
+
_applyStyles() {
|
|
557
|
+
// Check if styles already applied
|
|
558
|
+
if (document.getElementById('text-chat-screen-styles')) return;
|
|
559
|
+
const style = document.createElement("style");
|
|
560
|
+
style.id = 'text-chat-screen-styles';
|
|
561
|
+
style.textContent = "\n .text-chat-screen {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n \n }\n\n .text-chat-screen .chat-header {\n background: transparent;\n color: ".concat(this.primaryColor, ";\n padding: 20px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .text-chat-screen .chat-header-content {\n display: flex;\n align-items: center;\n gap: 12px;\n flex: 1;\n }\n\n .text-chat-screen .chat-back {\n background: ").concat(this.primaryColor, ";\n border: none;\n color: white;\n cursor: pointer;\n padding: 8px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n margin-right: 8px;\n }\n\n \n\n .text-chat-screen .chat-avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .text-chat-screen .chat-header-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .text-chat-screen .chat-title {\n font-weight: 600;\n font-size: 20px;\n }\n\n .text-chat-screen .chat-status {\n font-size: 12px;\n opacity: 0.9;\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .text-chat-screen .chat-close {\n background: ").concat(this.primaryColor, ";\n border: none;\n color: white;\n cursor: pointer;\n padding: 8px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n }\n\n \n\n .text-chat-screen .chat-messages {\n flex: 1;\n padding: 20px;\n overflow-y: auto;\n background: linear-gradient(180deg, white 10%, #E1EFCC );\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n .text-chat-screen .chat-messages::-webkit-scrollbar {\n width: 6px;\n }\n\n .text-chat-screen .chat-messages::-webkit-scrollbar-track {\n background: transparent;\n }\n\n .text-chat-screen .chat-messages::-webkit-scrollbar-thumb {\n background: #cbd5e1;\n border-radius: 3px;\n }\n\n .text-chat-screen .chat-welcome {\n text-align: center;\n padding: 100px 20px;\n color: #64748b;\n \n }\n\n .text-chat-screen .chat-welcome .welcome-icon {\n font-size: 48px;\n margin-bottom: 12px;\n }\n\n .text-chat-screen .welcome-text {\n font-size: 15px;\n font-weight: 500;\n color: #475569;\n }\n\n .text-chat-screen .chat-message {\n display: flex;\n gap: 8px;\n animation: messageSlide 0.3s ease-out;\n }\n\n @keyframes messageSlide {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .text-chat-screen .chat-message.user {\n flex-direction: row-reverse;\n }\n\n .text-chat-screen .message-content {\n max-width: 75%;\n }\n\n .text-chat-screen .message-bubble {\n padding: 12px 16px;\n border-radius: 16px;\n font-size: 14px;\n line-height: 1.5;\n word-wrap: break-word;\n }\n\n .text-chat-screen .chat-message.user .message-bubble {\n background: #e9f5d7;\n color: ").concat(this.primaryColor, ";\n border-bottom-right-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n .text-chat-screen .chat-message.bot .message-bubble {\n background: white;\n color: #1e293b;\n border-bottom-left-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n .text-chat-screen .message-time {\n font-size: 10px;\n color: #94a3b8;\n margin-top: 4px;\n text-align: right;\n }\n\n .text-chat-screen .typing-indicator {\n display: flex;\n gap: 4px;\n padding: 12px 16px;\n background: white;\n border-radius: 16px;\n border-bottom-left-radius: 4px;\n max-width: 60px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n .text-chat-screen .typing-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #94a3b8;\n animation: typing 1.4s infinite;\n }\n\n .text-chat-screen .typing-dot:nth-child(2) {\n animation-delay: 0.2s;\n }\n\n .text-chat-screen .typing-dot:nth-child(3) {\n animation-delay: 0.4s;\n }\n\n @keyframes typing {\n 0%, 60%, 100% {\n transform: translateY(0);\n opacity: 0.7;\n }\n 30% {\n transform: translateY(-10px);\n opacity: 1;\n }\n }\n\n .text-chat-screen .file-attachments-container {\n padding: 8px 12px;\n background: white;\n border-top: 1px solid #e2e8f0;\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n max-height: 120px;\n overflow-y: auto;\n }\n\n .text-chat-screen .file-attachments-container::-webkit-scrollbar {\n width: 4px;\n height: 4px;\n }\n\n .text-chat-screen .file-attachments-container::-webkit-scrollbar-track {\n background: transparent;\n }\n\n .text-chat-screen .file-attachments-container::-webkit-scrollbar-thumb {\n background: #cbd5e1;\n border-radius: 2px;\n }\n\n .text-chat-screen .file-attachment {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 10px;\n background: #f1f5f9;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n font-size: 12px;\n max-width: 200px;\n position: relative;\n }\n\n .text-chat-screen .file-attachment.has-thumbnail {\n padding: 4px;\n }\n\n .text-chat-screen .file-thumbnail {\n width: 40px;\n height: 40px;\n border-radius: 6px;\n overflow: hidden;\n flex-shrink: 0;\n background: #e2e8f0;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .text-chat-screen .file-thumbnail-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n cursor: pointer;\n }\n\n .text-chat-screen .file-attachment-icon {\n font-size: 24px;\n flex-shrink: 0;\n }\n\n .text-chat-screen .file-attachment-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .text-chat-screen .file-attachment-name {\n font-weight: 500;\n color: #1e293b;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 120px;\n }\n\n .text-chat-screen .file-attachment-size {\n font-size: 11px;\n color: #64748b;\n }\n\n .text-chat-screen .file-attachment-remove {\n background: transparent;\n border: none;\n color: #64748b;\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n font-size: 16px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n flex-shrink: 0;\n }\n\n .text-chat-screen .file-attachment-remove:hover {\n background: #fee2e2;\n color: #dc2626;\n }\n\n .text-chat-screen .chat-input-wrapper {\n display: flex;\n padding: 10px;\n gap: 8px;\n background: white;\n border-top: 1px solid #e2e8f0;\n align-items: center;\n }\n\n .text-chat-screen .file-upload-controls {\n display: flex;\n align-items: center;\n }\n\n .text-chat-screen .file-upload-button {\n background: transparent;\n border: none;\n color: ").concat(this.primaryColor, ";\n cursor: pointer;\n padding: 8px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n }\n\n .text-chat-screen .file-upload-button:hover {\n background: rgba(26, 92, 75, 0.1);\n }\n\n .text-chat-screen .file-upload-button:active {\n transform: scale(0.95);\n }\n\n .text-chat-screen #chat-input {\n flex: 1;\n border: 2px solid #e2e8f0;\n border-radius: 12px;\n padding: 12px 16px;\n font-size: 14px;\n outline: none;\n transition: all 0.2s;\n font-family: inherit;\n }\n\n .text-chat-screen #chat-input:focus {\n border-color: ").concat(this.primaryColor, ";\n box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\n }\n\n .text-chat-screen #chat-send {\n background: ").concat(this.primaryColor, ";\n color: white;\n border: none;\n border-radius: 12px;\n padding: 12px 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n }\n\n .text-chat-screen #chat-send:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n\n .text-chat-screen #chat-send:not(:disabled):hover {\n transform: scale(1.05);\n box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);\n }\n\n .text-chat-screen #chat-send:not(:disabled):active {\n transform: scale(0.95);\n }\n\n /* Message Attachments Styles */\n .text-chat-screen .message-attachments {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-bottom: 8px;\n }\n\n .text-chat-screen .message-attachment {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: #f1f5f9;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n font-size: 12px;\n max-width: 100%;\n }\n\n .text-chat-screen .message-attachment.image {\n background: #f8fafc;\n }\n\n .text-chat-screen .message-attachment.pdf {\n background: #fef2f2;\n }\n\n .text-chat-screen .message-attachment-icon {\n font-size: 20px;\n flex-shrink: 0;\n }\n\n .text-chat-screen .message-attachment-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .text-chat-screen .message-attachment-name {\n font-weight: 500;\n color: #1e293b;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .text-chat-screen .message-attachment-name.clickable:hover {\n color: ").concat(this.primaryColor, ";\n text-decoration: underline;\n }\n\n .text-chat-screen .message-attachment-size {\n font-size: 11px;\n color: #64748b;\n }\n\n /* Expanded Image Modal Styles */\n .expanded-image-modal {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n animation: fadeIn 0.2s ease;\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .expanded-image-container {\n position: relative;\n max-width: 90%;\n max-height: 90vh;\n background: #fff;\n border-radius: 8px;\n overflow: hidden;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n animation: scaleIn 0.2s ease;\n display: flex;\n flex-direction: column;\n }\n\n @keyframes scaleIn {\n from {\n transform: scale(0.9);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: 1;\n }\n }\n\n .expanded-image-close {\n position: absolute;\n top: 12px;\n right: 12px;\n background: rgba(0, 0, 0, 0.6);\n border: none;\n color: white;\n width: 32px;\n height: 32px;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n z-index: 1;\n transition: all 0.2s;\n }\n\n .expanded-image-close:hover {\n background: rgba(0, 0, 0, 0.8);\n transform: scale(1.1);\n }\n\n .expanded-image {\n max-width: 100%;\n max-height: calc(90vh - 60px);\n object-fit: contain;\n display: block;\n }\n\n .expanded-image-caption {\n padding: 12px 16px;\n background: #fff;\n color: #1e293b;\n font-size: 14px;\n text-align: center;\n border-top: 1px solid #e2e8f0;\n }\n ");
|
|
562
|
+
document.head.appendChild(style);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
class BhashiniFrontend {
|
|
567
|
+
constructor() {
|
|
568
|
+
this.apiKey = '23588533c1-d990-4a7d-b052-d970c886147e';
|
|
569
|
+
this.userId = '749bd0b0c65e4d17a372be0ad92af981';
|
|
570
|
+
this.callbackUrl = 'https://meity-auth.ulcacontrib.org/ulca/apis/v0/model/getModelsPipeline';
|
|
571
|
+
this.inferenceUrl = null;
|
|
572
|
+
this.inferenceHeaders = {};
|
|
573
|
+
this.asrServiceId = {
|
|
574
|
+
'hi': 'ai4bharat/conformer-hi-gpu--t4',
|
|
575
|
+
'en': 'ai4bharat/whisper-medium-en--gpu--t4'
|
|
576
|
+
};
|
|
577
|
+
this.ttsServiceId = {
|
|
578
|
+
'hi': 'ai4bharat/indic-tts-coqui-indo_aryan-gpu--t4',
|
|
579
|
+
'en': 'ai4bharat/indic-tts-coqui-misc-gpu--t4'
|
|
580
|
+
};
|
|
581
|
+
this.translationServiceId = 'ai4bharat/indictrans-v2-all-gpu--t4';
|
|
582
|
+
}
|
|
583
|
+
async initialize() {
|
|
584
|
+
try {
|
|
585
|
+
const response = await fetch(this.callbackUrl, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: {
|
|
588
|
+
'Content-Type': 'application/json',
|
|
589
|
+
'userID': this.userId,
|
|
590
|
+
'ulcaApiKey': this.apiKey
|
|
591
|
+
},
|
|
592
|
+
body: JSON.stringify({
|
|
593
|
+
pipelineTasks: [{
|
|
594
|
+
taskType: 'asr'
|
|
595
|
+
}],
|
|
596
|
+
pipelineRequestConfig: {
|
|
597
|
+
pipelineId: '64392f96daac500b55c543cd'
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
});
|
|
601
|
+
const data = await response.json();
|
|
602
|
+
if (data.pipelineInferenceAPIEndPoint) {
|
|
603
|
+
this.inferenceUrl = data.pipelineInferenceAPIEndPoint.callbackUrl;
|
|
604
|
+
const keyName = data.pipelineInferenceAPIEndPoint.inferenceApiKey.name;
|
|
605
|
+
const keyValue = data.pipelineInferenceAPIEndPoint.inferenceApiKey.value;
|
|
606
|
+
this.inferenceHeaders = {
|
|
607
|
+
'Content-Type': 'application/json',
|
|
608
|
+
[keyName]: keyValue
|
|
609
|
+
};
|
|
610
|
+
} else {
|
|
611
|
+
// Fallback to direct endpoint
|
|
612
|
+
this.inferenceUrl = 'https://dhruva-api.bhashini.gov.in/services/inference/pipeline';
|
|
613
|
+
this.inferenceHeaders = {
|
|
614
|
+
'Content-Type': 'application/json',
|
|
615
|
+
'userID': this.userId,
|
|
616
|
+
'ulcaApiKey': this.apiKey
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error('Failed to initialize Bhashini:', error);
|
|
622
|
+
// Use fallback endpoint
|
|
623
|
+
this.inferenceUrl = 'https://dhruva-api.bhashini.gov.in/services/inference/pipeline';
|
|
624
|
+
this.inferenceHeaders = {
|
|
625
|
+
'Content-Type': 'application/json',
|
|
626
|
+
'userID': this.userId,
|
|
627
|
+
'ulcaApiKey': this.apiKey
|
|
628
|
+
};
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async convertToWav(audioBlob) {
|
|
633
|
+
return new Promise((resolve, reject) => {
|
|
634
|
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
635
|
+
const reader = new FileReader();
|
|
636
|
+
reader.onload = async e => {
|
|
637
|
+
try {
|
|
638
|
+
const arrayBuffer = e.target.result;
|
|
639
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
640
|
+
|
|
641
|
+
// Convert to mono 16kHz
|
|
642
|
+
const offlineContext = new OfflineAudioContext(1, audioBuffer.duration * 16000, 16000);
|
|
643
|
+
const source = offlineContext.createBufferSource();
|
|
644
|
+
source.buffer = audioBuffer;
|
|
645
|
+
source.connect(offlineContext.destination);
|
|
646
|
+
source.start();
|
|
647
|
+
const renderedBuffer = await offlineContext.startRendering();
|
|
648
|
+
|
|
649
|
+
// Convert to WAV
|
|
650
|
+
const wav = this.audioBufferToWav(renderedBuffer);
|
|
651
|
+
const wavBlob = new Blob([wav], {
|
|
652
|
+
type: 'audio/wav'
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Convert to base64
|
|
656
|
+
const wavReader = new FileReader();
|
|
657
|
+
wavReader.onloadend = () => {
|
|
658
|
+
const base64 = wavReader.result.split(',')[1];
|
|
659
|
+
resolve(base64);
|
|
660
|
+
};
|
|
661
|
+
wavReader.readAsDataURL(wavBlob);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
reject(error);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
reader.onerror = reject;
|
|
667
|
+
reader.readAsArrayBuffer(audioBlob);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
audioBufferToWav(buffer) {
|
|
671
|
+
const length = buffer.length * buffer.numberOfChannels * 2 + 44;
|
|
672
|
+
const arrayBuffer = new ArrayBuffer(length);
|
|
673
|
+
const view = new DataView(arrayBuffer);
|
|
674
|
+
const channels = [];
|
|
675
|
+
let offset = 0;
|
|
676
|
+
let pos = 0;
|
|
677
|
+
|
|
678
|
+
// Write WAV header
|
|
679
|
+
const setUint16 = data => {
|
|
680
|
+
view.setUint16(pos, data, true);
|
|
681
|
+
pos += 2;
|
|
682
|
+
};
|
|
683
|
+
const setUint32 = data => {
|
|
684
|
+
view.setUint32(pos, data, true);
|
|
685
|
+
pos += 4;
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// "RIFF" chunk descriptor
|
|
689
|
+
setUint32(0x46464952); // "RIFF"
|
|
690
|
+
setUint32(length - 8); // file length - 8
|
|
691
|
+
setUint32(0x45564157); // "WAVE"
|
|
692
|
+
|
|
693
|
+
// "fmt " sub-chunk
|
|
694
|
+
setUint32(0x20746d66); // "fmt "
|
|
695
|
+
setUint32(16); // SubChunk1Size = 16
|
|
696
|
+
setUint16(1); // AudioFormat = 1 (PCM)
|
|
697
|
+
setUint16(buffer.numberOfChannels);
|
|
698
|
+
setUint32(buffer.sampleRate);
|
|
699
|
+
setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // byte rate
|
|
700
|
+
setUint16(buffer.numberOfChannels * 2); // block align
|
|
701
|
+
setUint16(16); // bits per sample
|
|
702
|
+
|
|
703
|
+
// "data" sub-chunk
|
|
704
|
+
setUint32(0x61746164); // "data"
|
|
705
|
+
setUint32(length - pos - 4); // SubChunk2Size
|
|
706
|
+
|
|
707
|
+
// Write interleaved data
|
|
708
|
+
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
|
709
|
+
channels.push(buffer.getChannelData(i));
|
|
710
|
+
}
|
|
711
|
+
while (pos < length) {
|
|
712
|
+
for (let i = 0; i < buffer.numberOfChannels; i++) {
|
|
713
|
+
let sample = Math.max(-1, Math.min(1, channels[i][offset]));
|
|
714
|
+
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
|
|
715
|
+
view.setInt16(pos, sample, true);
|
|
716
|
+
pos += 2;
|
|
717
|
+
}
|
|
718
|
+
offset++;
|
|
719
|
+
}
|
|
720
|
+
return arrayBuffer;
|
|
721
|
+
}
|
|
722
|
+
async speechToText(base64Audio) {
|
|
723
|
+
let sourceLanguage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'auto';
|
|
724
|
+
let targetLanguage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'hi';
|
|
725
|
+
if (!this.inferenceUrl) {
|
|
726
|
+
await this.initialize();
|
|
727
|
+
}
|
|
728
|
+
const pipelineTasks = [{
|
|
729
|
+
taskType: 'asr',
|
|
730
|
+
config: {
|
|
731
|
+
language: {
|
|
732
|
+
sourceLanguage
|
|
733
|
+
},
|
|
734
|
+
serviceId: this.asrServiceId[sourceLanguage] || this.asrServiceId['hi'],
|
|
735
|
+
audioFormat: 'wav',
|
|
736
|
+
samplingRate: 16000
|
|
737
|
+
}
|
|
738
|
+
}];
|
|
739
|
+
|
|
740
|
+
// Only translate if languages differ
|
|
741
|
+
if (sourceLanguage !== targetLanguage) {
|
|
742
|
+
pipelineTasks.push({
|
|
743
|
+
taskType: 'translation',
|
|
744
|
+
config: {
|
|
745
|
+
sourceLanguage: sourceLanguage === 'auto' ? undefined : sourceLanguage,
|
|
746
|
+
targetLanguage
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const response = await fetch(this.inferenceUrl, {
|
|
751
|
+
method: 'POST',
|
|
752
|
+
headers: this.inferenceHeaders,
|
|
753
|
+
body: JSON.stringify({
|
|
754
|
+
pipelineTasks,
|
|
755
|
+
inputData: {
|
|
756
|
+
audio: [{
|
|
757
|
+
audioContent: base64Audio
|
|
758
|
+
}]
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
});
|
|
762
|
+
if (!response.ok) {
|
|
763
|
+
throw new Error("API error: ".concat(response.status, " ").concat(response.statusText));
|
|
764
|
+
}
|
|
765
|
+
const data = await response.json();
|
|
766
|
+
|
|
767
|
+
// If translated, return translation
|
|
768
|
+
if (pipelineTasks.length > 1) {
|
|
769
|
+
return data.pipelineResponse[1].output[0].target;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Otherwise return raw transcription
|
|
773
|
+
return data.pipelineResponse[0].output[0].source;
|
|
774
|
+
}
|
|
775
|
+
async textToSpeech(text) {
|
|
776
|
+
let language = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en';
|
|
777
|
+
let gender = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'female';
|
|
778
|
+
if (!this.inferenceUrl) {
|
|
779
|
+
await this.initialize();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ✅ Clean the text first
|
|
783
|
+
const cleanedText = text.replace(/!/g, ".");
|
|
784
|
+
const response = await fetch(this.inferenceUrl, {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
headers: this.inferenceHeaders,
|
|
787
|
+
body: JSON.stringify({
|
|
788
|
+
pipelineTasks: [{
|
|
789
|
+
taskType: "tts",
|
|
790
|
+
config: {
|
|
791
|
+
language: {
|
|
792
|
+
sourceLanguage: language
|
|
793
|
+
},
|
|
794
|
+
serviceId: this.ttsServiceId[language] || this.ttsServiceId["en"],
|
|
795
|
+
samplingRate: 8000,
|
|
796
|
+
gender: gender
|
|
797
|
+
}
|
|
798
|
+
}],
|
|
799
|
+
inputData: {
|
|
800
|
+
input: [{
|
|
801
|
+
source: cleanedText
|
|
802
|
+
}]
|
|
803
|
+
}
|
|
804
|
+
})
|
|
805
|
+
});
|
|
806
|
+
if (!response.ok) {
|
|
807
|
+
throw new Error("TTS API error: ".concat(response.status, " ").concat(response.statusText));
|
|
808
|
+
}
|
|
809
|
+
const data = await response.json();
|
|
810
|
+
return data.pipelineResponse[0].audio[0].audioContent;
|
|
811
|
+
}
|
|
812
|
+
async translateText(text, sourceLanguage, targetLanguage) {
|
|
813
|
+
if (!this.inferenceUrl) {
|
|
814
|
+
await this.initialize();
|
|
815
|
+
}
|
|
816
|
+
const response = await fetch(this.inferenceUrl, {
|
|
817
|
+
method: 'POST',
|
|
818
|
+
headers: this.inferenceHeaders,
|
|
819
|
+
body: JSON.stringify({
|
|
820
|
+
pipelineTasks: [{
|
|
821
|
+
taskType: 'translation',
|
|
822
|
+
config: {
|
|
823
|
+
language: {
|
|
824
|
+
sourceLanguage: sourceLanguage,
|
|
825
|
+
targetLanguage: targetLanguage
|
|
826
|
+
},
|
|
827
|
+
serviceId: this.translationServiceId
|
|
828
|
+
}
|
|
829
|
+
}],
|
|
830
|
+
inputData: {
|
|
831
|
+
input: [{
|
|
832
|
+
source: text
|
|
833
|
+
}]
|
|
834
|
+
}
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
if (!response.ok) {
|
|
838
|
+
throw new Error("Translation API error: ".concat(response.status, " ").concat(response.statusText));
|
|
839
|
+
}
|
|
840
|
+
const data = await response.json();
|
|
841
|
+
return data.pipelineResponse[0].output[0].target;
|
|
842
|
+
}
|
|
843
|
+
async speechToEnglish(base64Audio) {
|
|
844
|
+
let sourceLanguage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'hi';
|
|
845
|
+
// Step 1: Transcribe audio to source language text
|
|
846
|
+
const transcribedText = await this.speechToText(base64Audio, sourceLanguage);
|
|
847
|
+
|
|
848
|
+
// Step 2: If already English, return as is
|
|
849
|
+
if (sourceLanguage === 'en') {
|
|
850
|
+
return {
|
|
851
|
+
transcription: transcribedText,
|
|
852
|
+
translation: transcribedText
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Step 3: Translate to English
|
|
857
|
+
const translatedText = await this.translateText(transcribedText, sourceLanguage, 'en');
|
|
858
|
+
return {
|
|
859
|
+
transcription: transcribedText,
|
|
860
|
+
translation: translatedText
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
async speechToHindi(base64Audio) {
|
|
864
|
+
let sourceLanguage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'en';
|
|
865
|
+
// Step 1: Transcribe audio to source language text
|
|
866
|
+
const transcribedText = await this.speechToText(base64Audio, sourceLanguage);
|
|
867
|
+
|
|
868
|
+
// Step 2: If already Hindi, return as is
|
|
869
|
+
if (sourceLanguage === 'hi') {
|
|
870
|
+
return {
|
|
871
|
+
transcription: transcribedText,
|
|
872
|
+
translation: transcribedText
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Step 3: Translate to Hindi
|
|
877
|
+
const translatedText = await this.translateText(transcribedText, sourceLanguage, 'hi');
|
|
878
|
+
return {
|
|
879
|
+
transcription: transcribedText,
|
|
880
|
+
translation: translatedText
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
async transcribeSpeechToEnglish(base64Audio) {
|
|
884
|
+
let sourceLanguage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "hi";
|
|
885
|
+
// Step 1: Transcribe audio to source language text
|
|
886
|
+
const transcribedText = await this.speechToText(base64Audio, sourceLanguage);
|
|
887
|
+
|
|
888
|
+
// Step 2: If already English, return as is
|
|
889
|
+
if (sourceLanguage === "en") {
|
|
890
|
+
return {
|
|
891
|
+
transcription: transcribedText,
|
|
892
|
+
translation: transcribedText
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Step 3: Translate to English
|
|
897
|
+
const translatedText = await this.translateText(transcribedText, sourceLanguage, "en");
|
|
898
|
+
return {
|
|
899
|
+
transcription: transcribedText,
|
|
900
|
+
translation: translatedText
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// audio-chat-screen.js - Audio chat screen component
|
|
906
|
+
class AudioChatScreen {
|
|
907
|
+
constructor() {
|
|
908
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
909
|
+
this.primaryColor = options.primaryColor || "#1a5c4b";
|
|
910
|
+
this.title = options.title || "Chat Assistant";
|
|
911
|
+
this.onRecordStart = options.onRecordStart || (() => {});
|
|
912
|
+
this.onRecordStop = options.onRecordStop || (() => {});
|
|
913
|
+
this.onBack = options.onBack || (() => {});
|
|
914
|
+
this.onOpenDrawer = options.onOpenDrawer || (() => {});
|
|
915
|
+
this.onClose = options.onClose || (() => {});
|
|
916
|
+
this.navigateToTextScreen = options.navigateToTextScreen;
|
|
917
|
+
this.sendMessage = options.sendMessage || null;
|
|
918
|
+
this.selectedLanguage = options.selectedLanguage || "en";
|
|
919
|
+
this.container = null;
|
|
920
|
+
this.messages = options.messages || [];
|
|
921
|
+
|
|
922
|
+
// Bhashini Integration
|
|
923
|
+
this.bhashini = new BhashiniFrontend();
|
|
924
|
+
this.mediaRecorder = null;
|
|
925
|
+
this.audioChunks = [];
|
|
926
|
+
this.isRecording = false;
|
|
927
|
+
|
|
928
|
+
// TTS tracking
|
|
929
|
+
this.playedMessageIds = new Set();
|
|
930
|
+
this.audioQueue = [];
|
|
931
|
+
this.isPlaying = false;
|
|
932
|
+
|
|
933
|
+
// File attachments
|
|
934
|
+
this.contentBlocks = [];
|
|
935
|
+
this.SUPPORTED_FILE_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
936
|
+
}
|
|
937
|
+
render(container) {
|
|
938
|
+
this.container = container;
|
|
939
|
+
this._applyStyles();
|
|
940
|
+
container.innerHTML = "\n <div class=\"audio-chat-screen\">\n <div class=\"chat-header\">\n <div class=\"chat-header-content\">\n <button class=\"chat-back\" id=\"audio-chat-menu\">\n <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>\n </button>\n \n <div class=\"chat-header-text\">\n <div class=\"chat-title\">".concat(this.title, "</div>\n </div>\n </div>\n <button class=\"chat-close\" id=\"navigate-to-text-screen\">\n <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>\n </button>\n ​ ​ ​\n <button class=\"chat-close\" id=\"audio-chat-close\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n \n \n <div class=\"chat-messages\" id=\"chat-messages\">\n <div class=\"chat-welcome\">\n <div class=\"welcome-text\">\uD83D\uDC4B Hello! I'm your AI assistant. How can I help you today?</div>\n </div>\n </div> \n \n \n <div class=\"file-attachments-container\" id=\"file-attachments-container\" style=\"display: none;\"></div>\n <div class=\"bottom-inputs\">\n <button id=\"record-button\">\n <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>\n </button>\n ​ ​ ​​ ​ ​\n <input\n type=\"file\"\n id=\"file-upload-input\"\n accept=\"image/jpeg,image/png,image/gif,image/webp\"\n style=\"display: none;\"\n />\n <button id=\"attachment-button\">\n <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>\n </button>\n </div>\n \n \n </div>\n ");
|
|
941
|
+
|
|
942
|
+
// Bind header events
|
|
943
|
+
const menuBtn = container.querySelector("#audio-chat-menu");
|
|
944
|
+
const closeBtn = container.querySelector("#audio-chat-close");
|
|
945
|
+
const navigateToTextScreen = container.querySelector("#navigate-to-text-screen");
|
|
946
|
+
if (menuBtn) {
|
|
947
|
+
menuBtn.addEventListener("click", () => {
|
|
948
|
+
if (this.onOpenDrawer) {
|
|
949
|
+
this.onOpenDrawer();
|
|
950
|
+
} else if (this.onBack) {
|
|
951
|
+
this.onBack();
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (closeBtn) {
|
|
956
|
+
closeBtn.addEventListener("click", () => {
|
|
957
|
+
if (this.onClose) this.onClose();
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
if (navigateToTextScreen) {
|
|
961
|
+
navigateToTextScreen.addEventListener("click", () => {
|
|
962
|
+
if (this.navigateToTextScreen) this.navigateToTextScreen();
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Bind audio chat events
|
|
967
|
+
const recordBtn = container.querySelector("#record-button");
|
|
968
|
+
const attachmentBtn = container.querySelector("#attachment-button");
|
|
969
|
+
if (recordBtn) {
|
|
970
|
+
recordBtn.addEventListener("click", () => {
|
|
971
|
+
this.toggleRecording(recordBtn);
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
const fileInput = container.querySelector("#file-upload-input");
|
|
975
|
+
if (attachmentBtn && fileInput) {
|
|
976
|
+
attachmentBtn.addEventListener("click", () => {
|
|
977
|
+
fileInput.click();
|
|
978
|
+
});
|
|
979
|
+
fileInput.addEventListener("change", e => {
|
|
980
|
+
this.handleFileUpload(e);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Sync existing messages on render
|
|
985
|
+
if (this.messages && this.messages.length > 0) {
|
|
986
|
+
setTimeout(() => {
|
|
987
|
+
this._syncMessages(this.messages);
|
|
988
|
+
}, 50);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Convert file to content block
|
|
993
|
+
fileToContentBlock(file) {
|
|
994
|
+
return new Promise((resolve, reject) => {
|
|
995
|
+
const reader = new FileReader();
|
|
996
|
+
reader.onload = () => {
|
|
997
|
+
const base64Data = reader.result.split(",")[1];
|
|
998
|
+
if (this.SUPPORTED_FILE_TYPES.includes(file.type)) {
|
|
999
|
+
resolve({
|
|
1000
|
+
type: "image",
|
|
1001
|
+
source_type: "base64",
|
|
1002
|
+
mime_type: file.type,
|
|
1003
|
+
data: base64Data,
|
|
1004
|
+
metadata: {
|
|
1005
|
+
name: file.name,
|
|
1006
|
+
size: file.size
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
} else {
|
|
1010
|
+
reject(new Error("Unsupported file type: ".concat(file.type)));
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
reader.onerror = () => {
|
|
1014
|
+
reject(new Error("Failed to read file: ".concat(file.name)));
|
|
1015
|
+
};
|
|
1016
|
+
reader.readAsDataURL(file);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Handle file upload
|
|
1021
|
+
async handleFileUpload(e) {
|
|
1022
|
+
const files = e.target.files;
|
|
1023
|
+
if (!files) return;
|
|
1024
|
+
const fileArray = Array.from(files);
|
|
1025
|
+
const validFiles = fileArray.filter(file => this.SUPPORTED_FILE_TYPES.includes(file.type));
|
|
1026
|
+
if (validFiles.length === 0) {
|
|
1027
|
+
if (fileArray.length > 0) {
|
|
1028
|
+
console.warn("Invalid file type.");
|
|
1029
|
+
}
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Replace existing with the new one
|
|
1034
|
+
const file = validFiles[0];
|
|
1035
|
+
try {
|
|
1036
|
+
const block = await this.fileToContentBlock(file);
|
|
1037
|
+
this.contentBlocks = [block];
|
|
1038
|
+
this.renderFilePreview();
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
console.error("Error processing file:", error);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Clear input
|
|
1044
|
+
e.target.value = "";
|
|
1045
|
+
}
|
|
1046
|
+
removeBlock(index) {
|
|
1047
|
+
this.contentBlocks = []; // Since we only have max 1, removing index 0 means clearing all
|
|
1048
|
+
this.renderFilePreview();
|
|
1049
|
+
}
|
|
1050
|
+
renderFilePreview() {
|
|
1051
|
+
if (!this.container) return;
|
|
1052
|
+
const container = this.container.querySelector("#file-attachments-container");
|
|
1053
|
+
if (!container) return;
|
|
1054
|
+
if (!this.contentBlocks || this.contentBlocks.length === 0) {
|
|
1055
|
+
container.innerHTML = "";
|
|
1056
|
+
container.style.display = "none";
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
container.style.display = "flex";
|
|
1060
|
+
container.innerHTML = this.contentBlocks.map((block, index) => {
|
|
1061
|
+
var _block$metadata, _block$metadata2, _block$metadata3;
|
|
1062
|
+
const displayName = ((_block$metadata = block.metadata) === null || _block$metadata === void 0 ? void 0 : _block$metadata.filename) || ((_block$metadata2 = block.metadata) === null || _block$metadata2 === void 0 ? void 0 : _block$metadata2.name) || "file";
|
|
1063
|
+
const size = this.formatFileSize(((_block$metadata3 = block.metadata) === null || _block$metadata3 === void 0 ? void 0 : _block$metadata3.size) || 0);
|
|
1064
|
+
const isImage = block.type === "image";
|
|
1065
|
+
const src = isImage ? "data:".concat(block.mime_type, ";base64,").concat(block.data) : null;
|
|
1066
|
+
return "\n <div class=\"file-attachment ".concat(isImage ? "has-thumbnail" : "", "\" data-index=\"").concat(index, "\">\n <div class=\"file-thumbnail\">\n <img src=\"").concat(src, "\" alt=\"").concat(displayName, "\" class=\"file-thumbnail-image\" />\n </div>\n <div class=\"file-attachment-info\">\n <div class=\"file-attachment-name\" title=\"").concat(displayName, "\">\n ").concat(displayName, "\n </div>\n <div class=\"file-attachment-size\">").concat(size, "</div>\n </div>\n <button\n class=\"file-attachment-remove\"\n data-index=\"").concat(index, "\"\n title=\"Remove file\"\n >\n \u2715\n </button>\n </div>\n ");
|
|
1067
|
+
}).join("");
|
|
1068
|
+
|
|
1069
|
+
// Bind remove buttons
|
|
1070
|
+
container.querySelectorAll(".file-attachment-remove").forEach(btn => {
|
|
1071
|
+
btn.addEventListener("click", e => {
|
|
1072
|
+
e.stopPropagation();
|
|
1073
|
+
const index = parseInt(btn.getAttribute("data-index"));
|
|
1074
|
+
this.removeBlock(index);
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async toggleRecording(btn) {
|
|
1079
|
+
if (this.isRecording) {
|
|
1080
|
+
this.stopRecording(btn);
|
|
1081
|
+
} else {
|
|
1082
|
+
await this.startRecording(btn);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async startRecording(btn) {
|
|
1086
|
+
try {
|
|
1087
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1088
|
+
audio: true
|
|
1089
|
+
});
|
|
1090
|
+
this.mediaRecorder = new MediaRecorder(stream);
|
|
1091
|
+
this.audioChunks = [];
|
|
1092
|
+
this.mediaRecorder.ondataavailable = event => {
|
|
1093
|
+
this.audioChunks.push(event.data);
|
|
1094
|
+
};
|
|
1095
|
+
this.mediaRecorder.onstop = async () => {
|
|
1096
|
+
const audioBlob = new Blob(this.audioChunks, {
|
|
1097
|
+
type: "audio/wav"
|
|
1098
|
+
});
|
|
1099
|
+
this.audioChunks = [];
|
|
1100
|
+
|
|
1101
|
+
// Stop all tracks
|
|
1102
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1103
|
+
|
|
1104
|
+
// Process audio
|
|
1105
|
+
await this.processAudioInput(audioBlob);
|
|
1106
|
+
};
|
|
1107
|
+
this.mediaRecorder.start();
|
|
1108
|
+
this.isRecording = true;
|
|
1109
|
+
this.onRecordStart();
|
|
1110
|
+
|
|
1111
|
+
// Update UI
|
|
1112
|
+
btn.classList.add("recording");
|
|
1113
|
+
btn.innerHTML = "\n <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>\n ";
|
|
1114
|
+
|
|
1115
|
+
// Stop current TTS if any
|
|
1116
|
+
this.stopTTS();
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.error("Error accessing microphone:", error);
|
|
1119
|
+
alert("Could not access microphone. Please check permissions.");
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
stopRecording(btn) {
|
|
1123
|
+
if (this.mediaRecorder && this.isRecording) {
|
|
1124
|
+
this.mediaRecorder.stop();
|
|
1125
|
+
this.isRecording = false;
|
|
1126
|
+
this.onRecordStop();
|
|
1127
|
+
|
|
1128
|
+
// Update UI
|
|
1129
|
+
btn.classList.remove("recording");
|
|
1130
|
+
btn.innerHTML = "\n <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>\n ";
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async processAudioInput(audioBlob) {
|
|
1134
|
+
try {
|
|
1135
|
+
// Show processing state?
|
|
1136
|
+
|
|
1137
|
+
// 1. Convert to WAV Base64 (using Bhashini helper)
|
|
1138
|
+
const base64Audio = await this.bhashini.convertToWav(audioBlob);
|
|
1139
|
+
|
|
1140
|
+
// 2. STT
|
|
1141
|
+
const text = await this.bhashini.speechToText(base64Audio, this.selectedLanguage, this.selectedLanguage);
|
|
1142
|
+
if (text && text.trim()) {
|
|
1143
|
+
// 3. Send Message
|
|
1144
|
+
if (this.sendMessage) {
|
|
1145
|
+
await this.sendMessage(text, this.contentBlocks);
|
|
1146
|
+
// Clear attachments after sending
|
|
1147
|
+
this.contentBlocks = [];
|
|
1148
|
+
this.renderFilePreview(); // Update UI to remove preview
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
console.error("Error processing audio:", error);
|
|
1153
|
+
this.addMessage("Sorry, I couldn't understand that.", false, {
|
|
1154
|
+
isError: true
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// 9113770648
|
|
1159
|
+
stopTTS() {
|
|
1160
|
+
// Cancel any active speech synthesis (if using browser API) or stop audio element
|
|
1161
|
+
if (this.currentAudio) {
|
|
1162
|
+
this.currentAudio.pause();
|
|
1163
|
+
this.currentAudio = null;
|
|
1164
|
+
}
|
|
1165
|
+
this.audioQueue = [];
|
|
1166
|
+
this.isPlaying = false;
|
|
1167
|
+
}
|
|
1168
|
+
async playNextInQueue() {
|
|
1169
|
+
// Check if audio screen is active
|
|
1170
|
+
const isVisible = this.container && this.container.querySelector(".audio-chat-screen");
|
|
1171
|
+
if (!isVisible) return;
|
|
1172
|
+
console.log("Playing next in queue", this.audioQueue);
|
|
1173
|
+
if (this.isPlaying || this.audioQueue.length === 0) return;
|
|
1174
|
+
this.isPlaying = true;
|
|
1175
|
+
const text = this.audioQueue.shift();
|
|
1176
|
+
try {
|
|
1177
|
+
const audioContent = await this.bhashini.textToSpeech(text, this.selectedLanguage);
|
|
1178
|
+
if (audioContent) {
|
|
1179
|
+
const audioSrc = "data:audio/wav;base64,".concat(audioContent);
|
|
1180
|
+
this.currentAudio = new Audio(audioSrc);
|
|
1181
|
+
this.currentAudio.onended = () => {
|
|
1182
|
+
this.isPlaying = false;
|
|
1183
|
+
this.playNextInQueue();
|
|
1184
|
+
};
|
|
1185
|
+
this.currentAudio.onerror = () => {
|
|
1186
|
+
this.isPlaying = false;
|
|
1187
|
+
this.playNextInQueue();
|
|
1188
|
+
};
|
|
1189
|
+
await this.currentAudio.play();
|
|
1190
|
+
} else {
|
|
1191
|
+
this.isPlaying = false;
|
|
1192
|
+
this.playNextInQueue();
|
|
1193
|
+
}
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
console.error("TTS Error:", error);
|
|
1196
|
+
this.isPlaying = false;
|
|
1197
|
+
this.playNextInQueue();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Format file size (mirrors TextChatScreen)
|
|
1202
|
+
formatFileSize(bytes) {
|
|
1203
|
+
if (bytes === 0) return "0 Bytes";
|
|
1204
|
+
const k = 1024;
|
|
1205
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
1206
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1207
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Get file type display name (mirrors TextChatScreen)
|
|
1211
|
+
getFileTypeDisplay(mimeType) {
|
|
1212
|
+
switch (mimeType) {
|
|
1213
|
+
case "image/jpeg":
|
|
1214
|
+
case "image/jpg":
|
|
1215
|
+
return "JPEG Image";
|
|
1216
|
+
case "image/png":
|
|
1217
|
+
return "PNG Image";
|
|
1218
|
+
case "image/gif":
|
|
1219
|
+
return "GIF Image";
|
|
1220
|
+
case "image/webp":
|
|
1221
|
+
return "WebP Image";
|
|
1222
|
+
case "application/pdf":
|
|
1223
|
+
return "PDF Document";
|
|
1224
|
+
default:
|
|
1225
|
+
return "Unknown File";
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
_formatTime(date) {
|
|
1229
|
+
let hours = date.getHours();
|
|
1230
|
+
const minutes = date.getMinutes();
|
|
1231
|
+
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
1232
|
+
hours = hours % 12;
|
|
1233
|
+
hours = hours ? hours : 12;
|
|
1234
|
+
const strTime = hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0') + ' ' + ampm;
|
|
1235
|
+
return strTime;
|
|
1236
|
+
}
|
|
1237
|
+
addMessage(text, isUser) {
|
|
1238
|
+
let messageData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
|
|
1239
|
+
if (!this.container) return;
|
|
1240
|
+
const messagesContainer = this.container.querySelector("#chat-messages");
|
|
1241
|
+
if (!messagesContainer) return;
|
|
1242
|
+
const welcome = messagesContainer.querySelector(".chat-welcome");
|
|
1243
|
+
if (welcome) welcome.remove();
|
|
1244
|
+
|
|
1245
|
+
// Check if message already exists (for syncing)
|
|
1246
|
+
const messageId = (messageData === null || messageData === void 0 ? void 0 : messageData.id) || Date.now();
|
|
1247
|
+
const existingMessage = messagesContainer.querySelector("[data-message-id=\"".concat(messageId, "\"]"));
|
|
1248
|
+
if (existingMessage && messageData) {
|
|
1249
|
+
// Update existing message
|
|
1250
|
+
const bubble = existingMessage.querySelector(".message-bubble");
|
|
1251
|
+
if (bubble) {
|
|
1252
|
+
bubble.innerHTML = this._escapeHtml(messageData.content || text);
|
|
1253
|
+
}
|
|
1254
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const messageEl = document.createElement("div");
|
|
1258
|
+
messageEl.className = "chat-message ".concat(isUser ? "user" : "bot");
|
|
1259
|
+
messageEl.setAttribute("data-message-id", messageId);
|
|
1260
|
+
|
|
1261
|
+
// Handle different message types
|
|
1262
|
+
let content = text;
|
|
1263
|
+
if (messageData) {
|
|
1264
|
+
if (messageData.isStreaming) {
|
|
1265
|
+
content = messageData.content || "Thinking...";
|
|
1266
|
+
} else if (messageData.isError) {
|
|
1267
|
+
content = messageData.content || "Error occurred";
|
|
1268
|
+
messageEl.classList.add("error");
|
|
1269
|
+
} else {
|
|
1270
|
+
content = messageData.content || text;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Build attachments HTML if present
|
|
1275
|
+
let attachmentsHTML = "";
|
|
1276
|
+
if (messageData && messageData.attachments && messageData.attachments.length > 0) {
|
|
1277
|
+
attachmentsHTML = "\n <div class=\"message-attachments\">\n ".concat(messageData.attachments.map((attachment, index) => {
|
|
1278
|
+
var _attachment$metadata, _attachment$metadata2, _attachment$metadata3;
|
|
1279
|
+
const displayName = ((_attachment$metadata = attachment.metadata) === null || _attachment$metadata === void 0 ? void 0 : _attachment$metadata.name) || ((_attachment$metadata2 = attachment.metadata) === null || _attachment$metadata2 === void 0 ? void 0 : _attachment$metadata2.filename) || "Unknown file";
|
|
1280
|
+
const isImage = attachment.type === "image";
|
|
1281
|
+
const imageSrc = isImage && attachment.data ? "data:".concat(attachment.mime_type, ";base64,").concat(attachment.data) : null;
|
|
1282
|
+
return "\n <div class=\"message-attachment ".concat(isImage ? "image" : "pdf", "\" data-attachment-index=\"").concat(index, "\">\n <span class=\"message-attachment-icon\">\n ").concat(isImage ? "🖼️" : "📄", "\n </span>\n <div class=\"message-attachment-info\">\n <div \n class=\"message-attachment-name ").concat(isImage ? "clickable" : "", "\"\n ").concat(isImage && imageSrc ? "data-image-src=\"".concat(imageSrc, "\" data-image-alt=\"").concat(displayName, "\"") : "", "\n style=\"cursor: ").concat(isImage ? "pointer" : "default", ";\"\n >\n ").concat(this._escapeHtml(displayName), "\n </div>\n <div class=\"message-attachment-size\">\n ").concat(this.formatFileSize(((_attachment$metadata3 = attachment.metadata) === null || _attachment$metadata3 === void 0 ? void 0 : _attachment$metadata3.size) || 0), " \u2022 ").concat(this.getFileTypeDisplay(attachment.mime_type), "\n </div>\n </div>\n </div>\n ");
|
|
1283
|
+
}).join(""), "\n </div>\n ");
|
|
1284
|
+
}
|
|
1285
|
+
const timestamp = messageData !== null && messageData !== void 0 && messageData.timestamp ? new Date(messageData.timestamp) : new Date();
|
|
1286
|
+
const formattedTime = this._formatTime(timestamp);
|
|
1287
|
+
messageEl.innerHTML = "\n <div class=\"message-content\">\n ".concat(attachmentsHTML, "\n <div class=\"message-bubble\">").concat(this._escapeHtml(content), "</div>\n <div class=\"message-time\">").concat(formattedTime, "</div>\n </div>\n ");
|
|
1288
|
+
|
|
1289
|
+
// Bind image click handlers for expansion
|
|
1290
|
+
if (messageData && messageData.attachments) {
|
|
1291
|
+
messageEl.querySelectorAll(".message-attachment-name.clickable").forEach(el => {
|
|
1292
|
+
el.addEventListener("click", e => {
|
|
1293
|
+
e.stopPropagation();
|
|
1294
|
+
const imageSrc = el.getAttribute("data-image-src");
|
|
1295
|
+
const imageAlt = el.getAttribute("data-image-alt");
|
|
1296
|
+
if (imageSrc) {
|
|
1297
|
+
this.showExpandedImage(imageSrc, imageAlt);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
messagesContainer.appendChild(messageEl);
|
|
1303
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Sync messages from shared array, using same DOM structure as TextChatScreen
|
|
1307
|
+
_syncMessages(messages) {
|
|
1308
|
+
if (!this.container) return;
|
|
1309
|
+
const messagesContainer = this.container.querySelector("#chat-messages");
|
|
1310
|
+
if (!messagesContainer) return;
|
|
1311
|
+
|
|
1312
|
+
// Remove welcome message if there are any messages
|
|
1313
|
+
if (messages.length > 0) {
|
|
1314
|
+
const welcome = messagesContainer.querySelector(".chat-welcome");
|
|
1315
|
+
if (welcome) welcome.remove();
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Get existing message elements
|
|
1319
|
+
const existingMessages = messagesContainer.querySelectorAll("[data-message-id]");
|
|
1320
|
+
const existingIds = new Set(Array.from(existingMessages).map(el => el.getAttribute("data-message-id")));
|
|
1321
|
+
|
|
1322
|
+
// Add or update messages
|
|
1323
|
+
messages.forEach(msg => {
|
|
1324
|
+
const msgId = String(msg.id);
|
|
1325
|
+
|
|
1326
|
+
// Handle TTS for new bot messages
|
|
1327
|
+
if (msg.sender !== "user" && !this.playedMessageIds.has(msgId) && !msg.isStreaming && !msg.isProcessing && msg.content) {
|
|
1328
|
+
this.playedMessageIds.add(msgId);
|
|
1329
|
+
this.audioQueue.push(msg.content);
|
|
1330
|
+
this.playNextInQueue();
|
|
1331
|
+
}
|
|
1332
|
+
if (existingIds.has(msgId)) {
|
|
1333
|
+
// Update existing message
|
|
1334
|
+
const existingEl = messagesContainer.querySelector("[data-message-id=\"".concat(msgId, "\"]"));
|
|
1335
|
+
if (existingEl) {
|
|
1336
|
+
const bubble = existingEl.querySelector(".message-bubble");
|
|
1337
|
+
if (bubble) {
|
|
1338
|
+
bubble.innerHTML = this._escapeHtml(msg.content || "");
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// Update attachments if they exist
|
|
1342
|
+
const messageContent = existingEl.querySelector(".message-content");
|
|
1343
|
+
if (messageContent && msg.attachments && msg.attachments.length > 0) {
|
|
1344
|
+
let attachmentsHTML = "\n <div class=\"message-attachments\">\n ".concat(msg.attachments.map((attachment, index) => {
|
|
1345
|
+
var _attachment$metadata4, _attachment$metadata5, _attachment$metadata6;
|
|
1346
|
+
const displayName = ((_attachment$metadata4 = attachment.metadata) === null || _attachment$metadata4 === void 0 ? void 0 : _attachment$metadata4.name) || ((_attachment$metadata5 = attachment.metadata) === null || _attachment$metadata5 === void 0 ? void 0 : _attachment$metadata5.filename) || "Unknown file";
|
|
1347
|
+
const isImage = attachment.type === "image";
|
|
1348
|
+
const imageSrc = isImage && attachment.data ? "data:".concat(attachment.mime_type, ";base64,").concat(attachment.data) : null;
|
|
1349
|
+
return "\n <div class=\"message-attachment ".concat(isImage ? "image" : "pdf", "\" data-attachment-index=\"").concat(index, "\">\n <span class=\"message-attachment-icon\">\n ").concat(isImage ? "🖼️" : "📄", "\n </span>\n <div class=\"message-attachment-info\">\n <div \n class=\"message-attachment-name ").concat(isImage ? "clickable" : "", "\"\n ").concat(isImage && imageSrc ? "data-image-src=\"".concat(imageSrc, "\" data-image-alt=\"").concat(displayName, "\"") : "", "\n style=\"cursor: ").concat(isImage ? "pointer" : "default", ";\"\n >\n ").concat(this._escapeHtml(displayName), "\n </div>\n <div class=\"message-attachment-size\">\n ").concat(this.formatFileSize(((_attachment$metadata6 = attachment.metadata) === null || _attachment$metadata6 === void 0 ? void 0 : _attachment$metadata6.size) || 0), " \u2022 ").concat(this.getFileTypeDisplay(attachment.mime_type), "\n </div>\n </div>\n </div>\n ");
|
|
1350
|
+
}).join(""), "\n </div>\n ");
|
|
1351
|
+
|
|
1352
|
+
// Insert or update attachments
|
|
1353
|
+
const existingAttachments = messageContent.querySelector(".message-attachments");
|
|
1354
|
+
if (existingAttachments) {
|
|
1355
|
+
existingAttachments.outerHTML = attachmentsHTML;
|
|
1356
|
+
} else {
|
|
1357
|
+
messageContent.insertAdjacentHTML("afterbegin", attachmentsHTML);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Re-bind image click handlers
|
|
1361
|
+
messageContent.querySelectorAll(".message-attachment-name.clickable").forEach(el => {
|
|
1362
|
+
// Remove existing listeners by cloning
|
|
1363
|
+
const newEl = el.cloneNode(true);
|
|
1364
|
+
el.parentNode.replaceChild(newEl, el);
|
|
1365
|
+
newEl.addEventListener("click", e => {
|
|
1366
|
+
e.stopPropagation();
|
|
1367
|
+
const imageSrc = newEl.getAttribute("data-image-src");
|
|
1368
|
+
const imageAlt = newEl.getAttribute("data-image-alt");
|
|
1369
|
+
if (imageSrc) {
|
|
1370
|
+
this.showExpandedImage(imageSrc, imageAlt);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Update classes based on message state
|
|
1377
|
+
if (msg.isStreaming) {
|
|
1378
|
+
existingEl.classList.add("streaming");
|
|
1379
|
+
} else {
|
|
1380
|
+
existingEl.classList.remove("streaming");
|
|
1381
|
+
}
|
|
1382
|
+
if (msg.isError) {
|
|
1383
|
+
existingEl.classList.add("error");
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
} else {
|
|
1387
|
+
// Add new message
|
|
1388
|
+
this.addMessage(msg.content || "", msg.sender === "user", msg);
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// Remove messages that are no longer in the array
|
|
1393
|
+
existingMessages.forEach(el => {
|
|
1394
|
+
const msgId = el.getAttribute("data-message-id");
|
|
1395
|
+
const exists = messages.some(msg => String(msg.id) === msgId);
|
|
1396
|
+
if (!exists) {
|
|
1397
|
+
el.remove();
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
1401
|
+
}
|
|
1402
|
+
_escapeHtml(text) {
|
|
1403
|
+
const div = document.createElement("div");
|
|
1404
|
+
div.textContent = text;
|
|
1405
|
+
return div.innerHTML;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Show expanded image modal (mirrors TextChatScreen)
|
|
1409
|
+
showExpandedImage(src, alt) {
|
|
1410
|
+
const existingModal = document.querySelector(".expanded-image-modal");
|
|
1411
|
+
if (existingModal) {
|
|
1412
|
+
existingModal.remove();
|
|
1413
|
+
}
|
|
1414
|
+
const modal = document.createElement("div");
|
|
1415
|
+
modal.className = "expanded-image-modal";
|
|
1416
|
+
modal.innerHTML = "\n <div class=\"expanded-image-container\">\n <button class=\"expanded-image-close\" title=\"Close\">\u2715</button>\n <img src=\"".concat(src, "\" alt=\"").concat(alt || "Image", "\" class=\"expanded-image\" />\n <div class=\"expanded-image-caption\">").concat(this._escapeHtml(alt || "Image preview"), "</div>\n </div>\n ");
|
|
1417
|
+
const closeModal = e => {
|
|
1418
|
+
if (e) e.stopPropagation();
|
|
1419
|
+
modal.remove();
|
|
1420
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
1421
|
+
};
|
|
1422
|
+
const handleClickOutside = e => {
|
|
1423
|
+
if (!e.target.closest(".expanded-image-container")) {
|
|
1424
|
+
closeModal(e);
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
modal.querySelector(".expanded-image-close").addEventListener("click", closeModal);
|
|
1428
|
+
modal.addEventListener("click", e => {
|
|
1429
|
+
if (e.target === modal) {
|
|
1430
|
+
closeModal(e);
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
1434
|
+
document.body.appendChild(modal);
|
|
1435
|
+
}
|
|
1436
|
+
_applyStyles() {
|
|
1437
|
+
// Check if styles already applied
|
|
1438
|
+
if (document.getElementById('audio-chat-screen-styles')) return;
|
|
1439
|
+
const style = document.createElement("style");
|
|
1440
|
+
style.id = 'audio-chat-screen-styles';
|
|
1441
|
+
style.textContent = "\n .audio-chat-screen {\n flex: 1;\n height: 100%;\n display: flex;\n flex-direction: column;\n position: relative;\n overflow: hidden;\n background: linear-gradient(180deg, white 10%, #E1EFCC );\n }\n\n .audio-chat-screen .chat-header {\n color: ".concat(this.primaryColor, ";\n padding: 20px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-shrink: 0;\n background: transparent;\n z-index: 20;\n }\n\n .audio-chat-screen .chat-header-content {\n display: flex;\n align-items: center;\n gap: 12px;\n flex: 1;\n }\n\n .audio-chat-screen .chat-back {\n background: ").concat(this.primaryColor, ";\n border: none;\n color: white;\n cursor: pointer;\n padding: 8px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n margin-right: 8px;\n }\n\n .bottom-inputs {\n width: 100%;\n padding: 20px;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 5px;\n z-index: 10;\n flex-shrink: 0;\n }\n\n .bottom-inputs button {\n border: none;\n border-radius: 9999px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.2s ease;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n\n #record-button {\n width: 50px;\n height: 50px;\n background: ").concat(this.primaryColor, ";\n color: #ffffff;\n }\n\n #record-button.recording {\n background: #ef4444;\n animation: pulse-ring 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;\n }\n\n @keyframes pulse-ring {\n 0% {\n box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7);\n }\n 70% {\n box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);\n }\n 100% {\n box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);\n }\n }\n\n #record-button:hover {\n transform: translateY(-2px) scale(1.03);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);\n }\n\n #record-button:active {\n transform: translateY(0) scale(0.97);\n box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);\n }\n\n #attachment-button {\n width: 50px;\n height: 50px;\n background: #ffffff;\n color: ").concat(this.primaryColor, ";\n border: 1px solid rgba(148, 163, 184, 0.5);\n }\n\n #attachment-button:hover {\n transform: translateY(-2px) scale(1.03);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);\n }\n\n #attachment-button:active {\n transform: translateY(1px);\n box-shadow: 0 3px 8px rgba(15, 23, 42, 0.15);\n }\n .audio-chat-screen .chat-avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .audio-chat-screen .chat-header-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .audio-chat-screen .chat-title {\n font-weight: 600;\n font-size: 20px;\n }\n\n .audio-chat-screen .chat-status {\n font-size: 12px;\n opacity: 0.9;\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .audio-chat-screen .chat-close {\n background: ").concat(this.primaryColor, ";\n border: none;\n color: white;\n cursor: pointer;\n padding: 8px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n }\n\n\n .audio-chat-screen .audio-chat-content {\n flex: 1;\n height:100%;\n display: flex;\n flex-direction: column;\n padding: 20px;\n gap: 20px;\n }\n\n .audio-chat-screen .audio-status {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 40px 20px;\n background: white;\n border-radius: 16px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n .audio-chat-screen .audio-status-icon {\n font-size: 48px;\n margin-bottom: 12px;\n transition: transform 0.3s;\n }\n\n .audio-chat-screen .audio-status-text {\n font-size: 16px;\n font-weight: 500;\n color: #475569;\n }\n\n /* Message list styles mirrored from TextChatScreen */\n .audio-chat-screen .chat-messages {\n flex: 1;\n padding: 20px;\n overflow-y: auto;\n overflow-x: hidden; /* Prevent horizontal scroll */\n min-height: 0; /* Crucial for nested flex scrolling */\n background: transparent;\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n .audio-chat-screen .chat-messages::-webkit-scrollbar {\n width: 6px;\n }\n\n .audio-chat-screen .chat-messages::-webkit-scrollbar-track {\n background: transparent;\n }\n\n .audio-chat-screen .chat-messages::-webkit-scrollbar-thumb {\n background: #cbd5e1;\n border-radius: 3px;\n }\n\n .audio-chat-screen .file-attachments-container {\n padding: 8px 12px;\n background: transparent;\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n max-height: 120px;\n overflow-y: auto;\n position: relative;\n z-index: 5;\n }\n\n .audio-chat-screen .file-attachment {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 10px;\n background: #f1f5f9;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n font-size: 12px;\n max-width: 200px;\n }\n\n .audio-chat-screen .file-thumbnail {\n width: 40px;\n height: 40px;\n border-radius: 6px;\n overflow: hidden;\n flex-shrink: 0;\n background: #e2e8f0;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .audio-chat-screen .file-thumbnail-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .audio-chat-screen .file-attachment-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .audio-chat-screen .file-attachment-name {\n font-weight: 500;\n color: #1e293b;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 120px;\n }\n\n .audio-chat-screen .file-attachment-size {\n font-size: 11px;\n color: #64748b;\n }\n\n .audio-chat-screen .file-attachment-remove {\n background: transparent;\n border: none;\n color: #64748b;\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n font-size: 16px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n flex-shrink: 0;\n }\n\n .audio-chat-screen .file-attachment-remove:hover {\n background: #fee2e2;\n color: #dc2626;\n }\n\n .audio-chat-screen .chat-welcome {\n text-align: center;\n padding: 100px 20px;\n color: #64748b;\n }\n\n .audio-chat-screen .chat-welcome .welcome-icon {\n font-size: 48px;\n margin-bottom: 12px;\n }\n\n .audio-chat-screen .welcome-text {\n font-size: 15px;\n font-weight: 500;\n color: #475569;\n }\n\n .audio-chat-screen .chat-message {\n display: flex;\n gap: 8px;\n animation: messageSlide 0.3s ease-out;\n }\n\n @keyframes messageSlide {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .audio-chat-screen .chat-message.user {\n flex-direction: row-reverse;\n }\n\n .audio-chat-screen .message-content {\n max-width: 75%;\n }\n\n .audio-chat-screen .message-bubble {\n padding: 12px 16px;\n border-radius: 16px;\n font-size: 14px;\n line-height: 1.5;\n word-wrap: break-word;\n }\n\n .audio-chat-screen .chat-message.user .message-bubble {\n background: #e9f5d7;\n color: ").concat(this.primaryColor, ";\n border-bottom-right-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n .audio-chat-screen .chat-message.bot .message-bubble {\n background: white;\n color: #1e293b;\n border-bottom-left-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n .audio-chat-screen .message-time {\n font-size: 10px;\n color: #94a3b8;\n margin-top: 4px;\n text-align: right;\n }\n\n /* Message Attachments Styles (mirrored from TextChatScreen) */\n .audio-chat-screen .message-attachments {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-bottom: 8px;\n }\n\n .audio-chat-screen .message-attachment {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: #f1f5f9;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n font-size: 12px;\n max-width: 100%;\n }\n\n .audio-chat-screen .message-attachment.image {\n background: #f8fafc;\n }\n\n .audio-chat-screen .message-attachment.pdf {\n background: #fef2f2;\n }\n\n .audio-chat-screen .message-attachment-icon {\n font-size: 20px;\n flex-shrink: 0;\n }\n\n .audio-chat-screen .message-attachment-info {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n\n .audio-chat-screen .message-attachment-name {\n font-weight: 500;\n color: #1e293b;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .audio-chat-screen .message-attachment-name.clickable:hover {\n color: ").concat(this.primaryColor, ";\n text-decoration: underline;\n }\n\n .audio-chat-screen .message-attachment-size {\n font-size: 11px;\n color: #64748b;\n }\n\n .audio-chat-screen .audio-messages {\n flex: 1;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 200px;\n }\n\n .audio-chat-screen .audio-message {\n display: flex;\n gap: 8px;\n animation: messageSlide 0.3s ease-out;\n }\n\n .audio-chat-screen .audio-message.user {\n flex-direction: row-reverse;\n }\n\n .audio-chat-screen .audio-message-content {\n max-width: 75%;\n }\n\n .audio-chat-screen .audio-message-bubble {\n padding: 12px 16px;\n border-radius: 16px;\n font-size: 14px;\n line-height: 1.5;\n word-wrap: break-word;\n }\n\n .audio-chat-screen .audio-message.user .audio-message-bubble {\n background: ").concat(this.primaryColor, ";\n color: white;\n border-bottom-right-radius: 4px;\n }\n\n .audio-chat-screen .audio-message.assistant .audio-message-bubble {\n background: white;\n color: #1e293b;\n border-bottom-left-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n }\n\n @keyframes messageSlide {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n .audio-chat-screen .audio-messages::-webkit-scrollbar {\n width: 6px;\n }\n\n .audio-chat-screen .audio-messages::-webkit-scrollbar-track {\n background: transparent;\n }\n\n .audio-chat-screen .audio-messages::-webkit-scrollbar-thumb {\n background: #cbd5e1;\n border-radius: 3px;\n }\n\n .audio-chat-screen .audio-controls {\n position: absolute;\n left: 50%;\n bottom: 20px;\n transform: translateX(-50%);\n display: flex;\n justify-content: center;\n align-items: center;\n pointer-events: none; /* let only the button receive events */\n }\n\n .audio-chat-screen .audio-controls .audio-record-btn {\n pointer-events: auto;\n }\n\n .audio-chat-screen .audio-record-btn {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: ").concat(this.primaryColor, ";\n color: white;\n border: none;\n cursor: pointer;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n transition: all 0.3s;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n }\n\n .audio-chat-screen .audio-record-btn:hover {\n transform: scale(1.05);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);\n }\n\n .audio-chat-screen .audio-record-btn:active {\n transform: scale(0.95);\n }\n\n .audio-chat-screen .audio-record-btn.recording {\n background: #ef4444;\n animation: pulse-record 1.5s infinite;\n }\n\n @keyframes pulse-record {\n 0%, 100% {\n box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);\n }\n 50% {\n box-shadow: 0 4px 24px rgba(239, 68, 68, 0.6);\n }\n }\n\n .audio-chat-screen .audio-record-btn span {\n font-size: 11px;\n font-weight: 500;\n margin-top: 4px;\n }\n\n .audio-chat-screen .audio-record-btn svg {\n width: 32px;\n height: 32px;\n }\n\n @media (max-width: 768px) {\n .audio-chat-screen .audio-record-btn {\n width: 100px;\n height: 100px;\n }\n }\n\n /* Expanded Image Modal Styles */\n .expanded-image-modal {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.8);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 10000;\n animation: fadeIn 0.2s ease;\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n\n .expanded-image-container {\n position: relative;\n max-width: 90%;\n max-height: 90vh;\n background: #fff;\n border-radius: 8px;\n overflow: hidden;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n animation: scaleIn 0.2s ease;\n display: flex;\n flex-direction: column;\n }\n\n @keyframes scaleIn {\n from {\n transform: scale(0.9);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: 1;\n }\n }\n\n .expanded-image-close {\n position: absolute;\n top: 12px;\n right: 12px;\n background: rgba(0, 0, 0, 0.6);\n border: none;\n color: white;\n width: 32px;\n height: 32px;\n border-radius: 50%;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 18px;\n z-index: 1;\n transition: all 0.2s;\n }\n\n .expanded-image-close:hover {\n background: rgba(0, 0, 0, 0.8);\n transform: scale(1.1);\n }\n\n .expanded-image {\n max-width: 100%;\n max-height: calc(90vh - 60px);\n object-fit: contain;\n display: block;\n }\n\n .expanded-image-caption {\n padding: 12px 16px;\n background: #fff;\n color: #1e293b;\n font-size: 14px;\n text-align: center;\n border-top: 1px solid #e2e8f0;\n }\n ");
|
|
1442
|
+
document.head.appendChild(style);
|
|
1443
|
+
}
|
|
32
1444
|
}
|
|
33
1445
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
1446
|
+
class ChatWidget {
|
|
1447
|
+
constructor() {
|
|
1448
|
+
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|
|
1449
|
+
this.title = options.title || "Chat Assistant";
|
|
1450
|
+
this.placeholder = options.placeholder || "Type your message...";
|
|
1451
|
+
this.primaryColor = options.primaryColor || "#1a5c4b";
|
|
1452
|
+
this.container = null;
|
|
1453
|
+
this.messages = [];
|
|
1454
|
+
this.isMinimized = false;
|
|
1455
|
+
this.currentScreen = "welcome"; // welcome, text, audio
|
|
1456
|
+
this.textChatScreen = null;
|
|
1457
|
+
this.audioChatScreen = null;
|
|
1458
|
+
|
|
1459
|
+
// Configuration options for message sending
|
|
1460
|
+
this.langgraphUrl = options.langgraphUrl || "http://localhost:8080";
|
|
1461
|
+
this.authToken = options.authToken || null;
|
|
1462
|
+
this.threadId = options.threadId || null;
|
|
1463
|
+
this.assistantId = options.assistantId || null;
|
|
1464
|
+
this.selectedLanguage = options.selectedLanguage || "en";
|
|
1465
|
+
this.accessToken = options.accessToken || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhpZHdnbHl5emRqc2dyaW92bWdtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDczOTE5NjEsImV4cCI6MjA2Mjk2Nzk2MX0.jAdwoGNbwK";
|
|
1466
|
+
this.supabaseToken = options.supabaseToken || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhpZHdnbHl5emRqc2dyaW92bWdtIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDczOTE5NjEsImV4cCI6MjA2Mjk2Nzk2MX0.jAdwoGNbwK";
|
|
1467
|
+
this.userInfo = options.userInfo || {};
|
|
1468
|
+
// Store the custom getHeaders function or use default
|
|
1469
|
+
this._customGetHeaders = options.getHeaders;
|
|
1470
|
+
|
|
1471
|
+
// Default getHeaders implementation
|
|
1472
|
+
this.getHeaders = () => {
|
|
1473
|
+
if (this._customGetHeaders) {
|
|
1474
|
+
return this._customGetHeaders();
|
|
1475
|
+
}
|
|
1476
|
+
const authToken = this.authToken || this.accessToken;
|
|
1477
|
+
const supabaseToken = this.supabaseToken || authToken;
|
|
1478
|
+
return _objectSpread2(_objectSpread2(_objectSpread2({
|
|
1479
|
+
"Content-Type": "application/json"
|
|
1480
|
+
}, authToken && {
|
|
1481
|
+
Authorization: "Bearer ".concat(authToken)
|
|
1482
|
+
}), supabaseToken && {
|
|
1483
|
+
"x-supabase-access-token": supabaseToken
|
|
1484
|
+
}), {}, {
|
|
1485
|
+
Origin: (typeof window !== 'undefined' ? window.location.origin : "*") || "*"
|
|
1486
|
+
});
|
|
1487
|
+
};
|
|
1488
|
+
this.getLatestCheckpoint = options.getLatestCheckpoint || (async () => null);
|
|
1489
|
+
this.updateThread = options.updateThread || (async () => {});
|
|
1490
|
+
this.getUserThreads = options.getUserThreads || (async _ref => {
|
|
1491
|
+
let {
|
|
1492
|
+
userId,
|
|
1493
|
+
userUuid
|
|
1494
|
+
} = _ref;
|
|
1495
|
+
try {
|
|
1496
|
+
const params = new URLSearchParams();
|
|
1497
|
+
if (userUuid) params.append("user_uuid", userUuid);
|
|
1498
|
+
const url = "".concat(this.langgraphUrl, "/history?").concat(params.toString());
|
|
1499
|
+
const response = await fetch(url, {
|
|
1500
|
+
method: "GET",
|
|
1501
|
+
headers: this.getHeaders()
|
|
1502
|
+
});
|
|
1503
|
+
if (!response.ok) return {
|
|
1504
|
+
threads: []
|
|
81
1505
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Simple echo bot – replace with real logic or API call
|
|
88
|
-
setTimeout(function () {
|
|
89
|
-
return addMessage("You said: " + text, false);
|
|
90
|
-
}, 500);
|
|
1506
|
+
return await response.json();
|
|
1507
|
+
} catch (e) {
|
|
1508
|
+
console.error("Error fetching threads", e);
|
|
1509
|
+
return {
|
|
1510
|
+
threads: []
|
|
91
1511
|
};
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
this.setUserInfoFromDirectChatLogin = options.setUserInfoFromDirectChatLogin || (() => {});
|
|
1515
|
+
this.setUserThreads = options.setUserThreads || (() => {});
|
|
1516
|
+
this.setUserThreadsMetaData = options.setUserThreadsMetaData || (() => {});
|
|
1517
|
+
this.languageOptions = [{
|
|
1518
|
+
label: "Assamese",
|
|
1519
|
+
value: "as"
|
|
1520
|
+
}, {
|
|
1521
|
+
label: "Bengali",
|
|
1522
|
+
value: "bn"
|
|
1523
|
+
}, {
|
|
1524
|
+
label: "Dogri",
|
|
1525
|
+
value: "doi"
|
|
1526
|
+
}, {
|
|
1527
|
+
label: "English",
|
|
1528
|
+
value: "en"
|
|
1529
|
+
}, {
|
|
1530
|
+
label: "Gujarati",
|
|
1531
|
+
value: "gu"
|
|
1532
|
+
}, {
|
|
1533
|
+
label: "Hindi",
|
|
1534
|
+
value: "hi"
|
|
1535
|
+
}, {
|
|
1536
|
+
label: "Kannada",
|
|
1537
|
+
value: "kn"
|
|
1538
|
+
}, {
|
|
1539
|
+
label: "Konkani",
|
|
1540
|
+
value: "gom"
|
|
1541
|
+
}, {
|
|
1542
|
+
label: "Maithili",
|
|
1543
|
+
value: "mai"
|
|
1544
|
+
}, {
|
|
1545
|
+
label: "Malayalam",
|
|
1546
|
+
value: "ml"
|
|
1547
|
+
}, {
|
|
1548
|
+
label: "Marathi",
|
|
1549
|
+
value: "mr"
|
|
1550
|
+
}, {
|
|
1551
|
+
label: "Oriya",
|
|
1552
|
+
value: "or"
|
|
1553
|
+
}, {
|
|
1554
|
+
label: "Punjabi",
|
|
1555
|
+
value: "pa"
|
|
1556
|
+
}, {
|
|
1557
|
+
label: "Sanskrit",
|
|
1558
|
+
value: "sa"
|
|
1559
|
+
}, {
|
|
1560
|
+
label: "Sindhi",
|
|
1561
|
+
value: "sd"
|
|
1562
|
+
}, {
|
|
1563
|
+
label: "Tamil",
|
|
1564
|
+
value: "ta"
|
|
1565
|
+
}, {
|
|
1566
|
+
label: "Telugu",
|
|
1567
|
+
value: "te"
|
|
1568
|
+
}, {
|
|
1569
|
+
label: "Urdu",
|
|
1570
|
+
value: "ur"
|
|
1571
|
+
}];
|
|
1572
|
+
|
|
1573
|
+
// State management
|
|
1574
|
+
this.isLoading = false;
|
|
1575
|
+
this.isConnected = false;
|
|
1576
|
+
this.initialized = false;
|
|
1577
|
+
this.contentBlocks = [];
|
|
1578
|
+
this.playedAudioIds = new Set();
|
|
1579
|
+
this.currentInputAudio = null;
|
|
1580
|
+
this.submissionInProgress = false;
|
|
1581
|
+
this.processingAudioId = null;
|
|
1582
|
+
this._init();
|
|
1583
|
+
|
|
1584
|
+
// Initialize chat after DOM is ready
|
|
1585
|
+
if (typeof window !== 'undefined') {
|
|
1586
|
+
// Use setTimeout to ensure DOM is ready
|
|
1587
|
+
setTimeout(() => {
|
|
1588
|
+
this.initializeChat();
|
|
1589
|
+
}, 100);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
_init() {
|
|
1593
|
+
// Create container
|
|
1594
|
+
this.container = document.createElement("div");
|
|
1595
|
+
this.container.id = "chat-widget-container";
|
|
1596
|
+
this.container.innerHTML = "\n <div class=\"chat-widget-minimized\" id=\"chat-toggle\">\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z\"></path>\n </svg>\n </div>\n <div class=\"chat-widget-expanded\" id=\"chat-expanded\">\n <div class=\"chat-screen-container\" id=\"chat-screen-container\">\n <!-- Screens will be rendered here -->\n </div>\n <div class=\"chat-drawer-overlay\" id=\"chat-drawer-overlay\">\n <div class=\"chat-drawer\">\n <div class=\"drawer-header\">\n <div class=\"drawer-title\">Menu</div>\n </div>\n <div class=\"drawer-content\">\n <button class=\"drawer-item\" id=\"drawer-new-chat\">\n New Chat\n </button> \n <div class=\"language-selector-container\">\n <div class=\"custom-select\" id=\"language-selector\">\n <div class=\"select-trigger\" id=\"language-trigger\">\n <span id=\"selected-language-text\">English</span>\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"chevron\"><path d=\"m6 9 6 6 6-6\"/></svg>\n </div>\n <div class=\"select-options\" id=\"language-options\"></div>\n </div>\n </div>\n <div class=\"drawer-divider\" style=\"height: 1px; background: #e2e8f0; margin: 8px 0;\"></div>\n \n <div id=\"drawer-threads\" class=\"threads-container\">\n <!-- Threads will be rendered here -->\n <div class=\"threads-loading\">Loading history...</div>\n </div>\n </div>\n \n </div>\n <div class=\"drawer-backdrop\" id=\"drawer-backdrop\"></div>\n </div>\n </div>\n ";
|
|
1597
|
+
document.body.appendChild(this.container);
|
|
1598
|
+
this._applyStyles();
|
|
1599
|
+
this._initScreens();
|
|
1600
|
+
this._renderScreen();
|
|
1601
|
+
this._populateLanguageOptions();
|
|
1602
|
+
this._bindEvents();
|
|
1603
|
+
}
|
|
1604
|
+
_applyStyles() {
|
|
1605
|
+
const style = document.createElement("style");
|
|
1606
|
+
style.textContent = "\n @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');\n \n #chat-widget-container {\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 10000;\n font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n }\n.text-chat-screen {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n .chat-header {\n color: ".concat(this.primaryColor, ";\n padding: 20px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .chat-header-content {\n display: flex;\n align-items: center;\n gap: 12px;\n flex: 1;\n }\n\n .chat-back {\n background: transparent;\n border: none;\n color: white;\n cursor: pointer;\n padding: 8px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n margin-right: 8px;\n }\n\n .chat-back:hover {\n background: rgba(255, 255, 255, 0.15);\n }\n\n .chat-avatar {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.2);\n backdrop-filter: blur(10px);\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .chat-header-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n }\n \n .chat-title {\n font-weight: 600;\n font-size: 20px;\n }\n\n .chat-status {\n font-size: 12px;\n opacity: 0.9;\n display: flex;\n align-items: center;\n gap: 4px;\n }\n\n .chat-close {\n background: ").concat(this.primaryColor, ";\n border: none;\n color: white;\n cursor: pointer;\n padding: 8px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.2s;\n }\n\n .chat-widget-minimized {\n width: 60px;\n height: 60px;\n border-radius: 50%;\n background: ").concat(this.primaryColor, ";\n color: white;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n .chat-widget-minimized:hover {\n transform: scale(1.1);\n }\n\n @keyframes pulse {\n 0%, 100% { box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4); }\n 50% { box-shadow: 0 8px 32px rgba(99, 102, 241, 0.6); }\n }\n\n .chat-widget-expanded {\n position: relative;\n width: 480px;\n height: 640px;\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);\n display: none;\n flex-direction: column;\n overflow: hidden;\n animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n @keyframes slideUp {\n from {\n opacity: 0;\n transform: translateY(20px) scale(0.95);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n }\n\n .chat-widget-expanded.visible {\n display: flex;\n }\n\n .chat-screen-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n /* Welcome Screen Styles */\n .welcome-screen {\n flex: 1;\n height: 100%;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n justify-content: start;\n background: linear-gradient(180deg, white 10%, #E1EFCC );\n padding: 0px;\n }\n\n .welcome-content {\n width: 80%;\n margin: auto;\n text-align: center;\n padding-top: 20px;\n }\n\n .welcome-icon {\n font-size: 64px;\n margin-bottom: 16px;\n animation: bounce 2s infinite;\n }\n\n @keyframes bounce {\n 0%, 100% { transform: translateY(0); }\n 50% { transform: translateY(-10px); }\n }\n\n .welcome-title {\n font-size: 24px;\n font-weight: 600;\n color: #1e293b;\n margin: 0 0 8px 0;\n }\n\n .welcome-subtitle {\n font-size: 14px;\n color: #64748b;\n margin: 0 0 32px 0;\n }\n\n .welcome-options {\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n\n .welcome-option-btn {\n background: white;\n border: 2px solid #e2e8f0;\n border-radius: 12px;\n padding: 16px;\n display: flex;\n align-items: center;\n gap: 12px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: left;\n width: 100%;\n }\n\n .welcome-option-btn:hover {\n border-color: ").concat(this.primaryColor, ";\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n transform: translateY(-2px);\n }\n\n .option-icon {\n font-size: 32px;\n flex-shrink: 0;\n }\n\n .option-content {\n flex: 1;\n }\n\n .option-title {\n font-size: 16px;\n font-weight: 600;\n color: #1e293b;\n margin-bottom: 4px;\n }\n\n .option-description {\n font-size: 13px;\n color: #64748b;\n }\n\n .welcome-option-btn svg {\n color: #94a3b8;\n flex-shrink: 0;\n }\n\n .welcome-option-btn:hover svg {\n color: ").concat(this.primaryColor, ";\n }\n.gradient-sphere-welcome-screen {\n width: 200px !important;\n height: 200px !important;\n border-radius: 50%;\n background: linear-gradient(135deg, #0a0d120f 0%, #85bc31 50%, #d0f19e 100%)\n !important;\n position: relative;\n margin: 20px auto 30px auto;\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);\n animation: float 3s ease-in-out infinite;\n flex-shrink: 0;\n}\n\n.sphere-highlight {\n position: absolute;\n top: 20%;\n right: 20%;\n width: 35px;\n height: 35px;\n background: radial-gradient(\n circle,\n rgba(255, 255, 255, 0.8) 0%,\n transparent 70%\n );\n border-radius: 50%;\n filter: blur(1px);\n}\n\n@keyframes float {\n 0%,\n 100% {\n transform: translateY(0px);\n }\n 50% {\n transform: translateY(-10px);\n }\n}\n\n.welcome-text {\n max-width: 400px;\n margin-bottom: 30px;\n flex-shrink: 0;\n text-align: left;\n}\n\n.greeting {\n font-size: 24px;\n font-weight: 600;\n color: #1a5c4b;\n margin: 0 0 16px 0;\n}\n\n.intro {\n font-size: 18px;\n font-weight: 500;\n color: #1a5c4b;\n margin: 0 0 12px 0;\n line-height: 1.4;\n}\n\n\n.action-buttons {\n display: flex;\n align-items: center;\n gap: 16px;\n flex-shrink: 0;\n margin-top: auto;\n /* padding-top: 10px; */\n padding-bottom: 20px;\n justify-content: center;\n width: 100%;\n align-self: center;\n}\n\n.primary-button {\n background: #1a5c4b;\n display:flex;\n color: white;\n border: none;\n padding: 16px 32px;\n border-radius: 25px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.3s ease;\n box-shadow: 0 4px 12px rgba(26, 92, 75, 0.3);\n}\n\n.primary-button:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(26, 92, 75, 0.4);\n}\n\n @media (max-width: 768px) {\n #chat-widget-container {\n bottom: 16px;\n right: 16px;\n }\n\n .chat-widget-expanded {\n width: 100vw;\n height: 100vh;\n border-radius: 0;\n max-width: 100vw;\n max-height: 100vh;\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n }\n\n .chat-widget-expanded.visible {\n display: flex;\n }\n\n .welcome-content {\n max-width: 100%;\n padding-top: 40px;\n }\n\n .chat-drawer {\n width: 85% !important; /* Wider drawer on mobile */\n }\n }\n\n /* Drawer Styles */\n .chat-drawer-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 2000;\n display: flex;\n pointer-events: none;\n visibility: hidden;\n }\n\n .chat-drawer-overlay.visible {\n pointer-events: auto;\n visibility: visible;\n }\n\n .chat-drawer {\n width: 65%;\n background: white;\n height: 100%;\n transform: translateX(-100%);\n transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n display: flex;\n flex-direction: column;\n z-index: 2002;\n box-shadow: 4px 0 24px rgba(0,0,0,0.1);\n }\n\n .chat-drawer-overlay.visible .chat-drawer {\n transform: translateX(0);\n }\n\n .drawer-backdrop {\n flex: 1;\n background: rgba(0, 0, 0, 0.5);\n opacity: 0;\n transition: opacity 0.3s ease;\n backdrop-filter: blur(2px);\n cursor: pointer;\n }\n\n .chat-drawer-overlay.visible .drawer-backdrop {\n opacity: 1;\n }\n\n .drawer-header {\n padding: 24px;\n border-bottom: 1px solid #f1f5f9;\n }\n\n .drawer-title {\n font-size: 20px;\n font-weight: 600;\n color: ").concat(this.primaryColor, ";\n }\n\n .drawer-content {\n flex: 1;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n overflow-y: auto;\n }\n\n .drawer-item {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 16px;\n background: transparent;\n border: none;\n border-radius: 8px;\n color: #475569;\n font-size: 15px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.2s;\n text-align: left;\n }\n\n .drawer-item:hover {\n background: #f1f5f9;\n color: ").concat(this.primaryColor, ";\n }\n\n .drawer-item svg {\n opacity: 0.7;\n }\n\n .drawer-item:hover svg {\n opacity: 1;\n color: ").concat(this.primaryColor, ";\n }\n\n .drawer-footer {\n padding: 16px 24px;\n border-top: 1px solid #f1f5f9;\n }\n\n .drawer-version {\n font-size: 12px;\n color: #94a3b8;\n text-align: center;\n }\n\n /* Language Selector Styles */\n .language-selector-container {\n margin-top: 8px;\n margin-bottom: 8px;\n }\n\n .language-label {\n font-size: 11px;\n font-weight: 600;\n color: #94a3b8;\n margin-bottom: 6px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n padding-left: 4px;\n }\n\n .custom-select {\n position: relative;\n width: 100%;\n user-select: none;\n }\n\n .select-trigger {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 12px;\n background: #f8fafc;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.2s;\n font-size: 14px;\n color: #334155;\n }\n\n .select-trigger:hover {\n border-color: ").concat(this.primaryColor, ";\n background: white;\n }\n\n .select-trigger.active {\n border-color: ").concat(this.primaryColor, ";\n background: white;\n box-shadow: 0 0 0 2px rgba(26, 92, 75, 0.1);\n }\n\n .select-options {\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n right: 0;\n background: white;\n border: 1px solid #e2e8f0;\n border-radius: 8px;\n box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);\n max-height: 200px;\n overflow-y: auto;\n z-index: 50;\n display: none;\n opacity: 0;\n transform: translateY(-10px);\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n }\n\n .select-options.open {\n display: block;\n opacity: 1;\n transform: translateY(0);\n }\n\n .select-option {\n padding: 10px 12px;\n font-size: 14px;\n color: #334155;\n cursor: pointer;\n transition: all 0.1s;\n }\n\n .select-option:hover {\n background: #f1f5f9;\n color: ").concat(this.primaryColor, ";\n }\n\n .select-option.selected {\n background: rgba(26, 92, 75, 0.08);\n color: ").concat(this.primaryColor, ";\n font-weight: 500;\n }\n \n .chevron {\n transition: transform 0.2s ease;\n color: #94a3b8;\n }\n \n .select-trigger.active .chevron {\n transform: rotate(180deg);\n color: ").concat(this.primaryColor, ";\n }\n \n /* Thread List Styles */\n .threads-container {\n flex: 1;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding-top: 8px;\n }\n \n .threads-loading {\n padding: 16px;\n text-align: center;\n color: #94a3b8;\n font-size: 13px;\n }\n \n .thread-item {\n display: flex;\n flex-direction: column;\n padding: 10px 12px;\n border-radius: 8px;\n cursor: pointer;\n transition: all 0.2s;\n text-align: left;\n border: none;\n background: transparent;\n width: 100%;\n color: #475569;\n }\n \n .thread-item:hover {\n background-color: #f1f5f9; /* action.hover */\n color: ").concat(this.primaryColor, ";\n }\n \n .thread-item.selected {\n background-color: rgba(26, 92, 75, 0.08); /* action.selected */\n }\n \n .thread-content {\n font-size: 0.9rem;\n color: #334155;\n line-height: 1.3;\n margin-bottom: 4px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n font-weight: 500;\n }\n \n .thread-date {\n font-size: 0.75rem;\n font-weight: 600;\n color: #94a3b8;\n }\n \n .no-history {\n padding: 16px;\n text-align: center;\n color: #94a3b8;\n font-size: 14px;\n }\n ");
|
|
1607
|
+
document.head.appendChild(style);
|
|
1608
|
+
}
|
|
1609
|
+
_renderScreen() {
|
|
1610
|
+
const screenContainer = this.container.querySelector("#chat-screen-container");
|
|
1611
|
+
switch (this.currentScreen) {
|
|
1612
|
+
case "welcome":
|
|
1613
|
+
this._renderWelcomeScreen(screenContainer);
|
|
1614
|
+
break;
|
|
1615
|
+
case "text":
|
|
1616
|
+
this._renderTextChatScreen(screenContainer);
|
|
1617
|
+
break;
|
|
1618
|
+
case "audio":
|
|
1619
|
+
this._renderAudioChatScreen(screenContainer);
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
_initScreens() {
|
|
1624
|
+
// Initialize text chat screen
|
|
1625
|
+
this.textChatScreen = new TextChatScreen({
|
|
1626
|
+
title: this.title,
|
|
1627
|
+
placeholder: this.placeholder,
|
|
1628
|
+
primaryColor: this.primaryColor,
|
|
1629
|
+
onMessage: (text, respond) => {
|
|
1630
|
+
this._handleUserMessage(text, respond);
|
|
1631
|
+
},
|
|
1632
|
+
onBack: () => {
|
|
1633
|
+
this.currentScreen = "welcome";
|
|
1634
|
+
this._renderScreen();
|
|
1635
|
+
},
|
|
1636
|
+
onOpenDrawer: () => {
|
|
1637
|
+
this._toggleDrawer(true);
|
|
1638
|
+
},
|
|
1639
|
+
onClose: () => {
|
|
1640
|
+
const expanded = this.container.querySelector("#chat-expanded");
|
|
1641
|
+
const toggle = this.container.querySelector("#chat-toggle");
|
|
1642
|
+
expanded.classList.remove("visible");
|
|
1643
|
+
toggle.style.display = "flex";
|
|
1644
|
+
this.currentScreen = "welcome";
|
|
1645
|
+
this.messages = [];
|
|
1646
|
+
this.contentBlocks = [];
|
|
1647
|
+
this._initScreens();
|
|
1648
|
+
},
|
|
1649
|
+
messages: this.messages,
|
|
1650
|
+
sendMessage: (text, contentBlocks) => {
|
|
1651
|
+
return this.sendMessage(text, contentBlocks);
|
|
1652
|
+
},
|
|
1653
|
+
contentBlocks: this.contentBlocks,
|
|
1654
|
+
onContentBlocksChange: contentBlocks => {
|
|
1655
|
+
this.contentBlocks = contentBlocks;
|
|
1656
|
+
},
|
|
1657
|
+
navigateToAudioScreen: () => {
|
|
1658
|
+
this._navigateToScreen("audio");
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// Initialize audio chat screen
|
|
1663
|
+
this.audioChatScreen = new AudioChatScreen({
|
|
1664
|
+
title: this.title,
|
|
1665
|
+
primaryColor: this.primaryColor,
|
|
1666
|
+
onRecordStart: () => {
|
|
1667
|
+
// Handle recording start
|
|
1668
|
+
},
|
|
1669
|
+
onRecordStop: () => {
|
|
1670
|
+
// Handle recording stop
|
|
1671
|
+
},
|
|
1672
|
+
onBack: () => {
|
|
1673
|
+
this.currentScreen = "welcome";
|
|
1674
|
+
this._renderScreen();
|
|
1675
|
+
},
|
|
1676
|
+
onOpenDrawer: () => {
|
|
1677
|
+
this._toggleDrawer(true);
|
|
1678
|
+
},
|
|
1679
|
+
onClose: () => {
|
|
1680
|
+
const expanded = this.container.querySelector("#chat-expanded");
|
|
1681
|
+
const toggle = this.container.querySelector("#chat-toggle");
|
|
1682
|
+
expanded.classList.remove("visible");
|
|
1683
|
+
toggle.style.display = "flex";
|
|
1684
|
+
this.currentScreen = "welcome";
|
|
1685
|
+
this.messages = [];
|
|
1686
|
+
this._initScreens();
|
|
1687
|
+
},
|
|
1688
|
+
messages: this.messages,
|
|
1689
|
+
sendMessage: (text, contentBlocks) => {
|
|
1690
|
+
return this.sendMessage(text, contentBlocks);
|
|
1691
|
+
},
|
|
1692
|
+
selectedLanguage: this.selectedLanguage,
|
|
1693
|
+
navigateToTextScreen: () => {
|
|
1694
|
+
this._navigateToScreen("text");
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
_renderWelcomeScreen(container) {
|
|
1699
|
+
container.innerHTML = "\n <main class=\"welcome-screen\">\n <div class=\"chat-header\">\n <div class=\"chat-header-content visible\">\n <div class=\"chat-header-text\">\n <div class=\"chat-title\">".concat(this.title, "</div>\n </div>\n </div>\n <button class=\"chat-close\" id=\"text-chat-close\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line>\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line>\n </svg>\n </button>\n </div>\n <div >\n <div class=\"welcome-content\">\n <div class=\"gradient-sphere-welcome-screen\">\n <div class=\"sphere-highlight\">\n </div>\n </div>\n \n <div class=\"welcome-text\">\n <p class=\"greeting\">Hello!</p>\n <p class=\"intro\">\n We are your Krishi Vigyan Sahayak\u2014KVS, a companion to help with every farming question! Tell us, what do you want to know today?\n </p>\n </div>\n \n <div class=\"action-buttons\">\n <button\n class=\"primary-button !rounded-full\"\n id=\"welcome-audio-btn\"\n >\n Let's talk ​ ​ <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-mic-icon lucide-mic\"><path d=\"M12 19v3\"/><path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"/><rect x=\"9\" y=\"2\" width=\"6\" height=\"13\" rx=\"3\"/></svg>\n </button>\n </div>\n \n \n\n </div>\n </div>\n </main>\n ");
|
|
1700
|
+
|
|
1701
|
+
// Bind welcome screen events
|
|
1702
|
+
// const textBtn = container.querySelector("#welcome-text-btn");
|
|
1703
|
+
const audioBtn = container.querySelector("#welcome-audio-btn");
|
|
1704
|
+
const closeBtn = container.querySelector("#text-chat-close");
|
|
1705
|
+
|
|
1706
|
+
// textBtn.addEventListener("click", () => this._navigateToScreen("text"));
|
|
1707
|
+
audioBtn.addEventListener("click", () => this._navigateToScreen("audio"));
|
|
1708
|
+
if (closeBtn) {
|
|
1709
|
+
closeBtn.addEventListener("click", () => {
|
|
1710
|
+
const expanded = this.container.querySelector("#chat-expanded");
|
|
1711
|
+
const toggle = this.container.querySelector("#chat-toggle");
|
|
1712
|
+
expanded.classList.remove("visible");
|
|
1713
|
+
toggle.style.display = "flex";
|
|
1714
|
+
this.currentScreen = "welcome";
|
|
1715
|
+
this.messages = [];
|
|
1716
|
+
this._initScreens();
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
_renderTextChatScreen(container) {
|
|
1721
|
+
if (this.textChatScreen) {
|
|
1722
|
+
// Sync contentBlocks before rendering
|
|
1723
|
+
this.textChatScreen.contentBlocks = this.contentBlocks;
|
|
1724
|
+
this.textChatScreen.messages = this.messages;
|
|
1725
|
+
this.textChatScreen.render(container);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
_renderAudioChatScreen(container) {
|
|
1729
|
+
if (this.audioChatScreen) {
|
|
1730
|
+
this.audioChatScreen.messages = this.messages;
|
|
1731
|
+
this.audioChatScreen.render(container);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
_navigateToScreen(screen) {
|
|
1735
|
+
this.currentScreen = screen;
|
|
1736
|
+
this._renderScreen();
|
|
1737
|
+
}
|
|
1738
|
+
_populateLanguageOptions() {
|
|
1739
|
+
const optionsContainer = this.container.querySelector("#language-options");
|
|
1740
|
+
if (!optionsContainer) return;
|
|
1741
|
+
optionsContainer.innerHTML = this.languageOptions.map(opt => "\n <div class=\"select-option ".concat(opt.value === this.selectedLanguage ? 'selected' : '', "\" data-value=\"").concat(opt.value, "\">\n ").concat(opt.label, "\n </div>\n ")).join('');
|
|
1742
|
+
this._updateSelectedLanguageDisplay();
|
|
1743
|
+
}
|
|
1744
|
+
_updateSelectedLanguageDisplay() {
|
|
1745
|
+
const textSpan = this.container.querySelector("#selected-language-text");
|
|
1746
|
+
if (textSpan) {
|
|
1747
|
+
const option = this.languageOptions.find(opt => opt.value === this.selectedLanguage);
|
|
1748
|
+
textSpan.textContent = option ? option.label : "English";
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
_bindLanguageSelectorEvents() {
|
|
1752
|
+
const selector = this.container.querySelector("#language-selector");
|
|
1753
|
+
const trigger = this.container.querySelector("#language-trigger");
|
|
1754
|
+
const optionsFn = this.container.querySelector("#language-options");
|
|
1755
|
+
if (trigger && optionsFn) {
|
|
1756
|
+
trigger.addEventListener("click", e => {
|
|
1757
|
+
e.stopPropagation();
|
|
1758
|
+
const isOpen = optionsFn.classList.contains("open");
|
|
1759
|
+
if (isOpen) {
|
|
1760
|
+
optionsFn.classList.remove("open");
|
|
1761
|
+
trigger.classList.remove("active");
|
|
1762
|
+
} else {
|
|
1763
|
+
optionsFn.classList.add("open");
|
|
1764
|
+
trigger.classList.add("active");
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
const options = optionsFn.querySelectorAll(".select-option");
|
|
1768
|
+
options.forEach(opt => {
|
|
1769
|
+
opt.addEventListener("click", e => {
|
|
1770
|
+
const value = e.currentTarget.dataset.value;
|
|
1771
|
+
this.selectedLanguage = value;
|
|
1772
|
+
|
|
1773
|
+
// Update UI
|
|
1774
|
+
this._updateSelectedLanguageDisplay();
|
|
1775
|
+
optionsFn.querySelectorAll(".select-option").forEach(o => o.classList.remove("selected"));
|
|
1776
|
+
e.currentTarget.classList.add("selected");
|
|
1777
|
+
|
|
1778
|
+
// Close dropdown
|
|
1779
|
+
optionsFn.classList.remove("open");
|
|
1780
|
+
trigger.classList.remove("active");
|
|
1781
|
+
console.log("Language selected:", this.selectedLanguage);
|
|
95
1782
|
});
|
|
1783
|
+
});
|
|
1784
|
+
|
|
1785
|
+
// Close on click outside
|
|
1786
|
+
document.addEventListener("click", e => {
|
|
1787
|
+
if (selector && !selector.contains(e.target)) {
|
|
1788
|
+
optionsFn.classList.remove("open");
|
|
1789
|
+
trigger.classList.remove("active");
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
async _fetchThreads() {
|
|
1795
|
+
const threadsContainer = this.container.querySelector("#drawer-threads");
|
|
1796
|
+
if (!threadsContainer) return;
|
|
1797
|
+
threadsContainer.innerHTML = '<div class="threads-loading">Loading history...</div>';
|
|
1798
|
+
try {
|
|
1799
|
+
const storedUser = localStorage.getItem("DfsWeb.user-info");
|
|
1800
|
+
const user = storedUser ? JSON.parse(storedUser) : null;
|
|
1801
|
+
if (!(user !== null && user !== void 0 && user.uuid)) {
|
|
1802
|
+
threadsContainer.innerHTML = '<div class="no-history">Please log in to view history</div>';
|
|
1803
|
+
return;
|
|
96
1804
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1805
|
+
console.log("USER ID", user.id, "USER UUID", user.uuid);
|
|
1806
|
+
const data = await this.getUserThreads({
|
|
1807
|
+
userId: user.id,
|
|
1808
|
+
userUuid: user.uuid
|
|
1809
|
+
});
|
|
1810
|
+
console.log("THREAD DATA", data);
|
|
1811
|
+
const threads = data.threads || [];
|
|
1812
|
+
const threadsWithHistory = threads.filter(thread => thread.history && thread.history.length > 0);
|
|
1813
|
+
this._renderThreadList(threadsWithHistory);
|
|
1814
|
+
} catch (error) {
|
|
1815
|
+
console.error("UNEXPECTED ERROR IN FETCHING THREADS", error.message);
|
|
1816
|
+
threadsContainer.innerHTML = '<div class="no-history">Failed to load history</div>';
|
|
1817
|
+
}
|
|
101
1818
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
1819
|
+
_renderThreadList(threads) {
|
|
1820
|
+
const threadsContainer = this.container.querySelector("#drawer-threads");
|
|
1821
|
+
if (!threadsContainer) return;
|
|
1822
|
+
if (threads.length === 0) {
|
|
1823
|
+
threadsContainer.innerHTML = '<div class="no-history">No history available</div>';
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
threadsContainer.innerHTML = '';
|
|
1827
|
+
threads.forEach(thread => {
|
|
1828
|
+
var _thread$history$;
|
|
1829
|
+
const isSelected = thread.thread_id === this.threadId; // Use current threadId
|
|
1830
|
+
|
|
1831
|
+
const threadItem = document.createElement('div');
|
|
1832
|
+
threadItem.className = "thread-item ".concat(isSelected ? 'selected' : '');
|
|
1833
|
+
|
|
1834
|
+
// Get primary text (first user message)
|
|
1835
|
+
const primaryText = ((_thread$history$ = thread.history[0]) === null || _thread$history$ === void 0 || (_thread$history$ = _thread$history$.values) === null || _thread$history$ === void 0 || (_thread$history$ = _thread$history$.messages[0]) === null || _thread$history$ === void 0 ? void 0 : _thread$history$.content) || "New Chat";
|
|
1836
|
+
// Handle array content (if content is array of blocks)
|
|
1837
|
+
let displayText = primaryText;
|
|
1838
|
+
if (Array.isArray(primaryText)) {
|
|
1839
|
+
const textBlock = primaryText.find(b => b.type === 'text');
|
|
1840
|
+
displayText = textBlock ? textBlock.text : "Multimedia Message";
|
|
1841
|
+
} else if (typeof primaryText === 'object') {
|
|
1842
|
+
displayText = "Message";
|
|
1843
|
+
}
|
|
1844
|
+
const date = new Date(thread.created_at).toLocaleString();
|
|
1845
|
+
threadItem.innerHTML = "\n <div class=\"thread-content\">".concat(displayText, "</div>\n <div class=\"thread-date\">").concat(date, "</div>\n ");
|
|
1846
|
+
threadItem.addEventListener('click', () => {
|
|
1847
|
+
this._handleThreadSelect(thread.thread_id);
|
|
1848
|
+
});
|
|
1849
|
+
threadsContainer.appendChild(threadItem);
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
_handleThreadSelect(threadId) {
|
|
1853
|
+
console.log("Select thread:", threadId);
|
|
1854
|
+
this.threadId = threadId;
|
|
1855
|
+
this._toggleDrawer(false);
|
|
1856
|
+
|
|
1857
|
+
// Switch to text screen to show history
|
|
1858
|
+
this.currentScreen = "text";
|
|
1859
|
+
|
|
1860
|
+
// Clear current messages
|
|
1861
|
+
this.messages = [];
|
|
1862
|
+
this.setIsLoading = true; // Set loading state if you have setter, otherwise this.isLoading = true
|
|
1863
|
+
this.isLoading = true;
|
|
1864
|
+
|
|
1865
|
+
// Re-initialize screens (clears them)
|
|
1866
|
+
// this._initScreens(); // Can't fully re-init as it might break event listeners
|
|
1867
|
+
// Better to just load history and render
|
|
1868
|
+
|
|
1869
|
+
this._renderScreen();
|
|
1870
|
+
|
|
1871
|
+
// Load history for this thread
|
|
1872
|
+
this.loadHistory(threadId).then(() => {
|
|
1873
|
+
this.isLoading = false;
|
|
1874
|
+
this._renderScreen(); // Re-render with new messages
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
_bindEvents() {
|
|
1878
|
+
const toggle = this.container.querySelector("#chat-toggle");
|
|
1879
|
+
const expanded = this.container.querySelector("#chat-expanded");
|
|
1880
|
+
|
|
1881
|
+
// Ensure toggle button is visible
|
|
1882
|
+
if (toggle) {
|
|
1883
|
+
toggle.style.display = "flex";
|
|
1884
|
+
}
|
|
1885
|
+
toggle.addEventListener("click", () => {
|
|
1886
|
+
console.log("Clicked");
|
|
1887
|
+
toggle.style.display = "none";
|
|
1888
|
+
expanded.classList.add("visible");
|
|
1889
|
+
this.currentScreen = "welcome";
|
|
1890
|
+
this._renderScreen();
|
|
1891
|
+
});
|
|
1892
|
+
|
|
1893
|
+
// Drawer events
|
|
1894
|
+
const drawerBackdrop = this.container.querySelector("#drawer-backdrop");
|
|
1895
|
+
if (drawerBackdrop) {
|
|
1896
|
+
drawerBackdrop.addEventListener("click", () => {
|
|
1897
|
+
this._toggleDrawer(false);
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
const drawerItems = this.container.querySelectorAll(".drawer-item");
|
|
1901
|
+
drawerItems.forEach(item => {
|
|
1902
|
+
item.addEventListener("click", e => {
|
|
1903
|
+
// Handle drawer item click
|
|
1904
|
+
this._toggleDrawer(false);
|
|
1905
|
+
const targetId = e.currentTarget.id;
|
|
1906
|
+
if (targetId === "drawer-new-chat") {
|
|
1907
|
+
this._resetChat();
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
});
|
|
1911
|
+
this._bindLanguageSelectorEvents();
|
|
1912
|
+
}
|
|
1913
|
+
_toggleDrawer(show) {
|
|
1914
|
+
const overlay = this.container.querySelector("#chat-drawer-overlay");
|
|
1915
|
+
if (!overlay) return;
|
|
1916
|
+
if (show) {
|
|
1917
|
+
overlay.classList.add("visible");
|
|
1918
|
+
// Fetch threads when drawer is opened
|
|
1919
|
+
this._fetchThreads();
|
|
1920
|
+
} else {
|
|
1921
|
+
overlay.classList.remove("visible");
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
_resetChat() {
|
|
1925
|
+
this.messages = [];
|
|
1926
|
+
this.threadId = null;
|
|
1927
|
+
this.contentBlocks = [];
|
|
1928
|
+
// this.currentScreen = "welcome";
|
|
1929
|
+
this._initScreens();
|
|
1930
|
+
this._renderScreen();
|
|
1931
|
+
}
|
|
1932
|
+
_handleUserMessage(text, respond) {
|
|
1933
|
+
// Simple echo response - just returns what the user typed
|
|
1934
|
+
// The respond callback is provided by TextChatScreen
|
|
1935
|
+
if (respond) {
|
|
1936
|
+
respond("You said: \"".concat(text, "\""));
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Add message to shared messages array and update UI
|
|
1941
|
+
_addMessageToArray(message) {
|
|
1942
|
+
this.messages.push(message);
|
|
1943
|
+
this._notifyScreensUpdate();
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Update message in shared messages array
|
|
1947
|
+
_updateMessageInArray(messageId, updates) {
|
|
1948
|
+
const index = this.messages.findIndex(msg => msg.id === messageId);
|
|
1949
|
+
if (index !== -1) {
|
|
1950
|
+
this.messages[index] = _objectSpread2(_objectSpread2({}, this.messages[index]), updates);
|
|
1951
|
+
this._notifyScreensUpdate();
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Notify all screens to update their UI
|
|
1956
|
+
_notifyScreensUpdate() {
|
|
1957
|
+
if (this.textChatScreen && this.textChatScreen.container) {
|
|
1958
|
+
this.textChatScreen._syncMessages(this.messages);
|
|
1959
|
+
}
|
|
1960
|
+
if (this.audioChatScreen && this.audioChatScreen.container) {
|
|
1961
|
+
this.audioChatScreen._syncMessages(this.messages);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// Send message function adapted from React version
|
|
1966
|
+
async sendMessage(inputValue) {
|
|
1967
|
+
let contentBlocks = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
|
|
1968
|
+
if (!inputValue.trim() && contentBlocks.length === 0 || this.isLoading) {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
console.log("📤 Sending text message - Language:", this.selectedLanguage, "Assistant ID:", this.assistantId || "Not available", "Thread ID:", this.threadId || "Not available", "Access token:", this.accessToken ? "Present" : "Empty - backend will handle");
|
|
1972
|
+
|
|
1973
|
+
// Prepare content blocks for the message
|
|
1974
|
+
const messageContent = [];
|
|
1975
|
+
|
|
1976
|
+
// Add text content if present
|
|
1977
|
+
if (inputValue.trim()) {
|
|
1978
|
+
const textContent = inputValue;
|
|
1979
|
+
messageContent.push({
|
|
1980
|
+
type: "text",
|
|
1981
|
+
text: textContent
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Add file attachments
|
|
1986
|
+
contentBlocks.forEach(block => {
|
|
1987
|
+
messageContent.push(block);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
// Create user message for display
|
|
1991
|
+
const userMessage = {
|
|
1992
|
+
id: Date.now(),
|
|
1993
|
+
content: inputValue.trim() || "📎 File attachments",
|
|
1994
|
+
sender: "user",
|
|
1995
|
+
timestamp: new Date().toISOString(),
|
|
1996
|
+
attachments: contentBlocks.length > 0 ? contentBlocks : null
|
|
1997
|
+
};
|
|
1998
|
+
console.log("USER MESSAGE", userMessage);
|
|
1999
|
+
console.log("USER MESSAGE CONTENTBLOCKS", contentBlocks);
|
|
2000
|
+
console.log("USER MESSAGE CONTENT", messageContent);
|
|
2001
|
+
this._addMessageToArray(userMessage);
|
|
2002
|
+
this.contentBlocks = [];
|
|
2003
|
+
|
|
2004
|
+
// Clear contentBlocks in text chat screen
|
|
2005
|
+
if (this.textChatScreen) {
|
|
2006
|
+
this.textChatScreen.contentBlocks = [];
|
|
2007
|
+
this.textChatScreen.renderFilePreview();
|
|
2008
|
+
this.textChatScreen.updateSendButton();
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// Reset states for text messages
|
|
2012
|
+
this.playedAudioIds.clear();
|
|
2013
|
+
this.currentInputAudio = null;
|
|
2014
|
+
this.submissionInProgress = false;
|
|
2015
|
+
this.processingAudioId = null;
|
|
2016
|
+
this.isLoading = true;
|
|
2017
|
+
try {
|
|
2018
|
+
// Always attempt to send message, let backend handle auth
|
|
2019
|
+
await this.sendMessageWithStreaming(messageContent);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
console.error("Error sending message:", error);
|
|
2022
|
+
const errorMessage = {
|
|
2023
|
+
id: Date.now() + 1,
|
|
2024
|
+
content: "Sorry, I encountered an error. Please try again.",
|
|
2025
|
+
sender: "assistant",
|
|
2026
|
+
timestamp: new Date().toISOString(),
|
|
2027
|
+
isError: true
|
|
2028
|
+
};
|
|
2029
|
+
this._addMessageToArray(errorMessage);
|
|
2030
|
+
} finally {
|
|
2031
|
+
this.isLoading = false;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Send message with streaming function adapted from React version
|
|
2036
|
+
async sendMessageWithStreaming(messageContent) {
|
|
2037
|
+
console.log("USER MESSAGE STREAMING", messageContent);
|
|
2038
|
+
console.log("📤 Sending text message - Language:", this.selectedLanguage, "Access token:", this.accessToken ? "Present" : "Not found");
|
|
2039
|
+
const assistantMessage = {
|
|
2040
|
+
id: Date.now() + 1,
|
|
2041
|
+
content: "",
|
|
2042
|
+
sender: "assistant",
|
|
2043
|
+
timestamp: new Date().toISOString(),
|
|
2044
|
+
isStreaming: true,
|
|
2045
|
+
isProcessing: true,
|
|
2046
|
+
textSessionId: Date.now() + 1,
|
|
2047
|
+
hasAudioResponse: false,
|
|
2048
|
+
inputType: "text"
|
|
2049
|
+
};
|
|
2050
|
+
this._addMessageToArray(assistantMessage);
|
|
2051
|
+
let latestRunId = null;
|
|
2052
|
+
try {
|
|
2053
|
+
var _currentAssistantMess;
|
|
2054
|
+
// Get the latest checkpoint to resume from the latest state
|
|
2055
|
+
const latestCheckpoint = await this.getLatestCheckpoint(this.threadId);
|
|
2056
|
+
console.log("📤 Sending message to backend:", _objectSpread2({
|
|
2057
|
+
input: {
|
|
2058
|
+
messages: [{
|
|
2059
|
+
id: "msg-".concat(Date.now()),
|
|
2060
|
+
type: "human",
|
|
2061
|
+
content: messageContent
|
|
2062
|
+
}],
|
|
2063
|
+
user_language: this.selectedLanguage,
|
|
2064
|
+
access_token: "[redacted]",
|
|
2065
|
+
user_info: JSON.stringify(this.userInfo)
|
|
2066
|
+
},
|
|
2067
|
+
config: {
|
|
2068
|
+
configurable: {}
|
|
2069
|
+
},
|
|
2070
|
+
metadata: {
|
|
2071
|
+
supabaseAccessToken: "[redacted]",
|
|
2072
|
+
user_language: this.selectedLanguage,
|
|
2073
|
+
input_type: "text",
|
|
2074
|
+
audio_content: null
|
|
2075
|
+
},
|
|
2076
|
+
stream_mode: ["values", "messages-tuple", "custom"],
|
|
2077
|
+
stream_subgraphs: true,
|
|
2078
|
+
assistant_id: this.assistantId,
|
|
2079
|
+
on_disconnect: "cancel"
|
|
2080
|
+
}, latestCheckpoint && {
|
|
2081
|
+
checkpoint: latestCheckpoint
|
|
2082
|
+
}));
|
|
2083
|
+
const response = await fetch("".concat(this.langgraphUrl, "/threads/").concat(this.threadId, "/runs/stream"), {
|
|
2084
|
+
method: "POST",
|
|
2085
|
+
headers: this.getHeaders(),
|
|
2086
|
+
body: JSON.stringify(_objectSpread2({
|
|
2087
|
+
input: {
|
|
2088
|
+
messages: [{
|
|
2089
|
+
id: "msg-".concat(Date.now()),
|
|
2090
|
+
type: "human",
|
|
2091
|
+
content: messageContent
|
|
2092
|
+
}],
|
|
2093
|
+
user_language: this.selectedLanguage,
|
|
2094
|
+
access_token: this.accessToken,
|
|
2095
|
+
user_info: JSON.stringify(this.userInfo)
|
|
2096
|
+
},
|
|
2097
|
+
config: {
|
|
2098
|
+
configurable: {}
|
|
2099
|
+
},
|
|
2100
|
+
metadata: {
|
|
2101
|
+
supabaseAccessToken: this.supabaseToken,
|
|
2102
|
+
user_language: this.selectedLanguage
|
|
2103
|
+
},
|
|
2104
|
+
stream_mode: ["values", "messages-tuple", "custom"],
|
|
2105
|
+
stream_subgraphs: true,
|
|
2106
|
+
assistant_id: this.assistantId,
|
|
2107
|
+
on_disconnect: "cancel"
|
|
2108
|
+
}, latestCheckpoint && {
|
|
2109
|
+
checkpoint: latestCheckpoint
|
|
2110
|
+
}))
|
|
2111
|
+
});
|
|
2112
|
+
if (!response.ok) {
|
|
2113
|
+
throw new Error("HTTP error! status: ".concat(response.status));
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Handle streaming response
|
|
2117
|
+
const reader = response.body.getReader();
|
|
2118
|
+
const decoder = new TextDecoder();
|
|
2119
|
+
let buffer = "";
|
|
2120
|
+
let currentMessage = ""; // Local to this function scope
|
|
2121
|
+
let bestMessageText = "";
|
|
2122
|
+
let bestMessagePriority = -1;
|
|
2123
|
+
let lastUpdateTime = 0;
|
|
2124
|
+
const UPDATE_THROTTLE = 150; // Throttle updates to max ~7 per second
|
|
2125
|
+
const requestId = Date.now(); // Unique ID for this request
|
|
2126
|
+
let currentEvent = null; // Track current event type
|
|
2127
|
+
let currentData = null; // Track current data
|
|
2128
|
+
let hasReceivedMeaningfulResponse = false; // Track if we've received a meaningful response
|
|
2129
|
+
let finalResponse = ""; // Store the final meaningful response
|
|
2130
|
+
let latestUiAction = null; // Track the last ui_action from the last ui_action_mapper
|
|
2131
|
+
let endEventFinalized = false; // Track if we finalized on 'end' event
|
|
2132
|
+
let endFinalContent = ""; // Content used at 'end' event
|
|
2133
|
+
let currentAdditionalKwargs = null; // Chart-related additional kwargs captured from stream
|
|
2134
|
+
let chartUrlFromViz = null; // If stream provides a chart image URL, store it here and use it for the assistant message
|
|
2135
|
+
|
|
2136
|
+
console.log("🔍 Starting to process streaming response for requestId:", requestId);
|
|
2137
|
+
|
|
2138
|
+
// Prioritize human-readable assistant text over tool/JSON outputs
|
|
2139
|
+
const getNodePriority = nodeKey => {
|
|
2140
|
+
if (!nodeKey) return 0;
|
|
2141
|
+
const key = String(nodeKey).toLowerCase();
|
|
2142
|
+
if (key.includes("mdms_assistant") || key.includes("grievance_assistant") || key.includes("mandiprice_assistant")) return 0;
|
|
2143
|
+
if (key.includes("assistant") && !key.includes("tools")) return 3;
|
|
2144
|
+
if (key.includes("response_processor")) return 2;
|
|
2145
|
+
if (key.includes("post_assistant")) return 2;
|
|
2146
|
+
if (key.includes("ui_action_mapper")) return 1;
|
|
2147
|
+
return 0;
|
|
2148
|
+
};
|
|
2149
|
+
while (true) {
|
|
2150
|
+
const {
|
|
2151
|
+
done,
|
|
2152
|
+
value
|
|
2153
|
+
} = await reader.read();
|
|
2154
|
+
if (done) break;
|
|
2155
|
+
buffer += decoder.decode(value, {
|
|
2156
|
+
stream: true
|
|
2157
|
+
});
|
|
2158
|
+
const lines = buffer.split("\n");
|
|
2159
|
+
buffer = lines.pop() || "";
|
|
2160
|
+
for (const line of lines) {
|
|
2161
|
+
if (line.trim() === "") continue;
|
|
2162
|
+
console.log("📝 Processing SSE line:", {
|
|
2163
|
+
line: line.substring(0, 100) + (line.length > 100 ? "..." : ""),
|
|
2164
|
+
requestId: requestId
|
|
2165
|
+
});
|
|
2166
|
+
if (line.startsWith("data:")) {
|
|
2167
|
+
try {
|
|
2168
|
+
const data = JSON.parse(line.slice(5).trim());
|
|
2169
|
+
if (data.run_id) {
|
|
2170
|
+
console.log("🟢 Run ID:", data.run_id);
|
|
2171
|
+
latestRunId = data.run_id;
|
|
2172
|
+
}
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
console.error("Error parsing JSON:", err, line);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (line.startsWith("event: ")) {
|
|
2178
|
+
currentEvent = line.slice(7).trim();
|
|
2179
|
+
console.log("🎯 Event type:", currentEvent);
|
|
2180
|
+
} else if (line.startsWith("data: ")) {
|
|
2181
|
+
const jsonStr = line.slice(6).trim();
|
|
2182
|
+
if (jsonStr === "[DONE]") {
|
|
2183
|
+
console.log("🏁 Stream completed");
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
const data = JSON.parse(jsonStr);
|
|
2188
|
+
currentData = data;
|
|
2189
|
+
if (data.run_id) {
|
|
2190
|
+
console.log("🟢 Capturing run_id from parsed data:", data.run_id);
|
|
2191
|
+
latestRunId = data.run_id;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Handle end event
|
|
2195
|
+
if (currentEvent === "end") {
|
|
2196
|
+
console.log("🏁 Received event: end - finalizing message with run_id:", latestRunId);
|
|
2197
|
+
if (data.run_id) {
|
|
2198
|
+
latestRunId = data.run_id;
|
|
2199
|
+
console.log("🟢 Updated run_id from end event:", latestRunId);
|
|
2200
|
+
}
|
|
2201
|
+
if (currentMessage && currentMessage.length > 0) {
|
|
2202
|
+
var _this$messages$find;
|
|
2203
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2204
|
+
content: currentMessage,
|
|
2205
|
+
additional_kwargs: currentAdditionalKwargs,
|
|
2206
|
+
isStreaming: false,
|
|
2207
|
+
isProcessing: false,
|
|
2208
|
+
hasAudioResponse: ((_this$messages$find = this.messages.find(m => m.id === assistantMessage.id)) === null || _this$messages$find === void 0 ? void 0 : _this$messages$find.hasAudioResponse) || false,
|
|
2209
|
+
latestRunId: latestRunId
|
|
2210
|
+
});
|
|
2211
|
+
endEventFinalized = true;
|
|
2212
|
+
endFinalContent = currentMessage;
|
|
2213
|
+
} else if (latestRunId) {
|
|
2214
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2215
|
+
latestRunId: latestRunId,
|
|
2216
|
+
isStreaming: false,
|
|
2217
|
+
isProcessing: false
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
console.log("🔍 Parsed SSE data:", {
|
|
2222
|
+
event: currentEvent,
|
|
2223
|
+
hasData: !!data,
|
|
2224
|
+
dataKeys: data ? Object.keys(data) : "no data",
|
|
2225
|
+
topLevelKeys: Object.keys(data),
|
|
2226
|
+
hasAudioContent: data !== null && data !== void 0 && data.audio_content ? "YES" : "NO",
|
|
2227
|
+
hasResponseProcessor: data !== null && data !== void 0 && data.response_processor ? "YES" : "NO",
|
|
2228
|
+
hasRunId: data !== null && data !== void 0 && data.run_id ? "YES" : "NO",
|
|
2229
|
+
runId: (data === null || data === void 0 ? void 0 : data.run_id) || "N/A",
|
|
2230
|
+
requestId: requestId
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
// Handle Authentication from smart_router
|
|
2234
|
+
if (data.smart_router && data.smart_router.access_token && data.smart_router.farmer_profile && data.smart_router.farmer_profile_fetched === true) {
|
|
2235
|
+
var _data$smart_router$fa, _data$smart_router$fa2;
|
|
2236
|
+
console.log("🔐 Authentication data detected in smart_router:", {
|
|
2237
|
+
hasAccessToken: !!data.smart_router.access_token,
|
|
2238
|
+
mobileNumber: data.smart_router.mobile_number,
|
|
2239
|
+
userUuid: data.smart_router.userUuid,
|
|
2240
|
+
hasFarmerProfile: !!data.smart_router.farmer_profile,
|
|
2241
|
+
farmerId: (_data$smart_router$fa = data.smart_router.farmer_profile) === null || _data$smart_router$fa === void 0 ? void 0 : _data$smart_router$fa.individualId,
|
|
2242
|
+
farmerName: (_data$smart_router$fa2 = data.smart_router.farmer_profile) === null || _data$smart_router$fa2 === void 0 ? void 0 : _data$smart_router$fa2.name,
|
|
2243
|
+
requestId: requestId
|
|
2244
|
+
});
|
|
2245
|
+
try {
|
|
2246
|
+
const smartRouter = data.smart_router;
|
|
2247
|
+
const farmerProfile = smartRouter.farmer_profile;
|
|
2248
|
+
const accessToken = smartRouter.access_token;
|
|
2249
|
+
localStorage.setItem("DfsWeb.access-token", accessToken);
|
|
2250
|
+
let userInfo = {};
|
|
2251
|
+
if (farmerProfile.user_details) {
|
|
2252
|
+
userInfo = _objectSpread2({}, farmerProfile.user_details);
|
|
2253
|
+
userInfo.name = farmerProfile.name || userInfo.name || "";
|
|
2254
|
+
if (!userInfo.uuid) {
|
|
2255
|
+
userInfo.uuid = smartRouter.userUuid;
|
|
2256
|
+
}
|
|
2257
|
+
if (!userInfo.mobileNumber) {
|
|
2258
|
+
userInfo.mobileNumber = smartRouter.mobile_number || farmerProfile.mobileNumber;
|
|
2259
|
+
}
|
|
2260
|
+
} else {
|
|
2261
|
+
userInfo = {
|
|
2262
|
+
id: null,
|
|
2263
|
+
uuid: smartRouter.userUuid,
|
|
2264
|
+
userName: smartRouter.mobile_number,
|
|
2265
|
+
name: farmerProfile.name || "",
|
|
2266
|
+
mobileNumber: smartRouter.mobile_number || farmerProfile.mobileNumber,
|
|
2267
|
+
emailId: farmerProfile.email || null,
|
|
2268
|
+
locale: null,
|
|
2269
|
+
type: "CITIZEN",
|
|
2270
|
+
roles: [{
|
|
2271
|
+
name: "Citizen",
|
|
2272
|
+
code: "CITIZEN",
|
|
2273
|
+
tenantId: farmerProfile.tenantId || "br"
|
|
2274
|
+
}],
|
|
2275
|
+
active: true,
|
|
2276
|
+
tenantId: farmerProfile.tenantId || "br",
|
|
2277
|
+
permanentCity: null
|
|
2278
|
+
};
|
|
2279
|
+
this.setUserInfoFromDirectChatLogin(userInfo);
|
|
2280
|
+
}
|
|
2281
|
+
this.userInfo = userInfo;
|
|
2282
|
+
this.accessToken = accessToken;
|
|
2283
|
+
if (userInfo.uuid) {
|
|
2284
|
+
console.log("USER INFO FOR UPDATING THREAD", userInfo);
|
|
2285
|
+
if (this.threadId) {
|
|
2286
|
+
this.updateThread(this.threadId, userInfo.uuid).then(updatedThread => {
|
|
2287
|
+
console.log("✅ Thread updated after authentication:", updatedThread);
|
|
2288
|
+
}).catch(error => {
|
|
2289
|
+
console.error("❌ Error updating thread after authentication:", error.message);
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
this.getUserThreads({
|
|
2293
|
+
userId: userInfo.id,
|
|
2294
|
+
userUuid: userInfo.uuid
|
|
2295
|
+
}).then(data => {
|
|
2296
|
+
console.log("✅ Thread data fetched after authentication:", data);
|
|
2297
|
+
this.setUserThreads(data.threads);
|
|
2298
|
+
this.setUserThreadsMetaData({
|
|
2299
|
+
threads_processed: data.threads_processed,
|
|
2300
|
+
total_history_items: data.total_history_items,
|
|
2301
|
+
total_threads: data.total_threads
|
|
2302
|
+
});
|
|
2303
|
+
}).catch(error => {
|
|
2304
|
+
console.error("❌ Error fetching threads after authentication:", error.message);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
} catch (authError) {
|
|
2308
|
+
console.error("❌ Error processing authentication data:", authError);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// Handle UI Actions
|
|
2313
|
+
let uiActionSource = null;
|
|
2314
|
+
let actions = null;
|
|
2315
|
+
if (data.response_processor && Array.isArray(data.response_processor.ui_actions)) {
|
|
2316
|
+
actions = data.response_processor.ui_actions;
|
|
2317
|
+
} else if (data.response_processor && data.response_processor.messages && Array.isArray(data.response_processor.messages) && data.response_processor.messages.length > 0 && data.response_processor.messages[0].additional_kwargs && Array.isArray(data.response_processor.messages[0].additional_kwargs.ui_actions)) {
|
|
2318
|
+
actions = data.response_processor.messages[0].additional_kwargs.ui_actions;
|
|
2319
|
+
} else if (data.ui_action_mapper && Array.isArray(data.ui_action_mapper.ui_actions)) {
|
|
2320
|
+
uiActionSource = data.ui_action_mapper;
|
|
2321
|
+
actions = uiActionSource.ui_actions;
|
|
2322
|
+
} else if (data.mdms_ui_action_mapper && Array.isArray(data.mdms_ui_action_mapper.ui_actions)) {
|
|
2323
|
+
uiActionSource = data.mdms_ui_action_mapper;
|
|
2324
|
+
actions = uiActionSource.ui_actions;
|
|
2325
|
+
}
|
|
2326
|
+
if (actions && actions.length > 0) {
|
|
2327
|
+
console.log("UI ACTIONS", actions);
|
|
2328
|
+
const action = actions[actions.length - 1];
|
|
2329
|
+
if (action.web && action.web.link) {
|
|
2330
|
+
var _action$web$parameter, _action$web$parameter2, _action$web$parameter3, _action$web$parameter4, _action$web$parameter5;
|
|
2331
|
+
let scrollToId = null;
|
|
2332
|
+
let navigationParams = {};
|
|
2333
|
+
if (action.web.parameters) {
|
|
2334
|
+
if (action.web.parameters.scrollTo) {
|
|
2335
|
+
scrollToId = action.web.parameters.scrollTo;
|
|
2336
|
+
}
|
|
2337
|
+
navigationParams = _objectSpread2({}, action.web.parameters);
|
|
2338
|
+
} else if (action.web.link && action.web.link.includes("scrollTo=")) {
|
|
2339
|
+
const urlParams = new URLSearchParams(action.web.link.split("?")[1]);
|
|
2340
|
+
scrollToId = urlParams.get("scrollTo");
|
|
2341
|
+
}
|
|
2342
|
+
let finalLink = action.web.link;
|
|
2343
|
+
let finalMessage = action.message || "";
|
|
2344
|
+
const hasSchemeId = ((_action$web$parameter = action.web.parameters) === null || _action$web$parameter === void 0 ? void 0 : _action$web$parameter.schemeId) && action.web.parameters.schemeId !== "";
|
|
2345
|
+
let hasHelpName = null;
|
|
2346
|
+
if ((_action$web$parameter2 = action.web.parameters) !== null && _action$web$parameter2 !== void 0 && _action$web$parameter2.button_title) {
|
|
2347
|
+
hasHelpName = true;
|
|
2348
|
+
} else if ((_action$web$parameter3 = action.web.parameters) !== null && _action$web$parameter3 !== void 0 && _action$web$parameter3.name) {
|
|
2349
|
+
hasHelpName = true;
|
|
2350
|
+
} else {
|
|
2351
|
+
hasHelpName = false;
|
|
2352
|
+
}
|
|
2353
|
+
if (!hasSchemeId && !hasHelpName) {
|
|
2354
|
+
finalLink = "/help";
|
|
2355
|
+
finalMessage = "Please find more information about available schemes and services";
|
|
2356
|
+
}
|
|
2357
|
+
latestUiAction = {
|
|
2358
|
+
content: "",
|
|
2359
|
+
url: finalLink && !finalLink.startsWith("http") ? window.location.origin + finalLink : finalLink,
|
|
2360
|
+
originalUrl: finalLink,
|
|
2361
|
+
uiActionType: action.ui_action,
|
|
2362
|
+
scrollToId: scrollToId,
|
|
2363
|
+
navigationParams: _objectSpread2(_objectSpread2({}, navigationParams), hasHelpName && {
|
|
2364
|
+
name: ((_action$web$parameter4 = action.web.parameters) === null || _action$web$parameter4 === void 0 ? void 0 : _action$web$parameter4.button_title) || ((_action$web$parameter5 = action.web.parameters) === null || _action$web$parameter5 === void 0 ? void 0 : _action$web$parameter5.name)
|
|
2365
|
+
})
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// Chart Image Extraction
|
|
2371
|
+
if (data.analytics_visualization_node && data.analytics_visualization_node.messages) {
|
|
2372
|
+
const vizMessages = data.analytics_visualization_node.messages;
|
|
2373
|
+
if (vizMessages.length > 0) {
|
|
2374
|
+
const vizMessage = vizMessages[vizMessages.length - 1];
|
|
2375
|
+
if (typeof vizMessage === "string" && vizMessage.includes("additional_kwargs")) {
|
|
2376
|
+
const kwargsStart = vizMessage.indexOf("additional_kwargs=") + 18;
|
|
2377
|
+
const kwargsEnd = vizMessage.indexOf("} response_metadata");
|
|
2378
|
+
if (kwargsStart < kwargsEnd) {
|
|
2379
|
+
const kwargsString = vizMessage.substring(kwargsStart, kwargsEnd + 1);
|
|
2380
|
+
try {
|
|
2381
|
+
let cleanedKwargsString = kwargsString.replace(/'/g, '"').replace(/None/g, "null").replace(/True/g, "true").replace(/False/g, "false");
|
|
2382
|
+
const additionalKwargs = JSON.parse(cleanedKwargsString);
|
|
2383
|
+
if (additionalKwargs.chart_image_url) {
|
|
2384
|
+
console.log("🎯 Found chart image URL:", additionalKwargs.chart_image_url);
|
|
2385
|
+
currentAdditionalKwargs = additionalKwargs;
|
|
2386
|
+
try {
|
|
2387
|
+
const chartUrl = String(additionalKwargs.chart_image_url).trim();
|
|
2388
|
+
if (chartUrl.startsWith("http")) {
|
|
2389
|
+
chartUrlFromViz = chartUrl;
|
|
2390
|
+
console.log("🔖 Stored chartUrlFromViz:", chartUrlFromViz);
|
|
2391
|
+
}
|
|
2392
|
+
} catch (e) {
|
|
2393
|
+
console.warn("Failed to store chart image URL:", e);
|
|
2394
|
+
}
|
|
2395
|
+
} else if (additionalKwargs.antv_chart_data) {
|
|
2396
|
+
console.log("🎯 Found chart data (fallback):", additionalKwargs.antv_chart_data);
|
|
2397
|
+
currentAdditionalKwargs = additionalKwargs;
|
|
2398
|
+
}
|
|
2399
|
+
} catch (e) {
|
|
2400
|
+
console.warn("Failed to parse chart image additional_kwargs:", e);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
if (typeof vizMessage === "string") {
|
|
2405
|
+
const contentMatch = vizMessage.match(/content=['"]([^'"]+)['"]/);
|
|
2406
|
+
if (contentMatch && contentMatch[1]) {
|
|
2407
|
+
const extractedUrl = contentMatch[1].trim();
|
|
2408
|
+
if (extractedUrl.startsWith("http")) {
|
|
2409
|
+
chartUrlFromViz = extractedUrl;
|
|
2410
|
+
console.log("🔖 Stored extracted chartUrlFromViz:", chartUrlFromViz);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
// Generic Content Extraction
|
|
2418
|
+
let lastContent = null;
|
|
2419
|
+
let lastNodeKey = null;
|
|
2420
|
+
let lastNodeMessages = null;
|
|
2421
|
+
Object.keys(data).forEach(nodeKey => {
|
|
2422
|
+
if (nodeKey !== "audio_content" && nodeKey !== "audio_processor" && nodeKey !== "ui_action_mapper") {
|
|
2423
|
+
const nodeData = data[nodeKey];
|
|
2424
|
+
if (nodeData && Array.isArray(nodeData.messages) && nodeData.messages.length > 0) {
|
|
2425
|
+
lastNodeKey = nodeKey;
|
|
2426
|
+
lastNodeMessages = nodeData.messages;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
});
|
|
2430
|
+
if (lastNodeMessages && lastNodeMessages.length > 0) {
|
|
2431
|
+
let lastMessage = lastNodeMessages[lastNodeMessages.length - 1];
|
|
2432
|
+
let content = "";
|
|
2433
|
+
if (typeof lastMessage === "string") {
|
|
2434
|
+
if (lastMessage.startsWith("content='") && lastMessage.includes("' additional_kwargs")) {
|
|
2435
|
+
const start = lastMessage.indexOf("content='") + 9;
|
|
2436
|
+
const end = lastMessage.indexOf("' additional_kwargs");
|
|
2437
|
+
content = lastMessage.substring(start, end).replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
2438
|
+
} else if (lastMessage.startsWith('content="') && lastMessage.includes('" additional_kwargs')) {
|
|
2439
|
+
const start = lastMessage.indexOf('content="') + 9;
|
|
2440
|
+
const end = lastMessage.indexOf('" additional_kwargs');
|
|
2441
|
+
content = lastMessage.substring(start, end).replace(/\\n/g, "\n").replace(/\\t/g, "\t").replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
2442
|
+
} else {
|
|
2443
|
+
const singleQuoteMatch = lastMessage.match(/content='([^']+)'/);
|
|
2444
|
+
const doubleQuoteMatch = lastMessage.match(/content="([^"]+)"/);
|
|
2445
|
+
if (singleQuoteMatch) {
|
|
2446
|
+
content = singleQuoteMatch[1];
|
|
2447
|
+
} else if (doubleQuoteMatch) {
|
|
2448
|
+
content = doubleQuoteMatch[1];
|
|
2449
|
+
} else {
|
|
2450
|
+
content = lastMessage;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
} else if (lastMessage && typeof lastMessage === "object") {
|
|
2454
|
+
if (typeof lastMessage.content === "string") {
|
|
2455
|
+
content = lastMessage.content;
|
|
2456
|
+
} else if (Array.isArray(lastMessage.content) && lastMessage.content.length > 0) {
|
|
2457
|
+
const textParts = lastMessage.content.map(part => typeof part === "string" ? part : typeof (part === null || part === void 0 ? void 0 : part.text) === "string" ? part.text : typeof (part === null || part === void 0 ? void 0 : part.value) === "string" ? part.value : "").filter(Boolean);
|
|
2458
|
+
content = textParts.join(" ").trim();
|
|
2459
|
+
} else {
|
|
2460
|
+
content = "";
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (typeof content === "string" && content.trim().length > 0) {
|
|
2464
|
+
const looksLikeJson = content.trim().startsWith("{") || content.trim().startsWith("[");
|
|
2465
|
+
const priority = getNodePriority(lastNodeKey);
|
|
2466
|
+
if (looksLikeJson && priority < 2) {
|
|
2467
|
+
console.log("\u23ED\uFE0F Skipping JSON-like content from ".concat(lastNodeKey));
|
|
2468
|
+
} else {
|
|
2469
|
+
const MAX_LEN = 8000;
|
|
2470
|
+
const safeContent = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n\n… (truncated)" : content;
|
|
2471
|
+
currentMessage = safeContent;
|
|
2472
|
+
if (priority > bestMessagePriority || priority === bestMessagePriority && safeContent.length > bestMessageText.length) {
|
|
2473
|
+
bestMessageText = safeContent;
|
|
2474
|
+
bestMessagePriority = priority;
|
|
2475
|
+
}
|
|
2476
|
+
hasReceivedMeaningfulResponse = true;
|
|
2477
|
+
console.log("\uD83D\uDFE2 Found content from ".concat(lastNodeKey, ":"), safeContent.substring(0, 100) + "...");
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// Process based on event type
|
|
2483
|
+
if (data) {
|
|
2484
|
+
// Set default event type for data messages without explicit event
|
|
2485
|
+
const eventType = currentEvent || "values";
|
|
2486
|
+
console.log("🎵 Processing data message:", {
|
|
2487
|
+
eventType: eventType,
|
|
2488
|
+
currentEvent: currentEvent,
|
|
2489
|
+
dataKeys: Object.keys(data),
|
|
2490
|
+
requestId: requestId
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
// Handle audio content in response
|
|
2494
|
+
if (data.audio_content && typeof data.audio_content === "string") {
|
|
2495
|
+
console.log("🎵 AUDIO CONTENT FOUND in text stream:", {
|
|
2496
|
+
audioContentLength: data.audio_content.length,
|
|
2497
|
+
audioContentPreview: data.audio_content.substring(0, 20) + "..."
|
|
2498
|
+
});
|
|
2499
|
+
const audioResponseId = "text_response_".concat(requestId, "_").concat(data.audio_content.substring(0, 30));
|
|
2500
|
+
const alreadyPlayed = this.playedAudioIds.has(audioResponseId);
|
|
2501
|
+
console.log("🔊 Text stream audio content received:", {
|
|
2502
|
+
audioResponseId: audioResponseId,
|
|
2503
|
+
requestId: requestId,
|
|
2504
|
+
alreadyPlayed: alreadyPlayed,
|
|
2505
|
+
audioContentLength: data.audio_content.length
|
|
2506
|
+
});
|
|
2507
|
+
if (!alreadyPlayed) {
|
|
2508
|
+
this.playedAudioIds.add(audioResponseId);
|
|
2509
|
+
// Mark the current assistant message as having audio response
|
|
2510
|
+
console.log("🔍 Setting audio content for message:", {
|
|
2511
|
+
targetMessageId: assistantMessage.id,
|
|
2512
|
+
audioContentLength: data.audio_content.length,
|
|
2513
|
+
audioContentPreview: data.audio_content.substring(0, 20) + "..."
|
|
2514
|
+
});
|
|
2515
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2516
|
+
hasAudioResponse: true,
|
|
2517
|
+
audioContent: data.audio_content,
|
|
2518
|
+
latestRunId: latestRunId
|
|
2519
|
+
});
|
|
2520
|
+
console.log("✅ Stored audio response (text stream)");
|
|
2521
|
+
|
|
2522
|
+
// Clean up old audio IDs to prevent memory issues
|
|
2523
|
+
if (this.playedAudioIds.size > 10) {
|
|
2524
|
+
const audioIds = Array.from(this.playedAudioIds);
|
|
2525
|
+
this.playedAudioIds.clear();
|
|
2526
|
+
audioIds.slice(-5).forEach(id => this.playedAudioIds.add(id));
|
|
2527
|
+
}
|
|
2528
|
+
} else {
|
|
2529
|
+
console.log("⏭️ Skipping audio: already played");
|
|
2530
|
+
}
|
|
2531
|
+
} else {
|
|
2532
|
+
console.log("❌ No audio_content found in text stream data");
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// Handle audio content in response_processor
|
|
2536
|
+
if (data.response_processor && data.response_processor.audio_content && typeof data.response_processor.audio_content === "string") {
|
|
2537
|
+
console.log("🔍 Found audio_content in response_processor:", {
|
|
2538
|
+
audioContentLength: data.response_processor.audio_content.length,
|
|
2539
|
+
audioContentPreview: data.response_processor.audio_content.substring(0, 50) + "..."
|
|
2540
|
+
});
|
|
2541
|
+
const audioResponseId = "text_response_".concat(requestId, "_").concat(data.response_processor.audio_content.substring(0, 30));
|
|
2542
|
+
const alreadyPlayed = this.playedAudioIds.has(audioResponseId);
|
|
2543
|
+
console.log("🔊 Response processor audio content received:", {
|
|
2544
|
+
audioResponseId: audioResponseId,
|
|
2545
|
+
requestId: requestId,
|
|
2546
|
+
alreadyPlayed: alreadyPlayed,
|
|
2547
|
+
audioContentLength: data.response_processor.audio_content.length
|
|
2548
|
+
});
|
|
2549
|
+
if (!alreadyPlayed) {
|
|
2550
|
+
this.playedAudioIds.add(audioResponseId);
|
|
2551
|
+
|
|
2552
|
+
// Mark the current assistant message as having audio response
|
|
2553
|
+
console.log("🔍 Setting response processor audio content for message:", {
|
|
2554
|
+
targetMessageId: assistantMessage.id,
|
|
2555
|
+
audioContentLength: data.response_processor.audio_content.length,
|
|
2556
|
+
audioContentPreview: data.response_processor.audio_content.substring(0, 20) + "..."
|
|
2557
|
+
});
|
|
2558
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2559
|
+
latestRunId: latestRunId,
|
|
2560
|
+
hasAudioResponse: true,
|
|
2561
|
+
audioContent: data.response_processor.audio_content
|
|
2562
|
+
});
|
|
2563
|
+
console.log("✅ Found matching message, setting response processor audio content");
|
|
2564
|
+
|
|
2565
|
+
// Clean up old audio IDs to prevent memory issues
|
|
2566
|
+
if (this.playedAudioIds.size > 10) {
|
|
2567
|
+
const audioIds = Array.from(this.playedAudioIds);
|
|
2568
|
+
this.playedAudioIds.clear();
|
|
2569
|
+
audioIds.slice(-5).forEach(id => this.playedAudioIds.add(id));
|
|
2570
|
+
}
|
|
2571
|
+
} else {
|
|
2572
|
+
console.log("⏭️ Skipping response processor audio: already played");
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
} catch (parseError) {
|
|
2577
|
+
console.log("⚠️ Failed to parse streaming data:", parseError);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// Ensure streaming is marked as complete for this specific assistant message
|
|
2584
|
+
console.log("🏁 Finalizing assistant message:", assistantMessage.id);
|
|
2585
|
+
|
|
2586
|
+
// Add a small delay to ensure audio content is properly set
|
|
2587
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2588
|
+
|
|
2589
|
+
// Get the current state to ensure we have the latest audio content
|
|
2590
|
+
const currentAssistantMessage = this.messages.find(msg => msg.id === assistantMessage.id);
|
|
2591
|
+
console.log("🔍 Current assistant message before finalization:", {
|
|
2592
|
+
messageId: assistantMessage.id,
|
|
2593
|
+
hasAudioContent: !!(currentAssistantMessage !== null && currentAssistantMessage !== void 0 && currentAssistantMessage.audioContent),
|
|
2594
|
+
hasAudioResponse: currentAssistantMessage === null || currentAssistantMessage === void 0 ? void 0 : currentAssistantMessage.hasAudioResponse,
|
|
2595
|
+
audioContentLength: (currentAssistantMessage === null || currentAssistantMessage === void 0 || (_currentAssistantMess = currentAssistantMessage.audioContent) === null || _currentAssistantMess === void 0 ? void 0 : _currentAssistantMess.length) || 0
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
// If message was already finalized at 'end' with content, do not override
|
|
2599
|
+
if (endEventFinalized && endFinalContent && endFinalContent.length > 10) {
|
|
2600
|
+
console.log("✅ Message already finalized on end event; skipping overwrite");
|
|
2601
|
+
} else {
|
|
2602
|
+
// Check if we have a meaningful response to display
|
|
2603
|
+
const finalText = bestMessageText && bestMessageText.length > 5 ? bestMessageText : currentMessage;
|
|
2604
|
+
// If a chart URL was captured from the viz node, prefer it and show only the image
|
|
2605
|
+
const contentToSet = chartUrlFromViz && String(chartUrlFromViz).startsWith("http") ? chartUrlFromViz : finalText;
|
|
2606
|
+
if (contentToSet && contentToSet.length > 10) {
|
|
2607
|
+
var _currentMsg$audioCont;
|
|
2608
|
+
console.log("🔍 Finalizing message with content:", {
|
|
2609
|
+
messageId: assistantMessage.id,
|
|
2610
|
+
currentMessageLength: String(contentToSet).length,
|
|
2611
|
+
currentMessagePreview: String(contentToSet).substring(0, 50) + "..."
|
|
2612
|
+
});
|
|
2613
|
+
const currentMsg = this.messages.find(m => m.id === assistantMessage.id);
|
|
2614
|
+
console.log("🔍 Finalizing message:", {
|
|
2615
|
+
latestRunId,
|
|
2616
|
+
messageId: assistantMessage.id,
|
|
2617
|
+
existingAudioContent: !!(currentMsg !== null && currentMsg !== void 0 && currentMsg.audioContent),
|
|
2618
|
+
existingAudioResponse: currentMsg === null || currentMsg === void 0 ? void 0 : currentMsg.hasAudioResponse,
|
|
2619
|
+
audioContentLength: (currentMsg === null || currentMsg === void 0 || (_currentMsg$audioCont = currentMsg.audioContent) === null || _currentMsg$audioCont === void 0 ? void 0 : _currentMsg$audioCont.length) || 0
|
|
2620
|
+
});
|
|
2621
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2622
|
+
latestRunId: latestRunId,
|
|
2623
|
+
content: contentToSet,
|
|
2624
|
+
additional_kwargs: currentAdditionalKwargs,
|
|
2625
|
+
isStreaming: false,
|
|
2626
|
+
isProcessing: false,
|
|
2627
|
+
hasAudioResponse: (currentMsg === null || currentMsg === void 0 ? void 0 : currentMsg.hasAudioResponse) || false,
|
|
2628
|
+
// Preserve existing audio response state
|
|
2629
|
+
audioContent: (currentMsg === null || currentMsg === void 0 ? void 0 : currentMsg.audioContent) || null // Preserve existing audio content
|
|
2630
|
+
});
|
|
2631
|
+
} else {
|
|
2632
|
+
// If no meaningful response was found, show a clear, user-friendly fallback
|
|
2633
|
+
console.log("🔍 Finalizing message with friendly fallback");
|
|
2634
|
+
const currentMsg = this.messages.find(m => m.id === assistantMessage.id);
|
|
2635
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2636
|
+
content: "Processing your request...",
|
|
2637
|
+
latestRunId: latestRunId,
|
|
2638
|
+
additional_kwargs: currentAdditionalKwargs,
|
|
2639
|
+
isStreaming: false,
|
|
2640
|
+
isProcessing: false,
|
|
2641
|
+
hasAudioResponse: (currentMsg === null || currentMsg === void 0 ? void 0 : currentMsg.hasAudioResponse) || false,
|
|
2642
|
+
audioContent: (currentMsg === null || currentMsg === void 0 ? void 0 : currentMsg.audioContent) || null
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Handle UI actions if present
|
|
2648
|
+
if (latestUiAction) {
|
|
2649
|
+
console.log("🔗 Adding UI action message:", latestUiAction);
|
|
2650
|
+
const uiActionMessage = {
|
|
2651
|
+
id: Date.now() + 2,
|
|
2652
|
+
content: "",
|
|
2653
|
+
sender: "assistant",
|
|
2654
|
+
timestamp: new Date().toISOString(),
|
|
2655
|
+
isUiAction: true,
|
|
2656
|
+
url: latestUiAction.url,
|
|
2657
|
+
uiActionType: latestUiAction.uiActionType,
|
|
2658
|
+
scrollToId: latestUiAction.scrollToId,
|
|
2659
|
+
navigationParams: latestUiAction.navigationParams,
|
|
2660
|
+
originalUrl: latestUiAction.originalUrl,
|
|
2661
|
+
isScrollAction: latestUiAction.uiActionType === "scroll",
|
|
2662
|
+
isNavigateStateAction: latestUiAction.uiActionType === "navigate-state"
|
|
2663
|
+
};
|
|
2664
|
+
const lastMessage = this.messages[this.messages.length - 1];
|
|
2665
|
+
if (lastMessage) {
|
|
2666
|
+
const updatedLastMessage = _objectSpread2(_objectSpread2({}, lastMessage), {}, {
|
|
2667
|
+
uiActionMessage,
|
|
2668
|
+
latestRunId
|
|
2669
|
+
});
|
|
2670
|
+
console.log("UPDATED LAST MESSAGE", updatedLastMessage);
|
|
2671
|
+
this._updateMessageInArray(lastMessage.id, {
|
|
2672
|
+
uiActionMessage,
|
|
2673
|
+
latestRunId
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
console.error("❌ Streaming error:", error);
|
|
2679
|
+
const currentMsg = this.messages.find(m => m.id === assistantMessage.id);
|
|
2680
|
+
this._updateMessageInArray(assistantMessage.id, {
|
|
2681
|
+
isStreaming: false,
|
|
2682
|
+
isProcessing: false,
|
|
2683
|
+
isError: true,
|
|
2684
|
+
content: "Error occurred while processing request",
|
|
2685
|
+
latestRunId: latestRunId || (currentMsg === null || currentMsg === void 0 ? void 0 : currentMsg.latestRunId) // Preserve or set latestRunId
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
// Fetch assistant ID from API
|
|
2691
|
+
async fetchAssistantId() {
|
|
2692
|
+
try {
|
|
2693
|
+
console.log("🔍 Fetching assistant ID from API...");
|
|
2694
|
+
|
|
2695
|
+
// Use provided authToken or fallback to accessToken
|
|
2696
|
+
const authToken = this.authToken || this.accessToken;
|
|
2697
|
+
const supabaseToken = this.supabaseToken || authToken;
|
|
2698
|
+
const response = await fetch("".concat(this.langgraphUrl, "/assistants/search"), {
|
|
2699
|
+
method: "POST",
|
|
2700
|
+
headers: {
|
|
2701
|
+
"Content-Type": "application/json",
|
|
2702
|
+
Authorization: "Bearer ".concat(authToken),
|
|
2703
|
+
"x-supabase-access-token": supabaseToken,
|
|
2704
|
+
Origin: window.location.origin || "*"
|
|
2705
|
+
},
|
|
2706
|
+
body: JSON.stringify({
|
|
2707
|
+
limit: 100,
|
|
2708
|
+
offset: 0
|
|
2709
|
+
})
|
|
2710
|
+
});
|
|
2711
|
+
if (!response.ok) {
|
|
2712
|
+
console.log("\u274C API call failed with status: ".concat(response.status, " - backend may have rejected due to auth"));
|
|
2713
|
+
return null; // Return null instead of throwing to let backend handle auth
|
|
2714
|
+
}
|
|
2715
|
+
const assistants = await response.json();
|
|
2716
|
+
console.log("📋 Available assistants:", assistants);
|
|
2717
|
+
|
|
2718
|
+
// Log assistant details for debugging
|
|
2719
|
+
assistants.forEach((assistant, index) => {
|
|
2720
|
+
console.log("Assistant ".concat(index, ":"), {
|
|
2721
|
+
id: assistant.assistant_id,
|
|
2722
|
+
name: assistant.name,
|
|
2723
|
+
hasName: !!assistant.name,
|
|
2724
|
+
type: typeof assistant.name
|
|
2725
|
+
});
|
|
2726
|
+
});
|
|
2727
|
+
|
|
2728
|
+
// Find the assistant with name "Default Assistant"
|
|
2729
|
+
const defaultAssistant = assistants.filter(assistant => assistant.name && assistant.name.includes("Default"))[0];
|
|
2730
|
+
if (defaultAssistant) {
|
|
2731
|
+
console.log("✅ Found Default Assistant:", defaultAssistant.assistant_id);
|
|
2732
|
+
this.assistantId = defaultAssistant.assistant_id;
|
|
2733
|
+
return defaultAssistant.assistant_id;
|
|
2734
|
+
} else {
|
|
2735
|
+
console.log("⚠️ Default Assistant not found in the list");
|
|
2736
|
+
|
|
2737
|
+
// Find first assistant with a valid name
|
|
2738
|
+
const firstValidAssistant = assistants.find(assistant => assistant.name && assistant.name.trim() !== "");
|
|
2739
|
+
if (firstValidAssistant) {
|
|
2740
|
+
console.log("⚠️ Using first valid assistant as fallback:", firstValidAssistant.assistant_id, "Name:", firstValidAssistant.name);
|
|
2741
|
+
this.assistantId = firstValidAssistant.assistant_id;
|
|
2742
|
+
return firstValidAssistant.assistant_id;
|
|
2743
|
+
} else {
|
|
2744
|
+
console.log("❌ No assistants with valid names available");
|
|
2745
|
+
return null;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
console.error("❌ Error fetching assistant ID:", error);
|
|
2750
|
+
console.log("🔍 Error details:", {
|
|
2751
|
+
name: error.name,
|
|
2752
|
+
message: error.message,
|
|
2753
|
+
isNetworkError: error.name === "TypeError"
|
|
2754
|
+
});
|
|
2755
|
+
return null; // Return null instead of throwing to let backend handle auth
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// Load conversation history for a thread
|
|
2760
|
+
async loadHistory(threadId) {
|
|
2761
|
+
try {
|
|
2762
|
+
const response = await fetch("".concat(this.langgraphUrl, "/threads/").concat(threadId, "/history"), {
|
|
2763
|
+
method: "POST",
|
|
2764
|
+
headers: this.getHeaders(),
|
|
2765
|
+
body: JSON.stringify({
|
|
2766
|
+
limit: 10
|
|
2767
|
+
})
|
|
2768
|
+
});
|
|
2769
|
+
if (response.ok) {
|
|
2770
|
+
var _latestState$values;
|
|
2771
|
+
const history = await response.json();
|
|
2772
|
+
console.log("📜 Loaded history:", history);
|
|
2773
|
+
|
|
2774
|
+
// Get only the LATEST state (first item) which contains the complete conversation
|
|
2775
|
+
const latestState = history[0];
|
|
2776
|
+
if (!(latestState !== null && latestState !== void 0 && (_latestState$values = latestState.values) !== null && _latestState$values !== void 0 && _latestState$values.messages)) {
|
|
2777
|
+
console.log("No messages found in history");
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// Convert messages from the latest state only
|
|
2782
|
+
const historyMessages = [];
|
|
2783
|
+
latestState.values.messages.forEach((msg, msgIndex) => {
|
|
2784
|
+
if (msg.type === "human") {
|
|
2785
|
+
// Handle both text and audio content
|
|
2786
|
+
let content = "";
|
|
2787
|
+
let isAudio = false;
|
|
2788
|
+
if (Array.isArray(msg.content)) {
|
|
2789
|
+
const textContent = msg.content.find(c => c.type === "text");
|
|
2790
|
+
const audioContent = msg.content.find(c => c.type === "audio");
|
|
2791
|
+
if (audioContent) {
|
|
2792
|
+
content = "🎤 Audio message";
|
|
2793
|
+
isAudio = true;
|
|
2794
|
+
} else if (textContent) {
|
|
2795
|
+
content = textContent.text;
|
|
2796
|
+
}
|
|
2797
|
+
} else {
|
|
2798
|
+
content = msg.content;
|
|
2799
|
+
}
|
|
2800
|
+
historyMessages.push({
|
|
2801
|
+
id: "history-".concat(msgIndex, "-human"),
|
|
2802
|
+
content: content,
|
|
2803
|
+
sender: "user",
|
|
2804
|
+
timestamp: new Date().toISOString(),
|
|
2805
|
+
isAudio: isAudio
|
|
2806
|
+
});
|
|
2807
|
+
} else if (msg.type === "ai") {
|
|
2808
|
+
historyMessages.push({
|
|
2809
|
+
id: "history-".concat(msgIndex, "-ai"),
|
|
2810
|
+
content: msg.content,
|
|
2811
|
+
sender: "assistant",
|
|
2812
|
+
timestamp: new Date().toISOString()
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
if (historyMessages && historyMessages.length > 0) {
|
|
2817
|
+
console.log("\uD83D\uDCDD Loading ".concat(historyMessages.length, " messages from history"));
|
|
2818
|
+
// Add history messages to the messages array
|
|
2819
|
+
historyMessages.forEach(msg => {
|
|
2820
|
+
this._addMessageToArray(msg);
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
} catch (error) {
|
|
2825
|
+
console.error("❌ Failed to load history:", error);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// Initialize chat - fetch assistant ID and create thread
|
|
2830
|
+
async initializeChat() {
|
|
2831
|
+
try {
|
|
2832
|
+
console.log("🔄 Initializing chat...");
|
|
2833
|
+
|
|
2834
|
+
// Always attempt API calls, let backend handle authentication
|
|
2835
|
+
const currentToken = localStorage.getItem("DfsWeb.access-token");
|
|
2836
|
+
console.log("🔑 Access token status:", currentToken ? "Present" : "Empty - backend will handle");
|
|
2837
|
+
|
|
2838
|
+
// Always attempt to fetch assistant ID
|
|
2839
|
+
console.log("📞 Making API call to /assistants/search...");
|
|
2840
|
+
const fetchedAssistantId = await this.fetchAssistantId();
|
|
2841
|
+
if (!fetchedAssistantId) {
|
|
2842
|
+
console.log("❌ Failed to fetch assistant ID - backend may have rejected due to auth");
|
|
2843
|
+
this.isConnected = false;
|
|
2844
|
+
this.initialized = true;
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
console.log("✅ Assistant ID fetched successfully:", fetchedAssistantId);
|
|
2848
|
+
const storedUser = localStorage.getItem("DfsWeb.user-info");
|
|
2849
|
+
const user = storedUser ? JSON.parse(storedUser) : null;
|
|
2850
|
+
|
|
2851
|
+
// Always attempt to create thread
|
|
2852
|
+
console.log("📞 Making API call to /threads...");
|
|
2853
|
+
const response = await fetch("".concat(this.langgraphUrl, "/threads"), {
|
|
2854
|
+
method: "POST",
|
|
2855
|
+
headers: this.getHeaders(),
|
|
2856
|
+
body: JSON.stringify({
|
|
2857
|
+
metadata: {
|
|
2858
|
+
user_id: (user === null || user === void 0 ? void 0 : user.id) || null,
|
|
2859
|
+
user_uuid: (user === null || user === void 0 ? void 0 : user.uuid) || null
|
|
2860
|
+
}
|
|
2861
|
+
})
|
|
2862
|
+
});
|
|
2863
|
+
if (response.ok) {
|
|
2864
|
+
const thread = await response.json();
|
|
2865
|
+
this.threadId = thread.thread_id;
|
|
2866
|
+
this.isConnected = true;
|
|
2867
|
+
console.log("✅ Thread created:", thread.thread_id, "\n", thread);
|
|
2868
|
+
|
|
2869
|
+
// Load conversation history
|
|
2870
|
+
await this.loadHistory(thread.thread_id);
|
|
2871
|
+
} else {
|
|
2872
|
+
console.log("Thread created NOT");
|
|
2873
|
+
const errorText = await response.text();
|
|
2874
|
+
console.error("❌ Failed to create thread:", response.status, errorText);
|
|
2875
|
+
this.isConnected = false;
|
|
2876
|
+
}
|
|
2877
|
+
this.initialized = true;
|
|
2878
|
+
} catch (error) {
|
|
2879
|
+
console.error("❌ Thread creation error:", error);
|
|
2880
|
+
this.isConnected = false;
|
|
2881
|
+
this.initialized = true;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
105
2885
|
|
|
106
|
-
export {
|
|
2886
|
+
export { ChatWidget as default };
|