open-chat-studio-widget 0.7.0 → 0.9.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 (35) hide show
  1. package/README.md +28 -26
  2. package/dist/cjs/{index-CvB341El.js → index-DDod9Zyw.js} +3 -3
  3. package/dist/cjs/{index-CvB341El.js.map → index-DDod9Zyw.js.map} +1 -1
  4. package/dist/cjs/loader.cjs.js +2 -2
  5. package/dist/cjs/open-chat-studio-widget.cjs.entry.js +282 -49
  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/ocs-chat.js +176 -29
  10. package/dist/collection/components/ocs-chat/ocs-chat.js.map +1 -1
  11. package/dist/collection/services/chat-session-service.js +112 -8
  12. package/dist/collection/services/chat-session-service.js.map +1 -1
  13. package/dist/collection/services/file-attachment-manager.js +6 -0
  14. package/dist/collection/services/file-attachment-manager.js.map +1 -1
  15. package/dist/components/open-chat-studio-widget.js +284 -49
  16. package/dist/components/open-chat-studio-widget.js.map +1 -1
  17. package/dist/esm/{index-C2QZK0Ui.js → index-iUBQH9om.js} +3 -3
  18. package/dist/esm/{index-C2QZK0Ui.js.map → index-iUBQH9om.js.map} +1 -1
  19. package/dist/esm/loader.js +3 -3
  20. package/dist/esm/open-chat-studio-widget.entry.js +282 -49
  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.entry.esm.js.map +1 -1
  24. package/dist/open-chat-studio-widget/open-chat-studio-widget.esm.js +1 -1
  25. package/dist/open-chat-studio-widget/p-9c925476.entry.js +4 -0
  26. package/dist/open-chat-studio-widget/p-9c925476.entry.js.map +1 -0
  27. package/dist/open-chat-studio-widget/{p-C2QZK0Ui.js → p-iUBQH9om.js} +2 -2
  28. package/dist/open-chat-studio-widget/{p-C2QZK0Ui.js.map → p-iUBQH9om.js.map} +1 -1
  29. package/dist/types/components/ocs-chat/ocs-chat.d.ts +29 -1
  30. package/dist/types/components.d.ts +18 -2
  31. package/dist/types/services/chat-session-service.d.ts +27 -0
  32. package/dist/types/services/file-attachment-manager.d.ts +2 -0
  33. package/package.json +1 -1
  34. package/dist/open-chat-studio-widget/p-e87d4e31.entry.js +0 -4
  35. package/dist/open-chat-studio-widget/p-e87d4e31.entry.js.map +0 -1
