@kodaris/krubble-app-components 1.0.71 → 1.0.72
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 +65 -0
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +583 -4
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +581 -4
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +396 -56
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +581 -4
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +431 -91
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
package/dist/chatbot.js
CHANGED
|
@@ -10,6 +10,8 @@ import { classMap } from 'lit/directives/class-map.js';
|
|
|
10
10
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
11
11
|
import { repeat } from 'lit/directives/repeat.js';
|
|
12
12
|
import { marked } from 'marked';
|
|
13
|
+
// Importing these named exports also registers their custom elements
|
|
14
|
+
// (kr-dialog, kr-tab-group, kr-tab, kr-text-field) used by the browse dialog.
|
|
13
15
|
import { KRFilePreview } from '@kodaris/krubble-components';
|
|
14
16
|
import { KRHttp } from '@kodaris/krubble-http';
|
|
15
17
|
/** Image MIME types the chatbot accepts (matches what Bedrock can ingest). */
|
|
@@ -111,6 +113,12 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
111
113
|
* Input placeholder text.
|
|
112
114
|
*/
|
|
113
115
|
this.placeholder = 'Type a message...';
|
|
116
|
+
/**
|
|
117
|
+
* Skills the user can invoke by typing `/` in the composer. Selecting one
|
|
118
|
+
* inserts `/name ` into the message. The `/` menu shows the first 15 (filtered
|
|
119
|
+
* as the user types); "Browse all skills" opens a dialog with the full list.
|
|
120
|
+
*/
|
|
121
|
+
this.skills = [];
|
|
114
122
|
/**
|
|
115
123
|
* Whether the chat panel is open. Only relevant in floating mode.
|
|
116
124
|
*/
|
|
@@ -140,6 +148,20 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
140
148
|
this._pendingInternalFiles = [];
|
|
141
149
|
/** True while a file is being dragged over the panel (shows the drop overlay). */
|
|
142
150
|
this._isDraggingFile = false;
|
|
151
|
+
/** Whether the `/` skill menu is open above the composer. */
|
|
152
|
+
this._skillMenuOpened = false;
|
|
153
|
+
/** Current `/` query (text after the slash), used to filter the skill menu. */
|
|
154
|
+
this._skillQuery = '';
|
|
155
|
+
/** Highlighted item in the skill menu for keyboard navigation (-1 = none). */
|
|
156
|
+
this._skillHighlighted = 0;
|
|
157
|
+
/** Index of the message whose copy button just flashed "Copied" (-1 = none). */
|
|
158
|
+
this._copiedIndex = -1;
|
|
159
|
+
/** Whether the "Browse all skills" dialog is open. */
|
|
160
|
+
this._skillBrowseOpened = false;
|
|
161
|
+
/** Active tab in the browse dialog. */
|
|
162
|
+
this._skillBrowseSource = 'org';
|
|
163
|
+
/** Search text in the browse dialog. */
|
|
164
|
+
this._skillBrowseQuery = '';
|
|
143
165
|
this._handleMouseMove = (e) => {
|
|
144
166
|
if (this._isDragging) {
|
|
145
167
|
let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
|
|
@@ -253,6 +275,40 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
253
275
|
}
|
|
254
276
|
}, 200);
|
|
255
277
|
}
|
|
278
|
+
// Position the fixed skill menu above the composer, following the krubble
|
|
279
|
+
// select-field pattern (fixed element positioned from the trigger's rect).
|
|
280
|
+
if (this._skillMenuOpened) {
|
|
281
|
+
this._positionSkillMenu();
|
|
282
|
+
}
|
|
283
|
+
// Keep the keyboard-highlighted skill scrolled into view.
|
|
284
|
+
if (changed.has('_skillHighlighted') && this._skillMenuOpened) {
|
|
285
|
+
this._skillMenuEl
|
|
286
|
+
?.querySelector('.skill-menu__item--highlighted')
|
|
287
|
+
?.scrollIntoView({ block: 'nearest' });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** Anchor the fixed skill menu to the composer, opening upward above it. */
|
|
291
|
+
_positionSkillMenu() {
|
|
292
|
+
requestAnimationFrame(() => {
|
|
293
|
+
if (!this._skillMenuEl || !this._inputWrapperEl) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const inputRect = this._inputWrapperEl.getBoundingClientRect();
|
|
297
|
+
this._skillMenuEl.style.left = inputRect.left + 'px';
|
|
298
|
+
this._skillMenuEl.style.width = inputRect.width + 'px';
|
|
299
|
+
// Bound the menu by the chat panel's own top edge so it stays within the
|
|
300
|
+
// widget. In floating mode that keeps it inside the card; in inline mode the
|
|
301
|
+
// panel top sits just below the app header/breadcrumb, so it stays clear of
|
|
302
|
+
// those too. An 8px gap keeps it off both edges.
|
|
303
|
+
const topLimit = this._panelEl.getBoundingClientRect().top + 8;
|
|
304
|
+
const spaceAbove = inputRect.top - 8 - topLimit;
|
|
305
|
+
this._skillMenuEl.style.maxHeight = spaceAbove + 'px';
|
|
306
|
+
// Measure after the cap is applied so the upward anchor uses the real
|
|
307
|
+
// height, then clamp the top edge to the panel bound.
|
|
308
|
+
const menuRect = this._skillMenuEl.getBoundingClientRect();
|
|
309
|
+
const top = Math.max(topLimit, inputRect.top - menuRect.height - 8);
|
|
310
|
+
this._skillMenuEl.style.top = top + 'px';
|
|
311
|
+
});
|
|
256
312
|
}
|
|
257
313
|
// --- Public methods ---
|
|
258
314
|
/**
|
|
@@ -446,13 +502,129 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
446
502
|
const textarea = e.target;
|
|
447
503
|
textarea.style.height = 'auto';
|
|
448
504
|
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
|
505
|
+
// Open the skill menu while the message is a `/` command being typed (a
|
|
506
|
+
// leading slash with no space yet). The text after the slash filters it.
|
|
507
|
+
const slashMatch = this._inputValue.match(/^\/(\S*)$/);
|
|
508
|
+
if (slashMatch && this.skills.length) {
|
|
509
|
+
this._skillMenuOpened = true;
|
|
510
|
+
this._skillQuery = slashMatch[1];
|
|
511
|
+
this._skillHighlighted = 0;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
this._skillMenuOpened = false;
|
|
515
|
+
}
|
|
449
516
|
}
|
|
450
517
|
_handleKeyDown(e) {
|
|
518
|
+
// When the skill menu is open, arrow keys / Enter / Escape drive it. The two
|
|
519
|
+
// top actions (Attach Files, Browse All Skills) are index 0 and 1, then the
|
|
520
|
+
// filtered skills follow — all navigable as one list.
|
|
521
|
+
if (this._skillMenuOpened) {
|
|
522
|
+
const count = 2 + this._filteredSkills().length;
|
|
523
|
+
if (e.key === 'ArrowDown') {
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
this._skillHighlighted = (this._skillHighlighted + 1) % count;
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (e.key === 'ArrowUp') {
|
|
529
|
+
e.preventDefault();
|
|
530
|
+
this._skillHighlighted = (this._skillHighlighted - 1 + count) % count;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
534
|
+
e.preventDefault();
|
|
535
|
+
this._activateSkillMenuItem(this._skillHighlighted);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (e.key === 'Escape') {
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
this._skillMenuOpened = false;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
451
544
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
452
545
|
e.preventDefault();
|
|
453
546
|
this._handleSubmit();
|
|
454
547
|
}
|
|
455
548
|
}
|
|
549
|
+
/** Run the skill-menu item at the given flat index (0/1 = actions, then skills). */
|
|
550
|
+
_activateSkillMenuItem(index) {
|
|
551
|
+
if (index === 0) {
|
|
552
|
+
this._openFilePicker();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (index === 1) {
|
|
556
|
+
this._handleSkillBrowseOpen();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const skill = this._filteredSkills()[index - 2];
|
|
560
|
+
if (skill) {
|
|
561
|
+
this._handleSkillSelect(skill);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// --- Skills ---
|
|
565
|
+
/** Skills matching the current `/` query, capped at the first 15. */
|
|
566
|
+
_filteredSkills() {
|
|
567
|
+
const query = this._skillQuery.toLowerCase();
|
|
568
|
+
return this.skills
|
|
569
|
+
.filter((skill) => skill.name.toLowerCase().includes(query) ||
|
|
570
|
+
skill.label.toLowerCase().includes(query))
|
|
571
|
+
.slice(0, 15);
|
|
572
|
+
}
|
|
573
|
+
/** The `+` button toggles the skill/attachment menu open (showing all skills). */
|
|
574
|
+
_handlePlusClick() {
|
|
575
|
+
if (this._skillMenuOpened) {
|
|
576
|
+
this._skillMenuOpened = false;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
this._skillQuery = '';
|
|
580
|
+
this._skillHighlighted = 0;
|
|
581
|
+
this._skillMenuOpened = true;
|
|
582
|
+
}
|
|
583
|
+
/** Insert `/name ` into the composer and focus it, ready for the message. */
|
|
584
|
+
_handleSkillSelect(skill) {
|
|
585
|
+
this._inputValue = `/${skill.name} `;
|
|
586
|
+
this._skillMenuOpened = false;
|
|
587
|
+
this._skillBrowseOpened = false;
|
|
588
|
+
this._inputEl?.focus();
|
|
589
|
+
}
|
|
590
|
+
/** Copy a message's text to the clipboard and briefly flash "Copied". */
|
|
591
|
+
_handleCopyMessage(content, index) {
|
|
592
|
+
navigator.clipboard.writeText(content).then(() => {
|
|
593
|
+
this._copiedIndex = index;
|
|
594
|
+
setTimeout(() => {
|
|
595
|
+
if (this._copiedIndex === index) {
|
|
596
|
+
this._copiedIndex = -1;
|
|
597
|
+
}
|
|
598
|
+
}, 2000);
|
|
599
|
+
}).catch(() => { });
|
|
600
|
+
}
|
|
601
|
+
_handleSkillBrowseOpen() {
|
|
602
|
+
this._skillMenuOpened = false;
|
|
603
|
+
this._skillBrowseQuery = '';
|
|
604
|
+
this._skillBrowseOpened = true;
|
|
605
|
+
}
|
|
606
|
+
_handleSkillBrowseClose() {
|
|
607
|
+
this._skillBrowseOpened = false;
|
|
608
|
+
}
|
|
609
|
+
_handleSkillBrowseSearch(e) {
|
|
610
|
+
this._skillBrowseQuery = e.target.value;
|
|
611
|
+
}
|
|
612
|
+
_handleSkillBrowseTabChange(e) {
|
|
613
|
+
this._skillBrowseSource = e.detail.activeTabId;
|
|
614
|
+
}
|
|
615
|
+
/** Skills for the browse dialog's active tab, filtered by its search box. */
|
|
616
|
+
_browseSkills() {
|
|
617
|
+
const query = this._skillBrowseQuery.toLowerCase();
|
|
618
|
+
return this.skills.filter((skill) => {
|
|
619
|
+
const source = skill.source || 'org';
|
|
620
|
+
if (source !== this._skillBrowseSource) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
return skill.name.toLowerCase().includes(query) ||
|
|
624
|
+
skill.label.toLowerCase().includes(query) ||
|
|
625
|
+
(skill.description || '').toLowerCase().includes(query);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
456
628
|
// --- Attachments ---
|
|
457
629
|
_openFilePicker() {
|
|
458
630
|
this._fileInputEl?.click();
|
|
@@ -810,6 +982,119 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
810
982
|
}
|
|
811
983
|
}
|
|
812
984
|
// --- Render ---
|
|
985
|
+
/** The `/` skill menu shown above the composer while typing a slash command. */
|
|
986
|
+
_renderSkillMenu() {
|
|
987
|
+
const skills = this._filteredSkills();
|
|
988
|
+
return html `
|
|
989
|
+
<div class="skill-menu">
|
|
990
|
+
<div class="skill-menu__actions">
|
|
991
|
+
<button
|
|
992
|
+
class=${classMap({
|
|
993
|
+
'skill-menu__action': true,
|
|
994
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 0,
|
|
995
|
+
})}
|
|
996
|
+
@click=${this._openFilePicker}
|
|
997
|
+
>
|
|
998
|
+
Attach Files
|
|
999
|
+
</button>
|
|
1000
|
+
<button
|
|
1001
|
+
class=${classMap({
|
|
1002
|
+
'skill-menu__action': true,
|
|
1003
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 1,
|
|
1004
|
+
})}
|
|
1005
|
+
@click=${this._handleSkillBrowseOpen}
|
|
1006
|
+
>
|
|
1007
|
+
Browse All Skills
|
|
1008
|
+
</button>
|
|
1009
|
+
</div>
|
|
1010
|
+
${skills.length ? html `
|
|
1011
|
+
<ul class="skill-menu__list">
|
|
1012
|
+
${skills.map((skill, index) => html `
|
|
1013
|
+
<li>
|
|
1014
|
+
<button
|
|
1015
|
+
class=${classMap({
|
|
1016
|
+
'skill-menu__item': true,
|
|
1017
|
+
'skill-menu__item--highlighted': index + 2 === this._skillHighlighted,
|
|
1018
|
+
})}
|
|
1019
|
+
@click=${() => this._handleSkillSelect(skill)}
|
|
1020
|
+
>
|
|
1021
|
+
<span class="skill-menu__name">${skill.label}</span>
|
|
1022
|
+
${skill.description ? html `
|
|
1023
|
+
<span class="skill-menu__desc">${skill.description}</span>
|
|
1024
|
+
` : nothing}
|
|
1025
|
+
</button>
|
|
1026
|
+
</li>
|
|
1027
|
+
`)}
|
|
1028
|
+
</ul>
|
|
1029
|
+
` : html `
|
|
1030
|
+
<div class="skill-menu__empty">No matching skills</div>
|
|
1031
|
+
`}
|
|
1032
|
+
</div>
|
|
1033
|
+
`;
|
|
1034
|
+
}
|
|
1035
|
+
/** The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards. */
|
|
1036
|
+
_renderSkillBrowse() {
|
|
1037
|
+
return html `
|
|
1038
|
+
<kr-dialog
|
|
1039
|
+
opened
|
|
1040
|
+
label="Browse Skills"
|
|
1041
|
+
width="720px"
|
|
1042
|
+
@close=${this._handleSkillBrowseClose}
|
|
1043
|
+
>
|
|
1044
|
+
<div class="skill-browse">
|
|
1045
|
+
<kr-text-field
|
|
1046
|
+
placeholder="Search skills..."
|
|
1047
|
+
.value=${this._skillBrowseQuery}
|
|
1048
|
+
@input=${this._handleSkillBrowseSearch}
|
|
1049
|
+
></kr-text-field>
|
|
1050
|
+
<kr-tab-group
|
|
1051
|
+
active-tab-id=${this._skillBrowseSource}
|
|
1052
|
+
@tab-change=${this._handleSkillBrowseTabChange}
|
|
1053
|
+
>
|
|
1054
|
+
<kr-tab id="org" label="Your Org">
|
|
1055
|
+
${this._renderSkillGrid()}
|
|
1056
|
+
</kr-tab>
|
|
1057
|
+
<kr-tab id="kodaris" label="Kodaris">
|
|
1058
|
+
${this._renderSkillGrid()}
|
|
1059
|
+
</kr-tab>
|
|
1060
|
+
</kr-tab-group>
|
|
1061
|
+
</div>
|
|
1062
|
+
</kr-dialog>
|
|
1063
|
+
`;
|
|
1064
|
+
}
|
|
1065
|
+
/** The card grid for the browse dialog's active tab. */
|
|
1066
|
+
_renderSkillGrid() {
|
|
1067
|
+
const skills = this._browseSkills();
|
|
1068
|
+
if (!skills.length) {
|
|
1069
|
+
return html `<div class="skill-browse__empty">No matching skills</div>`;
|
|
1070
|
+
}
|
|
1071
|
+
return html `
|
|
1072
|
+
<div class="skill-browse__grid">
|
|
1073
|
+
${skills.map((skill) => html `
|
|
1074
|
+
<button
|
|
1075
|
+
class="skill-card"
|
|
1076
|
+
@click=${() => this._handleSkillSelect(skill)}
|
|
1077
|
+
>
|
|
1078
|
+
<span class="skill-card__name">${skill.label}</span>
|
|
1079
|
+
${skill.description ? html `
|
|
1080
|
+
<span class="skill-card__desc">${skill.description}</span>
|
|
1081
|
+
` : nothing}
|
|
1082
|
+
</button>
|
|
1083
|
+
`)}
|
|
1084
|
+
</div>
|
|
1085
|
+
`;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Render a user message, highlighting a leading `/skill-name` token when it
|
|
1089
|
+
* matches a known skill (like the way Claude highlights slash commands).
|
|
1090
|
+
*/
|
|
1091
|
+
_renderUserContent(content) {
|
|
1092
|
+
const match = content.match(/^\/(\S+)(\s[\s\S]*)?$/);
|
|
1093
|
+
if (match && this.skills.some((skill) => skill.name === match[1])) {
|
|
1094
|
+
return html `<span class="message-skill">/${match[1]}</span>${match[2] || ''}`;
|
|
1095
|
+
}
|
|
1096
|
+
return content;
|
|
1097
|
+
}
|
|
813
1098
|
render() {
|
|
814
1099
|
return html `
|
|
815
1100
|
${(this.hideToggle || this.variant === 'inline') ? nothing : html `
|
|
@@ -966,7 +1251,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
966
1251
|
<span class="empty-text">What can I help you with?</span>
|
|
967
1252
|
</div>
|
|
968
1253
|
` : nothing}
|
|
969
|
-
${this.messages.map((msg) => html `
|
|
1254
|
+
${this.messages.map((msg, index) => html `
|
|
970
1255
|
<div class=${classMap({
|
|
971
1256
|
'message': true,
|
|
972
1257
|
'message--user': msg.type === 'user',
|
|
@@ -998,7 +1283,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
998
1283
|
${msg.content ? html `
|
|
999
1284
|
<div class="message-content">
|
|
1000
1285
|
${msg.type === 'user'
|
|
1001
|
-
? msg.content
|
|
1286
|
+
? this._renderUserContent(msg.content)
|
|
1002
1287
|
: unsafeHTML(marked.parse(msg.content, { breaks: true }))}
|
|
1003
1288
|
</div>
|
|
1004
1289
|
` : nothing}
|
|
@@ -1009,6 +1294,46 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1009
1294
|
<span></span>
|
|
1010
1295
|
</div>
|
|
1011
1296
|
` : nothing}
|
|
1297
|
+
${msg.content ? html `
|
|
1298
|
+
<button
|
|
1299
|
+
class="message-copy"
|
|
1300
|
+
title="Copy"
|
|
1301
|
+
@click=${() => this._handleCopyMessage(msg.content, index)}
|
|
1302
|
+
>
|
|
1303
|
+
${this._copiedIndex === index ? html `
|
|
1304
|
+
<svg
|
|
1305
|
+
viewBox="0 0 24 24"
|
|
1306
|
+
fill="none"
|
|
1307
|
+
stroke="currentColor"
|
|
1308
|
+
stroke-width="1.5"
|
|
1309
|
+
stroke-linecap="round"
|
|
1310
|
+
stroke-linejoin="round"
|
|
1311
|
+
>
|
|
1312
|
+
<path d="M20 6 9 17l-5-5" />
|
|
1313
|
+
</svg>
|
|
1314
|
+
Copied
|
|
1315
|
+
` : html `
|
|
1316
|
+
<svg
|
|
1317
|
+
viewBox="0 0 24 24"
|
|
1318
|
+
fill="none"
|
|
1319
|
+
stroke="currentColor"
|
|
1320
|
+
stroke-width="1.5"
|
|
1321
|
+
stroke-linecap="round"
|
|
1322
|
+
stroke-linejoin="round"
|
|
1323
|
+
>
|
|
1324
|
+
<rect
|
|
1325
|
+
x="9"
|
|
1326
|
+
y="9"
|
|
1327
|
+
width="13"
|
|
1328
|
+
height="13"
|
|
1329
|
+
rx="2"
|
|
1330
|
+
/>
|
|
1331
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
1332
|
+
</svg>
|
|
1333
|
+
Copy
|
|
1334
|
+
`}
|
|
1335
|
+
</button>
|
|
1336
|
+
` : nothing}
|
|
1012
1337
|
</div>
|
|
1013
1338
|
`)}
|
|
1014
1339
|
</div>
|
|
@@ -1073,11 +1398,11 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1073
1398
|
hidden
|
|
1074
1399
|
@change=${this._handleFileInputChange}
|
|
1075
1400
|
/>
|
|
1401
|
+
${this._skillMenuOpened ? this._renderSkillMenu() : nothing}
|
|
1076
1402
|
<div class="input-wrapper">
|
|
1077
1403
|
<button
|
|
1078
1404
|
class="input-plus"
|
|
1079
|
-
|
|
1080
|
-
@click=${this._openFilePicker}
|
|
1405
|
+
@click=${this._handlePlusClick}
|
|
1081
1406
|
>+</button>
|
|
1082
1407
|
<textarea
|
|
1083
1408
|
class="input"
|
|
@@ -1134,6 +1459,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1134
1459
|
`}
|
|
1135
1460
|
</div>
|
|
1136
1461
|
</div>
|
|
1462
|
+
${this._skillBrowseOpened ? this._renderSkillBrowse() : nothing}
|
|
1137
1463
|
</div>
|
|
1138
1464
|
`;
|
|
1139
1465
|
}
|
|
@@ -1373,6 +1699,7 @@ KRChatbot.styles = css `
|
|
|
1373
1699
|
}
|
|
1374
1700
|
|
|
1375
1701
|
.message {
|
|
1702
|
+
position: relative;
|
|
1376
1703
|
display: flex;
|
|
1377
1704
|
flex-direction: column;
|
|
1378
1705
|
max-width: 100%;
|
|
@@ -1417,6 +1744,58 @@ KRChatbot.styles = css `
|
|
|
1417
1744
|
font-size: 14px;
|
|
1418
1745
|
}
|
|
1419
1746
|
|
|
1747
|
+
/* Highlighted leading /skill token in a sent user message. Monospace + a
|
|
1748
|
+
stronger color makes it read as a distinct command without a pill. */
|
|
1749
|
+
.message-skill {
|
|
1750
|
+
font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
|
|
1751
|
+
font-weight: 600;
|
|
1752
|
+
color: #b45309;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/* Per-message copy button. Kept in flow with a fixed height that is always
|
|
1756
|
+
reserved (via a negative bottom margin that cancels it) so message spacing
|
|
1757
|
+
is identical whether or not it's shown; only its opacity toggles on hover. */
|
|
1758
|
+
.message-copy {
|
|
1759
|
+
align-self: flex-start;
|
|
1760
|
+
margin-top: 4px;
|
|
1761
|
+
margin-bottom: -27px;
|
|
1762
|
+
display: inline-flex;
|
|
1763
|
+
align-items: center;
|
|
1764
|
+
gap: 4px;
|
|
1765
|
+
height: 23px;
|
|
1766
|
+
padding: 4px 8px;
|
|
1767
|
+
background: none;
|
|
1768
|
+
border: none;
|
|
1769
|
+
border-radius: 6px;
|
|
1770
|
+
font: inherit;
|
|
1771
|
+
font-size: 12px;
|
|
1772
|
+
color: #64668b;
|
|
1773
|
+
cursor: pointer;
|
|
1774
|
+
opacity: 0;
|
|
1775
|
+
pointer-events: none;
|
|
1776
|
+
transition: opacity 0.15s, background-color 0.15s, color 0.15s;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
.message--user .message-copy {
|
|
1780
|
+
align-self: flex-end;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
.message:hover .message-copy,
|
|
1784
|
+
.message-copy:focus-visible {
|
|
1785
|
+
opacity: 1;
|
|
1786
|
+
pointer-events: auto;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
.message-copy:hover {
|
|
1790
|
+
background: rgba(0, 0, 0, 0.06);
|
|
1791
|
+
color: #1a1a2e;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
.message-copy svg {
|
|
1795
|
+
width: 14px;
|
|
1796
|
+
height: 14px;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1420
1799
|
.message--assistant .message-content {
|
|
1421
1800
|
background: transparent;
|
|
1422
1801
|
color: #000000;
|
|
@@ -1752,6 +2131,176 @@ KRChatbot.styles = css `
|
|
|
1752
2131
|
display: block;
|
|
1753
2132
|
}
|
|
1754
2133
|
|
|
2134
|
+
/* Skill menu (opened by typing '/'). Fixed + JS-positioned above the
|
|
2135
|
+
composer, following the krubble select-field dropdown pattern. */
|
|
2136
|
+
.skill-menu {
|
|
2137
|
+
position: fixed;
|
|
2138
|
+
z-index: 10000;
|
|
2139
|
+
background: white;
|
|
2140
|
+
border: 1px solid #9ba7b6;
|
|
2141
|
+
border-radius: 8px;
|
|
2142
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
2143
|
+
overflow: hidden;
|
|
2144
|
+
display: flex;
|
|
2145
|
+
flex-direction: column;
|
|
2146
|
+
/* max-height is set dynamically to the space above the composer. */
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/* Floating mode is a compact card — use a lighter shadow and softer border. */
|
|
2150
|
+
:host(:not([variant='inline'])) .skill-menu {
|
|
2151
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
2152
|
+
border-color: #b0b0b0;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
.skill-menu__list {
|
|
2156
|
+
list-style: none;
|
|
2157
|
+
margin: 0;
|
|
2158
|
+
padding: 8px 0;
|
|
2159
|
+
overflow-y: auto;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
.skill-menu__item {
|
|
2163
|
+
display: flex;
|
|
2164
|
+
flex-direction: column;
|
|
2165
|
+
gap: 2px;
|
|
2166
|
+
width: 100%;
|
|
2167
|
+
padding: 8px 16px;
|
|
2168
|
+
background: none;
|
|
2169
|
+
border: none;
|
|
2170
|
+
text-align: left;
|
|
2171
|
+
cursor: pointer;
|
|
2172
|
+
font: inherit;
|
|
2173
|
+
color: #000000;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
/* Floating mode is a compact card — tighten name/description spacing. */
|
|
2178
|
+
:host(:not([variant='inline'])) .skill-menu__item {
|
|
2179
|
+
gap: 1px;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
.skill-menu__name {
|
|
2183
|
+
font-weight: 500;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/* Floating mode is a compact card — shrink the skill name a touch. */
|
|
2187
|
+
:host(:not([variant='inline'])) .skill-menu__name {
|
|
2188
|
+
font-size: 13px;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
.skill-menu__desc {
|
|
2192
|
+
font-size: 12px;
|
|
2193
|
+
color: #000000;
|
|
2194
|
+
overflow: hidden;
|
|
2195
|
+
text-overflow: ellipsis;
|
|
2196
|
+
white-space: nowrap;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
.skill-menu__empty {
|
|
2200
|
+
padding: 16px;
|
|
2201
|
+
text-align: center;
|
|
2202
|
+
color: #000000;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
.skill-menu__actions {
|
|
2206
|
+
display: flex;
|
|
2207
|
+
flex-direction: column;
|
|
2208
|
+
padding: 8px 0;
|
|
2209
|
+
border-bottom: 1px solid #eee;
|
|
2210
|
+
flex-shrink: 0;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.skill-menu__action {
|
|
2214
|
+
width: 100%;
|
|
2215
|
+
padding: 6px 16px;
|
|
2216
|
+
background: transparent;
|
|
2217
|
+
border: none;
|
|
2218
|
+
text-align: left;
|
|
2219
|
+
cursor: pointer;
|
|
2220
|
+
font: inherit;
|
|
2221
|
+
font-size: 13px;
|
|
2222
|
+
color: var(--kr-chatbot-primary);
|
|
2223
|
+
font-weight: 500;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/* Highlight (keyboard) and hover (mouse) are independent — either shades the
|
|
2227
|
+
row. Declared after the base rules so it wins the cascade for both the
|
|
2228
|
+
action buttons and the skill items. */
|
|
2229
|
+
.skill-menu__action.skill-menu__item--highlighted,
|
|
2230
|
+
.skill-menu__item.skill-menu__item--highlighted,
|
|
2231
|
+
.skill-menu__action:hover,
|
|
2232
|
+
.skill-menu__item:hover {
|
|
2233
|
+
background: #f3f4f6;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
/* Browse-all dialog body. Owns the height so the dialog is the same size on
|
|
2237
|
+
every tab; the search is fixed and the card grid flexes/scrolls to fill. */
|
|
2238
|
+
.skill-browse {
|
|
2239
|
+
display: flex;
|
|
2240
|
+
flex-direction: column;
|
|
2241
|
+
height: 70vh;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
.skill-browse kr-text-field {
|
|
2245
|
+
display: block;
|
|
2246
|
+
margin: 20px 20px 16px;
|
|
2247
|
+
flex-shrink: 0;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
.skill-browse kr-tab-group {
|
|
2251
|
+
flex: 1;
|
|
2252
|
+
min-height: 0;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
/* The grid is the scroller and spans the full width, so its scrollbar sits
|
|
2256
|
+
flush against the dialog edge; padding-right insets the cards from it. */
|
|
2257
|
+
.skill-browse__grid {
|
|
2258
|
+
height: 100%;
|
|
2259
|
+
overflow-y: auto;
|
|
2260
|
+
display: grid;
|
|
2261
|
+
grid-template-columns: repeat(3, 1fr);
|
|
2262
|
+
grid-auto-rows: min-content;
|
|
2263
|
+
align-content: start;
|
|
2264
|
+
gap: 12px;
|
|
2265
|
+
padding: 16px 20px 20px;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
.skill-card {
|
|
2269
|
+
display: flex;
|
|
2270
|
+
flex-direction: column;
|
|
2271
|
+
gap: 6px;
|
|
2272
|
+
padding: 14px;
|
|
2273
|
+
background: white;
|
|
2274
|
+
border: 1px solid #e5e7eb;
|
|
2275
|
+
border-radius: 10px;
|
|
2276
|
+
text-align: left;
|
|
2277
|
+
cursor: pointer;
|
|
2278
|
+
font: inherit;
|
|
2279
|
+
color: #000000;
|
|
2280
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
.skill-card:hover {
|
|
2284
|
+
border-color: var(--kr-chatbot-primary);
|
|
2285
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
.skill-card__name {
|
|
2289
|
+
font-weight: 600;
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
.skill-card__desc {
|
|
2293
|
+
font-size: 12px;
|
|
2294
|
+
color: #000000;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
.skill-browse__empty {
|
|
2298
|
+
height: 100%;
|
|
2299
|
+
padding: 32px;
|
|
2300
|
+
text-align: center;
|
|
2301
|
+
color: #000000;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
1755
2304
|
/* Composer attachment chips (pre-send) */
|
|
1756
2305
|
.composer-attachments {
|
|
1757
2306
|
display: flex;
|
|
@@ -1851,6 +2400,9 @@ __decorate([
|
|
|
1851
2400
|
__decorate([
|
|
1852
2401
|
property({ type: String })
|
|
1853
2402
|
], KRChatbot.prototype, "placeholder", void 0);
|
|
2403
|
+
__decorate([
|
|
2404
|
+
property({ attribute: false })
|
|
2405
|
+
], KRChatbot.prototype, "skills", void 0);
|
|
1854
2406
|
__decorate([
|
|
1855
2407
|
property({
|
|
1856
2408
|
type: Boolean,
|
|
@@ -1885,6 +2437,27 @@ __decorate([
|
|
|
1885
2437
|
__decorate([
|
|
1886
2438
|
state()
|
|
1887
2439
|
], KRChatbot.prototype, "_isDraggingFile", void 0);
|
|
2440
|
+
__decorate([
|
|
2441
|
+
state()
|
|
2442
|
+
], KRChatbot.prototype, "_skillMenuOpened", void 0);
|
|
2443
|
+
__decorate([
|
|
2444
|
+
state()
|
|
2445
|
+
], KRChatbot.prototype, "_skillQuery", void 0);
|
|
2446
|
+
__decorate([
|
|
2447
|
+
state()
|
|
2448
|
+
], KRChatbot.prototype, "_skillHighlighted", void 0);
|
|
2449
|
+
__decorate([
|
|
2450
|
+
state()
|
|
2451
|
+
], KRChatbot.prototype, "_copiedIndex", void 0);
|
|
2452
|
+
__decorate([
|
|
2453
|
+
state()
|
|
2454
|
+
], KRChatbot.prototype, "_skillBrowseOpened", void 0);
|
|
2455
|
+
__decorate([
|
|
2456
|
+
state()
|
|
2457
|
+
], KRChatbot.prototype, "_skillBrowseSource", void 0);
|
|
2458
|
+
__decorate([
|
|
2459
|
+
state()
|
|
2460
|
+
], KRChatbot.prototype, "_skillBrowseQuery", void 0);
|
|
1888
2461
|
__decorate([
|
|
1889
2462
|
query('.panel')
|
|
1890
2463
|
], KRChatbot.prototype, "_panelEl", void 0);
|
|
@@ -1897,6 +2470,12 @@ __decorate([
|
|
|
1897
2470
|
__decorate([
|
|
1898
2471
|
query('.file-input')
|
|
1899
2472
|
], KRChatbot.prototype, "_fileInputEl", void 0);
|
|
2473
|
+
__decorate([
|
|
2474
|
+
query('.input-wrapper')
|
|
2475
|
+
], KRChatbot.prototype, "_inputWrapperEl", void 0);
|
|
2476
|
+
__decorate([
|
|
2477
|
+
query('.skill-menu')
|
|
2478
|
+
], KRChatbot.prototype, "_skillMenuEl", void 0);
|
|
1900
2479
|
KRChatbot = __decorate([
|
|
1901
2480
|
customElement('kr-chatbot')
|
|
1902
2481
|
], KRChatbot);
|