open-chat-studio-widget 0.4.7 → 0.5.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.
Files changed (78) hide show
  1. package/README.md +23 -20
  2. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  3. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  4. package/dist/cjs/{index-c9203be6.js → index-CC3Krx2K.js} +331 -238
  5. package/dist/cjs/index-CC3Krx2K.js.map +1 -0
  6. package/dist/cjs/index.cjs.js +1 -0
  7. package/dist/cjs/index.cjs.js.map +1 -1
  8. package/dist/cjs/loader.cjs.js +4 -5
  9. package/dist/cjs/loader.cjs.js.map +1 -1
  10. package/dist/cjs/open-chat-studio-widget.cjs.entry.js +5124 -4272
  11. package/dist/cjs/open-chat-studio-widget.cjs.entry.js.map +1 -1
  12. package/dist/cjs/open-chat-studio-widget.cjs.js +8 -7
  13. package/dist/cjs/open-chat-studio-widget.cjs.js.map +1 -1
  14. package/dist/cjs/open-chat-studio-widget.entry.cjs.js.map +1 -0
  15. package/dist/collection/collection-manifest.json +1 -1
  16. package/dist/collection/components/ocs-chat/{heroicons.js → icons.js} +23 -1
  17. package/dist/collection/components/ocs-chat/icons.js.map +1 -0
  18. package/dist/collection/components/ocs-chat/ocs-chat.css +596 -1947
  19. package/dist/collection/components/ocs-chat/ocs-chat.js +521 -293
  20. package/dist/collection/components/ocs-chat/ocs-chat.js.map +1 -1
  21. package/dist/collection/services/chat-session-service.js +145 -0
  22. package/dist/collection/services/chat-session-service.js.map +1 -0
  23. package/dist/collection/services/file-attachment-manager.js +125 -0
  24. package/dist/collection/services/file-attachment-manager.js.map +1 -0
  25. package/dist/collection/utils/cookies.js +5 -12
  26. package/dist/collection/utils/cookies.js.map +1 -1
  27. package/dist/collection/utils/markdown.js +1 -1
  28. package/dist/collection/utils/markdown.js.map +1 -1
  29. package/dist/collection/utils/translations.js +99 -0
  30. package/dist/collection/utils/translations.js.map +1 -0
  31. package/dist/components/index.js +2 -1
  32. package/dist/components/open-chat-studio-widget.js +5125 -4266
  33. package/dist/components/open-chat-studio-widget.js.map +1 -1
  34. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  35. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  36. package/dist/esm/{index-0349ca51.js → index-BF7CYZiN.js} +329 -217
  37. package/dist/esm/index-BF7CYZiN.js.map +1 -0
  38. package/dist/esm/index.js +1 -0
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/loader.js +5 -4
  41. package/dist/esm/loader.js.map +1 -1
  42. package/dist/esm/open-chat-studio-widget.entry.js +5125 -4271
  43. package/dist/esm/open-chat-studio-widget.entry.js.map +1 -1
  44. package/dist/esm/open-chat-studio-widget.js +7 -5
  45. package/dist/esm/open-chat-studio-widget.js.map +1 -1
  46. package/dist/open-chat-studio-widget/index.esm.js.map +1 -1
  47. package/dist/open-chat-studio-widget/loader.esm.js.map +1 -0
  48. package/dist/open-chat-studio-widget/open-chat-studio-widget.entry.esm.js.map +1 -0
  49. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js +1 -1
  50. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js.map +1 -1
  51. package/dist/open-chat-studio-widget/p-400b1f47.entry.js +4 -0
  52. package/dist/open-chat-studio-widget/p-400b1f47.entry.js.map +1 -0
  53. package/dist/open-chat-studio-widget/p-BF7CYZiN.js +3 -0
  54. package/dist/open-chat-studio-widget/p-BF7CYZiN.js.map +1 -0
  55. package/dist/open-chat-studio-widget/p-DQuL1Twl.js +2 -0
  56. package/dist/open-chat-studio-widget/p-DQuL1Twl.js.map +1 -0
  57. package/dist/types/components/ocs-chat/{heroicons.d.ts → icons.d.ts} +19 -0
  58. package/dist/types/components/ocs-chat/ocs-chat.d.ts +57 -36
  59. package/dist/types/components.d.ts +36 -2
  60. package/dist/types/services/chat-session-service.d.ts +78 -0
  61. package/dist/types/services/file-attachment-manager.d.ts +40 -0
  62. package/dist/types/stencil-public-runtime.d.ts +35 -6
  63. package/dist/types/utils/translations.d.ts +23 -0
  64. package/package.json +9 -4
  65. package/dist/cjs/app-globals-3a1e7e63.js +0 -7
  66. package/dist/cjs/app-globals-3a1e7e63.js.map +0 -1
  67. package/dist/cjs/index-c9203be6.js.map +0 -1
  68. package/dist/collection/components/ocs-chat/heroicons.js.map +0 -1
  69. package/dist/esm/app-globals-0f993ce5.js +0 -5
  70. package/dist/esm/app-globals-0f993ce5.js.map +0 -1
  71. package/dist/esm/index-0349ca51.js.map +0 -1
  72. package/dist/open-chat-studio-widget/p-3dc66a9a.js +0 -3
  73. package/dist/open-chat-studio-widget/p-3dc66a9a.js.map +0 -1
  74. package/dist/open-chat-studio-widget/p-6b9a332c.entry.js +0 -4
  75. package/dist/open-chat-studio-widget/p-6b9a332c.entry.js.map +0 -1
  76. package/dist/open-chat-studio-widget/p-e1255160.js +0 -2
  77. package/dist/open-chat-studio-widget/p-e1255160.js.map +0 -1
  78. package/loader/package.json +0 -11
