@kodaris/krubble-app-components 1.0.72 → 1.0.74

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.
@@ -5609,6 +5609,11 @@
5609
5609
  this._dragDepth = 0;
5610
5610
  /** Monotonic id source for pending attachments. */
5611
5611
  this._pendingSeq = 0;
5612
+ /**
5613
+ * Request-id guard for skill searches — bumped on each search so out-of-order
5614
+ * responses are dropped (mirrors the kr-combo-box `_requestId` pattern).
5615
+ */
5616
+ this._skillRequestId = 0;
5612
5617
  // --- @property (public API) ---
5613
5618
  /**
5614
5619
  * Callback function for sending messages. Receives the user's message and
@@ -5647,12 +5652,6 @@
5647
5652
  * Input placeholder text.
5648
5653
  */
5649
5654
  this.placeholder = 'Type a message...';
5650
- /**
5651
- * Skills the user can invoke by typing `/` in the composer. Selecting one
5652
- * inserts `/name ` into the message. The `/` menu shows the first 15 (filtered
5653
- * as the user types); "Browse all skills" opens a dialog with the full list.
5654
- */
5655
- this.skills = [];
5656
5655
  /**
5657
5656
  * Whether the chat panel is open. Only relevant in floating mode.
5658
5657
  */
@@ -5684,17 +5683,25 @@
5684
5683
  this._isDraggingFile = false;
5685
5684
  /** Whether the `/` skill menu is open above the composer. */
5686
5685
  this._skillMenuOpened = false;
5687
- /** Current `/` query (text after the slash), used to filter the skill menu. */
5686
+ /** Current `/` query (text after the slash), sent to `_searchSkills`. */
5688
5687
  this._skillQuery = '';
5688
+ /**
5689
+ * Latest skill search results. Shared by the `/` menu and the browse dialog —
5690
+ * they're mutually exclusive (opening browse closes the menu), so one buffer.
5691
+ */
5692
+ this._skillResults = [];
5689
5693
  /** Highlighted item in the skill menu for keyboard navigation (-1 = none). */
5690
5694
  this._skillHighlighted = 0;
5691
5695
  /** Index of the message whose copy button just flashed "Copied" (-1 = none). */
5692
5696
  this._copiedIndex = -1;
5693
5697
  /** Whether the "Browse all skills" dialog is open. */
5694
5698
  this._skillBrowseOpened = false;
5695
- /** Active tab in the browse dialog. */
5699
+ /**
5700
+ * Active tab in the browse dialog. `org` lists this org's skills (searched via
5701
+ * `_searchSkills`); `kodaris` is the shared global catalog, added later.
5702
+ */
5696
5703
  this._skillBrowseSource = 'org';
5697
- /** Search text in the browse dialog. */
5704
+ /** Search text in the browse dialog, sent to `_searchSkills`. */
5698
5705
  this._skillBrowseQuery = '';
