open-chat-studio-widget 0.4.8 → 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 (51) hide show
  1. package/README.md +23 -20
  2. package/dist/cjs/{index-AhSI5tER.js → index-CC3Krx2K.js} +4 -2
  3. package/dist/cjs/index-CC3Krx2K.js.map +1 -0
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/open-chat-studio-widget.cjs.entry.js +1116 -301
  6. package/dist/cjs/open-chat-studio-widget.cjs.entry.js.map +1 -1
  7. package/dist/cjs/open-chat-studio-widget.cjs.js +2 -2
  8. package/dist/cjs/open-chat-studio-widget.entry.cjs.js.map +1 -1
  9. package/dist/collection/components/ocs-chat/{heroicons.js → icons.js} +23 -1
  10. package/dist/collection/components/ocs-chat/icons.js.map +1 -0
  11. package/dist/collection/components/ocs-chat/ocs-chat.css +596 -1983
  12. package/dist/collection/components/ocs-chat/ocs-chat.js +480 -273
  13. package/dist/collection/components/ocs-chat/ocs-chat.js.map +1 -1
  14. package/dist/collection/services/chat-session-service.js +145 -0
  15. package/dist/collection/services/chat-session-service.js.map +1 -0
  16. package/dist/collection/services/file-attachment-manager.js +125 -0
  17. package/dist/collection/services/file-attachment-manager.js.map +1 -0
  18. package/dist/collection/utils/cookies.js +5 -12
  19. package/dist/collection/utils/cookies.js.map +1 -1
  20. package/dist/collection/utils/markdown.js +1 -1
  21. package/dist/collection/utils/markdown.js.map +1 -1
  22. package/dist/collection/utils/translations.js +99 -0
  23. package/dist/collection/utils/translations.js.map +1 -0
  24. package/dist/components/open-chat-studio-widget.js +1122 -302
  25. package/dist/components/open-chat-studio-widget.js.map +1 -1
  26. package/dist/esm/{index-DkJ7OJTS.js → index-BF7CYZiN.js} +4 -3
  27. package/dist/esm/index-BF7CYZiN.js.map +1 -0
  28. package/dist/esm/loader.js +3 -3
  29. package/dist/esm/open-chat-studio-widget.entry.js +1116 -301
  30. package/dist/esm/open-chat-studio-widget.entry.js.map +1 -1
  31. package/dist/esm/open-chat-studio-widget.js +3 -3
  32. package/dist/open-chat-studio-widget/open-chat-studio-widget.entry.esm.js.map +1 -1
  33. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js +1 -1
  34. package/dist/open-chat-studio-widget/p-400b1f47.entry.js +4 -0
  35. package/dist/open-chat-studio-widget/p-400b1f47.entry.js.map +1 -0
  36. package/dist/open-chat-studio-widget/p-BF7CYZiN.js +3 -0
  37. package/dist/open-chat-studio-widget/p-BF7CYZiN.js.map +1 -0
  38. package/dist/types/components/ocs-chat/{heroicons.d.ts → icons.d.ts} +19 -0
  39. package/dist/types/components/ocs-chat/ocs-chat.d.ts +52 -36
  40. package/dist/types/components.d.ts +22 -8
  41. package/dist/types/services/chat-session-service.d.ts +78 -0
  42. package/dist/types/services/file-attachment-manager.d.ts +40 -0
  43. package/dist/types/utils/translations.d.ts +23 -0
  44. package/package.json +8 -3
  45. package/dist/cjs/index-AhSI5tER.js.map +0 -1
  46. package/dist/collection/components/ocs-chat/heroicons.js.map +0 -1
  47. package/dist/esm/index-DkJ7OJTS.js.map +0 -1
  48. package/dist/open-chat-studio-widget/p-DkJ7OJTS.js +0 -3
  49. package/dist/open-chat-studio-widget/p-DkJ7OJTS.js.map +0 -1
  50. package/dist/open-chat-studio-widget/p-bde68fbd.entry.js +0 -4
  51. 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 "./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,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
- const newFiles = [];
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.selectedFiles.filter((_, i) => i !== index);
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.pauseMessagePolling();
553
+ this.stopMessagePolling();
634
554
  }
635
555
  else {
636
556
  this.scrollToBottom(true);
637
- this.resumeMessagePolling();
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
- getDefaultIconUrl() {
781
- 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;
782
922
  }
783
923
  getButtonClasses() {
784
- const hasText = this.buttonText && this.buttonText.trim();
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
- 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());
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.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."))));
796
946
  }
797
947
  else {
798
- 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."))));
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: "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'
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.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) => {
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: "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()
944
1094
  ? 'send-button-enabled'
945
- : '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"))))))));
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 (defaults to current origin)."
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://chatbots.dimagi.com\""
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
- "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
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"; }