@kodaris/krubble-app-components 1.0.125 → 1.0.127

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.
@@ -3344,11 +3344,6 @@ class KRChatbot extends i$3 {
3344
3344
  // --- Lifecycle ---
3345
3345
  connectedCallback() {
3346
3346
  super.connectedCallback();
3347
- // Capture the consumer's targeting BEFORE we default `load` below: a custom
3348
- // `load` or an explicitly-set `conversation-id` means the consumer owns which
3349
- // conversation shows, so the tab-scoped resume must not override it.
3350
- const usesBuiltInLoad = !this.load;
3351
- const consumerSetConversationId = this.conversationId != null;
3352
3347
  // Default to loading a conversation's messages when the consumer hasn't
3353
3348
  // provided their own `load`. With `conversationId` set we rebuild that
3354
3349
  // conversation; with none set the user wants a NEW conversation, so we start
@@ -3436,49 +3431,48 @@ class KRChatbot extends i$3 {
3436
3431
  // most-recent one. The pinned id lives in sessionStorage (per-tab — unlike
3437
3432
  // localStorage it isn't shared across tabs, so two tabs can sit on different
3438
3433
  // conversations). We resolve the id FIRST, then run the built-in load so it
3439
- // rebuilds the right transcript. Skipped when the consumer owns targeting (a
3440
- // custom `load` or an explicit `conversation-id`) or in inline mode (embedded
3441
- // as a page — the host route, not the tab, decides the conversation).
3442
- const resolveConversationId = () => {
3443
- if (this.variant !== 'floating' || !usesBuiltInLoad || consumerSetConversationId) {
3444
- return Promise.resolve();
3445
- }
3434
+ // rebuilds the right transcript. Skipped when the consumer owns targeting (an
3435
+ // explicit `conversation-id`) or in inline mode (embedded as a page — the host
3436
+ // route, not the tab, decides the conversation).
3437
+ let resolveConversationId = Promise.resolve();
3438
+ if (this.variant === 'floating' && this.conversationId == null) {
3446
3439
  const stored = Number(sessionStorage.getItem('kr-chatbot-conversation-id'));
3447
3440
  if (stored > 0) {
3448
3441
  this.conversationId = stored;
3449
3442
  // Fetch the conversation so the header shows its title on reload (the
3450
3443
  // transcript load doesn't carry it). Best-effort — the header falls back
3451
3444
  // to the default label if this fails.
3452
- return KRHttp.fetch({ url: `/api/system/conversation/${stored}` })
3445
+ resolveConversationId = KRHttp.fetch({ url: `/api/system/conversation/${stored}` })
3453
3446
  .then((r) => r.json())
3454
3447
  .then((res) => {
3455
3448
  this._conversationTitle = res?.data?.title || '';
3456
3449
  })
3457
3450
  .catch(() => { });
3458
3451
  }
3459
- // Fresh tab, nothing pinned yet — resume the user's most recent conversation
3460
- // (404 when they have none, which leaves us blank for a new one).
3461
- return KRHttp.fetch({ url: '/api/system/conversation/last' })
3462
- .then((r) => {
3463
- if (!r.ok) {
3464
- return;
3465
- }
3466
- return r.json().then((res) => {
3467
- const id = res?.data?.conversationID;
3468
- if (typeof id === 'number') {
3469
- this.conversationId = id;
3470
- this._conversationTitle = res?.data?.title || '';
3471
- this._persistConversationId();
3452
+ else {
3453
+ // Fresh tab, nothing pinned yet — resume the user's most recent conversation
3454
+ // (404 when they have none, which leaves us blank for a new one).
3455
+ resolveConversationId = KRHttp.fetch({ url: '/api/system/conversation/last' })
3456
+ .then((r) => {
3457
+ if (!r.ok) {
3458
+ return;
3472
3459
  }
3473
- });
3474
- })
3475
- .catch(() => { });
3476
- };
3460
+ return r.json().then((res) => {
3461
+ const id = res?.data?.conversationID;
3462
+ if (typeof id === 'number') {
3463
+ this.conversationId = id;
3464
+ this._conversationTitle = res?.data?.title || '';
3465
+ this._persistConversationId();
3466
+ }
3467
+ });
3468
+ })
3469
+ .catch(() => { });
3470
+ }
3471
+ }
3477
3472
  // `this.load` is always set by here (defaulted above when the consumer didn't
3478
- // supply one), so capture it for the async chain — TS can't narrow the nullable
3479
- // property across the promise boundary.
3480
- const load = this.load;
3481
- resolveConversationId().then(() => load()).then((messages) => {
3473
+ // supply one); the `!` is because TS can't narrow the nullable property across
3474
+ // the promise boundary.
3475
+ resolveConversationId.then(() => this.load()).then((messages) => {
3482
3476
  if (messages && messages.length) {
3483
3477
  this.messages = messages;
3484
3478
  }
@@ -3728,6 +3722,13 @@ class KRChatbot extends i$3 {
3728
3722
  * embeds and custom-`load` consumers own their own targeting.
3729
3723
  */
3730
3724
  _persistConversationId() {
3725
+ // Floating-only: inline embeds (and custom-`load` consumers) own their own
3726
+ // targeting and never resume from this key, so they must not write it either —
3727
+ // otherwise an inline chat sharing the page/origin would clobber the floating
3728
+ // widget's pinned id.
3729
+ if (this.variant !== 'floating') {
3730
+ return;
3731
+ }
3731
3732
  try {
3732
3733
  if (this.conversationId != null) {
3733
3734
  sessionStorage.setItem('kr-chatbot-conversation-id', String(this.conversationId));
@@ -3740,11 +3741,10 @@ class KRChatbot extends i$3 {
3740
3741
  }
3741
3742
  _saveState() {
3742
3743
  try {
3743
- // `toolDetail` and `history` are transient views, not saved ones — persist
3744
- // only the real preference views so a reload never reopens a stale tool or a
3745
- // stale conversation list.
3744
+ // `toolDetail` is a transient peek, not a saved view — persist only the two
3745
+ // real preference views so a reload never reopens a stale tool.
3746
3746
  let persistedDrawerView = this._drawerView;
3747
- if (persistedDrawerView === 'toolDetail' || persistedDrawerView === 'history') {
3747
+ if (persistedDrawerView === 'toolDetail') {
3748
3748
  persistedDrawerView = 'settings';
3749
3749
  }
3750
3750
  const state = {
@@ -3803,8 +3803,14 @@ class KRChatbot extends i$3 {
3803
3803
  this._showToolInputs = state.showToolInputs === 'true';
3804
3804
  this._showConfirmationInput = state.showConfirmationInput === 'true';
3805
3805
  this._drawerOpen = state.drawerOpen === 'true';
3806
- if (state.drawerView === 'settings' || state.drawerView === 'debug') {
3806
+ if (state.drawerView === 'settings' || state.drawerView === 'debug' || state.drawerView === 'history') {
3807
3807
  this._drawerView = state.drawerView;
3808
+ // History's list isn't persisted (it can go stale); refetch it so a reload
3809
+ // that was on the conversations panel comes back to a populated list rather
3810
+ // than an empty one.
3811
+ if (state.drawerView === 'history') {
3812
+ this._loadConversations();
3813
+ }
3808
3814
  }
3809
3815
  const w = Number(state.drawerWidth);
3810
3816
  if (w >= 300) {
@@ -4948,41 +4954,47 @@ class KRChatbot extends i$3 {
4948
4954
  else {
4949
4955
  this._drawerOpen = true;
4950
4956
  this._drawerView = view;
4951
- // Opening the history view refetches the user's previous conversations
4952
- // (most-recently modified first) so a just-created or renamed conversation
4953
- // shows without a reload. Ownership is enforced server-side (the endpoint
4954
- // scopes to the current administrator), so we only filter to active ones.
4957
+ // Opening the history view refetches the list so a just-created or renamed
4958
+ // conversation shows without a reload.
4955
4959
  if (view === 'history') {
4956
- this._conversationsLoading = true;
4957
- KRHttp.fetch({
4958
- url: '/api/system/conversation/list',
4959
- method: 'POST',
4960
- body: {
4961
- page: 0,
4962
- size: 50,
4963
- sortBy: 'lastModified',
4964
- sortDirection: 'desc',
4965
- filterFields: [{
4966
- name: 'active',
4967
- operation: 'IS',
4968
- value: 'true',
4969
- }],
4970
- },
4971
- })
4972
- .then((r) => r.json())
4973
- .then((res) => {
4974
- this._conversations = res?.data?.content || [];
4975
- })
4976
- .catch(() => {
4977
- this._conversations = [];
4978
- })
4979
- .then(() => {
4980
- this._conversationsLoading = false;
4981
- });
4960
+ this._loadConversations();
4982
4961
  }
4983
4962
  }
4984
4963
  this._saveState();
4985
4964
  }
4965
+ /**
4966
+ * Fetch the user's previous conversations for the history list, most-recently
4967
+ * modified first. Ownership is enforced server-side (the endpoint scopes to the
4968
+ * current administrator), so we only filter to active ones.
4969
+ */
4970
+ _loadConversations() {
4971
+ this._conversationsLoading = true;
4972
+ KRHttp.fetch({
4973
+ url: '/api/system/conversation/list',
4974
+ method: 'POST',
4975
+ body: {
4976
+ page: 0,
4977
+ size: 50,
4978
+ sortBy: 'lastModified',
4979
+ sortDirection: 'desc',
4980
+ filterFields: [{
4981
+ name: 'active',
4982
+ operation: 'IS',
4983
+ value: 'true',
4984
+ }],
4985
+ },
4986
+ })
4987
+ .then((r) => r.json())
4988
+ .then((res) => {
4989
+ this._conversations = res?.data?.content || [];
4990
+ })
4991
+ .catch(() => {
4992
+ this._conversations = [];
4993
+ })
4994
+ .then(() => {
4995
+ this._conversationsLoading = false;
4996
+ });
4997
+ }
4986
4998
  /**
4987
4999
  * Switch the floating widget to a previously-created conversation (clicked in the
4988
5000
  * history list): stop any live turn, reset the transient turn/context state, pin
@@ -5001,7 +5013,22 @@ class KRChatbot extends i$3 {
5001
5013
  // one, so the header falls back to the default label).
5002
5014
  this._conversationTitle = this._conversations.find((c) => c.conversationID === conversationID)?.title || '';
5003
5015
  this._persistConversationId();
5004
- this._resetConversationState();
5016
+ // Clear the transient, per-conversation state so it doesn't bleed into the one
5017
+ // we're switching to — pending confirmations, context banners, errors, the
5018
+ // reconnect counter, and staged composer files (the transcript is loaded below).
5019
+ this._pendingActions = [];
5020
+ this._inlineConfirm = true;
5021
+ this._contextWarning = '';
5022
+ this._contextLimitReached = false;
5023
+ this._contextLimitMessage = '';
5024
+ this._error = '';
5025
+ this._reconnectAttempts = 0;
5026
+ this._pendingConversationFiles.forEach((p) => {
5027
+ if (p.previewUrl) {
5028
+ URL.revokeObjectURL(p.previewUrl);
5029
+ }
5030
+ });
5031
+ this._pendingConversationFiles = [];
5005
5032
  this._drawerOpen = false;
5006
5033
  this._saveState();
5007
5034
  if (this.load) {
@@ -5028,17 +5055,9 @@ class KRChatbot extends i$3 {
5028
5055
  this._conversationTitle = '';
5029
5056
  this._persistConversationId();
5030
5057
  this.messages = [];
5031
- this._resetConversationState();
5032
- this._drawerOpen = false;
5033
- this._saveState();
5034
- }
5035
- /**
5036
- * Clear the transient, per-conversation state that must not bleed across a
5037
- * conversation switch/new — pending confirmations, context banners, errors, the
5038
- * reconnect counter, and staged composer files. The transcript itself is set by
5039
- * the caller (loaded for a switch, emptied for new).
5040
- */
5041
- _resetConversationState() {
5058
+ // Clear the transient, per-conversation state so it doesn't bleed into the new
5059
+ // conversation — pending confirmations, context banners, errors, the reconnect
5060
+ // counter, and staged composer files.
5042
5061
  this._pendingActions = [];
5043
5062
  this._inlineConfirm = true;
5044
5063
  this._contextWarning = '';
@@ -5052,6 +5071,8 @@ class KRChatbot extends i$3 {
5052
5071
  }
5053
5072
  });
5054
5073
  this._pendingConversationFiles = [];
5074
+ this._drawerOpen = false;
5075
+ this._saveState();
5055
5076
  }
5056
5077
  /** Close the drawer. `_drawerView` is left intact so its content stays visible
5057
5078
  * while the drawer slides shut. */
@@ -5934,55 +5955,58 @@ class KRChatbot extends i$3 {
5934
5955
  <div class="history__empty">No previous conversations</div>
5935
5956
  ` : b `
5936
5957
  <div class="history__list">
5937
- ${this._conversations.map((conversation) => b `
5938
- <button
5939
- type="button"
5940
- class=${e({
5941
- 'history__item': true,
5942
- 'history__item--active': conversation.conversationID === this.conversationId,
5958
+ ${this._conversations.map((conversation) => {
5959
+ // Format lastModified for the row: today shows the time, this year
5960
+ // shows month/day, older shows month/day/year; the raw value if it
5961
+ // isn't a parseable date. Empty when the conversation has no date.
5962
+ let dateLabel = '';
5963
+ if (conversation.lastModified) {
5964
+ const date = new Date(conversation.lastModified);
5965
+ const now = new Date();
5966
+ if (isNaN(date.getTime())) {
5967
+ dateLabel = conversation.lastModified;
5968
+ }
5969
+ else if (date.toDateString() === now.toDateString()) {
5970
+ dateLabel = date.toLocaleTimeString('en-US', {
5971
+ hour: 'numeric',
5972
+ minute: '2-digit',
5973
+ });
5974
+ }
5975
+ else if (date.getFullYear() === now.getFullYear()) {
5976
+ dateLabel = date.toLocaleDateString('en-US', {
5977
+ month: 'short',
5978
+ day: 'numeric',
5979
+ });
5980
+ }
5981
+ else {
5982
+ dateLabel = date.toLocaleDateString('en-US', {
5983
+ month: 'short',
5984
+ day: 'numeric',
5985
+ year: 'numeric',
5986
+ });
5987
+ }
5988
+ }
5989
+ return b `
5990
+ <button
5991
+ type="button"
5992
+ class=${e({
5993
+ 'history__item': true,
5994
+ 'history__item--active': conversation.conversationID === this.conversationId,
5995
+ })}
5996
+ @click=${() => this._handleSwitchConversation(conversation.conversationID)}
5997
+ >
5998
+ <span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
5999
+ ${dateLabel ? b `
6000
+ <span class="history__item-date">${dateLabel}</span>
6001
+ ` : A}
6002
+ </button>
6003
+ `;
5943
6004
  })}
5944
- @click=${() => this._handleSwitchConversation(conversation.conversationID)}
5945
- >
5946
- <span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
5947
- ${conversation.lastModified ? b `
5948
- <span class="history__item-date">${this._renderConversationDate(conversation.lastModified)}</span>
5949
- ` : A}
5950
- </button>
5951
- `)}
5952
6005
  </div>
5953
6006
  `}
5954
6007
  </div>
5955
6008
  `;
5956
6009
  }
5957
- /**
5958
- * Format a conversation's lastModified for the history list: today shows the
5959
- * time, this year shows month/day, older shows month/day/year. Falls back to the
5960
- * raw value if it isn't a parseable date.
5961
- */
5962
- _renderConversationDate(lastModified) {
5963
- const date = new Date(lastModified);
5964
- if (isNaN(date.getTime())) {
5965
- return lastModified;
5966
- }
5967
- const now = new Date();
5968
- if (date.toDateString() === now.toDateString()) {
5969
- return date.toLocaleTimeString('en-US', {
5970
- hour: 'numeric',
5971
- minute: '2-digit',
5972
- });
5973
- }
5974
- if (date.getFullYear() === now.getFullYear()) {
5975
- return date.toLocaleDateString('en-US', {
5976
- month: 'short',
5977
- day: 'numeric',
5978
- });
5979
- }
5980
- return date.toLocaleDateString('en-US', {
5981
- month: 'short',
5982
- day: 'numeric',
5983
- year: 'numeric',
5984
- });
5985
- }
5986
6010
  /**
5987
6011
  * The side drawer — one sliding panel that holds the Settings, Debug, History,
5988
6012
  * and tool-detail views. The header buttons switch `_drawerView`; the title and
@@ -6013,25 +6037,24 @@ class KRChatbot extends i$3 {
6013
6037
  // In the floating widget the drawer fills the panel over the transcript (a
6014
6038
  // full-panel view with a back button) rather than sliding in beside it — the
6015
6039
  // panel is too narrow to show both. Inline keeps the side-drawer behavior.
6016
- const fullPanel = this.variant === 'floating';
6017
6040
  return b `
6018
6041
  <div
6019
6042
  class=${e({
6020
6043
  'drawer': true,
6021
6044
  'drawer--open': this._drawerOpen,
6022
6045
  'drawer--resizing': this._isDrawerResizing,
6023
- 'drawer--full': fullPanel,
6046
+ 'drawer--full': this.variant === 'floating',
6024
6047
  })}
6025
- style=${o(fullPanel ? {} : { width: `${this._drawerWidth}px` })}
6048
+ style=${o(this.variant === 'floating' ? {} : { width: `${this._drawerWidth}px` })}
6026
6049
  >
6027
- ${fullPanel ? A : b `
6050
+ ${this.variant === 'floating' ? A : b `
6028
6051
  <div
6029
6052
  class="drawer__resize"
6030
6053
  @mousedown=${this._handleDrawerResizeMouseDown}
6031
6054
  ></div>
6032
6055
  `}
6033
6056
  <div class="drawer__header">
6034
- ${fullPanel ? b `
6057
+ ${this.variant === 'floating' ? b `
6035
6058
  <button
6036
6059
  class="header-action"
6037
6060
  @click=${this._handleCloseDrawer}
@@ -6079,7 +6102,7 @@ class KRChatbot extends i$3 {
6079
6102
  New conversation
6080
6103
  </kr-button>
6081
6104
  ` : A}
6082
- ${fullPanel ? A : b `
6105
+ ${this.variant === 'floating' ? A : b `
6083
6106
  <button
6084
6107
  class="header-action"
6085
6108
  @click=${this._handleCloseDrawer}