5699
5706
  this._handleMouseMove = (e) => {
5700
5707
  if (this._isDragging) {
@@ -6037,12 +6044,14 @@
6037
6044
  textarea.style.height = 'auto';
6038
6045
  textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
6039
6046
  // Open the skill menu while the message is a `/` command being typed (a
6040
- // leading slash with no space yet). The text after the slash filters it.
6047
+ // leading slash with no space yet). The text after the slash is the search
6048
+ // query, run server-side via `_searchSkills`.
6041
6049
  const slashMatch = this._inputValue.match(/^\/(\S*)$/);
6042
- if (slashMatch && this.skills.length) {
6050
+ if (slashMatch) {
6043
6051
  this._skillMenuOpened = true;
6044
6052
  this._skillQuery = slashMatch[1];
6045
6053
  this._skillHighlighted = 0;
6054
+ this._searchSkills(this._skillQuery);
6046
6055
  }
6047
6056
  else {
6048
6057
  this._skillMenuOpened = false;
@@ -6096,13 +6105,62 @@
6096
6105
  }
6097
6106
  }
6098
6107
  // --- Skills ---
6099
- /** Skills matching the current `/` query, capped at the first 15. */
6108
+ /**
6109
+ * Search the Kodaris AI skills index and store the results in `_skillResults`.
6110
+ * The `/` menu and the browse dialog both call this; results are shared since
6111
+ * the two are never open at once. Mirrors the backend's own skill search (BM25
6112
+ * over name^3/description^2/instructions, active-only) so the menu ranks skills
6113
+ * the way the model does. An empty query uses match_all — multi_match with an
6114
+ * empty string matches nothing, so the "show everything" case needs it.
6115
+ *
6116
+ * Follows the kr-combo-box `_fetch` pattern: a request-id guard drops responses
6117
+ * that arrive out of order. `/aiskills/search` returns a raw OpenSearch response,
6118
+ * so we lift each hit's `_source` (already in KRChatbotSkill shape) out.
6119
+ */
6120
+ _searchSkills(query) {
6121
+ this._skillRequestId++;
6122
+ const requestId = this._skillRequestId;
6123
+ const trimmed = query.trim();
6124
+ // multi_match with an empty string matches nothing, so an empty query falls
6125
+ // back to match_all to show everything.
6126
+ let must = { match_all: {} };
6127
+ if (trimmed) {
6128
+ must = {
6129
+ multi_match: {
6130
+ query: trimmed,
6131
+ fields: ['name^3', 'description^2', 'instructions'],
6132
+ type: 'best_fields',
6133
+ },
6134
+ };
6135
+ }
6136
+ krubbleHttp.Http.fetch({
6137
+ url: '/api/system/opensearch/aiskills/search',
6138
+ method: 'POST',
6139
+ body: {
6140
+ size: 100,
6141
+ query: {
6142
+ bool: {
6143
+ must: [must],
6144
+ filter: [{ term: { active: true } }],
6145
+ },
6146
+ },
6147
+ },
6148
+ })
6149
+ .then((r) => r.json())
6150
+ .then((res) => {
6151
+ // Ignore a stale response superseded by a newer search.
6152
+ if (requestId !== this._skillRequestId) {
6153
+ return;
6154
+ }
6155
+ this._skillResults = (res?.data?.hits?.hits || []).map((hit) => hit._source);
6156
+ })
6157
+ .catch((error) => {
6158
+ console.error('kr-chatbot: skill search failed', error);
6159
+ });
6160
+ }
6161
+ /** Skills shown in the `/` menu — the shared results, capped at the first 15. */
6100
6162
  _filteredSkills() {
6101
- const query = this._skillQuery.toLowerCase();
6102
- return this.skills
6103
- .filter((skill) => skill.name.toLowerCase().includes(query) ||
6104
- skill.label.toLowerCase().includes(query))
6105
- .slice(0, 15);
6163
+ return this._skillResults.slice(0, 15);
6106
6164
  }
6107
6165
  /** The `+` button toggles the skill/attachment menu open (showing all skills). */
6108
6166
  _handlePlusClick() {
@@ -6113,10 +6171,11 @@
6113
6171
  this._skillQuery = '';
6114
6172
  this._skillHighlighted = 0;
6115
6173
  this._skillMenuOpened = true;
6174
+ this._searchSkills('');
6116
6175
  }
6117
- /** Insert `/name ` into the composer and focus it, ready for the message. */
6176
+ /** Insert `/code ` into the composer and focus it, ready for the message. */
6118
6177
  _handleSkillSelect(skill) {
6119
- this._inputValue = `/${skill.name} `;
6178
+ this._inputValue = `/${skill.code} `;
6120
6179
  this._skillMenuOpened = false;
6121
6180
  this._skillBrowseOpened = false;
6122
6181
  this._inputEl?.focus();
@@ -6136,29 +6195,18 @@
6136
6195
  this._skillMenuOpened = false;
6137
6196
  this._skillBrowseQuery = '';
6138
6197
  this._skillBrowseOpened = true;
6198
+ this._searchSkills('');
6139
6199
  }
6140
6200
  _handleSkillBrowseClose() {
6141
6201
  this._skillBrowseOpened = false;
6142
6202
  }
6143
6203
  _handleSkillBrowseSearch(e) {
6144
6204
  this._skillBrowseQuery = e.target.value;
6205
+ this._searchSkills(this._skillBrowseQuery);
6145
6206
  }
6146
6207
  _handleSkillBrowseTabChange(e) {
6147
6208
  this._skillBrowseSource = e.detail.activeTabId;
6148
6209
  }
6149
- /** Skills for the browse dialog's active tab, filtered by its search box. */
6150
- _browseSkills() {
6151
- const query = this._skillBrowseQuery.toLowerCase();
6152
- return this.skills.filter((skill) => {
6153
- const source = skill.source || 'org';
6154
- if (source !== this._skillBrowseSource) {
6155
- return false;
6156
- }
6157
- return skill.name.toLowerCase().includes(query) ||
6158
- skill.label.toLowerCase().includes(query) ||
6159
- (skill.description || '').toLowerCase().includes(query);
6160
- });
6161
- }
6162
6210
  // --- Attachments ---
6163
6211
  _openFilePicker() {
6164
6212
  this._fileInputEl?.click();
@@ -6552,7 +6600,7 @@
6552
6600
  })}
6553
6601
  @click=${() => this._handleSkillSelect(skill)}
6554
6602
  >
6555
- <span class="skill-menu__name">${skill.label}</span>
6603
+ <span class="skill-menu__name">${skill.name}</span>
6556
6604
  ${skill.description ? b `
6557
6605
  <span class="skill-menu__desc">${skill.description}</span>
6558
6606
  ` : A}
