@kodaris/krubble-app-components 1.0.107 → 1.0.109

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.
@@ -1,4 +1,4 @@
1
- import { KRContextMenu, KRDialog, KRFilePreview } from '@kodaris/krubble-components';
1
+ import { KRContextMenu, KRDialog, KRSnackbar, KRFilePreview } from '@kodaris/krubble-components';
2
2
  import { KRHttp } from '@kodaris/krubble-http';
3
3
 
4
4
  /**
@@ -6356,76 +6356,30 @@ let KRChatbot = class KRChatbot extends i$3 {
6356
6356
  }
6357
6357
  // --- Skills ---
6358
6358
  /**
6359
- * Search the Kodaris AI skills index and store the results in `_orgSkills` — the
6360
- * browse dialog's "Your Org" tab. Mirrors the backend's own skill search (BM25
6361
- * over name^3/description^2/instructions, active-only). An empty query uses
6362
- * match_all — multi_match with an empty string matches nothing, so the "show
6363
- * everything" case needs it.
6359
+ * Load the browse dialog's "Your Org" tab into `_orgSkills` from
6360
+ * `/conversation/localSkillList` — this instance's own active skills, already
6361
+ * role-filtered to what the caller may run and ranked by the server.
6364
6362
  *
6365
- * Follows the kr-combo-box `_fetch` pattern: a request-id guard drops responses
6366
- * that arrive out of order. `/aiskills/search` returns a raw OpenSearch response,
6367
- * so we lift each hit's `_source` (already in KRChatbotSkill shape) out.
6363
+ * Not the `aiskills` search endpoint: that requires a skill-authoring role
6364
+ * (`skillView`/`skillEdit`), so an ordinary chat user got a 403 and an empty
6365
+ * tab. This one only needs a logged-in employee. It also means the query
6366
+ * matching, active-only filter and "exclude globally-published" rule all live
6367
+ * server-side instead of being rebuilt as OpenSearch DSL here.
6368
+ *
6369
+ * EXCEPTION — on kodaris.com this list also carries that instance's own
6370
+ * globally-published skills (flagged `global: true`), because there is no
6371
+ * Kodaris tab there for them to appear on.
6372
+ *
6373
+ * Request-id guard drops responses that arrive out of order (kr-combo-box
6374
+ * `_fetch` pattern).
6368
6375
  */