@@ -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 "./heroicons";
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 (defaults to current origin).
12
+ * The base URL for the API.
11
13
  */
12
- this.apiBaseUrl = "https://chatbots.dimagi.com";
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.startPolling();
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
- cleanup() {
204
- if (this.pollingIntervalRef) {
205
- clearInterval(this.pollingIntervalRef);
206
- this.pollingIntervalRef = undefined;
313
+ async initializeTranslations() {
314
+ let customTranslationsObj;
315
+ if (this.translationsUrl) {
316
+ customTranslationsObj = await this.loadTranslationsFromUrl(this.translationsUrl);
207
317
  }
208
- this.currentPollTaskId = '';
318
+ this.translationManager = new TranslationManager(this.language, customTranslationsObj);
209
319
  }
210
- getApiBaseUrl() {
211
- return this.apiBaseUrl || window.location.origin;
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
- getApiHeaders() {
214
- const headers = {
215
- 'Content-Type': 'application/json',
216
- };
217
- const csrfToken = getCSRFToken(this.getApiBaseUrl());
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
- return headers;
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 response = await fetch(`${this.getApiBaseUrl()}/api/chat/start/`, {
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.isTyping = true; // Show typing indicator for seed message
253
- this.currentPollTaskId = data.seed_message_task_id;
254
- await this.pollTaskResponse();
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 (error) {
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 formData = new FormData();
282
- // Add all files to form data
283
- for (const selectedFile of this.selectedFiles) {
284
- if (!selectedFile.error && !selectedFile.uploaded) {
285
- formData.append('files', selectedFile.file);
286
- }
287
- else if (selectedFile.uploaded) {
288
- uploadedIds.push(selectedFile.uploaded.id);
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 response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/message/`, {
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
- // Poll for the response - typing indicator will be managed in pollTaskResponse
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,114 +456,30 @@ 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
- scrollToBottom() {
459
+ /**
460
+ * Scroll the message container to the bottom.
461
+ * @param forceEnd When `false`, scroll the top of the last message into view.
462
+ * When `true`, scroll all the way to the end of the last message.
463
+ */
464
+ scrollToBottom(forceEnd = false) {
513
465
  setTimeout(() => {
514
466
  if (this.messageListRef) {
515
- this.messageListRef.scrollTop = this.messageListRef.scrollHeight;
467
+ const lastChild = this.messageListRef.lastElementChild;
468
+ if (!forceEnd && lastChild) {
469
+ // scroll so that the top of the last message is in the centre of the message container
470
+ const parentRect = this.messageListRef.getBoundingClientRect();
471
+ const childRect = lastChild.getBoundingClientRect();
472
+ const currentScrollTop = this.messageListRef.scrollTop;
473
+ const childTopRelativeToParent = childRect.top - parentRect.top;
474
+ const targetScroll = currentScrollTop + childTopRelativeToParent - (parentRect.height / 2);
475
+ this.messageListRef.scrollTo({
476
+ top: targetScroll,
477
+ behavior: 'smooth'
478
+ });
479
+ }
480
+ else {
481
+ this.messageListRef.scrollTop = this.messageListRef.scrollHeight;
482
+ }
516
483
  }
517
484
  }, OcsChat.SCROLL_DELAY_MS);
518
485
  }
@@ -533,50 +500,18 @@ export class OcsChat {
533
500
  this.messageInput = event.target.value;
534
501
  }
535
502
  handleFileSelect(event) {
536
- var _a;
537
503
  if (!this.allowAttachments)
538
504
  return;
539
505
  const input = event.target;
540
506
  if (!input.files || input.files.length === 0)
541
507
  return;
542
- const newFiles = [];
543
- let totalSize = this.selectedFiles.reduce((sum, f) => sum + f.file.size, 0);
544
- for (let i = 0; i < input.files.length; i++) {
545
- const file = input.files[i];
546
- const ext = '.' + ((_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase());
547
- if (!OcsChat.SUPPORTED_FILE_EXTENSIONS.includes(ext)) {
548
- newFiles.push({
549
- file,
550
- error: `File type ${ext} not supported`
551
- });
552
- continue;
553
- }
554
- const fileSizeMB = file.size / (1024 * 1024);
555
- if (fileSizeMB > OcsChat.MAX_FILE_SIZE_MB) {
556
- newFiles.push({
557
- file,
558
- error: `File exceeds ${OcsChat.MAX_FILE_SIZE_MB}MB limit`
559
- });
560
- continue;
561
- }
562
- totalSize += file.size;
563
- const totalSizeMB = totalSize / (1024 * 1024);
564
- if (totalSizeMB > OcsChat.MAX_TOTAL_SIZE_MB) {
565
- newFiles.push({
566
- file,
567
- error: `Total size exceeds ${OcsChat.MAX_TOTAL_SIZE_MB}MB limit`
568
- });
569
- continue;
570
- }
571
- newFiles.push({ file });
572
- }
573
- this.selectedFiles = [...this.selectedFiles, ...newFiles];
508
+ this.selectedFiles = this.attachmentManager.addFiles(this.selectedFiles, input.files);
574
509
  input.value = '';
575
510
  }
576
511
  removeSelectedFile(index) {
577
512
  if (!this.allowAttachments)
578
513
  return;
579
- this.selectedFiles = this.selectedFiles.filter((_, i) => i !== index);
514
+ this.selectedFiles = this.attachmentManager.removeFile(this.selectedFiles, index);
580
515
  }
581
516
  formatFileSize(bytes) {
582
517
  if (bytes === 0)
@@ -603,6 +538,11 @@ export class OcsChat {
603
538
  * @param visible - The new value for the field.
604
539
  */
605
540
  async visibilityHandler(visible) {
541
+ if (this.isButtonDragging) {
542
+ this.isButtonDragging = false;
543
+ this.buttonWasDragged = false;
544
+ this.removeButtonEventListeners();
545
+ }
606
546
  if (visible) {
607
547
  this.initializePosition();
608
548
  }
@@ -610,10 +550,86 @@ export class OcsChat {
610
550
  await this.startSession();
611
551
  }
612
552
  else if (!visible) {
613
- this.pauseMessagePolling();
553
+ this.stopMessagePolling();
554
+ }
555
+ else {
556
+ this.scrollToBottom(true);
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;
614
630
  }
615
631
  else {
616
- this.resumeMessagePolling();
632
+ (_a = this.chatService) === null || _a === void 0 ? void 0 : _a.stopMessagePolling();
617
633
  }
618
634
  }
619
635
  setPosition(position) {
@@ -634,7 +650,6 @@ export class OcsChat {
634
650
  const actualChatWidth = Math.min(windowWidth, this.chatWindowFullscreenWidth);
635
651
  const centeredX = (windowWidth - actualChatWidth) / 2;
636
652
  const maxOffset = (windowWidth - actualChatWidth) / 2;
637
- console.log(windowWidth, actualChatWidth, centeredX, maxOffset);
638
653
  return { windowWidth, actualChatWidth, centeredX, maxOffset };
639
654
  }
640
655
  getPositionStyles() {
@@ -756,25 +771,181 @@ export class OcsChat {
756
771
  document.removeEventListener('touchmove', this.handleTouchMove);
757
772
  document.removeEventListener('touchend', this.handleTouchEnd);
758
773
  }
759
- getDefaultIconUrl() {
760
- return `${this.getApiBaseUrl()}/static/images/favicons/favicon.svg`;
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;
761
922
  }
762
923
  getButtonClasses() {
763
- const hasText = this.buttonText && this.buttonText.trim();
924
+ const buttonText = this.translationManager.get('branding.buttonText', this.buttonText);
925
+ const hasText = !!(buttonText && buttonText.trim());
764
926
  const baseClass = hasText ? 'chat-btn-text' : 'chat-btn-icon';
765
927
  const shapeClass = this.buttonShape === 'round' ? 'round' : '';
766
928
  return `${baseClass} ${shapeClass}`.trim();
767
929
  }
768
930
  renderButton() {
769
- const hasText = this.buttonText && this.buttonText.trim();
931
+ var _a;
932
+ const buttonText = this.translationManager.get('branding.buttonText', this.buttonText);
933
+ const hasText = !!(buttonText && buttonText.trim());
770
934
  const hasCustomIcon = this.iconUrl && this.iconUrl.trim();
771
- const iconSrc = hasCustomIcon ? this.iconUrl : this.getDefaultIconUrl();
772
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
+ } : {};
773
944
  if (hasText) {
774
- return (h("button", { class: buttonClasses, onClick: () => this.toggleWindowVisibility(), "aria-label": `Open chat - ${this.buttonText}`, title: this.buttonText }, h("img", { src: iconSrc, alt: "" }), h("span", null, this.buttonText)));
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."))));
775
946
  }
776
947
  else {
777
- return (h("button", { class: buttonClasses, onClick: () => this.toggleWindowVisibility(), "aria-label": "Open chat", title: "Open chat" }, h("img", { src: iconSrc, alt: "Chat" })));
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."))));
778
949
  }
779
950
  }
780
951
  getStorageKeys() {
@@ -910,18 +1081,18 @@ export class OcsChat {
910
1081
  if (this.error && !this.sessionId) {
911
1082
  return (h(Host, null, h("p", { class: "error-message" }, this.error)));
912
1083
  }
913
- 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: "Start new chat", "aria-label": "Start new chat" }, h(PlusWithCircleIcon, null))), this.allowFullScreen && h("button", { class: "header-button fullscreen-button", onClick: () => this.toggleFullscreen(), title: this.isFullscreen ? "Exit fullscreen" : "Enter fullscreen", "aria-label": this.isFullscreen ? "Exit fullscreen" : "Enter fullscreen" }, this.isFullscreen ? h(ArrowsPointingInIcon, null) : h(ArrowsPointingOutIcon, null)), h("button", { class: "header-button", onClick: () => this.visible = false, "aria-label": "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" }, "Start New Chat"), h("p", { class: "confirmation-message" }, this.newChatConfirmationMessage), h("div", { class: "confirmation-buttons" }, h("button", { class: "confirmation-button confirmation-button-cancel", onClick: () => this.hideConfirmationDialog() }, "Cancel"), h("button", { class: "confirmation-button confirmation-button-confirm", onClick: () => this.confirmNewChat() }, "Continue")))))), h("div", { class: "chat-content" }, this.isLoading && !this.sessionId && (h("div", { class: "loading-container" }, h("div", { class: "loading-spinner" }), h("span", { class: "loading-text" }, "Starting chat..."))), (h("div", { ref: (el) => this.messageListRef = el, class: "messages-container" }, this.messages.length === 0 && this.parsedWelcomeMessages.length > 0 && (h("div", { class: "welcome-messages" }, this.parsedWelcomeMessages.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'
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'
914
1085
  ? 'message-bubble-user'
915
1086
  : message.role === 'assistant'
916
1087
  ? 'message-bubble-assistant'
917
- : '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.parsedStarterQuestions.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": "Remove file" }, 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: "Type your message...", value: this.messageInput, onInput: (e) => this.handleInputChange(e), onKeyPress: (e) => this.handleKeyPress(e), disabled: this.isTyping || this.isUploadingFiles }), this.allowAttachments && (h("input", { ref: (el) => {
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) => {
918
1089
  // Unclear why but after removing all attachments this is being set to `null`.
919
1090
  if (el) {
920
1091
  this.fileInputRef = el;
921
1092
  }
922
- }, 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: "Attach files", "aria-label": "Attach files" }, h(PaperClipIcon, null))), h("button", { class: `send-button ${!this.isTyping && !!this.messageInput.trim()
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()
923
1094
  ? 'send-button-enabled'
924
- : 'send-button-disabled'}`, onClick: () => this.sendMessage(this.messageInput), disabled: this.isTyping || this.isUploadingFiles || !this.messageInput.trim() }, this.isUploadingFiles ? 'Uploading...' : 'Send')))), h("div", { class: "flex items-center justify-center text-[0.8em] font-light w-full text-slate-500 py-[2px]" }, h("p", null, "Powered by ", h("a", { class: "underline", href: "https://www.dimagi.com", target: "_blank" }, "Dimagi"))))))));
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"))))))));
925
1096
  }
926
1097
  static get is() { return "open-chat-studio-widget"; }
927
1098
  static get encapsulation() { return "shadow"; }
@@ -939,6 +1110,7 @@ export class OcsChat {
939
1110
  return {
940
1111
  "chatbotId": {
941
1112
  "type": "string",
1113
+ "attribute": "chatbot-id",
942
1114
  "mutable": false,
943
1115
  "complexType": {
944
1116
  "original": "string",
@@ -953,11 +1125,11 @@ export class OcsChat {
953
1125
  },
954
1126
  "getter": false,
955
1127
  "setter": false,
956
- "attribute": "chatbot-id",
957
1128
  "reflect": false
958
1129
  },
959
1130
  "apiBaseUrl": {
960
1131
  "type": "string",
1132
+ "attribute": "api-base-url",
961
1133
  "mutable": false,
962
1134
  "complexType": {
963
1135
  "original": "string",
@@ -968,16 +1140,16 @@ export class OcsChat {
968
1140
  "optional": true,
969
1141
  "docs": {
970
1142
  "tags": [],
971
- "text": "The base URL for the API (defaults to current origin)."
1143
+ "text": "The base URL for the API."
972
1144
  },
973
1145
  "getter": false,
974
1146
  "setter": false,
975
- "attribute": "api-base-url",
976
1147
  "reflect": false,
977
- "defaultValue": "\"https://chatbots.dimagi.com\""
1148
+ "defaultValue": "\"https://www.openchatstudio.com\""
978
1149
  },
979
1150
  "buttonText": {
980
1151
  "type": "string",
1152
+ "attribute": "button-text",
981
1153
  "mutable": false,
982
1154
  "complexType": {
983
1155
  "original": "string",
@@ -992,11 +1164,11 @@ export class OcsChat {
992
1164
  },
993
1165
  "getter": false,
994
1166
  "setter": false,
995
- "attribute": "button-text",
996
1167
  "reflect": false
997
1168
  },
998
1169
  "iconUrl": {
999
1170
  "type": "string",
1171
+ "attribute": "icon-url",
1000
1172
  "mutable": false,
1001
1173
  "complexType": {
1002
1174
  "original": "string",
@@ -1011,11 +1183,30 @@ export class OcsChat {
1011
1183
  },
1012
1184
  "getter": false,
1013
1185
  "setter": false,
1014
- "attribute": "icon-url",
1186
+ "reflect": false
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,
1015
1205
  "reflect": false
1016
1206
  },
1017
1207
  "buttonShape": {
1018
1208
  "type": "string",
1209
+ "attribute": "button-shape",
1019
1210
  "mutable": false,
1020
1211
  "complexType": {
1021
1212
  "original": "'round' | 'square'",
@@ -1030,12 +1221,12 @@ export class OcsChat {
1030
1221
  },
1031
1222
  "getter": false,
1032
1223
  "setter": false,
1033
- "attribute": "button-shape",
1034
1224
  "reflect": false,
1035
1225
  "defaultValue": "'square'"
1036
1226
  },
1037
1227
  "headerText": {
1038
1228
  "type": "string",
1229
+ "attribute": "header-text",
1039
1230
  "mutable": false,
1040
1231
  "complexType": {
1041
1232
  "original": "''",
@@ -1050,11 +1241,11 @@ export class OcsChat {
1050
1241
  },
1051
1242
  "getter": false,
1052
1243
  "setter": false,
1053
- "attribute": "header-text",
1054
1244
  "reflect": false
1055
1245
  },
1056
1246
  "newChatConfirmationMessage": {
1057
1247
  "type": "string",
1248
+ "attribute": "new-chat-confirmation-message",
1058
1249
  "mutable": false,
1059
1250
  "complexType": {
1060
1251
  "original": "string",
@@ -1069,12 +1260,11 @@ export class OcsChat {
1069
1260
  },
1070
1261
  "getter": false,
1071
1262
  "setter": false,
1072
- "attribute": "new-chat-confirmation-message",
1073
- "reflect": false,
1074
- "defaultValue": "\"Starting a new chat will clear your current conversation. Continue?\""
1263
+ "reflect": false
1075
1264
  },
1076
1265
  "visible": {
1077
1266
  "type": "boolean",
1267
+ "attribute": "visible",
1078
1268
  "mutable": true,
1079
1269
  "complexType": {
1080
1270
  "original": "boolean",
@@ -1089,12 +1279,12 @@ export class OcsChat {
1089
1279
  },
1090
1280
  "getter": false,
1091
1281
  "setter": false,
1092
- "attribute": "visible",
1093
1282
  "reflect": false,
1094
1283
  "defaultValue": "false"
1095
1284
  },
1096
1285
  "position": {
1097
1286
  "type": "string",
1287
+ "attribute": "position",
1098
1288
  "mutable": true,
1099
1289
  "complexType": {
1100
1290
  "original": "'left' | 'center' | 'right'",
@@ -1109,12 +1299,12 @@ export class OcsChat {
1109
1299
  },
1110
1300
  "getter": false,
1111
1301
  "setter": false,
1112
- "attribute": "position",
1113
1302
  "reflect": false,
1114
1303
  "defaultValue": "'right'"
1115
1304
  },
1116
1305
  "welcomeMessages": {
1117
1306
  "type": "string",
1307
+ "attribute": "welcome-messages",
1118
1308
  "mutable": false,
1119
1309
  "complexType": {
1120
1310
  "original": "string",
@@ -1129,11 +1319,11 @@ export class OcsChat {
1129
1319
  },
1130
1320
  "getter": false,
1131
1321
  "setter": false,
1132
- "attribute": "welcome-messages",
1133
1322
  "reflect": false
1134
1323
  },
1135
1324
  "starterQuestions": {
1136
1325
  "type": "string",
1326
+ "attribute": "starter-questions",
1137
1327
  "mutable": false,
1138
1328
  "complexType": {
1139
1329
  "original": "string",
@@ -1148,11 +1338,11 @@ export class OcsChat {
1148
1338
  },
1149
1339
  "getter": false,
1150
1340
  "setter": false,
1151
- "attribute": "starter-questions",
1152
1341
  "reflect": false
1153
1342
  },
1154
1343
  "userId": {
1155
1344
  "type": "string",
1345
+ "attribute": "user-id",
1156
1346
  "mutable": false,
1157
1347
  "complexType": {
1158
1348
  "original": "string",
@@ -1167,11 +1357,11 @@ export class OcsChat {
1167
1357
  },
1168
1358
  "getter": false,
1169
1359
  "setter": false,
1170
- "attribute": "user-id",
1171
1360
  "reflect": false
1172
1361
  },
1173
1362
  "userName": {
1174
1363
  "type": "string",
1364
+ "attribute": "user-name",
1175
1365
  "mutable": false,
1176
1366
  "complexType": {
1177
1367
  "original": "string",
@@ -1186,11 +1376,11 @@ export class OcsChat {
1186
1376
  },
1187
1377
  "getter": false,
1188
1378
  "setter": false,
1189
- "attribute": "user-name",
1190
1379
  "reflect": false
1191
1380
  },
1192
1381
  "persistentSession": {
1193
1382
  "type": "boolean",
1383
+ "attribute": "persistent-session",
1194
1384
  "mutable": false,
1195
1385
  "complexType": {
1196
1386
  "original": "boolean",
@@ -1205,12 +1395,12 @@ export class OcsChat {
1205
1395
  },
1206
1396
  "getter": false,
1207
1397
  "setter": false,
1208
- "attribute": "persistent-session",
1209
1398
  "reflect": false,
1210
1399
  "defaultValue": "true"
1211
1400
  },
1212
1401
  "persistentSessionExpire": {
1213
1402
  "type": "number",
1403
+ "attribute": "persistent-session-expire",
1214
1404
  "mutable": false,
1215
1405
  "complexType": {
1216
1406
  "original": "number",
@@ -1225,12 +1415,12 @@ export class OcsChat {
1225
1415
  },
1226
1416
  "getter": false,
1227
1417
  "setter": false,
1228
- "attribute": "persistent-session-expire",
1229
1418
  "reflect": false,
1230
1419
  "defaultValue": "60 * 24"
1231
1420
  },
1232
1421
  "allowFullScreen": {
1233
1422
  "type": "boolean",
1423
+ "attribute": "allow-full-screen",
1234
1424
  "mutable": false,
1235
1425
  "complexType": {
1236
1426
  "original": "boolean",
@@ -1245,12 +1435,12 @@ export class OcsChat {
1245
1435
  },
1246
1436
  "getter": false,
1247
1437
  "setter": false,
1248
- "attribute": "allow-full-screen",
1249
1438
  "reflect": false,
1250
1439
  "defaultValue": "true"
1251
1440
  },
1252
1441
  "allowAttachments": {
1253
1442
  "type": "boolean",
1443
+ "attribute": "allow-attachments",
1254
1444
  "mutable": false,
1255
1445
  "complexType": {
1256
1446
  "original": "boolean",
@@ -1265,12 +1455,12 @@ export class OcsChat {
1265
1455
  },
1266
1456
  "getter": false,
1267
1457
  "setter": false,
1268
- "attribute": "allow-attachments",
1269
1458
  "reflect": false,
1270
1459
  "defaultValue": "false"
1271
1460
  },
1272
1461
  "typingIndicatorText": {
1273
1462
  "type": "string",
1463
+ "attribute": "typing-indicator-text",
1274
1464
  "mutable": false,
1275
1465
  "complexType": {
1276
1466
  "original": "string",
@@ -1285,9 +1475,45 @@ export class OcsChat {
1285
1475
  },
1286
1476
  "getter": false,
1287
1477
  "setter": false,
1288
- "attribute": "typing-indicator-text",
1289
- "reflect": false,
1290
- "defaultValue": "\"Preparing response\""
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
1291
1517
  }
1292
1518
  };
1293
1519
  }
@@ -1310,7 +1536,9 @@ export class OcsChat {
1310
1536
  "isFullscreen": {},
1311
1537
  "showNewChatConfirmation": {},
1312
1538
  "selectedFiles": {},
1313
- "isUploadingFiles": {}
1539
+ "isUploadingFiles": {},
1540
+ "isButtonDragging": {},
1541
+ "buttonWasDragged": {}
1314
1542
  };
1315
1543
  }
1316
1544
  static get elementRef() { return "host"; }