@kodaris/krubble-app-components 1.0.126 → 1.0.128

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.
@@ -3470,10 +3470,9 @@ class KRChatbot extends i$3 {
3470
3470
  }
3471
3471
  }
3472
3472
  // `this.load` is always set by here (defaulted above when the consumer didn't
3473
- // supply one), so capture it for the async chain — TS can't narrow the nullable
3474
- // property across the promise boundary.
3475
- const load = this.load;
3476
- 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) => {
3477
3476
  if (messages && messages.length) {
3478
3477
  this.messages = messages;
3479
3478
  }
@@ -3742,11 +3741,10 @@ class KRChatbot extends i$3 {
3742
3741
  }
3743
3742
  _saveState() {
3744
3743
  try {
3745
- // `toolDetail` and `history` are transient views, not saved ones — persist
3746
- // only the real preference views so a reload never reopens a stale tool or a
3747
- // 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.
3748
3746
  let persistedDrawerView = this._drawerView;
3749
- if (persistedDrawerView === 'toolDetail' || persistedDrawerView === 'history') {
3747
+ if (persistedDrawerView === 'toolDetail') {
3750
3748
  persistedDrawerView = 'settings';
3751
3749
  }
3752
3750
  const state = {
@@ -3805,8 +3803,14 @@ class KRChatbot extends i$3 {
3805
3803
  this._showToolInputs = state.showToolInputs === 'true';
3806
3804
  this._showConfirmationInput = state.showConfirmationInput === 'true';
3807
3805
  this._drawerOpen = state.drawerOpen === 'true';
3808
- if (state.drawerView === 'settings' || state.drawerView === 'debug') {
3806
+ if (state.drawerView === 'settings' || state.drawerView === 'debug' || state.drawerView === 'history') {
3809
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
+ }
3810
3814
  }
3811
3815
  const w = Number(state.drawerWidth);
3812
3816
  if (w >= 300) {
@@ -4950,41 +4954,47 @@ class KRChatbot extends i$3 {
4950
4954
  else {
4951
4955
  this._drawerOpen = true;
4952
4956
  this._drawerView = view;
4953
- // Opening the history view refetches the user's previous conversations
4954
- // (most-recently modified first) so a just-created or renamed conversation
4955
- // shows without a reload. Ownership is enforced server-side (the endpoint
4956
- // 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.
4957
4959
  if (view === 'history') {
4958
- this._conversationsLoading = true;
4959
- KRHttp.fetch({
4960
- url: '/api/system/conversation/list',
4961
- method: 'POST',
4962
- body: {
4963
- page: 0,
4964
- size: 50,
4965
- sortBy: 'lastModified',
4966
- sortDirection: 'desc',
4967
- filterFields: [{
4968
- name: 'active',
4969
- operation: 'IS',
4970
- value: 'true',
4971
- }],
4972
- },
4973
- })
4974
- .then((r) => r.json())
4975
- .then((res) => {
4976
- this._conversations = res?.data?.content || [];
4977
- })
4978
- .catch(() => {
4979
- this._conversations = [];
4980
- })
4981
- .then(() => {
4982
- this._conversationsLoading = false;
4983
- });
4960
+ this._loadConversations();
4984
4961
  }
4985
4962
  }
4986
4963
  this._saveState();
4987
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
+ }
4988
4998
  /**
4989
4999
  * Switch the floating widget to a previously-created conversation (clicked in the
4990
5000
  * history list): stop any live turn, reset the transient turn/context state, pin
@@ -5003,7 +5013,22 @@ class KRChatbot extends i$3 {
5003
5013
  // one, so the header falls back to the default label).
5004
5014
  this._conversationTitle = this._conversations.find((c) => c.conversationID === conversationID)?.title || '';
5005
5015
  this._persistConversationId();
5006
- 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 = [];
5007
5032
  this._drawerOpen = false;
5008
5033
  this._saveState();
5009
5034
  if (this.load) {
@@ -5030,17 +5055,9 @@ class KRChatbot extends i$3 {
5030
5055
  this._conversationTitle = '';
5031
5056
  this._persistConversationId();
5032
5057
  this.messages = [];
5033
- this._resetConversationState();
5034
- this._drawerOpen = false;
5035
- this._saveState();
5036
- }
5037
- /**
5038
- * Clear the transient, per-conversation state that must not bleed across a
5039
- * conversation switch/new — pending confirmations, context banners, errors, the
5040
- * reconnect counter, and staged composer files. The transcript itself is set by
5041
- * the caller (loaded for a switch, emptied for new).
5042
- */
5043
- _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.
5044
5061
  this._pendingActions = [];
5045
5062
  this._inlineConfirm = true;
5046
5063
  this._contextWarning = '';
@@ -5054,6 +5071,8 @@ class KRChatbot extends i$3 {
5054
5071
  }
5055
5072
  });
5056
5073
  this._pendingConversationFiles = [];
5074
+ this._drawerOpen = false;
5075
+ this._saveState();
5057
5076
  }
5058
5077
  /** Close the drawer. `_drawerView` is left intact so its content stays visible
5059
5078
  * while the drawer slides shut. */
@@ -5936,55 +5955,58 @@ class KRChatbot extends i$3 {
5936
5955
  <div class="history__empty">No previous conversations</div>
5937
5956
  ` : b `
5938
5957
  <div class="history__list">
5939
- ${this._conversations.map((conversation) => b `
5940
- <button
5941
- type="button"
5942
- class=${e({
5943
- 'history__item': true,
5944
- '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
+ `;
5945
6004
  })}
5946
- @click=${() => this._handleSwitchConversation(conversation.conversationID)}
5947
- >
5948
- <span class="history__item-title">${conversation.title || 'Untitled conversation'}</span>
5949
- ${conversation.lastModified ? b `
5950
- <span class="history__item-date">${this._renderConversationDate(conversation.lastModified)}</span>
5951
- ` : A}
5952
- </button>
5953
- `)}
5954
6005
  </div>
5955
6006
  `}
5956
6007
  </div>
5957
6008
  `;
5958
6009
  }
5959
- /**
5960
- * Format a conversation's lastModified for the history list: today shows the
5961
- * time, this year shows month/day, older shows month/day/year. Falls back to the
5962
- * raw value if it isn't a parseable date.
5963
- */
5964
- _renderConversationDate(lastModified) {
5965
- const date = new Date(lastModified);
5966
- if (isNaN(date.getTime())) {
5967
- return lastModified;
5968
- }
5969
- const now = new Date();
5970
- if (date.toDateString() === now.toDateString()) {
5971
- return date.toLocaleTimeString('en-US', {
5972
- hour: 'numeric',
5973
- minute: '2-digit',
5974
- });
5975
- }
5976
- if (date.getFullYear() === now.getFullYear()) {
5977
- return date.toLocaleDateString('en-US', {
5978
- month: 'short',
5979
- day: 'numeric',
5980
- });
5981
- }
5982
- return date.toLocaleDateString('en-US', {
5983
- month: 'short',
5984
- day: 'numeric',
5985
- year: 'numeric',
5986
- });
5987
- }
5988
6010
  /**
5989
6011
  * The side drawer — one sliding panel that holds the Settings, Debug, History,
5990
6012
  * and tool-detail views. The header buttons switch `_drawerView`; the title and
@@ -6015,25 +6037,24 @@ class KRChatbot extends i$3 {
6015
6037
  // In the floating widget the drawer fills the panel over the transcript (a
6016
6038
  // full-panel view with a back button) rather than sliding in beside it — the
6017
6039
  // panel is too narrow to show both. Inline keeps the side-drawer behavior.
6018
- const fullPanel = this.variant === 'floating';
6019
6040
  return b `
6020
6041
  <div
6021
6042
  class=${e({
6022
6043
  'drawer': true,
6023
6044
  'drawer--open': this._drawerOpen,
6024
6045
  'drawer--resizing': this._isDrawerResizing,
6025
- 'drawer--full': fullPanel,
6046
+ 'drawer--full': this.variant === 'floating',
6026
6047
  })}
6027
- style=${o(fullPanel ? {} : { width: `${this._drawerWidth}px` })}
6048
+ style=${o(this.variant === 'floating' ? {} : { width: `${this._drawerWidth}px` })}
6028
6049
  >
6029
- ${fullPanel ? A : b `
6050
+ ${this.variant === 'floating' ? A : b `
6030
6051
  <div
6031
6052
  class="drawer__resize"
6032
6053
  @mousedown=${this._handleDrawerResizeMouseDown}
6033
6054
  ></div>
6034
6055
  `}
6035
6056
  <div class="drawer__header">
6036
- ${fullPanel ? b `
6057
+ ${this.variant === 'floating' ? b `
6037
6058
  <button
6038
6059
  class="header-action"
6039
6060
  @click=${this._handleCloseDrawer}
@@ -6081,7 +6102,7 @@ class KRChatbot extends i$3 {
6081
6102
  New conversation
6082
6103
  </kr-button>
6083
6104
  ` : A}
6084
- ${fullPanel ? A : b `
6105
+ ${this.variant === 'floating' ? A : b `
6085
6106
  <button
6086
6107
  class="header-action"
6087
6108
  @click=${this._handleCloseDrawer}
@@ -11487,17 +11508,31 @@ var __decorate$2 = (undefined && undefined.__decorate) || function (decorators,
11487
11508
  * { id: 'activity', label: 'Activity', url: '/companies/123/activity' },
11488
11509
  * ]}
11489
11510
  * activeId="overview"
11511
+ * .actions=${[
11512
+ * { id: 'edit', label: 'Edit', variant: 'flat' },
11513
+ * { id: 'archive', label: 'Archive' },
11514
+ * { id: 'delete', label: 'Delete' },
11515
+ * { id: 'export', label: 'Export' },
11516
+ * ]}
11490
11517
  * >
11491
11518
  * <div>Screen content here</div>
11492
11519
  * </kr-screen-nav>
11493
11520
  * ```
11494
11521
  *
11522
+ * Actions render in a footer at the bottom of the nav column. With more than
11523
+ * `maxVisibleActions` (default 3), the overflow collapses into a "More" dropdown.
11524
+ * Listen for `action-click` to handle a click: `e.detail.action` is the action id.
11525
+ *
11495
11526
  * @slot - The main screen content
11496
11527
  *
11497
11528
  * @property {string} label - Main label (e.g., entity name like "Acme Corporation")
11498
11529
  * @property {string} subLabel - Sub label (e.g., entity type like "Company")
11499
11530
  * @property {KRScreenNavItem[]} items - Navigation items as JSON array
11500
11531
  * @property {string} activeId - Currently active item ID
11532
+ * @property {KRScreenNavAction[]} actions - Action buttons rendered in the nav footer
11533
+ * @property {number} maxVisibleActions - Max inline actions before collapsing into a "More" dropdown (default 3)
11534
+ *
11535
+ * @fires action-click - When an action button (inline or overflow) is clicked; detail is `{ action: id }`
11501
11536
  */
11502
11537
  class KRScreenNav extends i$3 {
11503
11538
  constructor() {
@@ -11519,6 +11554,17 @@ class KRScreenNav extends i$3 {
11519
11554
  * Can be set explicitly by the parent to override.
11520
11555
  */
11521
11556
  this.activeId = '';
11557
+ /**
11558
+ * Action buttons rendered in the nav footer.
11559
+ */
11560
+ this.actions = [];
11561
+ /**
11562
+ * Max number of inline action buttons before the overflow collapses into a
11563
+ * "More" dropdown. When actions exceed this count, the last visible slot is
11564
+ * used for the "More" button so the total footer button count never exceeds
11565
+ * this value.
11566
+ */
11567
+ this.maxVisibleActions = 3;
11522
11568
  this.handlePopstate = () => {
11523
11569
  this.syncActiveFromUrl();
11524
11570
  };
@@ -11579,7 +11625,35 @@ class KRScreenNav extends i$3 {
11579
11625
  this.activeId = match.id;
11580
11626
  }
11581
11627
  }
11628
+ handleActionClick(e, action) {
11629
+ this.dispatchEvent(new CustomEvent('action-click', {
11630
+ detail: { action: action.id },
11631
+ bubbles: true,
11632
+ composed: true,
11633
+ }));
11634
+ }
11635
+ // Fired by the "More" kr-button when an overflow option is chosen; detail is { id, label }.
11636
+ handleMoreSelect(e) {
11637
+ this.dispatchEvent(new CustomEvent('action-click', {
11638
+ detail: { action: e.detail.id },
11639
+ bubbles: true,
11640
+ composed: true,
11641
+ }));
11642
+ }
11582
11643
  render() {
11644
+ const hasOverflow = this.actions.length > this.maxVisibleActions;
11645
+ // When collapsing, the last visible slot is the "More" button, so only
11646
+ // show the first (maxVisibleActions - 1) actions inline.
11647
+ let inlineActions = this.actions;
11648
+ let overflowOptions = [];
11649
+ if (hasOverflow) {
11650
+ inlineActions = this.actions.slice(0, this.maxVisibleActions - 1);
11651
+ // Everything past the inline slots goes into the "More" dropdown.
11652
+ overflowOptions = this.actions.slice(this.maxVisibleActions - 1).map((action) => ({
11653
+ id: action.id,
11654
+ label: action.label,
11655
+ }));
11656
+ }
11583
11657
  return b `
11584
11658
  <nav class="nav">
11585
11659
  ${this.label ? b `
@@ -11606,6 +11680,31 @@ class KRScreenNav extends i$3 {
11606
11680
  `)}
11607
11681
  </div>
11608
11682
  </div>
11683
+ ${this.actions.length ? b `
11684
+ <div class="nav-actions">
11685
+ ${inlineActions.map((action) => b `
11686
+ <kr-button
11687
+ class="nav-actions__button"
11688
+ variant=${action.variant || 'outline'}
11689
+ size="small"
11690
+ @click=${(e) => this.handleActionClick(e, action)}
11691
+ >
11692
+ ${action.label}
11693
+ </kr-button>
11694
+ `)}
11695
+ ${hasOverflow ? b `
11696
+ <kr-button
11697
+ class="nav-actions__button"
11698
+ variant="outline"
11699
+ size="small"
11700
+ .options=${overflowOptions}
11701
+ @option-select=${this.handleMoreSelect}
11702
+ >
11703
+ More
11704
+ </kr-button>
11705
+ ` : A}
11706
+ </div>
11707
+ ` : A}
11609
11708
  </nav>
11610
11709
  <main class="content">
11611
11710
  <div class="content__main">
@@ -11694,6 +11793,19 @@ KRScreenNav.styles = i$6 `
11694
11793
  font-weight: 700;
11695
11794
  }
11696
11795
 
11796
+ .nav-actions {
11797
+ display: flex;
11798
+ flex-direction: column;
11799
+ gap: 8px;
11800
+ padding: 12px 16px;
11801
+ border-top: 1px solid rgba(0, 0, 0, 0.08);
11802
+ flex-shrink: 0;
11803
+ }
11804
+
11805
+ .nav-actions__button {
11806
+ width: 100%;
11807
+ }
11808
+
11697
11809
  .content {
11698
11810
  flex: 1;
11699
11811
  min-width: 0;
@@ -11718,6 +11830,12 @@ __decorate$2([
11718
11830
  __decorate$2([
11719
11831
  n$1({ type: String })
11720
11832
  ], KRScreenNav.prototype, "activeId", void 0);
11833
+ __decorate$2([
11834
+ n$1({ type: Array })
11835
+ ], KRScreenNav.prototype, "actions", void 0);
11836
+ __decorate$2([
11837
+ n$1({ type: Number })
11838
+ ], KRScreenNav.prototype, "maxVisibleActions", void 0);
11721
11839
  // Guarded registration: no-op if the tag is already defined, so a second copy of krubble on
11722
11840
  // the page (e.g. the Operations shell loads krubble globally while an embedded app bundled its
11723
11841
  // own copy) doesn't throw. First definition wins.