open-chat-studio-widget 0.4.8 → 0.5.1
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/README.md +23 -20
- package/dist/cjs/{index-AhSI5tER.js → index-Du0PBL6n.js} +4 -2
- package/dist/cjs/index-Du0PBL6n.js.map +1 -0
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/open-chat-studio-widget.cjs.entry.js +1116 -301
- package/dist/cjs/open-chat-studio-widget.cjs.entry.js.map +1 -1
- package/dist/cjs/open-chat-studio-widget.cjs.js +2 -2
- package/dist/cjs/open-chat-studio-widget.entry.cjs.js.map +1 -1
- package/dist/collection/components/ocs-chat/{heroicons.js → icons.js} +23 -1
- package/dist/collection/components/ocs-chat/icons.js.map +1 -0
- package/dist/collection/components/ocs-chat/ocs-chat.css +596 -1983
- package/dist/collection/components/ocs-chat/ocs-chat.js +480 -273
- package/dist/collection/components/ocs-chat/ocs-chat.js.map +1 -1
- package/dist/collection/services/chat-session-service.js +145 -0
- package/dist/collection/services/chat-session-service.js.map +1 -0
- package/dist/collection/services/file-attachment-manager.js +125 -0
- package/dist/collection/services/file-attachment-manager.js.map +1 -0
- package/dist/collection/utils/cookies.js +5 -12
- package/dist/collection/utils/cookies.js.map +1 -1
- package/dist/collection/utils/markdown.js +1 -1
- package/dist/collection/utils/markdown.js.map +1 -1
- package/dist/collection/utils/translations.js +99 -0
- package/dist/collection/utils/translations.js.map +1 -0
- package/dist/components/open-chat-studio-widget.js +1122 -302
- package/dist/components/open-chat-studio-widget.js.map +1 -1
- package/dist/esm/{index-DkJ7OJTS.js → index-CX3v6rTy.js} +4 -3
- package/dist/esm/index-CX3v6rTy.js.map +1 -0
- package/dist/esm/loader.js +3 -3
- package/dist/esm/open-chat-studio-widget.entry.js +1116 -301
- package/dist/esm/open-chat-studio-widget.entry.js.map +1 -1
- package/dist/esm/open-chat-studio-widget.js +3 -3
- package/dist/open-chat-studio-widget/open-chat-studio-widget.entry.esm.js.map +1 -1
- package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js +1 -1
- package/dist/open-chat-studio-widget/p-90719a8b.entry.js +4 -0
- package/dist/open-chat-studio-widget/p-90719a8b.entry.js.map +1 -0
- package/dist/open-chat-studio-widget/p-CX3v6rTy.js +3 -0
- package/dist/open-chat-studio-widget/p-CX3v6rTy.js.map +1 -0
- package/dist/types/components/ocs-chat/{heroicons.d.ts → icons.d.ts} +19 -0
- package/dist/types/components/ocs-chat/ocs-chat.d.ts +52 -36
- package/dist/types/components.d.ts +22 -8
- package/dist/types/services/chat-session-service.d.ts +78 -0
- package/dist/types/services/file-attachment-manager.d.ts +40 -0
- package/dist/types/utils/translations.d.ts +23 -0
- package/package.json +8 -3
- package/dist/cjs/index-AhSI5tER.js.map +0 -1
- package/dist/collection/components/ocs-chat/heroicons.js.map +0 -1
- package/dist/esm/index-DkJ7OJTS.js.map +0 -1
- package/dist/open-chat-studio-widget/p-DkJ7OJTS.js +0 -3
- package/dist/open-chat-studio-widget/p-DkJ7OJTS.js.map +0 -1
- package/dist/open-chat-studio-widget/p-bde68fbd.entry.js +0 -4
- package/dist/open-chat-studio-widget/p-bde68fbd.entry.js.map +0 -1
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2
|
-
import { Host, h } from "@stencil/core";
|
|
3
|
-
import { XMarkIcon, GripDotsVerticalIcon, PlusWithCircleIcon, ArrowsPointingOutIcon, ArrowsPointingInIcon, PaperClipIcon, CheckDocumentIcon, XIcon } from "./
|
|
2
|
+
import { Host, h, Env } from "@stencil/core";
|
|
3
|
+
import { XMarkIcon, GripDotsVerticalIcon, PlusWithCircleIcon, ArrowsPointingOutIcon, ArrowsPointingInIcon, PaperClipIcon, CheckDocumentIcon, XIcon, OcsWidgetAvatar } from "./icons";
|
|
4
4
|
import { renderMarkdownSync as renderMarkdownComplete } from "../../utils/markdown";
|
|
5
|
-
import { getCSRFToken } from "../../utils/cookies";
|
|
6
5
|
import { varToPixels } from "../../utils/utils";
|
|
6
|
+
import { TranslationManager, defaultTranslations } from "../../utils/translations";
|
|
7
|
+
import { ChatSessionService } from "../../services/chat-session-service";
|
|
8
|
+
import { FileAttachmentManager } from "../../services/file-attachment-manager";
|
|
7
9
|
export class OcsChat {
|
|
8
10
|
constructor() {
|
|
9
11
|
/**
|
|
10
|
-
* The base URL for the API
|
|
12
|
+
* The base URL for the API.
|
|
11
13
|
*/
|
|
12
|
-
this.apiBaseUrl = "https://
|
|
14
|
+
this.apiBaseUrl = "https://www.openchatstudio.com";
|
|
13
15
|
/**
|
|
14
16
|
* The shape of the chat button. 'round' makes it circular, 'square' keeps it rectangular.
|
|
15
17
|
*/
|
|
16
18
|
this.buttonShape = 'square';
|
|
17
|
-
/**
|
|
18
|
-
* The message to display in the new chat confirmation dialog.
|
|
19
|
-
*/
|
|
20
|
-
this.newChatConfirmationMessage = "Starting a new chat will clear your current conversation. Continue?";
|
|
21
19
|
/**
|
|
22
20
|
* Whether the chat widget is visible on load.
|
|
23
21
|
*/
|
|
@@ -43,10 +41,6 @@ export class OcsChat {
|
|
|
43
41
|
* Allow the user to attach files to their messages.
|
|
44
42
|
*/
|
|
45
43
|
this.allowAttachments = false;
|
|
46
|
-
/**
|
|
47
|
-
* The text to display while the assistant is typing/preparing a response.
|
|
48
|
-
*/
|
|
49
|
-
this.typingIndicatorText = "Preparing response";
|
|
50
44
|
this.error = "";
|
|
51
45
|
this.messages = [];
|
|
52
46
|
this.isLoading = false;
|
|
@@ -63,6 +57,20 @@ export class OcsChat {
|
|
|
63
57
|
this.showNewChatConfirmation = false;
|
|
64
58
|
this.selectedFiles = [];
|
|
65
59
|
this.isUploadingFiles = false;
|
|
60
|
+
this.buttonPosition = { x: 30, y: 30 };
|
|
61
|
+
this.buttonHorizontalSide = 'right';
|
|
62
|
+
this.buttonVerticalSide = 'bottom';
|
|
63
|
+
this.isButtonDragging = false;
|
|
64
|
+
this.buttonWasDragged = false;
|
|
65
|
+
this.translationManager = new TranslationManager();
|
|
66
|
+
this.attachmentManager = new FileAttachmentManager({
|
|
67
|
+
supportedExtensions: OcsChat.SUPPORTED_FILE_EXTENSIONS,
|
|
68
|
+
maxFileSizeMb: OcsChat.MAX_FILE_SIZE_MB,
|
|
69
|
+
maxTotalSizeMb: OcsChat.MAX_TOTAL_SIZE_MB,
|
|
70
|
+
});
|
|
71
|
+
this.buttonDragOffset = { x: 0, y: 0 };
|
|
72
|
+
this.rafId = null;
|
|
73
|
+
this.buttonListenersAttached = false;
|
|
66
74
|
this.chatWindowHeight = 600;
|
|
67
75
|
this.chatWindowWidth = 450;
|
|
68
76
|
this.chatWindowFullscreenWidth = 1024;
|
|
@@ -111,15 +119,101 @@ export class OcsChat {
|
|
|
111
119
|
this.endDrag();
|
|
112
120
|
};
|
|
113
121
|
this.handleWindowResize = () => {
|
|
122
|
+
var _a, _b;
|
|
114
123
|
this.positionInitialized = false;
|
|
115
124
|
this.initializePosition();
|
|
125
|
+
// Revalidate button position after resize to keep it within viewport bounds
|
|
126
|
+
if (this.isButtonDraggable()) {
|
|
127
|
+
const windowWidth = window.innerWidth;
|
|
128
|
+
const windowHeight = window.innerHeight;
|
|
129
|
+
const buttonWidth = ((_a = this.buttonRef) === null || _a === void 0 ? void 0 : _a.offsetWidth) || 60;
|
|
130
|
+
const buttonHeight = ((_b = this.buttonRef) === null || _b === void 0 ? void 0 : _b.offsetHeight) || 60;
|
|
131
|
+
const minPadding = 10;
|
|
132
|
+
this.buttonPosition = {
|
|
133
|
+
x: Math.max(minPadding, Math.min(this.buttonPosition.x, windowWidth - buttonWidth - minPadding)),
|
|
134
|
+
y: Math.max(minPadding, Math.min(this.buttonPosition.y, windowHeight - buttonHeight - minPadding))
|
|
135
|
+
};
|
|
136
|
+
this.updateHostPosition();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
this.handleButtonMouseDown = (event) => {
|
|
140
|
+
if (!this.buttonRef || !this.isButtonDraggable())
|
|
141
|
+
return;
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
const pointer = this.getPointerCoordinates(event);
|
|
145
|
+
if (!pointer)
|
|
146
|
+
return;
|
|
147
|
+
this.isButtonDragging = true;
|
|
148
|
+
this.buttonWasDragged = false; // Reset the drag flag
|
|
149
|
+
const rect = this.host.getBoundingClientRect();
|
|
150
|
+
this.buttonDragOffset = {
|
|
151
|
+
x: pointer.clientX - rect.left,
|
|
152
|
+
y: pointer.clientY - rect.top
|
|
153
|
+
};
|
|
154
|
+
this.addButtonEventListeners();
|
|
155
|
+
};
|
|
156
|
+
this.handleButtonTouchStart = (event) => {
|
|
157
|
+
if (!this.buttonRef || !this.isButtonDraggable())
|
|
158
|
+
return;
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
const pointer = this.getPointerCoordinates(event);
|
|
162
|
+
if (!pointer)
|
|
163
|
+
return;
|
|
164
|
+
this.isButtonDragging = true;
|
|
165
|
+
this.buttonWasDragged = false; // Reset the drag flag
|
|
166
|
+
const rect = this.host.getBoundingClientRect();
|
|
167
|
+
this.buttonDragOffset = {
|
|
168
|
+
x: pointer.clientX - rect.left,
|
|
169
|
+
y: pointer.clientY - rect.top
|
|
170
|
+
};
|
|
171
|
+
this.addButtonEventListeners();
|
|
172
|
+
};
|
|
173
|
+
this.handleButtonMouseMove = (event) => {
|
|
174
|
+
if (!this.isButtonDragging)
|
|
175
|
+
return;
|
|
176
|
+
const pointer = this.getPointerCoordinates(event);
|
|
177
|
+
if (!pointer)
|
|
178
|
+
return;
|
|
179
|
+
this.updateButtonPosition(pointer);
|
|
180
|
+
};
|
|
181
|
+
this.handleButtonTouchMove = (event) => {
|
|
182
|
+
if (!this.isButtonDragging)
|
|
183
|
+
return;
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
const pointer = this.getPointerCoordinates(event);
|
|
186
|
+
if (!pointer)
|
|
187
|
+
return;
|
|
188
|
+
this.updateButtonPosition(pointer);
|
|
189
|
+
};
|
|
190
|
+
this.handleButtonMouseUp = () => {
|
|
191
|
+
if (this.isButtonDragging) {
|
|
192
|
+
this.isButtonDragging = false;
|
|
193
|
+
this.removeButtonEventListeners();
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
this.handleButtonTouchEnd = () => {
|
|
197
|
+
if (this.isButtonDragging) {
|
|
198
|
+
this.isButtonDragging = false;
|
|
199
|
+
this.removeButtonEventListeners();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
this.handleButtonClick = () => {
|
|
203
|
+
// Only toggle visibility if the button wasn't dragged
|
|
204
|
+
if (!this.buttonWasDragged) {
|
|
205
|
+
this.toggleWindowVisibility();
|
|
206
|
+
}
|
|
207
|
+
// Reset the flag after handling the click
|
|
208
|
+
this.buttonWasDragged = false;
|
|
116
209
|
};
|
|
117
210
|
}
|
|
118
|
-
componentWillLoad() {
|
|
211
|
+
async componentWillLoad() {
|
|
119
212
|
if (!this.chatbotId) {
|
|
120
213
|
this.error = 'Chatbot ID is required';
|
|
121
214
|
return;
|
|
122
215
|
}
|
|
216
|
+
await this.initializeTranslations();
|
|
123
217
|
// Always try to load existing session if localStorage is available
|
|
124
218
|
if (this.persistentSession && this.isLocalStorageAvailable()) {
|
|
125
219
|
const { sessionId, messages } = this.loadSessionFromStorage();
|
|
@@ -139,6 +233,8 @@ export class OcsChat {
|
|
|
139
233
|
this.chatWindowHeight = varToPixels(windowHeightVar, window.innerHeight, this.chatWindowHeight);
|
|
140
234
|
this.chatWindowWidth = varToPixels(windowWidthVar, window.innerWidth, this.chatWindowWidth);
|
|
141
235
|
this.chatWindowFullscreenWidth = varToPixels(fullscreenWidthVar, window.innerWidth, this.chatWindowFullscreenWidth);
|
|
236
|
+
// Initialize button position from computed styles
|
|
237
|
+
this.initializeButtonPosition();
|
|
142
238
|
if (this.visible) {
|
|
143
239
|
this.initializePosition();
|
|
144
240
|
}
|
|
@@ -148,15 +244,29 @@ export class OcsChat {
|
|
|
148
244
|
}
|
|
149
245
|
else if (this.visible && this.sessionId) {
|
|
150
246
|
// Resume polling for existing session
|
|
151
|
-
this.
|
|
247
|
+
this.startMessagePolling();
|
|
152
248
|
}
|
|
153
249
|
window.addEventListener('resize', this.handleWindowResize);
|
|
154
250
|
}
|
|
155
251
|
disconnectedCallback() {
|
|
156
252
|
this.cleanup();
|
|
157
253
|
this.removeEventListeners();
|
|
254
|
+
this.removeButtonEventListeners();
|
|
158
255
|
window.removeEventListener('resize', this.handleWindowResize);
|
|
159
256
|
}
|
|
257
|
+
getChatService() {
|
|
258
|
+
if (!this.chatService) {
|
|
259
|
+
this.chatService = new ChatSessionService({
|
|
260
|
+
apiBaseUrl: this.apiBaseUrl || 'https://www.openchatstudio.com',
|
|
261
|
+
embedKey: this.embedKey,
|
|
262
|
+
widgetVersion: Env.version,
|
|
263
|
+
taskPollingIntervalMs: OcsChat.TASK_POLLING_INTERVAL_MS,
|
|
264
|
+
taskPollingMaxAttempts: OcsChat.TASK_POLLING_MAX_ATTEMPTS,
|
|
265
|
+
messagePollingIntervalMs: OcsChat.MESSAGE_POLLING_INTERVAL_MS,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return this.chatService;
|
|
269
|
+
}
|
|
160
270
|
addErrorMessage(errorText) {
|
|
161
271
|
const errorMessage = {
|
|
162
272
|
created_at: new Date().toISOString(),
|
|
@@ -200,25 +310,34 @@ export class OcsChat {
|
|
|
200
310
|
parseStarterQuestions() {
|
|
201
311
|
this.parsedStarterQuestions = this.parseJSONProp(this.starterQuestions, 'starter questions');
|
|
202
312
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
313
|
+
async initializeTranslations() {
|
|
314
|
+
let customTranslationsObj;
|
|
315
|
+
if (this.translationsUrl) {
|
|
316
|
+
customTranslationsObj = await this.loadTranslationsFromUrl(this.translationsUrl);
|
|
207
317
|
}
|
|
208
|
-
this.
|
|
318
|
+
this.translationManager = new TranslationManager(this.language, customTranslationsObj);
|
|
209
319
|
}
|
|
210
|
-
|
|
211
|
-
|
|
320
|
+
async loadTranslationsFromUrl(url) {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(url);
|
|
323
|
+
if (!response.ok) {
|
|
324
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
325
|
+
}
|
|
326
|
+
const translations = await response.json();
|
|
327
|
+
return translations;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error('Error loading translations from URL:', error);
|
|
331
|
+
return defaultTranslations;
|
|
332
|
+
}
|
|
212
333
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (csrfToken) {
|
|
219
|
-
headers['X-CSRFToken'] = csrfToken;
|
|
334
|
+
cleanup() {
|
|
335
|
+
this.stopMessagePolling();
|
|
336
|
+
if (this.taskPollingHandle) {
|
|
337
|
+
this.taskPollingHandle.cancel();
|
|
338
|
+
this.taskPollingHandle = undefined;
|
|
220
339
|
}
|
|
221
|
-
|
|
340
|
+
this.currentPollTaskId = '';
|
|
222
341
|
}
|
|
223
342
|
async startSession() {
|
|
224
343
|
try {
|
|
@@ -235,94 +354,38 @@ export class OcsChat {
|
|
|
235
354
|
if (this.userName) {
|
|
236
355
|
requestBody.participant_name = this.userName;
|
|
237
356
|
}
|
|
238
|
-
const
|
|
239
|
-
method: 'POST',
|
|
240
|
-
headers: this.getApiHeaders(),
|
|
241
|
-
body: JSON.stringify(requestBody)
|
|
242
|
-
});
|
|
243
|
-
if (!response.ok) {
|
|
244
|
-
this.handleError(`Failed to start session: ${response.statusText}`);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
const data = await response.json();
|
|
357
|
+
const data = await this.getChatService().startSession(requestBody);
|
|
248
358
|
this.sessionId = data.session_id;
|
|
249
359
|
this.saveSessionToStorage();
|
|
250
360
|
// Handle seed message if present
|
|
251
361
|
if (data.seed_message_task_id) {
|
|
252
|
-
this.
|
|
253
|
-
|
|
254
|
-
|
|
362
|
+
this.startTaskPolling(data.seed_message_task_id);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
this.startMessagePolling();
|
|
255
366
|
}
|
|
256
|
-
// Start polling for messages
|
|
257
|
-
this.startPolling();
|
|
258
367
|
}
|
|
259
|
-
catch (
|
|
368
|
+
catch (_error) {
|
|
260
369
|
this.handleError('Failed to start chat session');
|
|
261
370
|
}
|
|
262
371
|
finally {
|
|
263
372
|
this.isLoading = false;
|
|
264
373
|
}
|
|
265
374
|
}
|
|
266
|
-
markPendingFilesWithError(errorMessage) {
|
|
267
|
-
this.selectedFiles = this.selectedFiles.map(sf => {
|
|
268
|
-
if (!sf.error && !sf.uploaded) {
|
|
269
|
-
return Object.assign(Object.assign({}, sf), { error: errorMessage });
|
|
270
|
-
}
|
|
271
|
-
return sf;
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
375
|
async uploadFiles() {
|
|
275
376
|
if (this.selectedFiles.length === 0 || !this.sessionId || !this.allowAttachments) {
|
|
276
377
|
return [];
|
|
277
378
|
}
|
|
278
379
|
this.isUploadingFiles = true;
|
|
279
|
-
const uploadedIds = [];
|
|
280
380
|
try {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// Add user ID and name to the form data
|
|
292
|
-
const userId = this.getOrGenerateUserId();
|
|
293
|
-
formData.append('participant_remote_id', userId);
|
|
294
|
-
if (this.userName) {
|
|
295
|
-
formData.append('participant_name', this.userName);
|
|
296
|
-
}
|
|
297
|
-
// Only upload if there are new files
|
|
298
|
-
if (formData.has('files')) {
|
|
299
|
-
const response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/upload/`, {
|
|
300
|
-
method: 'POST',
|
|
301
|
-
body: formData,
|
|
302
|
-
});
|
|
303
|
-
if (!response.ok) {
|
|
304
|
-
const errorData = await response.json();
|
|
305
|
-
const errorMessage = errorData.error || 'Failed to upload files';
|
|
306
|
-
this.markPendingFilesWithError(errorMessage);
|
|
307
|
-
return uploadedIds;
|
|
308
|
-
}
|
|
309
|
-
const data = await response.json();
|
|
310
|
-
// Update selected files with upload results
|
|
311
|
-
let fileIndex = 0;
|
|
312
|
-
this.selectedFiles = this.selectedFiles.map(sf => {
|
|
313
|
-
if (!sf.error && !sf.uploaded) {
|
|
314
|
-
return Object.assign(Object.assign({}, sf), { uploaded: data.files[fileIndex++] });
|
|
315
|
-
}
|
|
316
|
-
return sf;
|
|
317
|
-
});
|
|
318
|
-
uploadedIds.push(...data.files.map((f) => f.id));
|
|
319
|
-
}
|
|
320
|
-
return uploadedIds;
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
const errorMessage = error instanceof Error ? error.message : 'Failed to upload files';
|
|
324
|
-
this.markPendingFilesWithError(errorMessage);
|
|
325
|
-
return uploadedIds;
|
|
381
|
+
const uploadResult = await this.attachmentManager.uploadPendingFiles(this.selectedFiles, {
|
|
382
|
+
apiBaseUrl: this.apiBaseUrl || 'https://www.openchatstudio.com',
|
|
383
|
+
sessionId: this.sessionId,
|
|
384
|
+
participantId: this.getOrGenerateUserId(),
|
|
385
|
+
participantName: this.userName,
|
|
386
|
+
});
|
|
387
|
+
this.selectedFiles = uploadResult.selectedFiles;
|
|
388
|
+
return uploadResult.uploadedIds;
|
|
326
389
|
}
|
|
327
390
|
finally {
|
|
328
391
|
this.isUploadingFiles = false;
|
|
@@ -375,27 +438,15 @@ export class OcsChat {
|
|
|
375
438
|
this.selectedFiles = []; // Clear selected files after sending
|
|
376
439
|
}
|
|
377
440
|
this.scrollToBottom();
|
|
378
|
-
// Start typing indicator - it will stay on during task polling
|
|
379
|
-
this.isTyping = true;
|
|
380
441
|
const requestBody = { message: message.trim() };
|
|
381
442
|
if (this.allowAttachments && attachmentIds.length > 0) {
|
|
382
443
|
requestBody.attachment_ids = attachmentIds;
|
|
383
444
|
}
|
|
384
|
-
const
|
|
385
|
-
method: 'POST',
|
|
386
|
-
headers: this.getApiHeaders(),
|
|
387
|
-
body: JSON.stringify(requestBody)
|
|
388
|
-
});
|
|
389
|
-
if (!response.ok) {
|
|
390
|
-
throw new Error(`Failed to send message: ${response.statusText}`);
|
|
391
|
-
}
|
|
392
|
-
const data = await response.json();
|
|
445
|
+
const data = await this.getChatService().sendMessage(this.sessionId, requestBody);
|
|
393
446
|
if (data.status === 'error') {
|
|
394
447
|
throw new Error(data.error || 'Failed to send message');
|
|
395
448
|
}
|
|
396
|
-
|
|
397
|
-
this.currentPollTaskId = data.task_id;
|
|
398
|
-
await this.pollTaskResponse();
|
|
449
|
+
this.startTaskPolling(data.task_id);
|
|
399
450
|
}
|
|
400
451
|
catch (error) {
|
|
401
452
|
const errorText = error instanceof Error ? error.message : 'Failed to send message';
|
|
@@ -405,110 +456,6 @@ export class OcsChat {
|
|
|
405
456
|
handleStarterQuestionClick(question) {
|
|
406
457
|
this.sendMessage(question);
|
|
407
458
|
}
|
|
408
|
-
async pollTaskResponse() {
|
|
409
|
-
if (!this.sessionId || !this.currentPollTaskId)
|
|
410
|
-
return;
|
|
411
|
-
// Stop message polling while task polling is active
|
|
412
|
-
this.pauseMessagePolling();
|
|
413
|
-
let attempts = 0;
|
|
414
|
-
const poll = async () => {
|
|
415
|
-
if (!this.sessionId || !this.currentPollTaskId)
|
|
416
|
-
return;
|
|
417
|
-
try {
|
|
418
|
-
const response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/${this.currentPollTaskId}/poll/`);
|
|
419
|
-
if (!response.ok) {
|
|
420
|
-
throw new Error(`Failed to poll task: ${response.statusText}`);
|
|
421
|
-
}
|
|
422
|
-
const data = await response.json();
|
|
423
|
-
if (data.error) {
|
|
424
|
-
throw new Error(data.error);
|
|
425
|
-
}
|
|
426
|
-
if (data.status === 'complete' && data.message) {
|
|
427
|
-
this.messages = [...this.messages, data.message];
|
|
428
|
-
this.saveSessionToStorage();
|
|
429
|
-
this.scrollToBottom();
|
|
430
|
-
// Task polling complete, clear typing indicator and resume message polling
|
|
431
|
-
this.isTyping = false;
|
|
432
|
-
this.currentPollTaskId = '';
|
|
433
|
-
this.resumeMessagePolling();
|
|
434
|
-
this.focusInput();
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
if (data.status === 'processing' && attempts < OcsChat.TASK_POLLING_MAX_ATTEMPTS) {
|
|
438
|
-
attempts++;
|
|
439
|
-
setTimeout(poll, OcsChat.TASK_POLLING_INTERVAL_MS);
|
|
440
|
-
}
|
|
441
|
-
else if (attempts >= OcsChat.TASK_POLLING_MAX_ATTEMPTS) {
|
|
442
|
-
// Task polling timed out - add timeout message and resume polling
|
|
443
|
-
const timeoutMessage = {
|
|
444
|
-
created_at: new Date().toISOString(),
|
|
445
|
-
role: 'system',
|
|
446
|
-
content: 'The response is taking longer than expected. The system may be experiencing delays. Please try sending your message again.',
|
|
447
|
-
attachments: []
|
|
448
|
-
};
|
|
449
|
-
this.messages = [...this.messages, timeoutMessage];
|
|
450
|
-
this.saveSessionToStorage();
|
|
451
|
-
this.scrollToBottom();
|
|
452
|
-
// Clear typing indicator and resume message polling
|
|
453
|
-
this.isTyping = false;
|
|
454
|
-
this.currentPollTaskId = '';
|
|
455
|
-
this.resumeMessagePolling();
|
|
456
|
-
this.focusInput();
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
catch (error) {
|
|
460
|
-
const errorText = error instanceof Error ? error.message : 'Failed to get response';
|
|
461
|
-
this.handleError(errorText);
|
|
462
|
-
// Clear states and resume polling
|
|
463
|
-
this.currentPollTaskId = '';
|
|
464
|
-
this.resumeMessagePolling();
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
await poll();
|
|
468
|
-
}
|
|
469
|
-
startPolling() {
|
|
470
|
-
if (this.pollingIntervalRef)
|
|
471
|
-
return;
|
|
472
|
-
this.pollingIntervalRef = setInterval(async () => {
|
|
473
|
-
// Only poll for messages if not currently polling for a task
|
|
474
|
-
if (!this.currentPollTaskId) {
|
|
475
|
-
await this.pollForMessages();
|
|
476
|
-
}
|
|
477
|
-
}, OcsChat.MESSAGE_POLLING_INTERVAL_MS);
|
|
478
|
-
}
|
|
479
|
-
pauseMessagePolling() {
|
|
480
|
-
if (this.pollingIntervalRef) {
|
|
481
|
-
clearInterval(this.pollingIntervalRef);
|
|
482
|
-
this.pollingIntervalRef = undefined;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
resumeMessagePolling() {
|
|
486
|
-
// Resume message polling after task polling is complete
|
|
487
|
-
this.startPolling();
|
|
488
|
-
}
|
|
489
|
-
async pollForMessages() {
|
|
490
|
-
if (!this.sessionId)
|
|
491
|
-
return;
|
|
492
|
-
try {
|
|
493
|
-
const url = new URL(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/poll/`);
|
|
494
|
-
if (this.messages && this.messages.length > 0) {
|
|
495
|
-
url.searchParams.set('since', this.messages.at(-1).created_at);
|
|
496
|
-
}
|
|
497
|
-
const response = await fetch(url.toString());
|
|
498
|
-
if (!response.ok)
|
|
499
|
-
return; // Silently fail for polling
|
|
500
|
-
const data = await response.json();
|
|
501
|
-
if (data.messages.length > 0) {
|
|
502
|
-
this.messages = [...this.messages, ...data.messages];
|
|
503
|
-
this.saveSessionToStorage();
|
|
504
|
-
this.scrollToBottom();
|
|
505
|
-
this.focusInput();
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
catch (_a) {
|
|
509
|
-
// Silently fail for polling
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
459
|
/**
|
|
513
460
|
* Scroll the message container to the bottom.
|
|
514
461
|
* @param forceEnd When `false`, scroll the top of the last message into view.
|
|
@@ -553,50 +500,18 @@ export class OcsChat {
|
|
|
553
500
|
this.messageInput = event.target.value;
|
|
554
501
|
}
|
|
555
502
|
handleFileSelect(event) {
|
|
556
|
-
var _a;
|
|
557
503
|
if (!this.allowAttachments)
|
|
558
504
|
return;
|
|
559
505
|
const input = event.target;
|
|
560
506
|
if (!input.files || input.files.length === 0)
|
|
561
507
|
return;
|
|
562
|
-
|
|
563
|
-
let totalSize = this.selectedFiles.reduce((sum, f) => sum + f.file.size, 0);
|
|
564
|
-
for (let i = 0; i < input.files.length; i++) {
|
|
565
|
-
const file = input.files[i];
|
|
566
|
-
const ext = '.' + ((_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase());
|
|
567
|
-
if (!OcsChat.SUPPORTED_FILE_EXTENSIONS.includes(ext)) {
|
|
568
|
-
newFiles.push({
|
|
569
|
-
file,
|
|
570
|
-
error: `File type ${ext} not supported`
|
|
571
|
-
});
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
const fileSizeMB = file.size / (1024 * 1024);
|
|
575
|
-
if (fileSizeMB > OcsChat.MAX_FILE_SIZE_MB) {
|
|
576
|
-
newFiles.push({
|
|
577
|
-
file,
|
|
578
|
-
error: `File exceeds ${OcsChat.MAX_FILE_SIZE_MB}MB limit`
|
|
579
|
-
});
|
|
580
|
-
continue;
|
|
581
|
-
}
|
|
582
|
-
totalSize += file.size;
|
|
583
|
-
const totalSizeMB = totalSize / (1024 * 1024);
|
|
584
|
-
if (totalSizeMB > OcsChat.MAX_TOTAL_SIZE_MB) {
|
|
585
|
-
newFiles.push({
|
|
586
|
-
file,
|
|
587
|
-
error: `Total size exceeds ${OcsChat.MAX_TOTAL_SIZE_MB}MB limit`
|
|
588
|
-
});
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
newFiles.push({ file });
|
|
592
|
-
}
|
|
593
|
-
this.selectedFiles = [...this.selectedFiles, ...newFiles];
|
|
508
|
+
this.selectedFiles = this.attachmentManager.addFiles(this.selectedFiles, input.files);
|
|
594
509
|
input.value = '';
|
|
595
510
|
}
|
|
596
511
|
removeSelectedFile(index) {
|
|
597
512
|
if (!this.allowAttachments)
|
|
598
513
|
return;
|
|
599
|
-
this.selectedFiles = this.
|
|
514
|
+
this.selectedFiles = this.attachmentManager.removeFile(this.selectedFiles, index);
|
|
600
515
|
}
|
|
601
516
|
formatFileSize(bytes) {
|
|
602
517
|
if (bytes === 0)
|
|
@@ -623,6 +538,11 @@ export class OcsChat {
|
|
|
623
538
|
* @param visible - The new value for the field.
|
|
624
539
|
*/
|
|
625
540
|
async visibilityHandler(visible) {
|
|
541
|
+
if (this.isButtonDragging) {
|
|
542
|
+
this.isButtonDragging = false;
|
|
543
|
+
this.buttonWasDragged = false;
|
|
544
|
+
this.removeButtonEventListeners();
|
|
545
|
+
}
|
|
626
546
|
if (visible) {
|
|
627
547
|
this.initializePosition();
|
|
628
548
|
}
|
|
@@ -630,11 +550,86 @@ export class OcsChat {
|
|
|
630
550
|
await this.startSession();
|
|
631
551
|
}
|
|
632
552
|
else if (!visible) {
|
|
633
|
-
this.
|
|
553
|
+
this.stopMessagePolling();
|
|
634
554
|
}
|
|
635
555
|
else {
|
|
636
556
|
this.scrollToBottom(true);
|
|
637
|
-
this.
|
|
557
|
+
this.startMessagePolling();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
startTaskPolling(taskId) {
|
|
561
|
+
if (!this.sessionId)
|
|
562
|
+
return;
|
|
563
|
+
this.currentPollTaskId = taskId;
|
|
564
|
+
this.isTyping = true;
|
|
565
|
+
this.stopMessagePolling();
|
|
566
|
+
if (this.taskPollingHandle) {
|
|
567
|
+
this.taskPollingHandle.cancel();
|
|
568
|
+
}
|
|
569
|
+
this.taskPollingHandle = this.getChatService().pollTask(this.sessionId, taskId, {
|
|
570
|
+
onMessage: (message) => {
|
|
571
|
+
this.messages = [...this.messages, message];
|
|
572
|
+
this.saveSessionToStorage();
|
|
573
|
+
this.scrollToBottom();
|
|
574
|
+
this.isTyping = false;
|
|
575
|
+
this.currentPollTaskId = '';
|
|
576
|
+
this.taskPollingHandle = undefined;
|
|
577
|
+
this.startMessagePolling();
|
|
578
|
+
this.focusInput();
|
|
579
|
+
},
|
|
580
|
+
onTimeout: () => {
|
|
581
|
+
const timeoutMessage = {
|
|
582
|
+
created_at: new Date().toISOString(),
|
|
583
|
+
role: 'system',
|
|
584
|
+
content: 'The response is taking longer than expected. The system may be experiencing delays. Please try sending your message again.',
|
|
585
|
+
attachments: []
|
|
586
|
+
};
|
|
587
|
+
this.messages = [...this.messages, timeoutMessage];
|
|
588
|
+
this.saveSessionToStorage();
|
|
589
|
+
this.scrollToBottom();
|
|
590
|
+
this.isTyping = false;
|
|
591
|
+
this.currentPollTaskId = '';
|
|
592
|
+
this.taskPollingHandle = undefined;
|
|
593
|
+
this.startMessagePolling();
|
|
594
|
+
this.focusInput();
|
|
595
|
+
},
|
|
596
|
+
onError: (error) => {
|
|
597
|
+
this.handleError(error.message);
|
|
598
|
+
this.taskPollingHandle = undefined;
|
|
599
|
+
this.startMessagePolling();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
startMessagePolling() {
|
|
604
|
+
if (!this.sessionId || this.currentPollTaskId || !this.visible) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (this.messagePollingHandle) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.messagePollingHandle = this.getChatService().startMessagePolling(this.sessionId, {
|
|
611
|
+
getSince: () => { var _a; return this.messages.length > 0 ? (_a = this.messages.at(-1)) === null || _a === void 0 ? void 0 : _a.created_at : undefined; },
|
|
612
|
+
onMessages: (messages) => {
|
|
613
|
+
if (messages.length === 0)
|
|
614
|
+
return;
|
|
615
|
+
this.messages = [...this.messages, ...messages];
|
|
616
|
+
this.saveSessionToStorage();
|
|
617
|
+
this.scrollToBottom();
|
|
618
|
+
this.focusInput();
|
|
619
|
+
},
|
|
620
|
+
onError: () => {
|
|
621
|
+
// Silently ignore polling errors to match previous behaviour
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
stopMessagePolling() {
|
|
626
|
+
var _a;
|
|
627
|
+
if (this.messagePollingHandle) {
|
|
628
|
+
this.messagePollingHandle.stop();
|
|
629
|
+
this.messagePollingHandle = undefined;
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
(_a = this.chatService) === null || _a === void 0 ? void 0 : _a.stopMessagePolling();
|
|
638
633
|
}
|
|
639
634
|
}
|
|
640
635
|
setPosition(position) {
|
|
@@ -655,7 +650,6 @@ export class OcsChat {
|
|
|
655
650
|
const actualChatWidth = Math.min(windowWidth, this.chatWindowFullscreenWidth);
|
|
656
651
|
const centeredX = (windowWidth - actualChatWidth) / 2;
|
|
657
652
|
const maxOffset = (windowWidth - actualChatWidth) / 2;
|
|
658
|
-
console.log(windowWidth, actualChatWidth, centeredX, maxOffset);
|
|
659
653
|
return { windowWidth, actualChatWidth, centeredX, maxOffset };
|
|
660
654
|
}
|
|
661
655
|
getPositionStyles() {
|
|
@@ -777,25 +771,181 @@ export class OcsChat {
|
|
|
777
771
|
document.removeEventListener('touchmove', this.handleTouchMove);
|
|
778
772
|
document.removeEventListener('touchend', this.handleTouchEnd);
|
|
779
773
|
}
|
|
780
|
-
|
|
781
|
-
|
|
774
|
+
// Button positioning and drag handlers
|
|
775
|
+
initializeButtonPosition() {
|
|
776
|
+
const computedStyle = getComputedStyle(this.host);
|
|
777
|
+
const position = computedStyle.getPropertyValue('position');
|
|
778
|
+
// Only enable dragging if the host element is positioned fixed
|
|
779
|
+
if (position !== 'fixed') {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const rect = this.host.getBoundingClientRect();
|
|
783
|
+
const windowWidth = window.innerWidth;
|
|
784
|
+
const windowHeight = window.innerHeight;
|
|
785
|
+
const left = computedStyle.getPropertyValue('left');
|
|
786
|
+
const right = computedStyle.getPropertyValue('right');
|
|
787
|
+
const top = computedStyle.getPropertyValue('top');
|
|
788
|
+
const bottom = computedStyle.getPropertyValue('bottom');
|
|
789
|
+
const hasLeft = !this.isAutoPosition(left);
|
|
790
|
+
const hasTop = !this.isAutoPosition(top);
|
|
791
|
+
this.buttonHorizontalSide = hasLeft ? 'left' : 'right';
|
|
792
|
+
this.buttonVerticalSide = hasTop ? 'top' : 'bottom';
|
|
793
|
+
const resolvedRight = this.getNumericPositionValue(right, Math.max(0, windowWidth - rect.right));
|
|
794
|
+
const resolvedLeft = this.getNumericPositionValue(left, Math.max(0, rect.left));
|
|
795
|
+
const resolvedBottom = this.getNumericPositionValue(bottom, Math.max(0, windowHeight - rect.bottom));
|
|
796
|
+
const resolvedTop = this.getNumericPositionValue(top, Math.max(0, rect.top));
|
|
797
|
+
const horizontalValue = this.buttonHorizontalSide === 'left' ? resolvedLeft : resolvedRight;
|
|
798
|
+
const verticalValue = this.buttonVerticalSide === 'top' ? resolvedTop : resolvedBottom;
|
|
799
|
+
this.buttonPosition = {
|
|
800
|
+
x: horizontalValue,
|
|
801
|
+
y: verticalValue
|
|
802
|
+
};
|
|
803
|
+
// Apply the position to the host
|
|
804
|
+
this.updateHostPosition();
|
|
805
|
+
}
|
|
806
|
+
updateHostPosition() {
|
|
807
|
+
this.host.style.position = 'fixed';
|
|
808
|
+
if (this.buttonHorizontalSide === 'left') {
|
|
809
|
+
this.host.style.left = `${this.buttonPosition.x}px`;
|
|
810
|
+
this.host.style.right = 'auto';
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
this.host.style.right = `${this.buttonPosition.x}px`;
|
|
814
|
+
this.host.style.left = 'auto';
|
|
815
|
+
}
|
|
816
|
+
if (this.buttonVerticalSide === 'top') {
|
|
817
|
+
this.host.style.top = `${this.buttonPosition.y}px`;
|
|
818
|
+
this.host.style.bottom = 'auto';
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
this.host.style.bottom = `${this.buttonPosition.y}px`;
|
|
822
|
+
this.host.style.top = 'auto';
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
isButtonDraggable() {
|
|
826
|
+
const computedStyle = getComputedStyle(this.host);
|
|
827
|
+
return computedStyle.getPropertyValue('position') === 'fixed';
|
|
828
|
+
}
|
|
829
|
+
updateButtonPosition(pointer) {
|
|
830
|
+
var _a, _b;
|
|
831
|
+
const windowWidth = window.innerWidth;
|
|
832
|
+
const windowHeight = window.innerHeight;
|
|
833
|
+
const buttonWidth = ((_a = this.buttonRef) === null || _a === void 0 ? void 0 : _a.offsetWidth) || 60;
|
|
834
|
+
const buttonHeight = ((_b = this.buttonRef) === null || _b === void 0 ? void 0 : _b.offsetHeight) || 60;
|
|
835
|
+
const minPadding = 10;
|
|
836
|
+
const candidateLeft = pointer.clientX - this.buttonDragOffset.x;
|
|
837
|
+
const candidateTop = pointer.clientY - this.buttonDragOffset.y;
|
|
838
|
+
const minLeft = minPadding;
|
|
839
|
+
const maxLeft = windowWidth - buttonWidth - minPadding;
|
|
840
|
+
const minTop = minPadding;
|
|
841
|
+
const maxTop = windowHeight - buttonHeight - minPadding;
|
|
842
|
+
const constrainedLeft = Math.max(minLeft, Math.min(candidateLeft, maxLeft));
|
|
843
|
+
const constrainedTop = Math.max(minTop, Math.min(candidateTop, maxTop));
|
|
844
|
+
const newHorizontalValue = this.buttonHorizontalSide === 'left'
|
|
845
|
+
? constrainedLeft
|
|
846
|
+
: Math.max(minPadding, windowWidth - (constrainedLeft + buttonWidth));
|
|
847
|
+
const newVerticalValue = this.buttonVerticalSide === 'top'
|
|
848
|
+
? constrainedTop
|
|
849
|
+
: Math.max(minPadding, windowHeight - (constrainedTop + buttonHeight));
|
|
850
|
+
if (newHorizontalValue !== this.buttonPosition.x || newVerticalValue !== this.buttonPosition.y) {
|
|
851
|
+
this.buttonWasDragged = true;
|
|
852
|
+
this.buttonPosition = { x: newHorizontalValue, y: newVerticalValue };
|
|
853
|
+
if (this.rafId === null) {
|
|
854
|
+
this.rafId = requestAnimationFrame(() => {
|
|
855
|
+
this.updateHostPosition();
|
|
856
|
+
this.rafId = null;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
addButtonEventListeners() {
|
|
862
|
+
if (this.buttonListenersAttached) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
document.addEventListener('mousemove', this.handleButtonMouseMove);
|
|
866
|
+
document.addEventListener('mouseup', this.handleButtonMouseUp);
|
|
867
|
+
document.addEventListener('touchmove', this.handleButtonTouchMove, { passive: false });
|
|
868
|
+
document.addEventListener('touchend', this.handleButtonTouchEnd);
|
|
869
|
+
this.buttonListenersAttached = true;
|
|
870
|
+
}
|
|
871
|
+
removeButtonEventListeners() {
|
|
872
|
+
if (!this.buttonListenersAttached) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (this.rafId !== null) {
|
|
876
|
+
cancelAnimationFrame(this.rafId);
|
|
877
|
+
this.rafId = null;
|
|
878
|
+
}
|
|
879
|
+
document.removeEventListener('mousemove', this.handleButtonMouseMove);
|
|
880
|
+
document.removeEventListener('mouseup', this.handleButtonMouseUp);
|
|
881
|
+
document.removeEventListener('touchmove', this.handleButtonTouchMove);
|
|
882
|
+
document.removeEventListener('touchend', this.handleButtonTouchEnd);
|
|
883
|
+
this.buttonListenersAttached = false;
|
|
884
|
+
}
|
|
885
|
+
isAutoPosition(value) {
|
|
886
|
+
const trimmed = value.trim();
|
|
887
|
+
return trimmed === '' || trimmed === 'auto';
|
|
888
|
+
}
|
|
889
|
+
parsePixelValue(value) {
|
|
890
|
+
const trimmed = value.trim();
|
|
891
|
+
if (trimmed === '' || trimmed === 'auto') {
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
if (trimmed.endsWith('px')) {
|
|
895
|
+
const parsed = parseFloat(trimmed);
|
|
896
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
897
|
+
}
|
|
898
|
+
const numeric = Number(trimmed);
|
|
899
|
+
if (Number.isFinite(numeric)) {
|
|
900
|
+
return numeric;
|
|
901
|
+
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
getNumericPositionValue(value, fallback) {
|
|
905
|
+
const parsed = this.parsePixelValue(value);
|
|
906
|
+
if (parsed !== null) {
|
|
907
|
+
return parsed;
|
|
908
|
+
}
|
|
909
|
+
return fallback;
|
|
910
|
+
}
|
|
911
|
+
getWelcomeMessages() {
|
|
912
|
+
const translated = this.translationManager.getArray("content.welcomeMessages");
|
|
913
|
+
return translated && translated.length > 0
|
|
914
|
+
? translated
|
|
915
|
+
: this.parsedWelcomeMessages;
|
|
916
|
+
}
|
|
917
|
+
getStarterQuestions() {
|
|
918
|
+
const translated = this.translationManager.getArray("content.starterQuestions");
|
|
919
|
+
return translated && translated.length > 0
|
|
920
|
+
? translated
|
|
921
|
+
: this.parsedStarterQuestions;
|
|
782
922
|
}
|
|
783
923
|
getButtonClasses() {
|
|
784
|
-
const
|
|
924
|
+
const buttonText = this.translationManager.get('branding.buttonText', this.buttonText);
|
|
925
|
+
const hasText = !!(buttonText && buttonText.trim());
|
|
785
926
|
const baseClass = hasText ? 'chat-btn-text' : 'chat-btn-icon';
|
|
786
927
|
const shapeClass = this.buttonShape === 'round' ? 'round' : '';
|
|
787
928
|
return `${baseClass} ${shapeClass}`.trim();
|
|
788
929
|
}
|
|
789
930
|
renderButton() {
|
|
790
|
-
|
|
931
|
+
var _a;
|
|
932
|
+
const buttonText = this.translationManager.get('branding.buttonText', this.buttonText);
|
|
933
|
+
const hasText = !!(buttonText && buttonText.trim());
|
|
791
934
|
const hasCustomIcon = this.iconUrl && this.iconUrl.trim();
|
|
792
|
-
const iconSrc = hasCustomIcon ? this.iconUrl : this.getDefaultIconUrl();
|
|
793
935
|
const buttonClasses = this.getButtonClasses();
|
|
936
|
+
const finalButtonText = buttonText !== null && buttonText !== void 0 ? buttonText : '';
|
|
937
|
+
const openLabel = (_a = this.translationManager.get('launcher.open')) !== null && _a !== void 0 ? _a : '';
|
|
938
|
+
const buttonAriaLabel = finalButtonText ? `${openLabel} - ${finalButtonText}` : openLabel;
|
|
939
|
+
// Only show drag cursor if button is draggable
|
|
940
|
+
const isDraggable = this.isButtonDraggable();
|
|
941
|
+
const buttonStyle = isDraggable ? {
|
|
942
|
+
cursor: this.isButtonDragging ? 'grabbing' : 'grab',
|
|
943
|
+
} : {};
|
|
794
944
|
if (hasText) {
|
|
795
|
-
return (h("button", { class: buttonClasses, onClick: () => this.
|
|
945
|
+
return (h("button", { ref: (el) => this.buttonRef = el, class: buttonClasses, "aria-label": buttonAriaLabel, title: finalButtonText || openLabel, style: buttonStyle, onClick: () => this.handleButtonClick(), onMouseDown: (e) => this.handleButtonMouseDown(e), onTouchStart: (e) => this.handleButtonTouchStart(e), "aria-grabbed": this.isButtonDragging, "aria-describedby": isDraggable ? "chat-button-drag-hint" : undefined }, hasCustomIcon ? h("img", { src: this.iconUrl, alt: "" }) : h(OcsWidgetAvatar, null), h("span", null, finalButtonText), isDraggable && (h("span", { id: "chat-button-drag-hint", style: { display: 'none' } }, "Draggable. Use mouse or touch to reposition."))));
|
|
796
946
|
}
|
|
797
947
|
else {
|
|
798
|
-
return (h("button", { class: buttonClasses, onClick: () => this.
|
|
948
|
+
return (h("button", { ref: (el) => this.buttonRef = el, class: buttonClasses, "aria-label": openLabel, title: openLabel, style: buttonStyle, onClick: () => this.handleButtonClick(), onMouseDown: (e) => this.handleButtonMouseDown(e), onTouchStart: (e) => this.handleButtonTouchStart(e), "aria-grabbed": this.isButtonDragging, "aria-describedby": isDraggable ? "chat-button-drag-hint" : undefined }, hasCustomIcon ? h("img", { src: this.iconUrl, alt: "" }) : h(OcsWidgetAvatar, null), isDraggable && (h("span", { id: "chat-button-drag-hint", style: { display: 'none' } }, "Draggable. Use mouse or touch to reposition."))));
|
|
799
949
|
}
|
|
800
950
|
}
|
|
801
951
|
getStorageKeys() {
|
|
@@ -931,18 +1081,18 @@ export class OcsChat {
|
|
|
931
1081
|
if (this.error && !this.sessionId) {
|
|
932
1082
|
return (h(Host, null, h("p", { class: "error-message" }, this.error)));
|
|
933
1083
|
}
|
|
934
|
-
return (h(Host, null, this.renderButton(), this.visible && (h("div", { ref: (el) => this.chatWindowRef = el, id: "ocs-chat-window", class: this.getPositionClasses(), style: this.getPositionStyles() }, h("div", { class: `chat-header ${this.isDragging ? 'chat-header-dragging' : 'chat-header-draggable'}`, onMouseDown: this.handleMouseDown, onTouchStart: this.handleTouchStart }, h("div", { class: "drag-indicator" }, h("div", { class: "drag-dots header-button" }, h(GripDotsVerticalIcon, null))), h("div", { class: "header-text" }, this.headerText), h("div", { class: "header-buttons" }, this.messages.length > 0 && (h("button", { class: "header-button", onClick: () => this.showConfirmationDialog(), title:
|
|
1084
|
+
return (h(Host, null, this.renderButton(), this.visible && (h("div", { ref: (el) => this.chatWindowRef = el, id: "ocs-chat-window", class: this.getPositionClasses(), style: this.getPositionStyles() }, h("div", { class: `chat-header ${this.isDragging ? 'chat-header-dragging' : 'chat-header-draggable'}`, onMouseDown: this.handleMouseDown, onTouchStart: this.handleTouchStart }, h("div", { class: "drag-indicator" }, h("div", { class: "drag-dots header-button" }, h(GripDotsVerticalIcon, null))), h("div", { class: "header-text" }, this.translationManager.get('branding.headerText', this.headerText)), h("div", { class: "header-buttons" }, this.messages.length > 0 && (h("button", { class: "header-button", onClick: () => this.showConfirmationDialog(), title: this.translationManager.get('window.newChat'), "aria-label": this.translationManager.get('window.newChat') }, h(PlusWithCircleIcon, null))), this.allowFullScreen && h("button", { class: "header-button fullscreen-button", onClick: () => this.toggleFullscreen(), title: this.isFullscreen ? this.translationManager.get('window.exitFullscreen') : this.translationManager.get('window.fullscreen'), "aria-label": this.isFullscreen ? this.translationManager.get('window.exitFullscreen') : this.translationManager.get('window.fullscreen') }, this.isFullscreen ? h(ArrowsPointingInIcon, null) : h(ArrowsPointingOutIcon, null)), h("button", { class: "header-button", onClick: () => this.visible = false, "aria-label": this.translationManager.get('window.close') }, h(XMarkIcon, null)))), this.showNewChatConfirmation && (h("div", { class: "confirmation-overlay" }, h("div", { class: "confirmation-dialog" }, h("div", { class: "confirmation-content" }, h("h3", { class: "confirmation-title" }, this.translationManager.get('modal.newChatTitle')), h("p", { class: "confirmation-message" }, this.translationManager.get('modal.newChatBody', this.newChatConfirmationMessage)), h("div", { class: "confirmation-buttons" }, h("button", { class: "confirmation-button confirmation-button-cancel", onClick: () => this.hideConfirmationDialog() }, this.translationManager.get('modal.cancel')), h("button", { class: "confirmation-button confirmation-button-confirm", onClick: () => this.confirmNewChat() }, this.translationManager.get('modal.confirm'))))))), h("div", { class: "chat-content" }, this.isLoading && !this.sessionId && (h("div", { class: "loading-container" }, h("div", { class: "loading-spinner" }), h("span", { class: "loading-text" }, this.translationManager.get('status.starting')))), (h("div", { ref: (el) => this.messageListRef = el, class: "messages-container" }, this.messages.length === 0 && this.parsedWelcomeMessages.length > 0 && (h("div", { class: "welcome-messages" }, this.getWelcomeMessages().map((message, index) => (h("div", { key: `welcome-${index}`, class: "message-row message-row-assistant" }, h("div", { class: "message-bubble message-bubble-assistant" }, h("div", { class: "chat-markdown", innerHTML: renderMarkdownComplete(message) }))))))), this.messages.map((message, index) => (h("div", { key: index, class: `message-row ${message.role === 'user' ? 'message-row-user' : 'message-row-assistant'}` }, h("div", { class: `message-bubble ${message.role === 'user'
|
|
935
1085
|
? 'message-bubble-user'
|
|
936
1086
|
: message.role === 'assistant'
|
|
937
1087
|
? 'message-bubble-assistant'
|
|
938
|
-
: 'message-bubble-system'}` }, h("div", { class: "chat-markdown", innerHTML: renderMarkdownComplete(message.content) }), message.attachments && message.attachments.length > 0 && (h("div", { class: "message-attachments" }, message.attachments.map((attachment, attachmentIndex) => (h("div", { key: attachmentIndex, class: "flex items-center gap-[0.5em]" }, h("span", { class: "message-attachment-icon" }, h(PaperClipIcon, null)), h("span", { class: "message-attachment-name" }, attachment.name)))))), h("div", { class: "message-timestamp" }, this.formatTime(message.created_at)))))), this.isTyping && (h("div", null, h("div", { class: "typing-indicator" }, h("div", { class: "typing-progress" })), h("div", { class: "typing-text" }, h("span", null, this.typingIndicatorText), h("span", { class: "typing-dots" })))))), this.messages.length === 0 && this.parsedStarterQuestions.length > 0 && (h("div", { class: "starter-questions" }, this.
|
|
1088
|
+
: 'message-bubble-system'}` }, h("div", { class: "chat-markdown", innerHTML: renderMarkdownComplete(message.content) }), message.attachments && message.attachments.length > 0 && (h("div", { class: "message-attachments" }, message.attachments.map((attachment, attachmentIndex) => (h("div", { key: attachmentIndex, class: "flex items-center gap-[0.5em]" }, h("span", { class: "message-attachment-icon" }, h(PaperClipIcon, null)), h("span", { class: "message-attachment-name" }, attachment.name)))))), h("div", { class: "message-timestamp" }, this.formatTime(message.created_at)))))), this.isTyping && (h("div", null, h("div", { class: "typing-indicator" }, h("div", { class: "typing-progress" })), h("div", { class: "typing-text" }, h("span", null, this.translationManager.get('status.typing', this.typingIndicatorText)), h("span", { class: "typing-dots loading" })))))), this.messages.length === 0 && this.parsedStarterQuestions.length > 0 && (h("div", { class: "starter-questions" }, this.getStarterQuestions().map((question, index) => (h("div", { key: `starter-${index}`, class: "starter-question-row" }, h("button", { class: "starter-question", onClick: () => this.handleStarterQuestionClick(question) }, question)))))), this.allowAttachments && this.selectedFiles.length > 0 && (h("div", { class: "selected-files-container" }, h("div", { class: "space-y-[0.25em]" }, this.selectedFiles.map((selectedFile, index) => (h("div", { key: index, class: "selected-file-item" }, h("div", { class: "flex items-center gap-[0.5em]" }, h("span", { class: "selected-file-icon" }, h(PaperClipIcon, null)), h("span", null, selectedFile.file.name), h("span", { class: "selected-file-size" }, "(", this.formatFileSize(selectedFile.file.size), ")"), selectedFile.error && (h("span", { class: "selected-file-error" }, selectedFile.error)), selectedFile.uploaded && (h("span", { class: "selected-file-success-icon" }, h(CheckDocumentIcon, null)))), h("button", { onClick: () => this.removeSelectedFile(index), class: "selected-file-remove-button", "aria-label": this.translationManager.get('attach.remove') }, h(XIcon, null)))))))), this.sessionId && (h("div", { class: "input-area" }, h("div", { class: "input-container" }, h("textarea", { ref: (el) => this.textareaRef = el, class: "message-textarea", rows: 1, placeholder: this.translationManager.get('composer.placeholder'), value: this.messageInput, onInput: (e) => this.handleInputChange(e), onKeyPress: (e) => this.handleKeyPress(e), disabled: this.isTyping || this.isUploadingFiles }), this.allowAttachments && (h("input", { ref: (el) => {
|
|
939
1089
|
// Unclear why but after removing all attachments this is being set to `null`.
|
|
940
1090
|
if (el) {
|
|
941
1091
|
this.fileInputRef = el;
|
|
942
1092
|
}
|
|
943
|
-
}, id: "ocs-file-input", type: "file", multiple: true, accept: OcsChat.SUPPORTED_FILE_EXTENSIONS.join(','), onChange: (e) => this.handleFileSelect(e), class: "hidden" })), this.allowAttachments && (h("button", { class: "file-attachment-button", onClick: () => { var _a; return (_a = this.fileInputRef) === null || _a === void 0 ? void 0 : _a.click(); }, disabled: this.isTyping || this.isUploadingFiles, title:
|
|
1093
|
+
}, id: "ocs-file-input", type: "file", multiple: true, accept: OcsChat.SUPPORTED_FILE_EXTENSIONS.join(','), onChange: (e) => this.handleFileSelect(e), class: "hidden" })), this.allowAttachments && (h("button", { class: "file-attachment-button", onClick: () => { var _a; return (_a = this.fileInputRef) === null || _a === void 0 ? void 0 : _a.click(); }, disabled: this.isTyping || this.isUploadingFiles, title: this.translationManager.get('attach.add'), "aria-label": this.translationManager.get('attach.add') }, h(PaperClipIcon, null))), h("button", { class: `send-button ${!this.isTyping && !!this.messageInput.trim()
|
|
944
1094
|
? 'send-button-enabled'
|
|
945
|
-
: 'send-button-disabled'}`, onClick: () => this.sendMessage(this.messageInput), disabled: this.isTyping || this.isUploadingFiles || !this.messageInput.trim() }, this.isUploadingFiles ? '
|
|
1095
|
+
: 'send-button-disabled'}`, onClick: () => this.sendMessage(this.messageInput), disabled: this.isTyping || this.isUploadingFiles || !this.messageInput.trim() }, this.isUploadingFiles ? `${this.translationManager.get('status.uploading')}...` : this.translationManager.get('composer.send'))))), h("div", { class: "flex items-center justify-center text-[0.8em] font-light w-full text-slate-500 py-[2px]" }, h("p", null, this.translationManager.get('branding.poweredBy'), ' ', " ", h("a", { class: "underline", href: "https://www.dimagi.com", target: "_blank" }, "Dimagi"))))))));
|
|
946
1096
|
}
|
|
947
1097
|
static get is() { return "open-chat-studio-widget"; }
|
|
948
1098
|
static get encapsulation() { return "shadow"; }
|
|
@@ -990,12 +1140,12 @@ export class OcsChat {
|
|
|
990
1140
|
"optional": true,
|
|
991
1141
|
"docs": {
|
|
992
1142
|
"tags": [],
|
|
993
|
-
"text": "The base URL for the API
|
|
1143
|
+
"text": "The base URL for the API."
|
|
994
1144
|
},
|
|
995
1145
|
"getter": false,
|
|
996
1146
|
"setter": false,
|
|
997
1147
|
"reflect": false,
|
|
998
|
-
"defaultValue": "\"https://
|
|
1148
|
+
"defaultValue": "\"https://www.openchatstudio.com\""
|
|
999
1149
|
},
|
|
1000
1150
|
"buttonText": {
|
|
1001
1151
|
"type": "string",
|
|
@@ -1035,6 +1185,25 @@ export class OcsChat {
|
|
|
1035
1185
|
"setter": false,
|
|
1036
1186
|
"reflect": false
|
|
1037
1187
|
},
|
|
1188
|
+
"embedKey": {
|
|
1189
|
+
"type": "string",
|
|
1190
|
+
"attribute": "embed-key",
|
|
1191
|
+
"mutable": false,
|
|
1192
|
+
"complexType": {
|
|
1193
|
+
"original": "string",
|
|
1194
|
+
"resolved": "string",
|
|
1195
|
+
"references": {}
|
|
1196
|
+
},
|
|
1197
|
+
"required": false,
|
|
1198
|
+
"optional": true,
|
|
1199
|
+
"docs": {
|
|
1200
|
+
"tags": [],
|
|
1201
|
+
"text": "Authentication key for embedded channels"
|
|
1202
|
+
},
|
|
1203
|
+
"getter": false,
|
|
1204
|
+
"setter": false,
|
|
1205
|
+
"reflect": false
|
|
1206
|
+
},
|
|
1038
1207
|
"buttonShape": {
|
|
1039
1208
|
"type": "string",
|
|
1040
1209
|
"attribute": "button-shape",
|
|
@@ -1091,8 +1260,7 @@ export class OcsChat {
|
|
|
1091
1260
|
},
|
|
1092
1261
|
"getter": false,
|
|
1093
1262
|
"setter": false,
|
|
1094
|
-
"reflect": false
|
|
1095
|
-
"defaultValue": "\"Starting a new chat will clear your current conversation. Continue?\""
|
|
1263
|
+
"reflect": false
|
|
1096
1264
|
},
|
|
1097
1265
|
"visible": {
|
|
1098
1266
|
"type": "boolean",
|
|
@@ -1307,8 +1475,45 @@ export class OcsChat {
|
|
|
1307
1475
|
},
|
|
1308
1476
|
"getter": false,
|
|
1309
1477
|
"setter": false,
|
|
1310
|
-
"reflect": false
|
|
1311
|
-
|
|
1478
|
+
"reflect": false
|
|
1479
|
+
},
|
|
1480
|
+
"language": {
|
|
1481
|
+
"type": "string",
|
|
1482
|
+
"attribute": "language",
|
|
1483
|
+
"mutable": false,
|
|
1484
|
+
"complexType": {
|
|
1485
|
+
"original": "string",
|
|
1486
|
+
"resolved": "string",
|
|
1487
|
+
"references": {}
|
|
1488
|
+
},
|
|
1489
|
+
"required": false,
|
|
1490
|
+
"optional": true,
|
|
1491
|
+
"docs": {
|
|
1492
|
+
"tags": [],
|
|
1493
|
+
"text": "The language code for the widget UI (e.g., 'en', 'es', 'fr'). Defaults to en"
|
|
1494
|
+
},
|
|
1495
|
+
"getter": false,
|
|
1496
|
+
"setter": false,
|
|
1497
|
+
"reflect": false
|
|
1498
|
+
},
|
|
1499
|
+
"translationsUrl": {
|
|
1500
|
+
"type": "string",
|
|
1501
|
+
"attribute": "translations-url",
|
|
1502
|
+
"mutable": false,
|
|
1503
|
+
"complexType": {
|
|
1504
|
+
"original": "string",
|
|
1505
|
+
"resolved": "string",
|
|
1506
|
+
"references": {}
|
|
1507
|
+
},
|
|
1508
|
+
"required": false,
|
|
1509
|
+
"optional": true,
|
|
1510
|
+
"docs": {
|
|
1511
|
+
"tags": [],
|
|
1512
|
+
"text": ""
|
|
1513
|
+
},
|
|
1514
|
+
"getter": false,
|
|
1515
|
+
"setter": false,
|
|
1516
|
+
"reflect": false
|
|
1312
1517
|
}
|
|
1313
1518
|
};
|
|
1314
1519
|
}
|
|
@@ -1331,7 +1536,9 @@ export class OcsChat {
|
|
|
1331
1536
|
"isFullscreen": {},
|
|
1332
1537
|
"showNewChatConfirmation": {},
|
|
1333
1538
|
"selectedFiles": {},
|
|
1334
|
-
"isUploadingFiles": {}
|
|
1539
|
+
"isUploadingFiles": {},
|
|
1540
|
+
"isButtonDragging": {},
|
|
1541
|
+
"buttonWasDragged": {}
|
|
1335
1542
|
};
|
|
1336
1543
|
}
|
|
1337
1544
|
static get elementRef() { return "host"; }
|