@@ -6560,9 +6608,7 @@
6560
6608
  </li>
6561
6609
  `)}
6562
6610
  </ul>
6563
- ` : b `
6564
- <div class="skill-menu__empty">No matching skills</div>
6565
- `}
6611
+ ` : A}
6566
6612
  </div>
6567
6613
  `;
6568
6614
  }
@@ -6589,18 +6635,20 @@
6589
6635
  ${this._renderSkillGrid()}
6590
6636
  </kr-tab>
6591
6637
  <kr-tab id="kodaris" label="Kodaris">
6592
- ${this._renderSkillGrid()}
6638
+ <div class="skill-browse__empty">
6639
+ Shared Kodaris skills are coming soon.
6640
+ </div>
6593
6641
  </kr-tab>
6594
6642
  </kr-tab-group>
6595
6643
  </div>
6596
6644
  </kr-dialog>
6597
6645
  `;
6598
6646
  }
6599
- /** The card grid for the browse dialog's active tab. */
6647
+ /** The card grid of this org's skill search results for the browse dialog. */
6600
6648
  _renderSkillGrid() {
6601
- const skills = this._browseSkills();
6649
+ const skills = this._skillResults;
6602
6650
  if (!skills.length) {
6603
- return b `<div class="skill-browse__empty">No matching skills</div>`;
6651
+ return A;
6604
6652
  }
6605
6653
  return b `
6606
6654
  <div class="skill-browse__grid">
@@ -6609,7 +6657,7 @@
6609
6657
  class="skill-card"
6610
6658
  @click=${() => this._handleSkillSelect(skill)}
6611
6659
  >
6612
- <span class="skill-card__name">${skill.label}</span>
6660
+ <span class="skill-card__name">${skill.name}</span>
6613
6661
  ${skill.description ? b `
6614
6662
  <span class="skill-card__desc">${skill.description}</span>
6615
6663
  ` : A}
@@ -6619,12 +6667,13 @@
6619
6667
  `;
6620
6668
  }
6621
6669
  /**
6622
- * Render a user message, highlighting a leading `/skill-name` token when it
6623
- * matches a known skill (like the way Claude highlights slash commands).
6670
+ * Render a user message, highlighting a leading `/skill-code` token (like the
6671
+ * way Claude highlights slash commands). The composer only inserts valid skill
6672
+ * codes, so any leading slash token is treated as one.
6624
6673
  */
6625
6674
  _renderUserContent(content) {
6626
6675
  const match = content.match(/^\/(\S+)(\s[\s\S]*)?$/);
6627
- if (match && this.skills.some((skill) => skill.name === match[1])) {
6676
+ if (match) {
6628
6677
  return b `<span class="message-skill">/${match[1]}</span>${match[2] || ''}`;
6629
6678
  }
6630
6679
  return content;
@@ -7730,12 +7779,6 @@
7730
7779
  white-space: nowrap;
7731
7780
  }
7732
7781
 
7733
- .skill-menu__empty {
7734
- padding: 16px;
7735
- text-align: center;
7736
- color: #000000;
7737
- }
7738
-
7739
7782
  .skill-menu__actions {
7740
7783
  display: flex;
7741
7784
  flex-direction: column;
@@ -7826,6 +7869,12 @@
7826
7869
  .skill-card__desc {
7827
7870
  font-size: 12px;
7828
7871
  color: #000000;
7872
+ /* Clamp to 3 lines so a long description doesn't make one card tower over
7873
+ its row-mates in the grid. */
7874
+ display: -webkit-box;
7875
+ -webkit-line-clamp: 3;
7876
+ -webkit-box-orient: vertical;
7877
+ overflow: hidden;
7829
7878
  }
7830
7879
 
7831
7880
  .skill-browse__empty {
@@ -7934,9 +7983,6 @@
7934
7983
  __decorate([
7935
7984
  n({ type: String })
7936
7985
  ], exports.Chatbot.prototype, "placeholder", void 0);
7937
- __decorate([
7938
- n({ attribute: false })
7939
- ], exports.Chatbot.prototype, "skills", void 0);
7940
7986
  __decorate([
7941
7987
  n({
7942
7988
  type: Boolean,
@@ -7977,6 +8023,9 @@
7977
8023
  __decorate([
7978
8024
  r()
7979
8025
  ], exports.Chatbot.prototype, "_skillQuery", void 0);
8026
+ __decorate([
8027
+ r()
8028
+ ], exports.Chatbot.prototype, "_skillResults", void 0);
7980
8029
  __decorate([
7981
8030
  r()
7982
8031
  ], exports.Chatbot.prototype, "_skillHighlighted", void 0);