open-chat-studio-widget 0.4.5 → 0.4.7

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 (35) hide show
  1. package/README.md +118 -94
  2. package/dist/cjs/{index-bcb28089.js → index-c9203be6.js} +29 -3
  3. package/dist/cjs/index-c9203be6.js.map +1 -0
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/open-chat-studio-widget.cjs.entry.js +314 -73
  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/collection/components/ocs-chat/heroicons.js +11 -2
  9. package/dist/collection/components/ocs-chat/heroicons.js.map +1 -1
  10. package/dist/collection/components/ocs-chat/ocs-chat.css +362 -110
  11. package/dist/collection/components/ocs-chat/ocs-chat.js +367 -78
  12. package/dist/collection/components/ocs-chat/ocs-chat.js.map +1 -1
  13. package/dist/components/open-chat-studio-widget.js +323 -78
  14. package/dist/components/open-chat-studio-widget.js.map +1 -1
  15. package/dist/esm/{index-205c77bc.js → index-0349ca51.js} +29 -3
  16. package/dist/esm/index-0349ca51.js.map +1 -0
  17. package/dist/esm/loader.js +3 -3
  18. package/dist/esm/open-chat-studio-widget.entry.js +314 -73
  19. package/dist/esm/open-chat-studio-widget.entry.js.map +1 -1
  20. package/dist/esm/open-chat-studio-widget.js +3 -3
  21. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js +1 -1
  22. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js.map +1 -1
  23. package/dist/open-chat-studio-widget/{p-78d09c6b.js → p-3dc66a9a.js} +3 -3
  24. package/dist/open-chat-studio-widget/p-3dc66a9a.js.map +1 -0
  25. package/dist/open-chat-studio-widget/p-6b9a332c.entry.js +4 -0
  26. package/dist/open-chat-studio-widget/p-6b9a332c.entry.js.map +1 -0
  27. package/dist/types/components/ocs-chat/heroicons.d.ts +4 -1
  28. package/dist/types/components/ocs-chat/ocs-chat.d.ts +51 -9
  29. package/dist/types/components.d.ts +24 -0
  30. package/package.json +1 -1
  31. package/dist/cjs/index-bcb28089.js.map +0 -1
  32. package/dist/esm/index-205c77bc.js.map +0 -1
  33. package/dist/open-chat-studio-widget/p-5d6bd56a.entry.js +0 -4
  34. package/dist/open-chat-studio-widget/p-5d6bd56a.entry.js.map +0 -1
  35. package/dist/open-chat-studio-widget/p-78d09c6b.js.map +0 -1
@@ -1,5 +1,6 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1
2
  import { Host, h } from "@stencil/core";
2
- import { XMarkIcon, GripDotsVerticalIcon, PencilSquare, ArrowsPointingOutIcon, ArrowsPointingInIcon, } from "./heroicons";
3
+ import { XMarkIcon, GripDotsVerticalIcon, PlusWithCircleIcon, ArrowsPointingOutIcon, ArrowsPointingInIcon, PaperClipIcon, CheckDocumentIcon, XIcon } from "./heroicons";
3
4
  import { renderMarkdownSync as renderMarkdownComplete } from "../../utils/markdown";
4
5
  import { getCSRFToken } from "../../utils/cookies";
5
6
  import { varToPixels } from "../../utils/utils";