@@ -4,7 +4,7 @@ import { XMarkIcon, GripDotsVerticalIcon, PlusWithCircleIcon, ArrowsPointingOutI
4
4
  import { renderMarkdownSync as renderMarkdownComplete } from "../../utils/markdown";
5
5
  import { varToPixels } from "../../utils/utils";
6
6
  import { TranslationManager, defaultTranslations } from "../../utils/translations";
7
- import { ChatSessionService } from "../../services/chat-session-service";
7
+ import { ChatSessionService, SessionAccessError } from "../../services/chat-session-service";
8
8
  import { FileAttachmentManager } from "../../services/file-attachment-manager";
9
9
  export class OcsChat {
10
10
  constructor() {
@@ -38,6 +38,7 @@ export class OcsChat {
38
38
  this.position = 'right';
39
39
  /**
40
40
  * Whether to persist session data to local storage to allow resuming previous conversations after page reload.
41
+ * Ignored when `sessionId` is provided.
41
42
  */
42
43
  this.persistentSession = true;
43
44
  /**
@@ -233,12 +234,18 @@ export class OcsChat {
233
234
  this.visible = true;
234
235
  }
235
236
  await this.initializeTranslations();
236
- // Always try to load existing session if localStorage is available
237
- if (this.persistentSession && this.isLocalStorageAvailable()) {
238
- const { sessionId, messages } = this.loadSessionFromStorage();
237
+ if (this.isSessionBound()) {
238
+ // Bound to an externally-managed session: the host page is the source of truth.
239
+ this.activeSessionId = this.sessionId;
240
+ this.applySessionToken(this.sessionToken);
241
+ }
242
+ else if (this.persistentSession && this.isLocalStorageAvailable()) {
243
+ // Always try to load existing session if localStorage is available
244
+ const { sessionId, messages, sessionToken } = this.loadSessionFromStorage();
239
245
  if (sessionId && messages) {
240
- this.sessionId = sessionId;
246
+ this.activeSessionId = sessionId;
241
247
  this.messages = messages;
248
+ this.applySessionToken(sessionToken);
242
249
  }
243
250
  }
244
251
  this.parseWelcomeMessages();
@@ -270,8 +277,13 @@ export class OcsChat {
270
277
  }
271
278
  }
272
279
  // Resume polling for existing session (don't auto-start new sessions)
273
- if (this.visible && this.sessionId) {
274
- this.startMessagePolling();
280
+ if (this.visible && this.activeSessionId) {
281
+ if (this.isSessionBound()) {
282
+ void this.loadBoundSessionHistory();
283
+ }
284
+ else {
285
+ this.startMessagePolling();
286
+ }
275
287
  }
276
288
  }, 0);
277
289
  window.addEventListener('resize', this.handleWindowResize);
@@ -282,6 +294,11 @@ export class OcsChat {
282
294
  this.removeButtonEventListeners();
283
295
  window.removeEventListener('resize', this.handleWindowResize);
284
296
  }
297
+ applySessionToken(token) {
298
+ var _a;
299
+ this.currentSessionToken = token;
300
+ (_a = this.chatService) === null || _a === void 0 ? void 0 : _a.setSessionToken(token);
301
+ }
285
302
  getChatService() {
286
303
  if (!this.chatService) {
287
304
  this.chatService = new ChatSessionService({
@@ -291,6 +308,7 @@ export class OcsChat {
291
308
  taskPollingIntervalMs: OcsChat.TASK_POLLING_INTERVAL_MS,
292
309
  taskPollingMaxAttempts: OcsChat.TASK_POLLING_MAX_ATTEMPTS,
293
310
  messagePollingIntervalMs: OcsChat.MESSAGE_POLLING_INTERVAL_MS,
311
+ sessionToken: this.currentSessionToken,
294
312
  });
295
313
  }
296
314
  return this.chatService;
@@ -306,6 +324,27 @@ export class OcsChat {
306
324
  this.saveSessionToStorage();
307
325
  this.scrollToBottom();
308
326
  }
327
+ /**
328
+ * Recover from a rejected session token (403). Unbound widgets discard the
329
+ * dead session/token, show a notice, and start fresh on the next send; bound
330
+ * widgets cannot restart a host-owned session, so they surface an error.
331
+ */
332
+ handleSessionAccessError() {
333
+ this.cleanup();
334
+ this.isLoading = false;
335
+ this.isTyping = false;
336
+ this.isUploadingFiles = false;
337
+ this.typingProgressMessage = '';
338
+ if (this.isSessionBound()) {
339
+ this.addErrorMessage(this.translationManager.get('status.sessionError', 'This chat session is no longer available.'));
340
+ return;
341
+ }
342
+ this.sessionEpoch += 1;
343
+ this.activeSessionId = undefined;
344
+ this.applySessionToken(undefined);
345
+ this.clearSessionStorage();
346
+ this.addErrorMessage(this.translationManager.get('status.sessionExpired', 'Your chat session expired. Starting a new chat — please resend your message.'));
347
+ }
309
348
  handleError(errorText) {
310
349
  // show as system message
311
350
  this.addErrorMessage(errorText);
@@ -378,12 +417,14 @@ export class OcsChat {
378
417
  this.currentPollTaskId = '';
379
418
  }
380
419
  async startSession() {
420
+ var _a;
381
421
  const epoch = this.sessionEpoch;
382
422
  try {
383
423
  this.isLoading = true;
384
424
  const userId = this.getOrGenerateUserId();
385
425
  const requestBody = {
386
426
  chatbot_id: this.chatbotId,
427
+ use_session_token: true,
387
428
  session_data: {
388
429
  source: 'widget',
389
430
  page_url: window.location.href,
@@ -399,7 +440,8 @@ export class OcsChat {
399
440
  const data = await this.getChatService().startSession(requestBody);
400
441
  if (epoch !== this.sessionEpoch)
401
442
  return;
402
- this.sessionId = data.session_id;
443
+ this.activeSessionId = data.session_id;
444
+ this.applySessionToken((_a = data.session_token) !== null && _a !== void 0 ? _a : undefined);
403
445
  this.saveSessionToStorage();
404
446
  this.startMessagePolling();
405
447
  }
@@ -412,19 +454,51 @@ export class OcsChat {
412
454
  this.isLoading = false;
413
455
  }
414
456
  }
457
+ /**
458
+ * Load the full message history for a session provided via the `session-id`
459
+ * prop, then begin regular polling.
460
+ */
461
+ async loadBoundSessionHistory() {
462
+ const epoch = this.sessionEpoch;
463
+ try {
464
+ const history = await this.getChatService().fetchAllMessages(this.activeSessionId);
465
+ if (epoch !== this.sessionEpoch)
466
+ return;
467
+ // Keep messages added while the history was loading (e.g. an optimistic
468
+ // user message) by appending any that aren't part of the fetched history.
469
+ const known = new Set(history.map(m => `${m.created_at}|${m.role}|${m.content}`));
470
+ const pending = this.messages.filter(m => !known.has(`${m.created_at}|${m.role}|${m.content}`));
471
+ this.messages = [...history, ...pending];
472
+ this.scrollToBottom(true);
473
+ }
474
+ catch (error) {
475
+ if (epoch !== this.sessionEpoch)
476
+ return;
477
+ if (error instanceof SessionAccessError) {
478
+ this.handleSessionAccessError();
479
+ return;
480
+ }
481
+ console.warn('Failed to load chat history:', error);
482
+ }
483
+ this.startMessagePolling();
484
+ }
415
485
  async uploadFiles() {
416
- if (this.selectedFiles.length === 0 || !this.sessionId || !this.allowAttachments) {
486
+ if (this.selectedFiles.length === 0 || !this.activeSessionId || !this.allowAttachments) {
417
487
  return [];
418
488
  }
419
489
  this.isUploadingFiles = true;
420
490
  try {
421
491
  const uploadResult = await this.attachmentManager.uploadPendingFiles(this.selectedFiles, {
422
492
  apiBaseUrl: this.apiBaseUrl || 'https://www.openchatstudio.com',
423
- sessionId: this.sessionId,
493
+ sessionId: this.activeSessionId,
424
494
  participantId: this.getOrGenerateUserId(),
425
495
  participantName: this.userName,
496
+ sessionToken: this.currentSessionToken,
426
497
  });
427
498
  this.selectedFiles = uploadResult.selectedFiles;
499
+ if (uploadResult.tokenRejected) {
500
+ throw new SessionAccessError(403, 'session_token_required', uploadResult.errorMessage || 'Session token rejected');
501
+ }
428
502
  return uploadResult.uploadedIds;
429
503
  }
430
504
  finally {
@@ -436,14 +510,14 @@ export class OcsChat {
436
510
  return;
437
511
  const epoch = this.sessionEpoch;
438
512
  // Start session if we don't have one yet
439
- if (!this.sessionId) {
513
+ if (!this.activeSessionId) {
440
514
  // Prevent concurrent session initialization
441
515
  if (this.isLoading) {
442
516
  return;
443
517
  }
444
518
  await this.startSession();
445
519
  // Check if session started successfully
446
- if (!this.sessionId) {
520
+ if (!this.activeSessionId) {
447
521
  return; // startSession already handled the error
448
522
  }
449
523
  }
@@ -504,7 +578,7 @@ export class OcsChat {
504
578
  if (this.versionNumber != null) {
505
579
  requestBody.version_number = this.versionNumber;
506
580
  }
507
- const data = await this.getChatService().sendMessage(this.sessionId, requestBody);
581
+ const data = await this.getChatService().sendMessage(this.activeSessionId, requestBody);
508
582
  if (epoch !== this.sessionEpoch)
509
583
  return;
510
584
  if (data.status === 'error') {
@@ -516,6 +590,10 @@ export class OcsChat {
516
590
  catch (error) {
517
591
  if (epoch !== this.sessionEpoch)
518
592
  return;
593
+ if (error instanceof SessionAccessError) {
594
+ this.handleSessionAccessError();
595
+ return;
596
+ }
519
597
  const errorText = error instanceof Error ? error.message : 'Failed to send message';
520
598
  this.handleError(errorText);
521
599
  }
@@ -632,9 +710,15 @@ export class OcsChat {
632
710
  this.initializePosition();
633
711
  }
634
712
  // Resume polling for existing session (don't auto-start new sessions)
635
- if (this.sessionId) {
713
+ if (this.activeSessionId) {
636
714
  this.scrollToBottom(true);
637
- this.startMessagePolling();
715
+ if (this.isSessionBound() && this.messages.length === 0) {
716
+ // A bound widget that was hidden at load has not fetched its history yet.
717
+ void this.loadBoundSessionHistory();
718
+ }
719
+ else {
720
+ this.startMessagePolling();
721
+ }
638
722
  }
639
723
  }
640
724
  else {
@@ -642,7 +726,7 @@ export class OcsChat {
642
726
  }
643
727
  }
644
728
  startTaskPolling(taskId) {
645
- if (!this.sessionId)
729
+ if (!this.activeSessionId)
646
730
  return;
647
731
  this.currentPollTaskId = taskId;
648
732
  this.isTyping = true;
@@ -650,7 +734,7 @@ export class OcsChat {
650
734
  if (this.taskPollingHandle) {
651
735
  this.taskPollingHandle.cancel();
652
736
  }
653
- this.taskPollingHandle = this.getChatService().pollTask(this.sessionId, taskId, {
737
+ this.taskPollingHandle = this.getChatService().pollTask(this.activeSessionId, taskId, {
654
738
  onMessage: message => {
655
739
  this.messages = [...this.messages, message];
656
740
  this.saveSessionToStorage();
@@ -684,20 +768,24 @@ export class OcsChat {
684
768
  },
685
769
  onError: error => {
686
770
  this.typingProgressMessage = '';
687
- this.handleError(error.message);
688
771
  this.taskPollingHandle = undefined;
772
+ if (error instanceof SessionAccessError) {
773
+ this.handleSessionAccessError();
774
+ return;
775
+ }
776
+ this.handleError(error.message);
689
777
  this.startMessagePolling();
690
778
  },
691
779
  });
692
780
  }
693
781
  startMessagePolling() {
694
- if (!this.sessionId || this.currentPollTaskId || !this.visible) {
782
+ if (!this.activeSessionId || this.currentPollTaskId || !this.visible) {
695
783
  return;
696
784
  }
697
785
  if (this.messagePollingHandle) {
698
786
  return;
699
787
  }
700
- this.messagePollingHandle = this.getChatService().startMessagePolling(this.sessionId, {
788
+ this.messagePollingHandle = this.getChatService().startMessagePolling(this.activeSessionId, {
701
789
  getSince: () => { var _a; return (this.messages.length > 0 ? (_a = this.messages.at(-1)) === null || _a === void 0 ? void 0 : _a.created_at : undefined); },
702
790
  onMessages: messages => {
703
791
  if (messages.length === 0)
@@ -1047,17 +1135,24 @@ export class OcsChat {
1047
1135
  messages: `ocs-chat-messages-${this.chatbotId}`,
1048
1136
  lastActivity: `ocs-chat-activity-${this.chatbotId}`,
1049
1137
  visible: `ocs-chat-visible-${this.chatbotId}`,
1138
+ sessionToken: `ocs-chat-token-${this.chatbotId}`,
1050
1139
  };
1051
1140
  }
1052
1141
  saveSessionToStorage() {
1053
- if (!this.persistentSession) {
1142
+ if (!this.persistentSession || this.isSessionBound()) {
1054
1143
  return;
1055
1144
  }
1056
1145
  const keys = this.getStorageKeys();
1057
1146
  try {
1058
- if (this.sessionId) {
1059
- localStorage.setItem(keys.sessionId, this.sessionId);
1147
+ if (this.activeSessionId) {
1148
+ localStorage.setItem(keys.sessionId, this.activeSessionId);
1060
1149
  localStorage.setItem(keys.lastActivity, new Date().toISOString());
1150
+ if (this.currentSessionToken) {
1151
+ localStorage.setItem(keys.sessionToken, this.currentSessionToken);
1152
+ }
1153
+ else {
1154
+ localStorage.removeItem(keys.sessionToken);
1155
+ }
1061
1156
  }
1062
1157
  localStorage.setItem(keys.messages, JSON.stringify(this.messages));
1063
1158
  }
@@ -1066,6 +1161,7 @@ export class OcsChat {
1066
1161
  }
1067
1162
  }
1068
1163
  loadSessionFromStorage() {
1164
+ var _a;
1069
1165
  const keys = this.getStorageKeys();
1070
1166
  try {
1071
1167
  if (this.persistentSessionExpire > 0) {
@@ -1093,7 +1189,8 @@ export class OcsChat {
1093
1189
  messages = [];
1094
1190
  }
1095
1191
  }
1096
- return { sessionId, messages };
1192
+ const sessionToken = (_a = localStorage.getItem(keys.sessionToken)) !== null && _a !== void 0 ? _a : undefined;
1193
+ return { sessionId, messages, sessionToken };
1097
1194
  }
1098
1195
  catch (error) {
1099
1196
  // fall back to starting a new session
@@ -1165,6 +1262,7 @@ export class OcsChat {
1165
1262
  localStorage.removeItem(keys.messages);
1166
1263
  localStorage.removeItem(keys.lastActivity);
1167
1264
  localStorage.removeItem(keys.visible);
1265
+ localStorage.removeItem(keys.sessionToken);
1168
1266
  }
1169
1267
  catch (error) {
1170
1268
  console.warn('Failed to clear chat session from localStorage:', error);
@@ -1173,6 +1271,9 @@ export class OcsChat {
1173
1271
  isKioskMode() {
1174
1272
  return this.mode === 'kiosk';
1175
1273
  }
1274
+ isSessionBound() {
1275
+ return !!this.sessionId;
1276
+ }
1176
1277
  isLocalStorageAvailable() {
1177
1278
  try {
1178
1279
  localStorage.setItem(OcsChat.LOCALSTORAGE_TEST_KEY, 'test');
@@ -1200,7 +1301,10 @@ export class OcsChat {
1200
1301
  async clearSession() {
1201
1302
  this.sessionEpoch += 1;
1202
1303
  this.clearSessionStorage();
1203
- this.sessionId = undefined;
1304
+ // A session provided by the host page (session-id prop) cannot be cleared;
1305
+ // stay bound to it. Unbound widgets start a new session on the next message.
1306
+ this.activeSessionId = this.sessionId;
1307
+ this.applySessionToken(this.isSessionBound() ? this.sessionToken : undefined);
1204
1308
  this.messages = [];
1205
1309
  this.isTyping = false;
1206
1310
  this.currentPollTaskId = '';
@@ -1208,6 +1312,11 @@ export class OcsChat {
1208
1312
  this.selectedFiles = [];
1209
1313
  }
1210
1314
  this.cleanup();
1315
+ if (this.isSessionBound()) {
1316
+ // The host-owned session cannot be cleared: reload its history and
1317
+ // resume polling so the widget doesn't end up in a dead state.
1318
+ void this.loadBoundSessionHistory();
1319
+ }
1211
1320
  }
1212
1321
  toggleFullscreen() {
1213
1322
  this.isFullscreen = !this.isFullscreen;
@@ -1216,10 +1325,10 @@ export class OcsChat {
1216
1325
  }
1217
1326
  render() {
1218
1327
  // Only show error state for critical errors that prevent the widget from functioning
1219
- if (this.error && !this.sessionId) {
1328
+ if (this.error && !this.activeSessionId) {
1220
1329
  return (h(Host, null, h("p", { class: "error-message" }, this.error)));
1221
1330
  }
1222
- 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() }, !this.isKioskMode() && (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.isKioskMode() && 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.getWelcomeMessages().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' ? 'message-bubble-user' : message.role === 'assistant' ? 'message-bubble-assistant' : '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.typingProgressMessage || this.translationManager.get('status.typing', this.typingIndicatorText)), h("span", { class: "typing-dots loading" }))))), this.messages.length === 0 && this.getStarterQuestions().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)))))))), 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.isLoading }), this.allowAttachments && (h("input", { ref: el => {
1331
+ 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() }, !this.isKioskMode() && (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 && !this.isSessionBound() && (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.isKioskMode() && 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.activeSessionId && (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.getWelcomeMessages().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' ? 'message-bubble-user' : message.role === 'assistant' ? 'message-bubble-assistant' : '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.typingProgressMessage || this.translationManager.get('status.typing', this.typingIndicatorText)), h("span", { class: "typing-dots loading" }))))), this.messages.length === 0 && this.getStarterQuestions().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)))))))), 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.isLoading }), this.allowAttachments && (h("input", { ref: el => {
1223
1332
  // Unclear why but after removing all attachments this is being set to `null`.
1224
1333
  if (el) {
1225
1334
  this.fileInputRef = el;
@@ -1563,7 +1672,7 @@ export class OcsChat {
1563
1672
  "optional": false,
1564
1673
  "docs": {
1565
1674
  "tags": [],
1566
- "text": "Whether to persist session data to local storage to allow resuming previous conversations after page reload."
1675
+ "text": "Whether to persist session data to local storage to allow resuming previous conversations after page reload.\nIgnored when `sessionId` is provided."
1567
1676
  },
1568
1677
  "getter": false,
1569
1678
  "setter": false,
@@ -1731,6 +1840,44 @@ export class OcsChat {
1731
1840
  "getter": false,
1732
1841
  "setter": false,
1733
1842
  "reflect": false
1843
+ },
1844
+ "sessionId": {
1845
+ "type": "string",
1846
+ "attribute": "session-id",
1847
+ "mutable": false,
1848
+ "complexType": {
1849
+ "original": "string",
1850
+ "resolved": "string",
1851
+ "references": {}
1852
+ },
1853
+ "required": false,
1854
+ "optional": true,
1855
+ "docs": {
1856
+ "tags": [],
1857
+ "text": "The ID of an existing chat session to connect to. When provided, the widget\nis bound to that session: local session persistence is disabled and the\nmessage history is loaded from the server. Intended for host pages that\ncreate the session server-side (e.g. the OCS web chat page)."
1858
+ },
1859
+ "getter": false,
1860
+ "setter": false,
1861
+ "reflect": false
1862
+ },
1863
+ "sessionToken": {
1864
+ "type": "string",
1865
+ "attribute": "session-token",
1866
+ "mutable": false,
1867
+ "complexType": {
1868
+ "original": "string",
1869
+ "resolved": "string",
1870
+ "references": {}
1871
+ },
1872
+ "required": false,
1873
+ "optional": true,
1874
+ "docs": {
1875
+ "tags": [],
1876
+ "text": "A session token proving access to the session named by `session-id`. Host\npages that create the session server-side pass a server-minted token here so\nthe widget can authenticate its requests. Only meaningful with `session-id`."
1877
+ },
1878
+ "getter": false,
1879
+ "setter": false,
1880
+ "reflect": false
1734
1881
  }
1735
1882
  };
1736
1883
  }
@@ -1738,7 +1885,7 @@ export class OcsChat {
1738
1885
  return {
1739
1886
  "error": {},
1740
1887
  "messages": {},
1741
- "sessionId": {},
1888
+ "activeSessionId": {},
1742
1889
  "isLoading": {},
1743
1890
  "isTyping": {},
1744
1891
  "typingProgressMessage": {},