open-chat-studio-widget 0.4.4 → 0.4.6

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 (38) 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 +500 -79
  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 +1042 -129
  11. package/dist/collection/components/ocs-chat/ocs-chat.js +380 -84
  12. package/dist/collection/components/ocs-chat/ocs-chat.js.map +1 -1
  13. package/dist/collection/utils/cookies.js +28 -0
  14. package/dist/collection/utils/cookies.js.map +1 -0
  15. package/dist/components/open-chat-studio-widget.js +509 -84
  16. package/dist/components/open-chat-studio-widget.js.map +1 -1
  17. package/dist/esm/{index-205c77bc.js → index-0349ca51.js} +29 -3
  18. package/dist/esm/index-0349ca51.js.map +1 -0
  19. package/dist/esm/loader.js +3 -3
  20. package/dist/esm/open-chat-studio-widget.entry.js +500 -79
  21. package/dist/esm/open-chat-studio-widget.entry.js.map +1 -1
  22. package/dist/esm/open-chat-studio-widget.js +3 -3
  23. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js +1 -1
  24. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js.map +1 -1
  25. package/dist/open-chat-studio-widget/p-16df1b20.entry.js +4 -0
  26. package/dist/open-chat-studio-widget/p-16df1b20.entry.js.map +1 -0
  27. package/dist/open-chat-studio-widget/{p-78d09c6b.js → p-3dc66a9a.js} +3 -3
  28. package/dist/open-chat-studio-widget/p-3dc66a9a.js.map +1 -0
  29. package/dist/types/components/ocs-chat/heroicons.d.ts +4 -1
  30. package/dist/types/components/ocs-chat/ocs-chat.d.ts +52 -9
  31. package/dist/types/components.d.ts +24 -0
  32. package/dist/types/utils/cookies.d.ts +4 -0
  33. package/package.json +3 -2
  34. package/dist/cjs/index-bcb28089.js.map +0 -1
  35. package/dist/esm/index-205c77bc.js.map +0 -1
  36. package/dist/open-chat-studio-widget/p-19d45fe5.entry.js +0 -3
  37. package/dist/open-chat-studio-widget/p-19d45fe5.entry.js.map +0 -1
  38. package/dist/open-chat-studio-widget/p-78d09c6b.js.map +0 -1
@@ -1,6 +1,8 @@
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";
5
+ import { getCSRFToken } from "../../utils/cookies";
4
6
  import { varToPixels } from "../../utils/utils";