@@ -13,6 +14,10 @@ export class OcsChat {
13
14
  * The shape of the chat button. 'round' makes it circular, 'square' keeps it rectangular.
14
15
  */
15
16
  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?";
16
21
  /**
17
22
  * Whether the chat widget is visible on load.
18
23
  */
@@ -34,24 +39,34 @@ export class OcsChat {
34
39
  * Allow the user to make the chat window full screen.
35
40
  */
36
41
  this.allowFullScreen = true;
37
- this.loaded = false;
42
+ /**
43
+ * Allow the user to attach files to their messages.
44
+ */
45
+ this.allowAttachments = false;
46
+ /**
47
+ * The text to display while the assistant is typing/preparing a response.
48
+ */
49
+ this.typingIndicatorText = "Preparing response";
38
50
  this.error = "";
39
51
  this.messages = [];
40
52
  this.isLoading = false;
41
53
  this.isTyping = false;
42
54
  this.messageInput = "";
43
- this.isTaskPolling = false;
55
+ this.currentPollTaskId = "";
44
56
  this.isDragging = false;
45
57
  this.dragOffset = { x: 0, y: 0 };
46
58
  this.windowPosition = { x: 0, y: 0 };
47
59
  this.fullscreenPosition = { x: 0 };
48
- this.showStarterQuestions = true;
49
60
  this.parsedWelcomeMessages = [];
50
61
  this.parsedStarterQuestions = [];
51
62
  this.isFullscreen = false;
63
+ this.showNewChatConfirmation = false;
64
+ this.selectedFiles = [];
65
+ this.isUploadingFiles = false;
52
66
  this.chatWindowHeight = 600;
53
67
  this.chatWindowWidth = 450;
54
68
  this.chatWindowFullscreenWidth = 1024;
69
+ this.positionInitialized = false;
55
70
  this.handleMouseDown = (event) => {
56
71
  if (!this.isFullscreen && window.innerWidth < OcsChat.MOBILE_BREAKPOINT)
57
72
  return;
@@ -96,11 +111,11 @@ export class OcsChat {
96
111
  this.endDrag();
97
112
  };
98
113
  this.handleWindowResize = () => {
114
+ this.positionInitialized = false;
99
115
  this.initializePosition();
100
116
  };
101
117
  }
102
118
  componentWillLoad() {
103
- this.loaded = this.visible;
104
119
  if (!this.chatbotId) {
105
120
  this.error = 'Chatbot ID is required';
106
121
  return;
@@ -111,13 +126,22 @@ export class OcsChat {
111
126
  if (sessionId && messages) {
112
127
  this.sessionId = sessionId;
113
128
  this.messages = messages;
114
- this.showStarterQuestions = messages.length === 0;
115
129
  }
116
130
  }
117
131
  this.parseWelcomeMessages();
118
132
  this.parseStarterQuestions();
119
133
  }
120
134
  componentDidLoad() {
135
+ const computedStyle = getComputedStyle(this.host);
136
+ const windowHeightVar = computedStyle.getPropertyValue('--chat-window-height');
137
+ const windowWidthVar = computedStyle.getPropertyValue('--chat-window-width');
138
+ const fullscreenWidthVar = computedStyle.getPropertyValue('--chat-window-fullscreen-width');
139
+ this.chatWindowHeight = varToPixels(windowHeightVar, window.innerHeight, this.chatWindowHeight);
140
+ this.chatWindowWidth = varToPixels(windowWidthVar, window.innerWidth, this.chatWindowWidth);
141
+ this.chatWindowFullscreenWidth = varToPixels(fullscreenWidthVar, window.innerWidth, this.chatWindowFullscreenWidth);
142
+ if (this.visible) {
143
+ this.initializePosition();
144
+ }
121
145
  // Only auto-start session if we don't have an existing one
122
146
  if (this.visible && !this.sessionId) {
123
147
  this.startSession();
@@ -126,14 +150,6 @@ export class OcsChat {
126
150
  // Resume polling for existing session
127
151
  this.startPolling();
128
152
  }
129
- const computedStyle = getComputedStyle(this.host);
130
- const windowHeightVar = computedStyle.getPropertyValue('--chat-window-height');
131
- const windowWidthVar = computedStyle.getPropertyValue('--chat-window-width');
132
- const fullscreenWidthVar = computedStyle.getPropertyValue('--chat-window-fullscreen-width');
133
- this.chatWindowHeight = varToPixels(windowHeightVar, window.innerHeight, this.chatWindowHeight);
134
- this.chatWindowWidth = varToPixels(windowWidthVar, window.innerWidth, this.chatWindowWidth);
135
- this.chatWindowFullscreenWidth = varToPixels(fullscreenWidthVar, window.innerWidth, this.chatWindowFullscreenWidth);
136
- this.initializePosition();
137
153
  window.addEventListener('resize', this.handleWindowResize);
138
154
  }
139
155
  disconnectedCallback() {
@@ -141,6 +157,26 @@ export class OcsChat {
141
157
  this.removeEventListeners();
142
158
  window.removeEventListener('resize', this.handleWindowResize);
143
159
  }
160
+ addErrorMessage(errorText) {
161
+ const errorMessage = {
162
+ created_at: new Date().toISOString(),
163
+ role: 'system',
164
+ content: `**Error:** ${errorText}\nPlease try again.`,
165
+ attachments: []
166
+ };
167
+ this.messages = [...this.messages, errorMessage];
168
+ this.saveSessionToStorage();
169
+ this.scrollToBottom();
170
+ }
171
+ handleError(errorText) {
172
+ // show as system message
173
+ this.addErrorMessage(errorText);
174
+ // Clear any loading/typing states
175
+ this.isLoading = false;
176
+ this.isTyping = false;
177
+ this.isUploadingFiles = false;
178
+ this.currentPollTaskId = '';
179
+ }
144
180
  parseJSONProp(propValue, propName) {
145
181
  try {
146
182
  if (propValue) {
@@ -165,11 +201,11 @@ export class OcsChat {
165
201
  this.parsedStarterQuestions = this.parseJSONProp(this.starterQuestions, 'starter questions');
166
202
  }
167
203
  cleanup() {
168
- if (this.pollingInterval) {
169
- clearInterval(this.pollingInterval);
170
- this.pollingInterval = undefined;
204
+ if (this.pollingIntervalRef) {
205
+ clearInterval(this.pollingIntervalRef);
206
+ this.pollingIntervalRef = undefined;
171
207
  }
172
- this.isTaskPolling = false;
208
+ this.currentPollTaskId = '';
173
209
  }
174
210
  getApiBaseUrl() {
175
211
  return this.apiBaseUrl || window.location.origin;
@@ -187,7 +223,6 @@ export class OcsChat {
187
223
  async startSession() {
188
224
  try {
189
225
  this.isLoading = true;
190
- this.error = '';
191
226
  const userId = this.getOrGenerateUserId();
192
227
  const requestBody = {
193
228
  chatbot_id: this.chatbotId,
@@ -206,7 +241,8 @@ export class OcsChat {
206
241
  body: JSON.stringify(requestBody)
207
242
  });
208
243
  if (!response.ok) {
209
- throw new Error(`Failed to start session: ${response.statusText}`);
244
+ this.handleError(`Failed to start session: ${response.statusText}`);
245
+ return;
210
246
  }
211
247
  const data = await response.json();
212
248
  this.sessionId = data.session_id;
@@ -214,24 +250,99 @@ export class OcsChat {
214
250
  // Handle seed message if present
215
251
  if (data.seed_message_task_id) {
216
252
  this.isTyping = true; // Show typing indicator for seed message
217
- await this.pollTaskResponse(data.seed_message_task_id);
253
+ this.currentPollTaskId = data.seed_message_task_id;
254
+ await this.pollTaskResponse();
218
255
  }
219
256
  // Start polling for messages
220
257
  this.startPolling();
221
258
  }
222
259
  catch (error) {
223
- this.error = error instanceof Error ? error.message : 'Failed to start chat session';
260
+ this.handleError('Failed to start chat session');
224
261
  }
225
262
  finally {
226
263
  this.isLoading = false;
227
264
  }
228
265
  }
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
+ async uploadFiles() {
275
+ if (this.selectedFiles.length === 0 || !this.sessionId || !this.allowAttachments) {
276
+ return [];
277
+ }
278
+ this.isUploadingFiles = true;
279
+ const uploadedIds = [];
280
+ 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;
326
+ }
327
+ finally {
328
+ this.isUploadingFiles = false;
329
+ }
330
+ }
229
331
  async sendMessage(message) {
230
332
  if (!this.sessionId || !message.trim())
231
333
  return;
232
- // Hide starter questions on any user interaction
233
- this.showStarterQuestions = false;
234
334
  try {
335
+ let attachmentIds = [];
336
+ if (this.allowAttachments && this.selectedFiles.length > 0) {
337
+ attachmentIds = await this.uploadFiles();
338
+ // Check if any files have errors after upload attempt
339
+ const hasErrors = this.selectedFiles.some(sf => sf.error);
340
+ if (hasErrors) {
341
+ // Don't send the message, let user fix file issues first
342
+ this.handleError('Please remove or fix file errors before sending your message.');
343
+ return;
344
+ }
345
+ }
235
346
  // If this is the first user message and there are welcome messages,
236
347
  // add them to chat history as assistant messages
237
348
  if (this.messages.length === 0 && this.parsedWelcomeMessages.length > 0) {
@@ -244,23 +355,36 @@ export class OcsChat {
244
355
  }));
245
356
  this.messages = [...this.messages, ...welcomeMessages];
246
357
  }
247
- // Add user message immediately
358
+ // Add user message immediately with attachments info
248
359
  const userMessage = {
249
360
  created_at: new Date().toISOString(),
250
361
  role: 'user',
251
362
  content: message.trim(),
252
- attachments: []
363
+ attachments: this.allowAttachments ? this.selectedFiles
364
+ .filter(sf => !sf.error && sf.uploaded)
365
+ .map(sf => ({
366
+ name: sf.file.name,
367
+ content_type: sf.file.type,
368
+ size: sf.file.size,
369
+ })) : []
253
370
  };
254
371
  this.messages = [...this.messages, userMessage];
255
372
  this.saveSessionToStorage();
256
373
  this.messageInput = '';
374
+ if (this.allowAttachments) {
375
+ this.selectedFiles = []; // Clear selected files after sending
376
+ }
257
377
  this.scrollToBottom();
258
378
  // Start typing indicator - it will stay on during task polling
259
379
  this.isTyping = true;
380
+ const requestBody = { message: message.trim() };
381
+ if (this.allowAttachments && attachmentIds.length > 0) {
382
+ requestBody.attachment_ids = attachmentIds;
383
+ }
260
384
  const response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/message/`, {
261
385
  method: 'POST',
262
386
  headers: this.getApiHeaders(),
263
- body: JSON.stringify({ message: message.trim() })
387
+ body: JSON.stringify(requestBody)
264
388
  });
265
389
  if (!response.ok) {
266
390
  throw new Error(`Failed to send message: ${response.statusText}`);
@@ -270,27 +394,28 @@ export class OcsChat {
270
394
  throw new Error(data.error || 'Failed to send message');
271
395
  }
272
396
  // Poll for the response - typing indicator will be managed in pollTaskResponse
273
- await this.pollTaskResponse(data.task_id);
397
+ this.currentPollTaskId = data.task_id;
398
+ await this.pollTaskResponse();
274
399
  }
275
400
  catch (error) {
276
- this.error = error instanceof Error ? error.message : 'Failed to send message';
277
- // Clear typing indicator on error
278
- this.isTyping = false;
401
+ const errorText = error instanceof Error ? error.message : 'Failed to send message';
402
+ this.handleError(errorText);
279
403
  }
280
404
  }
281
405
  handleStarterQuestionClick(question) {
282
406
  this.sendMessage(question);
283
407
  }
284
- async pollTaskResponse(taskId) {
285
- if (!this.sessionId)
408
+ async pollTaskResponse() {
409
+ if (!this.sessionId || !this.currentPollTaskId)
286
410
  return;
287
411
  // Stop message polling while task polling is active
288
- this.isTaskPolling = true;
289
412
  this.pauseMessagePolling();
290
413
  let attempts = 0;
291
414
  const poll = async () => {
415
+ if (!this.sessionId || !this.currentPollTaskId)
416
+ return;
292
417
  try {
293
- const response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/${taskId}/poll/`);
418
+ const response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/${this.currentPollTaskId}/poll/`);
294
419
  if (!response.ok) {
295
420
  throw new Error(`Failed to poll task: ${response.statusText}`);
296
421
  }
@@ -304,7 +429,7 @@ export class OcsChat {
304
429
  this.scrollToBottom();
305
430
  // Task polling complete, clear typing indicator and resume message polling
306
431
  this.isTyping = false;
307
- this.isTaskPolling = false;
432
+ this.currentPollTaskId = '';
308
433
  this.resumeMessagePolling();
309
434
  this.focusInput();
310
435
  return;
@@ -314,36 +439,47 @@ export class OcsChat {
314
439
  setTimeout(poll, OcsChat.TASK_POLLING_INTERVAL_MS);
315
440
  }
316
441
  else if (attempts >= OcsChat.TASK_POLLING_MAX_ATTEMPTS) {
317
- // Task polling timed out, clear typing indicator and resume message polling
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
318
453
  this.isTyping = false;
319
- this.isTaskPolling = false;
454
+ this.currentPollTaskId = '';
320
455
  this.resumeMessagePolling();
456
+ this.focusInput();
321
457
  }
322
458
  }
323
459
  catch (error) {
324
- this.error = error instanceof Error ? error.message : 'Failed to get response';
325
- // Error in task polling, clear typing indicator and resume message polling
326
- this.isTyping = false;
327
- this.isTaskPolling = false;
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 = '';
328
464
  this.resumeMessagePolling();
329
465
  }
330
466
  };
331
467
  await poll();
332
468
  }
333
469
  startPolling() {
334
- if (this.pollingInterval)
470
+ if (this.pollingIntervalRef)
335
471
  return;
336
- this.pollingInterval = setInterval(async () => {
472
+ this.pollingIntervalRef = setInterval(async () => {
337
473
  // Only poll for messages if not currently polling for a task
338
- if (!this.isTaskPolling) {
474
+ if (!this.currentPollTaskId) {
339
475
  await this.pollForMessages();
340
476
  }
341
477
  }, OcsChat.MESSAGE_POLLING_INTERVAL_MS);
342
478
  }
343
479
  pauseMessagePolling() {
344
- if (this.pollingInterval) {
345
- clearInterval(this.pollingInterval);
346
- this.pollingInterval = undefined;
480
+ if (this.pollingIntervalRef) {
481
+ clearInterval(this.pollingIntervalRef);
482
+ this.pollingIntervalRef = undefined;
347
483
  }
348
484
  }
349
485
  resumeMessagePolling() {
@@ -368,15 +504,11 @@ export class OcsChat {
368
504
  this.scrollToBottom();
369
505
  this.focusInput();
370
506
  }
371
- this.lastPollTime = new Date();
372
507
  }
373
- catch (error) {
508
+ catch (_a) {
374
509
  // Silently fail for polling
375
510
  }
376
511
  }
377
- clearError() {
378
- this.error = '';
379
- }
380
512
  scrollToBottom() {
381
513
  setTimeout(() => {
382
514
  if (this.messageListRef) {
@@ -399,24 +531,89 @@ export class OcsChat {
399
531
  }
400
532
  handleInputChange(event) {
401
533
  this.messageInput = event.target.value;
402
- // Hide starter questions when user starts typing
403
- if (this.messageInput.trim().length > 0) {
404
- this.showStarterQuestions = false;
534
+ }
535
+ handleFileSelect(event) {
536
+ var _a;
537
+ if (!this.allowAttachments)
538
+ return;
539
+ const input = event.target;
540
+ if (!input.files || input.files.length === 0)
541
+ 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];
574
+ input.value = '';
575
+ }
576
+ removeSelectedFile(index) {
577
+ if (!this.allowAttachments)
578
+ return;
579
+ this.selectedFiles = this.selectedFiles.filter((_, i) => i !== index);
580
+ }
581
+ formatFileSize(bytes) {
582
+ if (bytes === 0)
583
+ return '0 KB';
584
+ const k = 1024;
585
+ if (bytes < k * k) {
586
+ // Less than 1MB, show in KB
587
+ return Math.round(bytes / k * 100) / 100 + ' KB';
588
+ }
589
+ else {
590
+ return Math.round(bytes / (k * k) * 100) / 100 + ' MB';
405
591
  }
406
592
  }
407
593
  formatTime(dateString) {
408
594
  const date = new Date(dateString);
409
595
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
410
596
  }
411
- async load() {
597
+ toggleWindowVisibility() {
412
598
  this.visible = !this.visible;
413
- this.loaded = true;
414
- if (this.visible && !this.sessionId) {
415
- this.clearError();
599
+ }
600
+ /**
601
+ * Watch for changes to the `visible` attribute and update accordingly.
602
+ *
603
+ * @param visible - The new value for the field.
604
+ */
605
+ async visibilityHandler(visible) {
606
+ if (visible) {
607
+ this.initializePosition();
608
+ }
609
+ if (visible && !this.sessionId) {
416
610
  await this.startSession();
417
611
  }
418
- else if (!this.visible) {
419
- // Don't reset session when closing, allow resume
612
+ else if (!visible) {
613
+ this.pauseMessagePolling();
614
+ }
615
+ else {
616
+ this.resumeMessagePolling();
420
617
  }
421
618
  }
422
619
  setPosition(position) {
@@ -437,6 +634,7 @@ export class OcsChat {
437
634
  const actualChatWidth = Math.min(windowWidth, this.chatWindowFullscreenWidth);
438
635
  const centeredX = (windowWidth - actualChatWidth) / 2;
439
636
  const maxOffset = (windowWidth - actualChatWidth) / 2;
637
+ console.log(windowWidth, actualChatWidth, centeredX, maxOffset);
440
638
  return { windowWidth, actualChatWidth, centeredX, maxOffset };
441
639
  }
442
640
  getPositionStyles() {
@@ -455,6 +653,10 @@ export class OcsChat {
455
653
  };
456
654
  }
457
655
  initializePosition() {
656
+ if (this.positionInitialized) {
657
+ return;
658
+ }
659
+ this.positionInitialized = true;
458
660
  const windowWidth = window.innerWidth;
459
661
  const windowHeight = window.innerHeight;
460
662
  const chatWidth = windowWidth < OcsChat.MOBILE_BREAKPOINT ? windowWidth : this.chatWindowWidth;
@@ -569,10 +771,10 @@ export class OcsChat {
569
771
  const iconSrc = hasCustomIcon ? this.iconUrl : this.getDefaultIconUrl();
570
772
  const buttonClasses = this.getButtonClasses();
571
773
  if (hasText) {
572
- return (h("button", { class: buttonClasses, onClick: () => this.load(), "aria-label": `Open chat - ${this.buttonText}`, title: this.buttonText }, h("img", { src: iconSrc, alt: "" }), h("span", null, this.buttonText)));
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)));
573
775
  }
574
776
  else {
575
- return (h("button", { class: buttonClasses, onClick: () => this.load(), "aria-label": "Open chat", title: "Open chat" }, h("img", { src: iconSrc, alt: "Chat" })));
777
+ return (h("button", { class: buttonClasses, onClick: () => this.toggleWindowVisibility(), "aria-label": "Open chat", title: "Open chat" }, h("img", { src: iconSrc, alt: "Chat" })));
576
778
  }
577
779
  }
578
780
  getStorageKeys() {
@@ -676,13 +878,25 @@ export class OcsChat {
676
878
  return false;
677
879
  }
678
880
  }
679
- async startNewChat() {
881
+ showConfirmationDialog() {
882
+ this.showNewChatConfirmation = true;
883
+ }
884
+ hideConfirmationDialog() {
885
+ this.showNewChatConfirmation = false;
886
+ }
887
+ async confirmNewChat() {
888
+ this.hideConfirmationDialog();
889
+ await this.actuallyStartNewChat();
890
+ }
891
+ async actuallyStartNewChat() {
680
892
  this.clearSessionStorage();
681
893
  this.sessionId = undefined;
682
894
  this.messages = [];
683
- this.showStarterQuestions = true;
684
895
  this.isTyping = false;
685
- this.error = '';
896
+ this.currentPollTaskId = '';
897
+ if (this.allowAttachments) {
898
+ this.selectedFiles = [];
899
+ }
686
900
  this.cleanup();
687
901
  await this.startSession();
688
902
  }
@@ -692,16 +906,22 @@ export class OcsChat {
692
906
  this.fullscreenPosition = { x: 0 };
693
907
  }
694
908
  render() {
695
- if (this.error) {
909
+ // Only show error state for critical errors that prevent the widget from functioning
910
+ if (this.error && !this.sessionId) {
696
911
  return (h(Host, null, h("p", { class: "error-message" }, this.error)));
697
912
  }
698
- 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.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)), this.sessionId && this.messages.length > 0 && (h("button", { class: "header-button", onClick: () => this.startNewChat(), title: "Start new chat", "aria-label": "Start new chat" }, h(PencilSquare, null))), h("button", { class: "header-button", onClick: () => this.visible = false, "aria-label": "Close" }, h(XMarkIcon, null)))), 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..."))), this.sessionId && (h("div", { ref: (el) => this.messageListRef = el, class: "messages-container" }, this.messages.length === 0 && !this.isTyping && 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'
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'
699
914
  ? 'message-bubble-user'
700
915
  : message.role === 'assistant'
701
916
  ? 'message-bubble-assistant'
702
- : '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("a", { key: attachmentIndex, href: attachment.content_url, target: "_blank", rel: "noopener noreferrer", class: "attachment-link" }, "\uD83D\uDCCE ", 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, "Preparing response"), h("span", { class: "typing-dots" })))))), this.sessionId && this.showStarterQuestions && this.messages.length === 0 && !this.isTyping && (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.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 }), h("button", { class: `send-button ${!this.isTyping && !!this.messageInput.trim()
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) => {
918
+ // Unclear why but after removing all attachments this is being set to `null`.
919
+ if (el) {
920
+ this.fileInputRef = el;
921
+ }
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()
703
923
  ? 'send-button-enabled'
704
- : 'send-button-disabled'}`, onClick: () => this.sendMessage(this.messageInput), disabled: this.isTyping || !this.messageInput.trim() }, "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"))))))));
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"))))))));
705
925
  }
706
926
  static get is() { return "open-chat-studio-widget"; }
707
927
  static get encapsulation() { return "shadow"; }
@@ -833,6 +1053,26 @@ export class OcsChat {
833
1053
  "attribute": "header-text",
834
1054
  "reflect": false
835
1055
  },
1056
+ "newChatConfirmationMessage": {
1057
+ "type": "string",
1058
+ "mutable": false,
1059
+ "complexType": {
1060
+ "original": "string",
1061
+ "resolved": "string",
1062
+ "references": {}
1063
+ },
1064
+ "required": false,
1065
+ "optional": true,
1066
+ "docs": {
1067
+ "tags": [],
1068
+ "text": "The message to display in the new chat confirmation dialog."
1069
+ },
1070
+ "getter": false,
1071
+ "setter": false,
1072
+ "attribute": "new-chat-confirmation-message",
1073
+ "reflect": false,
1074
+ "defaultValue": "\"Starting a new chat will clear your current conversation. Continue?\""
1075
+ },
836
1076
  "visible": {
837
1077
  "type": "boolean",
838
1078
  "mutable": true,
@@ -1008,35 +1248,80 @@ export class OcsChat {
1008
1248
  "attribute": "allow-full-screen",
1009
1249
  "reflect": false,
1010
1250
  "defaultValue": "true"
1251
+ },
1252
+ "allowAttachments": {
1253
+ "type": "boolean",
1254
+ "mutable": false,
1255
+ "complexType": {
1256
+ "original": "boolean",
1257
+ "resolved": "boolean",
1258
+ "references": {}
1259
+ },
1260
+ "required": false,
1261
+ "optional": false,
1262
+ "docs": {
1263
+ "tags": [],
1264
+ "text": "Allow the user to attach files to their messages."
1265
+ },
1266
+ "getter": false,
1267
+ "setter": false,
1268
+ "attribute": "allow-attachments",
1269
+ "reflect": false,
1270
+ "defaultValue": "false"
1271
+ },
1272
+ "typingIndicatorText": {
1273
+ "type": "string",
1274
+ "mutable": false,
1275
+ "complexType": {
1276
+ "original": "string",
1277
+ "resolved": "string",
1278
+ "references": {}
1279
+ },
1280
+ "required": false,
1281
+ "optional": true,
1282
+ "docs": {
1283
+ "tags": [],
1284
+ "text": "The text to display while the assistant is typing/preparing a response."
1285
+ },
1286
+ "getter": false,
1287
+ "setter": false,
1288
+ "attribute": "typing-indicator-text",
1289
+ "reflect": false,
1290
+ "defaultValue": "\"Preparing response\""
1011
1291
  }
1012
1292
  };
1013
1293
  }
1014
1294
  static get states() {
1015
1295
  return {
1016
- "loaded": {},
1017
1296
  "error": {},
1018
1297
  "messages": {},
1019
1298
  "sessionId": {},
1020
1299
  "isLoading": {},
1021
1300
  "isTyping": {},
1022
1301
  "messageInput": {},
1023
- "pollingInterval": {},
1024
- "lastPollTime": {},
1025
- "isTaskPolling": {},
1302
+ "currentPollTaskId": {},
1026
1303
  "isDragging": {},
1027
1304
  "dragOffset": {},
1028
1305
  "windowPosition": {},
1029
1306
  "fullscreenPosition": {},
1030
- "showStarterQuestions": {},
1031
1307
  "parsedWelcomeMessages": {},
1032
1308
  "parsedStarterQuestions": {},
1033
1309
  "generatedUserId": {},
1034
- "isFullscreen": {}
1310
+ "isFullscreen": {},
1311
+ "showNewChatConfirmation": {},
1312
+ "selectedFiles": {},
1313
+ "isUploadingFiles": {}
1035
1314
  };
1036
1315
  }
1037
1316
  static get elementRef() { return "host"; }
1317
+ static get watchers() {
1318
+ return [{
1319
+ "propName": "visible",
1320
+ "methodName": "visibilityHandler"
1321
+ }];
1322
+ }
1038
1323
  }
1039
- OcsChat.TASK_POLLING_MAX_ATTEMPTS = 30;
1324
+ OcsChat.TASK_POLLING_MAX_ATTEMPTS = 120;
1040
1325
  OcsChat.TASK_POLLING_INTERVAL_MS = 1000;
1041
1326
  OcsChat.MESSAGE_POLLING_INTERVAL_MS = 30000;
1042
1327
  OcsChat.SCROLL_DELAY_MS = 100;
@@ -1044,4 +1329,8 @@ OcsChat.FOCUS_DELAY_MS = 100;
1044
1329
  OcsChat.MOBILE_BREAKPOINT = 640;
1045
1330
  OcsChat.WINDOW_MARGIN = 20;
1046
1331
  OcsChat.LOCALSTORAGE_TEST_KEY = '__ocs_test__';
1332
+ OcsChat.MAX_FILE_SIZE_MB = 50;
1333
+ OcsChat.MAX_TOTAL_SIZE_MB = 50;
1334
+ OcsChat.SUPPORTED_FILE_EXTENSIONS = ['.txt', '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.jpg', '.jpeg',
1335
+ '.png', '.gif', '.bmp', '.webp', '.svg', '.mp4', '.mov', '.avi', '.mp3', '.wav'];
1047
1336
  //# sourceMappingURL=ocs-chat.js.map