@kodaris/krubble-app-components 1.0.106 → 1.0.108
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.
- package/dist/chatbot.d.ts +123 -38
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +212 -165
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +212 -165
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +178 -149
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +211 -164
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +44 -15
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/chatbot.js
CHANGED
|
@@ -13,7 +13,7 @@ import { repeat } from 'lit/directives/repeat.js';
|
|
|
13
13
|
import { marked } from 'marked';
|
|
14
14
|
// Importing these named exports also registers their custom elements
|
|
15
15
|
// (kr-dialog, kr-tab-group, kr-tab, kr-text-field) used by the browse dialog.
|
|
16
|
-
import { KRFilePreview } from '@kodaris/krubble-components';
|
|
16
|
+
import { KRFilePreview, KRSnackbar } from '@kodaris/krubble-components';
|
|
17
17
|
import { KRHttp } from '@kodaris/krubble-http';
|
|
18
18
|
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
19
19
|
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
@@ -262,10 +262,10 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
262
262
|
*/
|
|
263
263
|
this._globalSkills = [];
|
|
264
264
|
/**
|
|
265
|
-
* Latest results for the `/` composer menu
|
|
266
|
-
*
|
|
267
|
-
* buffer from the two browse-tab lists
|
|
268
|
-
* menu never
|
|
265
|
+
* Latest results for the `/` composer menu — everything the current user can
|
|
266
|
+
* actually invoke, merged and role-filtered server-side by
|
|
267
|
+
* `/conversation/availableSkills`. A separate buffer from the two browse-tab lists
|
|
268
|
+
* so installed globals in the menu never leak into the "Your Org" tab.
|
|
269
269
|
*/
|
|
270
270
|
this._invocableSkills = [];
|
|
271
271
|
/**
|
|
@@ -843,77 +843,30 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
843
843
|
}
|
|
844
844
|
// --- Skills ---
|
|
845
845
|
/**
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
* over name^3/description^2/instructions, active-only) so the menu ranks skills
|
|
850
|
-
* the way the model does. An empty query uses match_all — multi_match with an
|
|
851
|
-
* empty string matches nothing, so the "show everything" case needs it.
|
|
846
|
+
* Load the browse dialog's "Your Org" tab into `_orgSkills` from
|
|
847
|
+
* `/conversation/localSkillList` — this instance's own active skills, already
|
|
848
|
+
* role-filtered to what the caller may run and ranked by the server.
|
|
852
849
|
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
850
|
+
* Not the `aiskills` search endpoint: that requires a skill-authoring role
|
|
851
|
+
* (`skillView`/`skillEdit`), so an ordinary chat user got a 403 and an empty
|
|
852
|
+
* tab. This one only needs a logged-in employee. It also means the query
|
|
853
|
+
* matching, active-only filter and "exclude globally-published" rule all live
|
|
854
|
+
* server-side instead of being rebuilt as OpenSearch DSL here.
|
|
855
|
+
*
|
|
856
|
+
* EXCEPTION — on kodaris.com this list also carries that instance's own
|
|
857
|
+
* globally-published skills (flagged `global: true`), because there is no
|
|
858
|
+
* Kodaris tab there for them to appear on.
|
|
859
|
+
*
|
|
860
|
+
* Request-id guard drops responses that arrive out of order (kr-combo-box
|
|
861
|
+
* `_fetch` pattern).
|
|
856
862
|
*/
|
|
857
863
|
_searchOrgSkills(query) {
|
|
858
864
|
this._orgSkillRequestId++;
|
|
859
865
|
const requestId = this._orgSkillRequestId;
|
|
860
|
-
const trimmed = query.trim();
|
|
861
|
-
// An empty query matches everything; otherwise substring-match the query
|
|
862
|
-
// against name/code/description. We use case-insensitive wildcard queries on
|
|
863
|
-
// the `.raw` keyword subfields so "ord" matches anywhere in "Create Order"
|
|
864
|
-
// (a plain match/multi_match only matches whole tokens). Escape the wildcard
|
|
865
|
-
// specials so user input is treated literally, then wrap with `*...*`.
|
|
866
|
-
let must = { match_all: {} };
|
|
867
|
-
if (trimmed) {
|
|
868
|
-
must = {
|
|
869
|
-
bool: {
|
|
870
|
-
should: [
|
|
871
|
-
{
|
|
872
|
-
wildcard: {
|
|
873
|
-
'name.raw': {
|
|
874
|
-
value: `*${trimmed.replace(/([*?\\])/g, '\\$1')}*`,
|
|
875
|
-
case_insensitive: true,
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
wildcard: {
|
|
881
|
-
'code.raw': {
|
|
882
|
-
value: `*${trimmed.replace(/([*?\\])/g, '\\$1')}*`,
|
|
883
|
-
case_insensitive: true,
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
},
|
|
887
|
-
{
|
|
888
|
-
wildcard: {
|
|
889
|
-
'description.raw': {
|
|
890
|
-
value: `*${trimmed.replace(/([*?\\])/g, '\\$1')}*`,
|
|
891
|
-
case_insensitive: true,
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
},
|
|
895
|
-
],
|
|
896
|
-
minimum_should_match: 1,
|
|
897
|
-
},
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
866
|
KRHttp.fetch({
|
|
901
|
-
url: '/api/system/
|
|
867
|
+
url: '/api/system/conversation/localSkillList',
|
|
902
868
|
method: 'POST',
|
|
903
|
-
body: {
|
|
904
|
-
size: 100,
|
|
905
|
-
query: {
|
|
906
|
-
bool: {
|
|
907
|
-
must: [must],
|
|
908
|
-
filter: [{ term: { active: true } }],
|
|
909
|
-
// The "Your Org" tab lists only this instance's own skills. Globally-
|
|
910
|
-
// published skills belong to the "Kodaris" tab (fed by the registry),
|
|
911
|
-
// so exclude them here. Documents without the field aren't matched by
|
|
912
|
-
// the term, so ordinary org skills are kept.
|
|
913
|
-
must_not: [{ term: { globalPublished: true } }],
|
|
914
|
-
},
|
|
915
|
-
},
|
|
916
|
-
},
|
|
869
|
+
body: { query: query.trim() },
|
|
917
870
|
})
|
|
918
871
|
.then((r) => r.json())
|
|
919
872
|
.then((res) => {
|
|
@@ -921,114 +874,63 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
921
874
|
if (requestId !== this._orgSkillRequestId) {
|
|
922
875
|
return;
|
|
923
876
|
}
|
|
924
|
-
this._orgSkills = (res?.data
|
|
877
|
+
this._orgSkills = (res?.data || []);
|
|
925
878
|
})
|
|
926
879
|
.catch((error) => {
|
|
927
880
|
console.error('kr-chatbot: skill search failed', error);
|
|
928
881
|
});
|
|
929
882
|
}
|
|
930
883
|
/**
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
884
|
+
* Load the skills for the `/` composer menu into `_invocableSkills` from
|
|
885
|
+
* `/conversation/availableSkills` — this instance's own skills plus the installed
|
|
886
|
+
* global ones, already merged, role-filtered and ranked by the server. That's the
|
|
887
|
+
* same set the model sees via `list_skills`, so the menu can't offer a skill the
|
|
888
|
+
* chat would then refuse.
|
|
889
|
+
*
|
|
890
|
+
* Not the `aiskills` endpoints the browse dialog's Org tab uses: those require a
|
|
891
|
+
* skill-authoring role, which an ordinary chat user doesn't have.
|
|
892
|
+
*
|
|
893
|
+
* Request-id guard drops responses that arrive out of order (kr-combo-box `_fetch`
|
|
894
|
+
* pattern).
|
|
939
895
|
*/
|
|
940
896
|
_searchInvocableSkills(query) {
|
|
941
897
|
this._invocableSkillRequestId++;
|
|
942
898
|
const requestId = this._invocableSkillRequestId;
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (query.trim()) {
|
|
946
|
-
must = {
|
|
947
|
-
bool: {
|
|
948
|
-
should: [
|
|
949
|
-
{
|
|
950
|
-
wildcard: {
|
|
951
|
-
'name.raw': {
|
|
952
|
-
value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
|
|
953
|
-
case_insensitive: true,
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
wildcard: {
|
|
959
|
-
'code.raw': {
|
|
960
|
-
value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
|
|
961
|
-
case_insensitive: true,
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
wildcard: {
|
|
967
|
-
'description.raw': {
|
|
968
|
-
value: `*${query.trim().replace(/([*?\\])/g, '\\$1')}*`,
|
|
969
|
-
case_insensitive: true,
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
},
|
|
973
|
-
],
|
|
974
|
-
minimum_should_match: 1,
|
|
975
|
-
},
|
|
976
|
-
};
|
|
977
|
-
}
|
|
978
|
-
// Org skills — this instance's own catalog (globalPublished excluded).
|
|
979
|
-
const orgSearch = KRHttp.fetch({
|
|
980
|
-
url: '/api/system/opensearch/aiskills/search',
|
|
981
|
-
method: 'POST',
|
|
982
|
-
body: {
|
|
983
|
-
size: 100,
|
|
984
|
-
query: {
|
|
985
|
-
bool: {
|
|
986
|
-
must: [must],
|
|
987
|
-
filter: [{ term: { active: true } }],
|
|
988
|
-
must_not: [{ term: { globalPublished: true } }],
|
|
989
|
-
},
|
|
990
|
-
},
|
|
991
|
-
},
|
|
992
|
-
})
|
|
993
|
-
.then((r) => r.json())
|
|
994
|
-
.then((res) => (res?.data?.hits?.hits || []).map((hit) => hit._source))
|
|
995
|
-
.catch((error) => {
|
|
996
|
-
console.error('kr-chatbot: invocable skill search failed', error);
|
|
997
|
-
return [];
|
|
998
|
-
});
|
|
999
|
-
// Installed globals — the registry catalog, kept only where enabled=true.
|
|
1000
|
-
const globalSearch = KRHttp.fetch({
|
|
1001
|
-
url: '/api/system/opensearch/aiskills/global/search',
|
|
899
|
+
KRHttp.fetch({
|
|
900
|
+
url: '/api/system/conversation/availableSkills',
|
|
1002
901
|
method: 'POST',
|
|
1003
902
|
body: { query: query.trim() },
|
|
1004
903
|
})
|
|
1005
904
|
.then((r) => r.json())
|
|
1006
|
-
.then((res) =>
|
|
1007
|
-
.catch((error) => {
|
|
1008
|
-
console.error('kr-chatbot: installed global skill search failed', error);
|
|
1009
|
-
return [];
|
|
1010
|
-
});
|
|
1011
|
-
Promise.all([orgSearch, globalSearch]).then(([orgSkills, globalSkills]) => {
|
|
905
|
+
.then((res) => {
|
|
1012
906
|
// Ignore a stale response superseded by a newer search.
|
|
1013
907
|
if (requestId !== this._invocableSkillRequestId) {
|
|
1014
908
|
return;
|
|
1015
909
|
}
|
|
1016
|
-
this._invocableSkills = [
|
|
910
|
+
this._invocableSkills = (res?.data || []);
|
|
911
|
+
})
|
|
912
|
+
.catch((error) => {
|
|
913
|
+
console.error('kr-chatbot: invocable skill search failed', error);
|
|
1017
914
|
});
|
|
1018
915
|
}
|
|
1019
916
|
/**
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
917
|
+
* Load the browse dialog's "Kodaris" tab into `_globalSkills` — the whole
|
|
918
|
+
* published Kodaris catalog for discovery, installed or not, runnable by this
|
|
919
|
+
* user or not. Each row carries `enabled` (installed here), `canEnable` (may
|
|
920
|
+
* install it) and `canRun` (roles allow invoking it).
|
|
921
|
+
*
|
|
922
|
+
* Uses `/conversation/globalSkillList` rather than the `/aiskills/global/search`
|
|
923
|
+
* twin, which requires a skill-authoring role an ordinary chat user doesn't have.
|
|
924
|
+
*
|
|
925
|
+
* Returns `[]` — not an error — when there's no reachable registry, and always
|
|
926
|
+
* on kodaris.com, which IS the catalog source and so has no remote to pull. The
|
|
927
|
+
* tab itself is hidden there (see `_renderSkillBrowse`).
|
|
1026
928
|
*/
|
|
1027
929
|
_searchGlobalSkills(query) {
|
|
1028
930
|
this._globalSkillRequestId++;
|
|
1029
931
|
const requestId = this._globalSkillRequestId;
|
|
1030
932
|
KRHttp.fetch({
|
|
1031
|
-
url: '/api/system/
|
|
933
|
+
url: '/api/system/conversation/globalSkillList',
|
|
1032
934
|
method: 'POST',
|
|
1033
935
|
body: { query: query.trim() },
|
|
1034
936
|
})
|
|
@@ -1052,6 +954,17 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1052
954
|
_handleGlobalSkillEnable(skill, e) {
|
|
1053
955
|
// The card body inserts the skill; the install button must not also do that.
|
|
1054
956
|
e.stopPropagation();
|
|
957
|
+
// The button is shown to everyone so the catalog reads the same for all users,
|
|
958
|
+
// but the enable endpoint requires administrator/superuser/skillEdit. Without
|
|
959
|
+
// it, say who to ask instead of firing a request that comes back 403.
|
|
960
|
+
if (!skill.canEnable) {
|
|
961
|
+
KRSnackbar.show({
|
|
962
|
+
title: 'Permission Required',
|
|
963
|
+
message: 'You do not have permission to install skills. Please contact your site administrator.',
|
|
964
|
+
type: 'error',
|
|
965
|
+
});
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
1055
968
|
if (this._globalSkillPending.has(skill.code)) {
|
|
1056
969
|
return;
|
|
1057
970
|
}
|
|
@@ -1178,13 +1091,33 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1178
1091
|
recognition.start();
|
|
1179
1092
|
this._inputEl?.focus();
|
|
1180
1093
|
}
|
|
1094
|
+
/**
|
|
1181
1095
|
/** Insert `/code ` into the composer and focus it, ready for the message. */
|
|
1182
1096
|
_handleSkillSelect(skill) {
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1187
|
-
|
|
1097
|
+
// The user's roles don't allow this skill — inserting /code would just fail in the
|
|
1098
|
+
// chat. Name the roles that would grant it, so the user knows what to ask for and
|
|
1099
|
+
// whoever grants it knows what to give.
|
|
1100
|
+
if (skill.canRun === false) {
|
|
1101
|
+
let message = `You do not have access to "${skill.name}". Ask your site administrator to grant it.`;
|
|
1102
|
+
if (skill.allowedRoles?.length) {
|
|
1103
|
+
message = `"${skill.name}" requires the ${skill.allowedRoles.join(' or ')} role. Ask your site administrator to grant it.`;
|
|
1104
|
+
}
|
|
1105
|
+
KRSnackbar.show({
|
|
1106
|
+
title: 'Access Required',
|
|
1107
|
+
message: message,
|
|
1108
|
+
type: 'error',
|
|
1109
|
+
});
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
// A global skill nobody has installed here yet isn't runnable either — point at the
|
|
1113
|
+
// Install button rather than appearing to ignore the click. Local skills carry no
|
|
1114
|
+
// install state, so this only applies to global ones.
|
|
1115
|
+
if (skill.global && !skill.enabled) {
|
|
1116
|
+
KRSnackbar.show({
|
|
1117
|
+
title: 'Not Installed',
|
|
1118
|
+
message: `"${skill.name}" has to be installed on this site before you can use it — use the Install button on the card.`,
|
|
1119
|
+
type: 'info',
|
|
1120
|
+
});
|
|
1188
1121
|
return;
|
|
1189
1122
|
}
|
|
1190
1123
|
this._inputValue = `/${skill.code} `;
|
|
@@ -1216,8 +1149,11 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1216
1149
|
this._skillBrowseQuery = '';
|
|
1217
1150
|
this._skillBrowseOpened = true;
|
|
1218
1151
|
// Fill both tabs up front so switching between them is instant, not stale.
|
|
1152
|
+
// The Kodaris tab isn't rendered on the registry server, so skip its fetch there.
|
|
1219
1153
|
this._searchOrgSkills('');
|
|
1220
|
-
this.
|
|
1154
|
+
if (this._aiFrameworkConfig.registryServer !== true) {
|
|
1155
|
+
this._searchGlobalSkills('');
|
|
1156
|
+
}
|
|
1221
1157
|
}
|
|
1222
1158
|
_handleSkillBrowseClose() {
|
|
1223
1159
|
this._skillBrowseOpened = false;
|
|
@@ -1226,7 +1162,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1226
1162
|
this._skillBrowseQuery = e.target.value;
|
|
1227
1163
|
// Filter both catalogs so whichever tab the user switches to is already current.
|
|
1228
1164
|
this._searchOrgSkills(this._skillBrowseQuery);
|
|
1229
|
-
|
|
1165
|
+
if (this._aiFrameworkConfig.registryServer !== true) {
|
|
1166
|
+
this._searchGlobalSkills(this._skillBrowseQuery);
|
|
1167
|
+
}
|
|
1230
1168
|
}
|
|
1231
1169
|
_handleSkillBrowseTabChange(e) {
|
|
1232
1170
|
// Both buffers are kept current by open/search, so switching just swaps which
|
|
@@ -1568,6 +1506,15 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1568
1506
|
userText: params.message,
|
|
1569
1507
|
fileIds: files.map((f) => f.conversationFileID),
|
|
1570
1508
|
};
|
|
1509
|
+
// Ground the model in where the user is and what they've highlighted.
|
|
1510
|
+
// Only sent when present so an empty selection / failed capture doesn't
|
|
1511
|
+
// add noise to the request.
|
|
1512
|
+
if (params.pageContext) {
|
|
1513
|
+
body.pageContext = params.pageContext;
|
|
1514
|
+
}
|
|
1515
|
+
if (params.selectionContext) {
|
|
1516
|
+
body.selectionContext = params.selectionContext;
|
|
1517
|
+
}
|
|
1571
1518
|
if (this.conversationId != null) {
|
|
1572
1519
|
body.conversationID = this.conversationId;
|
|
1573
1520
|
}
|
|
@@ -1612,10 +1559,16 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1612
1559
|
});
|
|
1613
1560
|
};
|
|
1614
1561
|
}
|
|
1562
|
+
// Capture where the user is and what they've highlighted at send time (not
|
|
1563
|
+
// earlier — selection is transient and the route may have changed since the
|
|
1564
|
+
// panel opened), so every turn grounds the model in the user's current spot.
|
|
1565
|
+
const context = this._captureContext();
|
|
1615
1566
|
this.send({
|
|
1616
1567
|
message: params.message,
|
|
1617
1568
|
messages: this.messages.slice(0, -1),
|
|
1618
1569
|
conversationFiles: params.conversationFiles,
|
|
1570
|
+
pageContext: context.pageContext,
|
|
1571
|
+
selectionContext: context.selectionContext,
|
|
1619
1572
|
}).then((response) => {
|
|
1620
1573
|
if (!response.ok) {
|
|
1621
1574
|
this._handleStreamError('Request failed: ' + response.status);
|
|
@@ -1635,6 +1588,56 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1635
1588
|
}
|
|
1636
1589
|
});
|
|
1637
1590
|
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Capture ambient "where is the user / what are they looking at" context at
|
|
1593
|
+
* send time, so the AI is grounded without the host passing anything in.
|
|
1594
|
+
*
|
|
1595
|
+
* Two universal sources that exist on every page the chatbot mounts in:
|
|
1596
|
+
* - `pageContext` — the location (full url, which already carries the path and
|
|
1597
|
+
* query) and title, plus, when the page renders the server-side
|
|
1598
|
+
* `#kodaris-page` JSON island (storefront content pages do), its parsed
|
|
1599
|
+
* contents (page name/type/ids). A page without the island still yields url
|
|
1600
|
+
* + title, so context is never empty on a real page.
|
|
1601
|
+
* - `selectionContext` — the text the user currently has highlighted, read
|
|
1602
|
+
* live here because a selection is transient (it may be gone a moment later).
|
|
1603
|
+
*
|
|
1604
|
+
* Both are returned as strings because that's what the backend `ModelForText`
|
|
1605
|
+
* fields (`pageContext` / `selectionContext`) accept. Capture is best-effort:
|
|
1606
|
+
* any failure yields an empty string for that field rather than blocking send.
|
|
1607
|
+
*/
|
|
1608
|
+
_captureContext() {
|
|
1609
|
+
let pageContext = '';
|
|
1610
|
+
try {
|
|
1611
|
+
const page = {
|
|
1612
|
+
url: window.location.href,
|
|
1613
|
+
title: document.title,
|
|
1614
|
+
};
|
|
1615
|
+
// Structured page context is opt-in by convention: a page that renders the
|
|
1616
|
+
// `#kodaris-page` island gets its name/type/ids folded in for free. The
|
|
1617
|
+
// island's text is JSON; a malformed/absent island just leaves it out.
|
|
1618
|
+
const island = document.getElementById('kodaris-page');
|
|
1619
|
+
if (island && island.textContent) {
|
|
1620
|
+
try {
|
|
1621
|
+
page.page = JSON.parse(island.textContent);
|
|
1622
|
+
}
|
|
1623
|
+
catch { /* island present but not valid JSON — location context still stands */ }
|
|
1624
|
+
}
|
|
1625
|
+
pageContext = JSON.stringify(page);
|
|
1626
|
+
}
|
|
1627
|
+
catch { /* leave pageContext empty — never block a send on context capture */ }
|
|
1628
|
+
let selectionContext = '';
|
|
1629
|
+
try {
|
|
1630
|
+
const selection = window.getSelection();
|
|
1631
|
+
if (selection) {
|
|
1632
|
+
selectionContext = selection.toString().trim();
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
catch { /* leave selectionContext empty */ }
|
|
1636
|
+
return {
|
|
1637
|
+
pageContext,
|
|
1638
|
+
selectionContext,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1638
1641
|
_handleStop() {
|
|
1639
1642
|
if (this._reader) {
|
|
1640
1643
|
this._reader.cancel();
|
|
@@ -1908,7 +1911,14 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1908
1911
|
</div>
|
|
1909
1912
|
`;
|
|
1910
1913
|
}
|
|
1911
|
-
/**
|
|
1914
|
+
/**
|
|
1915
|
+
* The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards.
|
|
1916
|
+
*
|
|
1917
|
+
* The Kodaris tab is omitted on the registry server (kodaris.com): that instance
|
|
1918
|
+
* IS the catalog source, so `globalSkillList` has no remote to pull and always
|
|
1919
|
+
* returns `[]`. Its own published skills appear on the Your Org tab instead,
|
|
1920
|
+
* carrying a "Global" badge.
|
|
1921
|
+
*/
|
|
1912
1922
|
_renderSkillBrowse() {
|
|
1913
1923
|
return html `
|
|
1914
1924
|
<kr-dialog
|
|
@@ -1930,9 +1940,11 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1930
1940
|
<kr-tab id="org" label="Your Org">
|
|
1931
1941
|
${this._renderSkillGrid()}
|
|
1932
1942
|
</kr-tab>
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1943
|
+
${this._aiFrameworkConfig.registryServer === true ? nothing : html `
|
|
1944
|
+
<kr-tab id="kodaris" label="Kodaris">
|
|
1945
|
+
${this._renderSkillGrid(true)}
|
|
1946
|
+
</kr-tab>
|
|
1947
|
+
`}
|
|
1936
1948
|
</kr-tab-group>
|
|
1937
1949
|
</div>
|
|
1938
1950
|
</kr-dialog>
|
|
@@ -1941,8 +1953,13 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1941
1953
|
/**
|
|
1942
1954
|
* The card grid of skill search results for the browse dialog. Clicking a card
|
|
1943
1955
|
* inserts `/code ` into the composer. In the shared-Kodaris (`global`) tab each
|
|
1944
|
-
* card also gets a footer row with an
|
|
1945
|
-
*
|
|
1956
|
+
* card also gets a footer row with an Install/Uninstall button.
|
|
1957
|
+
*
|
|
1958
|
+
* The Kodaris tab is the whole published catalog, deliberately NOT role-filtered so
|
|
1959
|
+
* users can discover skills they don't have yet. So a card there can be in one of two
|
|
1960
|
+
* non-runnable states — the user's roles don't allow it (`canRun: false`), or it isn't
|
|
1961
|
+
* installed here (not `enabled`). Neither looks clickable; clicking either explains
|
|
1962
|
+
* why via `_handleSkillSelect`.
|
|
1946
1963
|
*/
|
|
1947
1964
|
_renderSkillGrid(global = false) {
|
|
1948
1965
|
let skills = this._orgSkills;
|
|
@@ -1956,7 +1973,10 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1956
1973
|
<div class="skill-browse__grid">
|
|
1957
1974
|
${skills.map((skill) => html `
|
|
1958
1975
|
<div
|
|
1959
|
-
class
|
|
1976
|
+
class=${classMap({
|
|
1977
|
+
'skill-card': true,
|
|
1978
|
+
'skill-card--unavailable': skill.canRun === false || (!!skill.global && !skill.enabled),
|
|
1979
|
+
})}
|
|
1960
1980
|
role="button"
|
|
1961
1981
|
tabindex="0"
|
|
1962
1982
|
@click=${() => this._handleSkillSelect(skill)}
|
|
@@ -1966,6 +1986,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1966
1986
|
${skill.description ? html `
|
|
1967
1987
|
<span class="skill-card__desc">${skill.description}</span>
|
|
1968
1988
|
` : nothing}
|
|
1989
|
+
${!global && skill.global ? html `
|
|
1990
|
+
<span class="skill-card__badge">Global</span>
|
|
1991
|
+
` : nothing}
|
|
1969
1992
|
${global ? html `
|
|
1970
1993
|
<div class="skill-card__footer">
|
|
1971
1994
|
<span class="skill-card__price">Free</span>
|
|
@@ -3835,6 +3858,30 @@ KRChatbot.styles = css `
|
|
|
3835
3858
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
3836
3859
|
}
|
|
3837
3860
|
|
|
3861
|
+
/* A Kodaris-tab card the user can't invoke — not in their roles, or not installed
|
|
3862
|
+
here yet. Clicking it does nothing, so it drops the pointer cursor and the hover
|
|
3863
|
+
highlight. The Install button inside keeps its own pointer cursor. */
|
|
3864
|
+
.skill-card--unavailable {
|
|
3865
|
+
cursor: default;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
.skill-card--unavailable:hover {
|
|
3869
|
+
border-color: #e5e7eb;
|
|
3870
|
+
box-shadow: none;
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
/* Marks a globally-published skill listed on the Your Org tab — only happens on
|
|
3874
|
+
kodaris.com, which has no Kodaris tab for its own published skills. */
|
|
3875
|
+
.skill-card__badge {
|
|
3876
|
+
align-self: flex-start;
|
|
3877
|
+
padding: 2px 8px;
|
|
3878
|
+
border-radius: 10px;
|
|
3879
|
+
background: #f3f4f6;
|
|
3880
|
+
font-size: 11px;
|
|
3881
|
+
font-weight: 600;
|
|
3882
|
+
color: #000000;
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3838
3885
|
/* Footer row on global skill cards: install button on the left, price on the
|
|
3839
3886
|
right. margin-top:auto pins it to the bottom so cards in a row align their
|
|
3840
3887
|
footers regardless of description length. Negative horizontal margins
|