5
7
  export class OcsChat {
6
8
  constructor() {
@@ -12,6 +14,10 @@ export class OcsChat {
12
14
  * The shape of the chat button. 'round' makes it circular, 'square' keeps it rectangular.
13
15
  */
14
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?";
15
21
  /**
16
22
  * Whether the chat widget is visible on load.
17
23
  */
@@ -33,24 +39,34 @@ export class OcsChat {
33
39
  * Allow the user to make the chat window full screen.
34
40
  */
35
41
  this.allowFullScreen = true;
36
- 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";
37
50
  this.error = "";
38
51
  this.messages = [];
39
52
  this.isLoading = false;
40
53
  this.isTyping = false;
41
54
  this.messageInput = "";
42
- this.isTaskPolling = false;
55
+ this.currentPollTaskId = "";
43
56
  this.isDragging = false;
44
57
  this.dragOffset = { x: 0, y: 0 };
45
58
  this.windowPosition = { x: 0, y: 0 };
46
59
  this.fullscreenPosition = { x: 0 };
47
- this.showStarterQuestions = true;
48
60
  this.parsedWelcomeMessages = [];
49
61
  this.parsedStarterQuestions = [];
50
62
  this.isFullscreen = false;
63
+ this.showNewChatConfirmation = false;
64
+ this.selectedFiles = [];
65
+ this.isUploadingFiles = false;
51
66
  this.chatWindowHeight = 600;
52
67
  this.chatWindowWidth = 450;
53
68
  this.chatWindowFullscreenWidth = 1024;
69
+ this.positionInitialized = false;
54
70
  this.handleMouseDown = (event) => {
55
71
  if (!this.isFullscreen && window.innerWidth < OcsChat.MOBILE_BREAKPOINT)
56
72
  return;
@@ -95,11 +111,11 @@ export class OcsChat {
95
111
  this.endDrag();
96
112
  };
97
113
  this.handleWindowResize = () => {
114
+ this.positionInitialized = false;
98
115
  this.initializePosition();
99
116
  };
100
117
  }
101
118
  componentWillLoad() {
102
- this.loaded = this.visible;
103
119
  if (!this.chatbotId) {
104
120
  this.error = 'Chatbot ID is required';
105
121
  return;
@@ -110,13 +126,22 @@ export class OcsChat {
110
126
  if (sessionId && messages) {
111
127
  this.sessionId = sessionId;
112
128
  this.messages = messages;
113
- this.showStarterQuestions = messages.length === 0;
114
129
  }
115
130
  }
116
131
  this.parseWelcomeMessages();
117
132
  this.parseStarterQuestions();
118
133
  }
119
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
+ }
120
145
  // Only auto-start session if we don't have an existing one
121
146
  if (this.visible && !this.sessionId) {
122
147
  this.startSession();
@@ -125,14 +150,6 @@ export class OcsChat {
125
150
  // Resume polling for existing session
126
151
  this.startPolling();
127
152
  }
128
- const computedStyle = getComputedStyle(this.host);
129
- const windowHeightVar = computedStyle.getPropertyValue('--chat-window-height');
130
- const windowWidthVar = computedStyle.getPropertyValue('--chat-window-width');
131
- const fullscreenWidthVar = computedStyle.getPropertyValue('--chat-window-fullscreen-width');
132
- this.chatWindowHeight = varToPixels(windowHeightVar, window.innerHeight, this.chatWindowHeight);
133
- this.chatWindowWidth = varToPixels(windowWidthVar, window.innerWidth, this.chatWindowWidth);
134
- this.chatWindowFullscreenWidth = varToPixels(fullscreenWidthVar, window.innerWidth, this.chatWindowFullscreenWidth);
135
- this.initializePosition();
136
153
  window.addEventListener('resize', this.handleWindowResize);
137
154
  }
138
155
  disconnectedCallback() {
@@ -140,6 +157,26 @@ export class OcsChat {
140
157
  this.removeEventListeners();
141
158
  window.removeEventListener('resize', this.handleWindowResize);
142
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
+ }
143
180
  parseJSONProp(propValue, propName) {
144
181
  try {
145
182
  if (propValue) {
@@ -164,19 +201,28 @@ export class OcsChat {
164
201
  this.parsedStarterQuestions = this.parseJSONProp(this.starterQuestions, 'starter questions');
165
202
  }
166
203
  cleanup() {
167
- if (this.pollingInterval) {
168
- clearInterval(this.pollingInterval);
169
- this.pollingInterval = undefined;
204
+ if (this.pollingIntervalRef) {
205
+ clearInterval(this.pollingIntervalRef);
206
+ this.pollingIntervalRef = undefined;
170
207
  }
171
- this.isTaskPolling = false;
208
+ this.currentPollTaskId = '';
172
209
  }
173
210
  getApiBaseUrl() {
174
211
  return this.apiBaseUrl || window.location.origin;
175
212
  }
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;
220
+ }
221
+ return headers;
222
+ }
176
223
  async startSession() {
177
224
  try {
178
225
  this.isLoading = true;
179
- this.error = '';
180
226
  const userId = this.getOrGenerateUserId();
181
227
  const requestBody = {
182
228
  chatbot_id: this.chatbotId,
@@ -191,13 +237,12 @@ export class OcsChat {
191
237
  }
192
238
  const response = await fetch(`${this.getApiBaseUrl()}/api/chat/start/`, {
193
239
  method: 'POST',
194
- headers: {
195
- 'Content-Type': 'application/json',
196
- },
240
+ headers: this.getApiHeaders(),
197
241
  body: JSON.stringify(requestBody)
198
242
  });
199
243
  if (!response.ok) {
200
- throw new Error(`Failed to start session: ${response.statusText}`);
244
+ this.handleError(`Failed to start session: ${response.statusText}`);
245
+ return;
201
246
  }
202
247
  const data = await response.json();
203
248
  this.sessionId = data.session_id;
@@ -205,24 +250,99 @@ export class OcsChat {
205
250
  // Handle seed message if present
206
251
  if (data.seed_message_task_id) {
207
252
  this.isTyping = true; // Show typing indicator for seed message
208
- await this.pollTaskResponse(data.seed_message_task_id);
253
+ this.currentPollTaskId = data.seed_message_task_id;
254
+ await this.pollTaskResponse();
209
255
  }
210
256
  // Start polling for messages
211
257
  this.startPolling();
212
258
  }
213
259
  catch (error) {
214
- this.error = error instanceof Error ? error.message : 'Failed to start chat session';
260
+ this.handleError('Failed to start chat session');
215
261
  }
216
262
  finally {
217
263
  this.isLoading = false;
218
264
  }
219
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
+ }
220
331
  async sendMessage(message) {
221
332
  if (!this.sessionId || !message.trim())
222
333
  return;
223
- // Hide starter questions on any user interaction
224
- this.showStarterQuestions = false;
225
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
+ }
226
346
  // If this is the first user message and there are welcome messages,
227
347
  // add them to chat history as assistant messages
228
348
  if (this.messages.length === 0 && this.parsedWelcomeMessages.length > 0) {
@@ -235,25 +355,36 @@ export class OcsChat {
235
355
  }));
236
356
  this.messages = [...this.messages, ...welcomeMessages];
237
357
  }
238
- // Add user message immediately
358
+ // Add user message immediately with attachments info
239
359
  const userMessage = {
240
360
  created_at: new Date().toISOString(),
241
361
  role: 'user',
242
362
  content: message.trim(),
243
- 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
+ })) : []
244
370
  };
245
371
  this.messages = [...this.messages, userMessage];
246
372
  this.saveSessionToStorage();
247
373
  this.messageInput = '';
374
+ if (this.allowAttachments) {
375
+ this.selectedFiles = []; // Clear selected files after sending
376
+ }
248
377
  this.scrollToBottom();
249
378
  // Start typing indicator - it will stay on during task polling
250
379
  this.isTyping = true;
380
+ const requestBody = { message: message.trim() };
381
+ if (this.allowAttachments && attachmentIds.length > 0) {
382
+ requestBody.attachment_ids = attachmentIds;
383
+ }
251
384
  const response = await fetch(`${this.getApiBaseUrl()}/api/chat/${this.sessionId}/message/`, {
252
385
  method: 'POST',
253
- headers: {
254
- 'Content-Type': 'application/json',
255
- },
256
- body: JSON.stringify({ message: message.trim() })
386
+ headers: this.getApiHeaders(),
387
+ body: JSON.stringify(requestBody)
257
388
  });
258
389
  if (!response.ok) {
259
390
  throw new Error(`Failed to send message: ${response.statusText}`);
@@ -263,27 +394,28 @@ export class OcsChat {
263
394
  throw new Error(data.error || 'Failed to send message');
264
395
  }
265
396
  // Poll for the response - typing indicator will be managed in pollTaskResponse
266
- await this.pollTaskResponse(data.task_id);
397
+ this.currentPollTaskId = data.task_id;
398
+ await this.pollTaskResponse();
267
399
  }
268
400
  catch (error) {
269
- this.error = error instanceof Error ? error.message : 'Failed to send message';
270
- // Clear typing indicator on error
271
- this.isTyping = false;
401
+ const errorText = error instanceof Error ? error.message : 'Failed to send message';
402
+ this.handleError(errorText);
272
403
  }
273
404
  }
274
405
  handleStarterQuestionClick(question) {
275
406
  this.sendMessage(question);
276
407
  }
277
- async pollTaskResponse(taskId) {
278
- if (!this.sessionId)
408
+ async pollTaskResponse() {
409
+ if (!this.sessionId || !this.currentPollTaskId)
279
410
  return;
280
411
  // Stop message polling while task polling is active
281
- this.isTaskPolling = true;
282
412
  this.pauseMessagePolling();
283
413
  let attempts = 0;
284
414
  const poll = async () => {
415
+ if (!this.sessionId || !this.currentPollTaskId)
416
+ return;
285
417
  try {
286
- 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/`);
287
419
  if (!response.ok) {
288
420
  throw new Error(`Failed to poll task: ${response.statusText}`);
289
421
  }
@@ -297,7 +429,7 @@ export class OcsChat {
297
429
  this.scrollToBottom();
298
430
  // Task polling complete, clear typing indicator and resume message polling
299
431
  this.isTyping = false;
300
- this.isTaskPolling = false;
432
+ this.currentPollTaskId = '';
301
433
  this.resumeMessagePolling();
302
434
  this.focusInput();
303
435
  return;
@@ -307,36 +439,47 @@ export class OcsChat {
307
439
  setTimeout(poll, OcsChat.TASK_POLLING_INTERVAL_MS);
308
440
  }
309
441
  else if (attempts >= OcsChat.TASK_POLLING_MAX_ATTEMPTS) {
310
- // 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
311
453
  this.isTyping = false;
312
- this.isTaskPolling = false;
454
+ this.currentPollTaskId = '';
313
455
  this.resumeMessagePolling();
456
+ this.focusInput();
314
457
  }
315
458
  }
316
459
  catch (error) {
317
- this.error = error instanceof Error ? error.message : 'Failed to get response';
318
- // Error in task polling, clear typing indicator and resume message polling
319
- this.isTyping = false;
320
- 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 = '';
321
464
  this.resumeMessagePolling();
322
465
  }
323
466
  };
324
467
  await poll();
325
468
  }
326
469
  startPolling() {
327
- if (this.pollingInterval)
470
+ if (this.pollingIntervalRef)
328
471
  return;
329
- this.pollingInterval = setInterval(async () => {
472
+ this.pollingIntervalRef = setInterval(async () => {
330
473
  // Only poll for messages if not currently polling for a task
331
- if (!this.isTaskPolling) {
474
+ if (!this.currentPollTaskId) {
332
475
  await this.pollForMessages();
333
476
  }
334
477
  }, OcsChat.MESSAGE_POLLING_INTERVAL_MS);
335
478
  }
336
479
  pauseMessagePolling() {
337
- if (this.pollingInterval) {
338
- clearInterval(this.pollingInterval);
339
- this.pollingInterval = undefined;
480
+ if (this.pollingIntervalRef) {
481
+ clearInterval(this.pollingIntervalRef);
482
+ this.pollingIntervalRef = undefined;
340
483
  }
341
484
  }
342
485
  resumeMessagePolling() {
@@ -361,15 +504,11 @@ export class OcsChat {
361
504
  this.scrollToBottom();
362
505
  this.focusInput();
363
506
  }
364
- this.lastPollTime = new Date();
365
507
  }
366
- catch (error) {
508
+ catch (_a) {
367
509
  // Silently fail for polling
368
510
  }
369
511
  }
370
- clearError() {
371
- this.error = '';
372
- }
373
512
  scrollToBottom() {
374
513
  setTimeout(() => {
375
514
  if (this.messageListRef) {
@@ -392,24 +531,89 @@ export class OcsChat {
392
531
  }
393
532
  handleInputChange(event) {
394
533
  this.messageInput = event.target.value;
395
- // Hide starter questions when user starts typing
396
- if (this.messageInput.trim().length > 0) {
397
- 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';
398
591
  }
399
592
  }
400
593
  formatTime(dateString) {
401
594
  const date = new Date(dateString);
402
595
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
403
596
  }
404
- async load() {
597
+ toggleWindowVisibility() {
405
598
  this.visible = !this.visible;
406
- this.loaded = true;
407
- if (this.visible && !this.sessionId) {
408
- 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) {
409
610
  await this.startSession();
410
611
  }
411
- else if (!this.visible) {
412
- // Don't reset session when closing, allow resume
612
+ else if (!visible) {
613
+ this.pauseMessagePolling();
614
+ }
615
+ else {
616
+ this.resumeMessagePolling();
413
617
  }
414
618
  }
415
619
  setPosition(position) {
@@ -430,6 +634,7 @@ export class OcsChat {
430
634
  const actualChatWidth = Math.min(windowWidth, this.chatWindowFullscreenWidth);
431
635
  const centeredX = (windowWidth - actualChatWidth) / 2;
432
636
  const maxOffset = (windowWidth - actualChatWidth) / 2;
637
+ console.log(windowWidth, actualChatWidth, centeredX, maxOffset);
433
638
  return { windowWidth, actualChatWidth, centeredX, maxOffset };
434
639
  }
435
640
  getPositionStyles() {
@@ -448,6 +653,10 @@ export class OcsChat {
448
653
  };
449
654
  }
450
655
  initializePosition() {
656
+ if (this.positionInitialized) {
657
+ return;
658
+ }
659
+ this.positionInitialized = true;
451
660
  const windowWidth = window.innerWidth;
452
661
  const windowHeight = window.innerHeight;
453
662
  const chatWidth = windowWidth < OcsChat.MOBILE_BREAKPOINT ? windowWidth : this.chatWindowWidth;
@@ -562,10 +771,10 @@ export class OcsChat {
562
771
  const iconSrc = hasCustomIcon ? this.iconUrl : this.getDefaultIconUrl();
563
772
  const buttonClasses = this.getButtonClasses();
564
773
  if (hasText) {
565
- 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)));
566
775
  }
567
776
  else {
568
- 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" })));
569
778
  }
570
779
  }
571
780
  getStorageKeys() {
@@ -669,13 +878,25 @@ export class OcsChat {
669
878
  return false;
670
879
  }
671
880
  }
672
- 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() {
673
892
  this.clearSessionStorage();
674
893
  this.sessionId = undefined;
675
894
  this.messages = [];
676
- this.showStarterQuestions = true;
677
895
  this.isTyping = false;
678
- this.error = '';
896
+ this.currentPollTaskId = '';
897
+ if (this.allowAttachments) {
898
+ this.selectedFiles = [];
899
+ }
679
900
  this.cleanup();
680
901
  await this.startSession();
681
902
  }
@@ -685,16 +906,22 @@ export class OcsChat {
685
906
  this.fullscreenPosition = { x: 0 };
686
907
  }
687
908
  render() {
688
- if (this.error) {
909
+ // Only show error state for critical errors that prevent the widget from functioning
910
+ if (this.error && !this.sessionId) {
689
911
  return (h(Host, null, h("p", { class: "error-message" }, this.error)));
690
912
  }
691
- 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'
692
914
  ? 'message-bubble-user'
693
915
  : message.role === 'assistant'
694
916
  ? 'message-bubble-assistant'
695
- : '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()
696
923
  ? 'send-button-enabled'
697
- : '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"))))))));
698
925
  }
699
926
  static get is() { return "open-chat-studio-widget"; }
700
927
  static get encapsulation() { return "shadow"; }
@@ -826,6 +1053,26 @@ export class OcsChat {
826
1053
  "attribute": "header-text",
827
1054
  "reflect": false
828
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
+ },
829
1076
  "visible": {
830
1077
  "type": "boolean",
831
1078
  "mutable": true,
@@ -1001,35 +1248,80 @@ export class OcsChat {
1001
1248
  "attribute": "allow-full-screen",
1002
1249
  "reflect": false,
1003
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\""
1004
1291
  }
1005
1292
  };
1006
1293
  }
1007
1294
  static get states() {
1008
1295
  return {
1009
- "loaded": {},
1010
1296
  "error": {},
1011
1297
  "messages": {},
1012
1298
  "sessionId": {},
1013
1299
  "isLoading": {},
1014
1300
  "isTyping": {},
1015
1301
  "messageInput": {},
1016
- "pollingInterval": {},
1017
- "lastPollTime": {},
1018
- "isTaskPolling": {},
1302
+ "currentPollTaskId": {},
1019
1303
  "isDragging": {},
1020
1304
  "dragOffset": {},
1021
1305
  "windowPosition": {},
1022
1306
  "fullscreenPosition": {},
1023
- "showStarterQuestions": {},
1024
1307
  "parsedWelcomeMessages": {},
1025
1308
  "parsedStarterQuestions": {},
1026
1309
  "generatedUserId": {},
1027
- "isFullscreen": {}
1310
+ "isFullscreen": {},
1311
+ "showNewChatConfirmation": {},
1312
+ "selectedFiles": {},
1313
+ "isUploadingFiles": {}
1028
1314
  };
1029
1315
  }
1030
1316
  static get elementRef() { return "host"; }
1317
+ static get watchers() {
1318
+ return [{
1319
+ "propName": "visible",
1320
+ "methodName": "visibilityHandler"
1321
+ }];
1322
+ }
1031
1323
  }
1032
- OcsChat.TASK_POLLING_MAX_ATTEMPTS = 30;
1324
+ OcsChat.TASK_POLLING_MAX_ATTEMPTS = 120;
1033
1325
  OcsChat.TASK_POLLING_INTERVAL_MS = 1000;
1034
1326
  OcsChat.MESSAGE_POLLING_INTERVAL_MS = 30000;
1035
1327
  OcsChat.SCROLL_DELAY_MS = 100;
@@ -1037,4 +1329,8 @@ OcsChat.FOCUS_DELAY_MS = 100;
1037
1329
  OcsChat.MOBILE_BREAKPOINT = 640;
1038
1330
  OcsChat.WINDOW_MARGIN = 20;
1039
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'];
1040
1336
  //# sourceMappingURL=ocs-chat.js.map