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