6369
6376
  _searchOrgSkills(query) {
6370
6377
  this._orgSkillRequestId++;
6371
6378
  const requestId = this._orgSkillRequestId;
6372
- const trimmed = query.trim();
6373
- // An empty query matches everything; otherwise substring-match the query
6374
- // against name/code/description. We use case-insensitive wildcard queries on
6375
- // the `.raw` keyword subfields so "ord" matches anywhere in "Create Order"
6376
- // (a plain match/multi_match only matches whole tokens). Escape the wildcard
6377
- // specials so user input is treated literally, then wrap with `*...*`.
6378
- let must = { match_all: {} };
6379
- if (trimmed) {
6380
- must = {
6381
- bool: {
6382
- should: [
6383
- {
6384
- wildcard: {
6385
- 'name.raw': {
6386
- value: `*${trimmed.replace(/([*?\\])/g, '\\$1')}*`,
6387
- case_insensitive: true,
6388
- }
6389
- }
6390
- },
6391
- {
6392
- wildcard: {
6393
- 'code.raw': {
6394
- value: `*${trimmed.replace(/([*?\\])/g, '\\$1')}*`,
6395
- case_insensitive: true,
6396
- }
6397
- }
6398
- },
6399
- {
6400
- wildcard: {
6401
- 'description.raw': {
6402
- value: `*${trimmed.replace(/([*?\\])/g, '\\$1')}*`,
6403
- case_insensitive: true,
6404
- }
6405
- }
6406
- },
6407
- ],
6408
- minimum_should_match: 1,
6409
- },
6410
- };
6411
- }
6412
6379
  KRHttp.fetch({
6413
- url: '/api/system/opensearch/aiskills/search',
6380
+ url: '/api/system/conversation/localSkillList',
6414
6381
  method: 'POST',
6415
- body: {
6416
- size: 100,
6417
- query: {
6418
- bool: {
6419
- must: [must],
6420
- filter: [{ term: { active: true } }],
6421
- // The "Your Org" tab lists only this instance's own skills. Globally-
6422
- // published skills belong to the "Kodaris" tab (fed by the registry),
6423
- // so exclude them here. Documents without the field aren't matched by
6424
- // the term, so ordinary org skills are kept.
6425
- must_not: [{ term: { globalPublished: true } }],
6426
- },
6427
- },
6428
- },
6382
+ body: { query: query.trim() },
6429
6383
  })
6430
6384
  .then((r) => r.json())
6431
6385
  .then((res) => {
@@ -6433,7 +6387,7 @@ let KRChatbot = class KRChatbot extends i$3 {
6433
6387
  if (requestId !== this._orgSkillRequestId) {
6434
6388
  return;
6435
6389
  }
6436
- this._orgSkills = (res?.data?.hits?.hits || []).map((hit) => hit._source);
6390
+ this._orgSkills = (res?.data || []);
6437
6391
  })
6438
6392
  .catch((error) => {
6439
6393
  console.error('kr-chatbot: skill search failed', error);
@@ -6473,19 +6427,23 @@ let KRChatbot = class KRChatbot extends i$3 {
6473
6427
  });
6474
6428
  }
6475
6429
  /**
6476
- * Load the browse dialog's "Kodaris" tab into `_globalSkills` — the full Kodaris
6477
- * catalog, installed or not, each row carrying `enabled` = installed here.
6430
+ * Load the browse dialog's "Kodaris" tab into `_globalSkills` — the whole
6431
+ * published Kodaris catalog for discovery, installed or not, runnable by this
6432
+ * user or not. Each row carries `enabled` (installed here), `canEnable` (may
6433
+ * install it) and `canRun` (roles allow invoking it).
6478
6434
  *
6479
- * Uses `/conversation/globalSkillCatalog` rather than the `/aiskills/global/search`
6435
+ * Uses `/conversation/globalSkillList` rather than the `/aiskills/global/search`
6480
6436
  * twin, which requires a skill-authoring role an ordinary chat user doesn't have.
6481
- * Both return the same catalog in `KRChatbotSkill` shape
6482
- * (`data: [{ code, name, description, enabled }]`), so no `_source` lifting is needed.
6437
+ *
6438
+ * Returns `[]` — not an error — when there's no reachable registry, and always
6439
+ * on kodaris.com, which IS the catalog source and so has no remote to pull. The
6440
+ * tab itself is hidden there (see `_renderSkillBrowse`).
6483
6441
  */
6484
6442
  _searchGlobalSkills(query) {
6485
6443
  this._globalSkillRequestId++;
6486
6444
  const requestId = this._globalSkillRequestId;
6487
6445
  KRHttp.fetch({
6488
- url: '/api/system/conversation/globalSkillCatalog',
6446
+ url: '/api/system/conversation/globalSkillList',
6489
6447
  method: 'POST',
6490
6448
  body: { query: query.trim() },
6491
6449
  })
@@ -6509,6 +6467,17 @@ let KRChatbot = class KRChatbot extends i$3 {
6509
6467
  _handleGlobalSkillEnable(skill, e) {
6510
6468
  // The card body inserts the skill; the install button must not also do that.
6511
6469
  e.stopPropagation();
6470
+ // The button is shown to everyone so the catalog reads the same for all users,
6471
+ // but the enable endpoint requires administrator/superuser/skillEdit. Without
6472
+ // it, say who to ask instead of firing a request that comes back 403.
6473
+ if (!skill.canEnable) {
6474
+ KRSnackbar.show({
6475
+ title: 'Permission Required',
6476
+ message: 'You do not have permission to install skills. Please contact your site administrator.',
6477
+ type: 'error',
6478
+ });
6479
+ return;
6480
+ }
6512
6481
  if (this._globalSkillPending.has(skill.code)) {
6513
6482
  return;
6514
6483
  }
@@ -6635,13 +6604,33 @@ let KRChatbot = class KRChatbot extends i$3 {
6635
6604
  recognition.start();
6636
6605
  this._inputEl?.focus();
6637
6606
  }
6607
+ /**
6638
6608
  /** Insert `/code ` into the composer and focus it, ready for the message. */
6639
6609
  _handleSkillSelect(skill) {
6640
- // A global skill must be installed before it can be chosen — inserting /code
6641
- // for one not enabled on this instance would just fail. `enabled` is only set
6642
- // on global skills (false = not installed); org skills leave it undefined and
6643
- // are always selectable. So an uninstalled global card click does nothing.
6644
- if (skill.enabled === false) {
6610
+ // The user's roles don't allow this skill — inserting /code would just fail in the
6611
+ // chat. Name the roles that would grant it, so the user knows what to ask for and
6612
+ // whoever grants it knows what to give.
6613
+ if (skill.canRun === false) {
6614
+ let message = `You do not have access to "${skill.name}". Ask your site administrator to grant it.`;
6615
+ if (skill.allowedRoles?.length) {
6616
+ message = `"${skill.name}" requires the ${skill.allowedRoles.join(' or ')} role. Ask your site administrator to grant it.`;
6617
+ }
6618
+ KRSnackbar.show({
6619
+ title: 'Access Required',
6620
+ message: message,
6621
+ type: 'error',
6622
+ });
6623
+ return;
6624
+ }
6625
+ // A global skill nobody has installed here yet isn't runnable either — point at the
6626
+ // Install button rather than appearing to ignore the click. Local skills carry no
6627
+ // install state, so this only applies to global ones.
6628
+ if (skill.global && !skill.enabled) {
6629
+ KRSnackbar.show({
6630
+ title: 'Not Installed',
6631
+ message: `"${skill.name}" has to be installed on this site before you can use it — use the Install button on the card.`,
6632
+ type: 'info',
6633
+ });
6645
6634
  return;
6646
6635
  }
6647
6636
  this._inputValue = `/${skill.code} `;
@@ -6673,8 +6662,11 @@ let KRChatbot = class KRChatbot extends i$3 {
6673
6662
  this._skillBrowseQuery = '';
6674
6663
  this._skillBrowseOpened = true;
6675
6664
  // Fill both tabs up front so switching between them is instant, not stale.
6665
+ // The Kodaris tab isn't rendered on the registry server, so skip its fetch there.
6676
6666
  this._searchOrgSkills('');
6677
- this._searchGlobalSkills('');
6667
+ if (this._aiFrameworkConfig.registryServer !== true) {
6668
+ this._searchGlobalSkills('');
6669
+ }
6678
6670
  }
6679
6671
  _handleSkillBrowseClose() {
6680
6672
  this._skillBrowseOpened = false;
@@ -6683,7 +6675,9 @@ let KRChatbot = class KRChatbot extends i$3 {
6683
6675
  this._skillBrowseQuery = e.target.value;
6684
6676
  // Filter both catalogs so whichever tab the user switches to is already current.
6685
6677
  this._searchOrgSkills(this._skillBrowseQuery);
6686
- this._searchGlobalSkills(this._skillBrowseQuery);
6678
+ if (this._aiFrameworkConfig.registryServer !== true) {
6679
+ this._searchGlobalSkills(this._skillBrowseQuery);
6680
+ }
6687
6681
  }
6688
6682
  _handleSkillBrowseTabChange(e) {
6689
6683
  // Both buffers are kept current by open/search, so switching just swaps which
@@ -7025,6 +7019,15 @@ let KRChatbot = class KRChatbot extends i$3 {
7025
7019
  userText: params.message,
7026
7020
  fileIds: files.map((f) => f.conversationFileID),
7027
7021
  };
7022
+ // Ground the model in where the user is and what they've highlighted.
7023
+ // Only sent when present so an empty selection / failed capture doesn't
7024
+ // add noise to the request.
7025
+ if (params.pageContext) {
7026
+ body.pageContext = params.pageContext;
7027
+ }
7028
+ if (params.selectionContext) {
7029
+ body.selectionContext = params.selectionContext;
7030
+ }
7028
7031
  if (this.conversationId != null) {
7029
7032
  body.conversationID = this.conversationId;
7030
7033
  }
@@ -7069,10 +7072,16 @@ let KRChatbot = class KRChatbot extends i$3 {
7069
7072
  });
7070
7073
  };
7071
7074
  }
7075
+ // Capture where the user is and what they've highlighted at send time (not
7076
+ // earlier — selection is transient and the route may have changed since the
7077
+ // panel opened), so every turn grounds the model in the user's current spot.
7078
+ const context = this._captureContext();
7072
7079
  this.send({
7073
7080
  message: params.message,
7074
7081
  messages: this.messages.slice(0, -1),
7075
7082
  conversationFiles: params.conversationFiles,
7083
+ pageContext: context.pageContext,
7084
+ selectionContext: context.selectionContext,
7076
7085
  }).then((response) => {
7077
7086
  if (!response.ok) {
7078
7087
  this._handleStreamError('Request failed: ' + response.status);
@@ -7092,6 +7101,56 @@ let KRChatbot = class KRChatbot extends i$3 {
7092
7101
  }
7093
7102
  });
7094
7103
  }
7104
+ /**
7105
+ * Capture ambient "where is the user / what are they looking at" context at
7106
+ * send time, so the AI is grounded without the host passing anything in.
7107
+ *
7108
+ * Two universal sources that exist on every page the chatbot mounts in:
7109
+ * - `pageContext` — the location (full url, which already carries the path and
7110
+ * query) and title, plus, when the page renders the server-side
7111
+ * `#kodaris-page` JSON island (storefront content pages do), its parsed
7112
+ * contents (page name/type/ids). A page without the island still yields url
7113
+ * + title, so context is never empty on a real page.
7114
+ * - `selectionContext` — the text the user currently has highlighted, read
7115
+ * live here because a selection is transient (it may be gone a moment later).
7116
+ *
7117
+ * Both are returned as strings because that's what the backend `ModelForText`
7118
+ * fields (`pageContext` / `selectionContext`) accept. Capture is best-effort:
7119
+ * any failure yields an empty string for that field rather than blocking send.
7120
+ */
7121
+ _captureContext() {
7122
+ let pageContext = '';
7123
+ try {
7124
+ const page = {
7125
+ url: window.location.href,
7126
+ title: document.title,
7127
+ };
7128
+ // Structured page context is opt-in by convention: a page that renders the
7129
+ // `#kodaris-page` island gets its name/type/ids folded in for free. The
7130
+ // island's text is JSON; a malformed/absent island just leaves it out.
7131
+ const island = document.getElementById('kodaris-page');
7132
+ if (island && island.textContent) {
7133
+ try {
7134
+ page.page = JSON.parse(island.textContent);
7135
+ }
7136
+ catch { /* island present but not valid JSON — location context still stands */ }
7137
+ }
7138
+ pageContext = JSON.stringify(page);
7139
+ }
7140
+ catch { /* leave pageContext empty — never block a send on context capture */ }
7141
+ let selectionContext = '';
7142
+ try {
7143
+ const selection = window.getSelection();
7144
+ if (selection) {
7145
+ selectionContext = selection.toString().trim();
7146
+ }
7147
+ }
7148
+ catch { /* leave selectionContext empty */ }
7149
+ return {
7150
+ pageContext,
7151
+ selectionContext,
7152
+ };
7153
+ }
7095
7154
  _handleStop() {
7096
7155
  if (this._reader) {
7097
7156
  this._reader.cancel();
@@ -7365,7 +7424,14 @@ let KRChatbot = class KRChatbot extends i$3 {
7365
7424
  </div>
7366
7425
  `;
7367
7426
  }
7368
- /** The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards. */
7427
+ /**
7428
+ * The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards.
7429
+ *
7430
+ * The Kodaris tab is omitted on the registry server (kodaris.com): that instance
7431
+ * IS the catalog source, so `globalSkillList` has no remote to pull and always
7432
+ * returns `[]`. Its own published skills appear on the Your Org tab instead,
7433
+ * carrying a "Global" badge.
7434
+ */
7369
7435
  _renderSkillBrowse() {
7370
7436
  return b `
7371
7437
  <kr-dialog
@@ -7387,9 +7453,11 @@ let KRChatbot = class KRChatbot extends i$3 {
7387
7453
  <kr-tab id="org" label="Your Org">
7388
7454
  ${this._renderSkillGrid()}
7389
7455
  </kr-tab>
7390
- <kr-tab id="kodaris" label="Kodaris">
7391
- ${this._renderSkillGrid(true)}
7392
- </kr-tab>
7456
+ ${this._aiFrameworkConfig.registryServer === true ? A : b `
7457
+ <kr-tab id="kodaris" label="Kodaris">
7458
+ ${this._renderSkillGrid(true)}
7459
+ </kr-tab>
7460
+ `}
7393
7461
  </kr-tab-group>
7394
7462
  </div>
7395
7463
  </kr-dialog>
@@ -7398,8 +7466,13 @@ let KRChatbot = class KRChatbot extends i$3 {
7398
7466
  /**
7399
7467
  * The card grid of skill search results for the browse dialog. Clicking a card
7400
7468
  * inserts `/code ` into the composer. In the shared-Kodaris (`global`) tab each
7401
- * card also gets a footer row with an Enabled toggle that installs/removes the
7402
- * skill on this instance.
7469
+ * card also gets a footer row with an Install/Uninstall button.
7470
+ *
7471
+ * The Kodaris tab is the whole published catalog, deliberately NOT role-filtered so
7472
+ * users can discover skills they don't have yet. So a card there can be in one of two
7473
+ * non-runnable states — the user's roles don't allow it (`canRun: false`), or it isn't
7474
+ * installed here (not `enabled`). Neither looks clickable; clicking either explains
7475
+ * why via `_handleSkillSelect`.
7403
7476
  */
7404
7477
  _renderSkillGrid(global = false) {
7405
7478
  let skills = this._orgSkills;
@@ -7413,7 +7486,10 @@ let KRChatbot = class KRChatbot extends i$3 {
7413
7486
  <div class="skill-browse__grid">
7414
7487
  ${skills.map((skill) => b `
7415
7488
  <div
7416
- class="skill-card"
7489
+ class=${e({
7490
+ 'skill-card': true,
7491
+ 'skill-card--unavailable': skill.canRun === false || (!!skill.global && !skill.enabled),
7492
+ })}
7417
7493
  role="button"
7418
7494
  tabindex="0"
7419
7495
  @click=${() => this._handleSkillSelect(skill)}
@@ -7423,6 +7499,9 @@ let KRChatbot = class KRChatbot extends i$3 {
7423
7499
  ${skill.description ? b `
7424
7500
  <span class="skill-card__desc">${skill.description}</span>
7425
7501
  ` : A}
7502
+ ${!global && skill.global ? b `
7503
+ <span class="skill-card__badge">Global</span>
7504
+ ` : A}
7426
7505
  ${global ? b `
7427
7506
  <div class="skill-card__footer">
7428
7507
  <span class="skill-card__price">Free</span>
@@ -9292,6 +9371,30 @@ KRChatbot.styles = i$6 `
9292
9371
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
9293
9372
  }
9294
9373
 
9374
+ /* A Kodaris-tab card the user can't invoke — not in their roles, or not installed
9375
+ here yet. Clicking it does nothing, so it drops the pointer cursor and the hover
9376
+ highlight. The Install button inside keeps its own pointer cursor. */
9377
+ .skill-card--unavailable {
9378
+ cursor: default;
9379
+ }
9380
+
9381
+ .skill-card--unavailable:hover {
9382
+ border-color: #e5e7eb;
9383
+ box-shadow: none;
9384
+ }
9385
+
9386
+ /* Marks a globally-published skill listed on the Your Org tab — only happens on
9387
+ kodaris.com, which has no Kodaris tab for its own published skills. */
9388
+ .skill-card__badge {
9389
+ align-self: flex-start;
9390
+ padding: 2px 8px;
9391
+ border-radius: 10px;
9392
+ background: #f3f4f6;
9393
+ font-size: 11px;
9394
+ font-weight: 600;
9395
+ color: #000000;
9396
+ }
9397
+
9295
9398
  /* Footer row on global skill cards: install button on the left, price on the
9296
9399
  right. margin-top:auto pins it to the bottom so cards in a row align their
9297
9400
  footers regardless of description length. Negative horizontal margins