@kodaris/krubble-app-components 1.0.93 → 1.0.95
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 +45 -10
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +311 -87
- package/dist/chatbot.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/krubble-app.bundled.js +381 -193
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +195 -63
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +381 -193
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +188 -56
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/dist/scaffold.d.ts +4 -25
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +52 -76
- package/dist/scaffold.js.map +1 -1
- package/dist/subbar.d.ts +0 -15
- package/dist/subbar.d.ts.map +1 -1
- package/dist/subbar.js +18 -30
- package/dist/subbar.js.map +1 -1
- package/package.json +1 -1
|
@@ -674,14 +674,6 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
674
674
|
handleNavScroll(e) {
|
|
675
675
|
this.isNavScrolled = e.target.scrollTop > 0;
|
|
676
676
|
}
|
|
677
|
-
/**
|
|
678
|
-
* Toggles the expanded/collapsed state of a nav group with animation.
|
|
679
|
-
* Uses the Web Animations API to animate height transitions.
|
|
680
|
-
* When collapsing, the `nav-group--expanded` class is removed after animation completes.
|
|
681
|
-
* When expanding, the class is added immediately before animation starts.
|
|
682
|
-
*
|
|
683
|
-
* @param itemId - The unique identifier of the nav group to toggle
|
|
684
|
-
*/
|
|
685
677
|
/**
|
|
686
678
|
* Expand a nav group by id. Public so consumers can keep a group open after
|
|
687
679
|
* adding it dynamically — the scaffold only auto-expands groups once at startup,
|
|
@@ -695,37 +687,6 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
695
687
|
this.navItemsExpanded.add(groupId);
|
|
696
688
|
this.requestUpdate();
|
|
697
689
|
}
|
|
698
|
-
toggleNavItem(itemId) {
|
|
699
|
-
const navItem = this.shadowRoot?.querySelector(`.nav-item[data-id="${itemId}"]`);
|
|
700
|
-
const groupEl = navItem?.nextElementSibling;
|
|
701
|
-
if (!groupEl) {
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (this.navItemsExpanded.has(itemId)) {
|
|
705
|
-
this.navItemsExpanded.delete(itemId);
|
|
706
|
-
groupEl.animate([
|
|
707
|
-
{ height: `${groupEl.scrollHeight}px` },
|
|
708
|
-
{ height: '0px' }
|
|
709
|
-
], {
|
|
710
|
-
duration: 300,
|
|
711
|
-
easing: 'ease-in-out'
|
|
712
|
-
}).onfinish = () => {
|
|
713
|
-
groupEl.classList.remove('nav-group--expanded');
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
this.navItemsExpanded.add(itemId);
|
|
718
|
-
groupEl.classList.add('nav-group--expanded');
|
|
719
|
-
groupEl.animate([
|
|
720
|
-
{ height: '0px' },
|
|
721
|
-
{ height: `${groupEl.scrollHeight}px` }
|
|
722
|
-
], {
|
|
723
|
-
duration: 300,
|
|
724
|
-
easing: 'ease-in-out'
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
this.requestUpdate();
|
|
728
|
-
}
|
|
729
690
|
/**
|
|
730
691
|
* Handles click events on navigation items.
|
|
731
692
|
*
|
|
@@ -746,7 +707,38 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
746
707
|
*/
|
|
747
708
|
handleNavItemClick(e, item) {
|
|
748
709
|
if (item.type === 'group') {
|
|
749
|
-
|
|
710
|
+
// Animate the group's height with the Web Animations API. Collapsing removes
|
|
711
|
+
// `nav-group--expanded` only once the animation finishes; expanding adds it
|
|
712
|
+
// up front so there's a height to animate from.
|
|
713
|
+
const navItem = this.shadowRoot?.querySelector(`.nav-item[data-id="${item.id}"]`);
|
|
714
|
+
const groupEl = navItem?.nextElementSibling;
|
|
715
|
+
if (!groupEl) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (this.navItemsExpanded.has(item.id)) {
|
|
719
|
+
this.navItemsExpanded.delete(item.id);
|
|
720
|
+
groupEl.animate([
|
|
721
|
+
{ height: `${groupEl.scrollHeight}px` },
|
|
722
|
+
{ height: '0px' }
|
|
723
|
+
], {
|
|
724
|
+
duration: 300,
|
|
725
|
+
easing: 'ease-in-out'
|
|
726
|
+
}).onfinish = () => {
|
|
727
|
+
groupEl.classList.remove('nav-group--expanded');
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
this.navItemsExpanded.add(item.id);
|
|
732
|
+
groupEl.classList.add('nav-group--expanded');
|
|
733
|
+
groupEl.animate([
|
|
734
|
+
{ height: '0px' },
|
|
735
|
+
{ height: `${groupEl.scrollHeight}px` }
|
|
736
|
+
], {
|
|
737
|
+
duration: 300,
|
|
738
|
+
easing: 'ease-in-out'
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
this.requestUpdate();
|
|
750
742
|
}
|
|
751
743
|
else {
|
|
752
744
|
// Allow Ctrl/Cmd+click and middle-click to open in new tab without SPA navigation
|
|
@@ -805,18 +797,15 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
805
797
|
// =========================================================================
|
|
806
798
|
// Navigation Customization
|
|
807
799
|
// =========================================================================
|
|
808
|
-
|
|
809
|
-
this.isEditing = true;
|
|
810
|
-
}
|
|
811
|
-
cancelEditing() {
|
|
800
|
+
handleEditCancel() {
|
|
812
801
|
this.isEditing = false;
|
|
813
802
|
// Reload to revert changes
|
|
814
803
|
this.loadPref();
|
|
815
804
|
}
|
|
816
|
-
|
|
805
|
+
handlePrefReset() {
|
|
817
806
|
this.pref.nav = {};
|
|
818
807
|
}
|
|
819
|
-
|
|
808
|
+
handlePrefSave() {
|
|
820
809
|
if (this.userType === 'customer') {
|
|
821
810
|
return;
|
|
822
811
|
}
|
|
@@ -1116,9 +1105,17 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
1116
1105
|
else {
|
|
1117
1106
|
// Middle third - drop into the group (as a child)
|
|
1118
1107
|
position = 'center';
|
|
1119
|
-
// Auto-expand collapsed groups after a delay
|
|
1108
|
+
// Auto-expand collapsed groups after a delay. The 500ms wait keeps groups
|
|
1109
|
+
// from expanding too eagerly when the user is just passing over them, and
|
|
1110
|
+
// the check inside confirms they're still on this group's center.
|
|
1120
1111
|
if (!this.navItemsExpanded.has(item.id) && !this.navItemDragExpandTimeout) {
|
|
1121
|
-
this.
|
|
1112
|
+
this.navItemDragExpandTimeout = window.setTimeout(() => {
|
|
1113
|
+
if (this.navItemDropTargetId === item.id && this.navItemDropPosition === 'center') {
|
|
1114
|
+
this.navItemsExpanded.add(item.id);
|
|
1115
|
+
this.requestUpdate();
|
|
1116
|
+
}
|
|
1117
|
+
this.navItemDragExpandTimeout = null;
|
|
1118
|
+
}, 500);
|
|
1122
1119
|
}
|
|
1123
1120
|
}
|
|
1124
1121
|
}
|
|
@@ -1173,24 +1170,6 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
1173
1170
|
this.navItemDragExpandTimeout = null;
|
|
1174
1171
|
}
|
|
1175
1172
|
}
|
|
1176
|
-
/**
|
|
1177
|
-
* Auto-expands a collapsed nav group after a delay while dragging over it.
|
|
1178
|
-
*
|
|
1179
|
-
* Uses a 500ms timeout to prevent groups from expanding too eagerly when the
|
|
1180
|
-
* user is just passing over them. Only expands if the user is still hovering
|
|
1181
|
-
* over the group's center when the timeout fires.
|
|
1182
|
-
*
|
|
1183
|
-
* @param itemId - The ID of the group to expand
|
|
1184
|
-
*/
|
|
1185
|
-
expandNavGroupOnDrag(itemId) {
|
|
1186
|
-
this.navItemDragExpandTimeout = window.setTimeout(() => {
|
|
1187
|
-
if (this.navItemDropTargetId === itemId && this.navItemDropPosition === 'center') {
|
|
1188
|
-
this.navItemsExpanded.add(itemId);
|
|
1189
|
-
this.requestUpdate();
|
|
1190
|
-
}
|
|
1191
|
-
this.navItemDragExpandTimeout = null;
|
|
1192
|
-
}, 500);
|
|
1193
|
-
}
|
|
1194
1173
|
/**
|
|
1195
1174
|
* Executes the drop operation after a nav item drag completes.
|
|
1196
1175
|
*
|
|
@@ -1278,15 +1257,12 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
1278
1257
|
// =========================================================================
|
|
1279
1258
|
// User Menu
|
|
1280
1259
|
// =========================================================================
|
|
1281
|
-
|
|
1260
|
+
handleUserMenuToggle() {
|
|
1282
1261
|
this.isUserMenuOpen = !this.isUserMenuOpen;
|
|
1283
1262
|
}
|
|
1284
|
-
closeUserMenu() {
|
|
1285
|
-
this.isUserMenuOpen = false;
|
|
1286
|
-
}
|
|
1287
1263
|
handleCustomize() {
|
|
1288
|
-
this.
|
|
1289
|
-
this.
|
|
1264
|
+
this.isUserMenuOpen = false;
|
|
1265
|
+
this.isEditing = true;
|
|
1290
1266
|
}
|
|
1291
1267
|
// =========================================================================
|
|
1292
1268
|
// Rendering
|
|
@@ -1311,7 +1287,7 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
1311
1287
|
</button>
|
|
1312
1288
|
</div>
|
|
1313
1289
|
` : A}
|
|
1314
|
-
<div class="user" @click=${this.
|
|
1290
|
+
<div class="user" @click=${this.handleUserMenuToggle}>
|
|
1315
1291
|
<div class="user__avatar">
|
|
1316
1292
|
${this.user.avatar
|
|
1317
1293
|
? b `<img src=${this.user.avatar} alt=${this.user.name} />`
|
|
@@ -1329,14 +1305,14 @@ let KRScaffold = class KRScaffold extends i$3 {
|
|
|
1329
1305
|
renderEditFooter() {
|
|
1330
1306
|
return b `
|
|
1331
1307
|
<div class="edit-actions">
|
|
1332
|
-
<button class="edit-actions__btn edit-actions__btn--primary" @click=${this.
|
|
1308
|
+
<button class="edit-actions__btn edit-actions__btn--primary" @click=${this.handlePrefSave}>
|
|
1333
1309
|
Save
|
|
1334
1310
|
</button>
|
|
1335
1311
|
<div class="edit-actions__secondary">
|
|
1336
|
-
<button class="edit-actions__btn" @click=${this.
|
|
1312
|
+
<button class="edit-actions__btn" @click=${this.handlePrefReset}>
|
|
1337
1313
|
Reset
|
|
1338
1314
|
</button>
|
|
1339
|
-
<button class="edit-actions__btn" @click=${this.
|
|
1315
|
+
<button class="edit-actions__btn" @click=${this.handleEditCancel}>
|
|
1340
1316
|
Cancel
|
|
1341
1317
|
</button>
|
|
1342
1318
|
</div>
|
|
@@ -2735,35 +2711,6 @@ let KRSubbar = class KRSubbar extends i$3 {
|
|
|
2735
2711
|
composed: true
|
|
2736
2712
|
}));
|
|
2737
2713
|
}
|
|
2738
|
-
/**
|
|
2739
|
-
* Resolves a breadcrumb URL by prepending the base href.
|
|
2740
|
-
*
|
|
2741
|
-
* This method is used for setting href attributes on anchor elements
|
|
2742
|
-
* (so Cmd/Ctrl+click opens correct URL in new tab).
|
|
2743
|
-
*
|
|
2744
|
-
* The original crumb.url is kept intact (without base href) and passed to Angular
|
|
2745
|
-
* via the breadcrumb-click event. Angular Router expects paths relative to base,
|
|
2746
|
-
* so it handles the base href internally.
|
|
2747
|
-
*
|
|
2748
|
-
* Example with base href "/operations/":
|
|
2749
|
-
* - crumb.url = "/settings" (used for Angular routing)
|
|
2750
|
-
* - resolveUrl(crumb) = "/operations/settings" (used for href)
|
|
2751
|
-
*/
|
|
2752
|
-
resolveUrl(crumb) {
|
|
2753
|
-
if (!crumb.url) {
|
|
2754
|
-
return '#';
|
|
2755
|
-
}
|
|
2756
|
-
// Remove leading slash from url if present, then combine with base
|
|
2757
|
-
let path = crumb.url;
|
|
2758
|
-
if (crumb.url.startsWith('/')) {
|
|
2759
|
-
path = crumb.url.slice(1);
|
|
2760
|
-
}
|
|
2761
|
-
const baseHref = document.querySelector('base')?.getAttribute('href') || '/';
|
|
2762
|
-
if (baseHref.endsWith('/')) {
|
|
2763
|
-
return baseHref + path;
|
|
2764
|
-
}
|
|
2765
|
-
return baseHref + '/' + path;
|
|
2766
|
-
}
|
|
2767
2714
|
/**
|
|
2768
2715
|
* Handles click on a breadcrumb link.
|
|
2769
2716
|
* Dispatches a cancelable event so SPA routers can intercept.
|
|
@@ -2825,6 +2772,23 @@ let KRSubbar = class KRSubbar extends i$3 {
|
|
|
2825
2772
|
<div class="breadcrumbs">
|
|
2826
2773
|
${this.breadcrumbs.map((crumb, index) => {
|
|
2827
2774
|
const isCurrent = index === this.breadcrumbs.length - 1;
|
|
2775
|
+
// The href gets the base href prepended so Cmd/Ctrl+click opens the right
|
|
2776
|
+
// URL in a new tab. crumb.url itself is left intact (base-relative) and
|
|
2777
|
+
// passed to Angular via breadcrumb-click — its Router applies the base
|
|
2778
|
+
// itself. e.g. base "/operations/" + crumb.url "/settings" ->
|
|
2779
|
+
// "/operations/settings".
|
|
2780
|
+
const baseHref = document.querySelector('base')?.getAttribute('href') || '/';
|
|
2781
|
+
let href = '#';
|
|
2782
|
+
if (crumb.url) {
|
|
2783
|
+
let path = crumb.url;
|
|
2784
|
+
if (crumb.url.startsWith('/')) {
|
|
2785
|
+
path = crumb.url.slice(1);
|
|
2786
|
+
}
|
|
2787
|
+
href = baseHref + path;
|
|
2788
|
+
if (!baseHref.endsWith('/')) {
|
|
2789
|
+
href = baseHref + '/' + path;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2828
2792
|
return b `
|
|
2829
2793
|
${index > 0 ? b `<span class="breadcrumb-separator">/</span>` : ''}
|
|
2830
2794
|
<a
|
|
@@ -2832,7 +2796,7 @@ let KRSubbar = class KRSubbar extends i$3 {
|
|
|
2832
2796
|
'breadcrumb': true,
|
|
2833
2797
|
'breadcrumb--current': isCurrent
|
|
2834
2798
|
})}
|
|
2835
|
-
href=${
|
|
2799
|
+
href=${href}
|
|
2836
2800
|
@click=${(e) => this._handleBreadcrumbClick(e, crumb, isCurrent)}
|
|
2837
2801
|
>${crumb.label}</a>
|
|
2838
2802
|
`;
|
|
@@ -5564,8 +5528,18 @@ var __decorate = (undefined && undefined.__decorate) || function (decorators, ta
|
|
|
5564
5528
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5565
5529
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
5566
5530
|
};
|
|
5567
|
-
/** Image
|
|
5568
|
-
const
|
|
5531
|
+
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
5532
|
+
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
5533
|
+
/**
|
|
5534
|
+
* File extensions the picker accepts. Mirrors what the backend can read: the
|
|
5535
|
+
* document/spreadsheet family Bedrock ingests, the plain-text family read as
|
|
5536
|
+
* fallback, and the image formats. This is a UX convenience — the backend
|
|
5537
|
+
* accepts anything and just tells the model when a type can't be read.
|
|
5538
|
+
*/
|
|
5539
|
+
const ACCEPTED_FILE_EXTENSIONS = [
|
|
5540
|
+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'html', 'md', 'txt', 'json', 'xml',
|
|
5541
|
+
...IMAGE_EXTENSIONS,
|
|
5542
|
+
];
|
|
5569
5543
|
/**
|
|
5570
5544
|
* AI chatbot component with SSE streaming.
|
|
5571
5545
|
*
|
|
@@ -5630,6 +5604,13 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
5630
5604
|
this._dragDepth = 0;
|
|
5631
5605
|
/** Monotonic id source for pending attachments. */
|
|
5632
5606
|
this._pendingSeq = 0;
|
|
5607
|
+
/**
|
|
5608
|
+
* The `aiAnalyze` mode for the next picked files — set when the attach action
|
|
5609
|
+
* opens the picker (true = "Attach Files", false = "Attach Files for
|
|
5610
|
+
* Reference"), read by the file-input change handler. Not reactive: it's read
|
|
5611
|
+
* synchronously in the change handler, never rendered.
|
|
5612
|
+
*/
|
|
5613
|
+
this._pendingAttachAnalyze = true;
|
|
5633
5614
|
/**
|
|
5634
5615
|
* Request-id guards for skill searches — bumped on each search so out-of-order
|
|
5635
5616
|
* responses are dropped (mirrors the kr-combo-box `_requestId` pattern). Org and
|
|
@@ -5746,7 +5727,7 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
5746
5727
|
this._debugDrawerWidth = 430;
|
|
5747
5728
|
/** True while the user is dragging the debug drawer's resize handle. */
|
|
5748
5729
|
this._isDebugResizing = false;
|
|
5749
|
-
/**
|
|
5730
|
+
/** Files staged in the composer, before the message is sent. */
|
|
5750
5731
|
this._pendingConversationFiles = [];
|
|
5751
5732
|
/** True while a file is being dragged over the panel (shows the drop overlay). */
|
|
5752
5733
|
this._isDraggingFile = false;
|
|
@@ -5845,7 +5826,9 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
5845
5826
|
this._isResizing = false;
|
|
5846
5827
|
this._panelEl.classList.remove('panel--resizing');
|
|
5847
5828
|
}
|
|
5848
|
-
this.
|
|
5829
|
+
if (this._overlay) {
|
|
5830
|
+
this._overlay.style.display = 'none';
|
|
5831
|
+
}
|
|
5849
5832
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
|
5850
5833
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
5851
5834
|
this._saveState();
|
|
@@ -5944,7 +5927,11 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
5944
5927
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
|
5945
5928
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
5946
5929
|
// Release any object URLs still held by staged attachments.
|
|
5947
|
-
this._pendingConversationFiles.forEach((p) =>
|
|
5930
|
+
this._pendingConversationFiles.forEach((p) => {
|
|
5931
|
+
if (p.previewUrl) {
|
|
5932
|
+
URL.revokeObjectURL(p.previewUrl);
|
|
5933
|
+
}
|
|
5934
|
+
});
|
|
5948
5935
|
}
|
|
5949
5936
|
updated(changed) {
|
|
5950
5937
|
if (changed.has('messages')) {
|
|
@@ -6049,11 +6036,6 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6049
6036
|
}
|
|
6050
6037
|
this._overlay.style.display = 'block';
|
|
6051
6038
|
}
|
|
6052
|
-
_hideOverlay() {
|
|
6053
|
-
if (this._overlay) {
|
|
6054
|
-
this._overlay.style.display = 'none';
|
|
6055
|
-
}
|
|
6056
|
-
}
|
|
6057
6039
|
_handleHeaderMouseDown(e) {
|
|
6058
6040
|
// Inline mode fills its parent — dragging the panel doesn't apply.
|
|
6059
6041
|
if (this.variant === 'inline') {
|
|
@@ -6223,13 +6205,14 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6223
6205
|
}
|
|
6224
6206
|
_handleKeyDown(e) {
|
|
6225
6207
|
// When the skill menu is open, arrow keys / Enter / Escape drive it. The top
|
|
6226
|
-
// actions (Attach Files, Browse All Skills, plus
|
|
6227
|
-
// when enabled) come first, then the filtered
|
|
6208
|
+
// actions (Attach Files, Attach Files for Reference, Browse All Skills, plus
|
|
6209
|
+
// the two debug test triggers when enabled) come first, then the filtered
|
|
6210
|
+
// skills — all one navigable list.
|
|
6228
6211
|
if (this._skillMenuOpened) {
|
|
6229
6212
|
const debugActions = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
6230
|
-
let actionCount =
|
|
6213
|
+
let actionCount = 3;
|
|
6231
6214
|
if (debugActions) {
|
|
6232
|
-
actionCount =
|
|
6215
|
+
actionCount = 5;
|
|
6233
6216
|
}
|
|
6234
6217
|
const count = actionCount + this._invocableSkills.length;
|
|
6235
6218
|
if (e.key === 'ArrowDown') {
|
|
@@ -6261,19 +6244,23 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6261
6244
|
/** Run the skill-menu item at the given flat index (actions first, then skills). */
|
|
6262
6245
|
_activateSkillMenuItem(index) {
|
|
6263
6246
|
const debugActions = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
6264
|
-
let actionCount =
|
|
6247
|
+
let actionCount = 3;
|
|
6265
6248
|
if (debugActions) {
|
|
6266
|
-
actionCount =
|
|
6249
|
+
actionCount = 5;
|
|
6267
6250
|
}
|
|
6268
6251
|
if (index === 0) {
|
|
6269
|
-
this._openFilePicker();
|
|
6252
|
+
this._openFilePicker(true);
|
|
6270
6253
|
return;
|
|
6271
6254
|
}
|
|
6272
6255
|
if (index === 1) {
|
|
6256
|
+
this._openFilePicker(false);
|
|
6257
|
+
return;
|
|
6258
|
+
}
|
|
6259
|
+
if (index === 2) {
|
|
6273
6260
|
this._handleSkillBrowseOpen();
|
|
6274
6261
|
return;
|
|
6275
6262
|
}
|
|
6276
|
-
if (debugActions && index ===
|
|
6263
|
+
if (debugActions && index === 3) {
|
|
6277
6264
|
this._skillMenuOpened = false;
|
|
6278
6265
|
this._send({
|
|
6279
6266
|
message: '/test-context-warning',
|
|
@@ -6281,7 +6268,7 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6281
6268
|
});
|
|
6282
6269
|
return;
|
|
6283
6270
|
}
|
|
6284
|
-
if (debugActions && index ===
|
|
6271
|
+
if (debugActions && index === 4) {
|
|
6285
6272
|
this._skillMenuOpened = false;
|
|
6286
6273
|
this._send({
|
|
6287
6274
|
message: '/test-context-limit',
|
|
@@ -6601,17 +6588,26 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6601
6588
|
this._skillBrowseSource = e.detail.activeTabId;
|
|
6602
6589
|
}
|
|
6603
6590
|
// --- Attachments ---
|
|
6604
|
-
|
|
6591
|
+
/**
|
|
6592
|
+
* Open the OS file picker. `aiAnalyze` records which attach action was used so
|
|
6593
|
+
* the change handler stages the picked files in the right mode: true for
|
|
6594
|
+
* "Attach Files" (the model reads them), false for "Attach Files for Reference"
|
|
6595
|
+
* (attached for a tool to act on — the model is told about them but not asked
|
|
6596
|
+
* to read them). Chosen once at the picker, so selecting 30 files never means
|
|
6597
|
+
* toggling 30 chips.
|
|
6598
|
+
*/
|
|
6599
|
+
_openFilePicker(aiAnalyze = true) {
|
|
6605
6600
|
// Close the skill/attachment menu — opening the OS file picker is the end of
|
|
6606
6601
|
// the menu interaction, and the picker is a separate window so the menu would
|
|
6607
6602
|
// otherwise linger open behind it (and after a file is chosen).
|
|
6608
6603
|
this._skillMenuOpened = false;
|
|
6604
|
+
this._pendingAttachAnalyze = aiAnalyze;
|
|
6609
6605
|
this._fileInputEl?.click();
|
|
6610
6606
|
}
|
|
6611
6607
|
_handleFileInputChange(e) {
|
|
6612
6608
|
const input = e.target;
|
|
6613
6609
|
if (input.files) {
|
|
6614
|
-
this._addFiles(input.files);
|
|
6610
|
+
this._addFiles(input.files, this._pendingAttachAnalyze);
|
|
6615
6611
|
}
|
|
6616
6612
|
// Reset so picking the same file again re-fires change.
|
|
6617
6613
|
input.value = '';
|
|
@@ -6674,35 +6670,62 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6674
6670
|
_hasFiles(e) {
|
|
6675
6671
|
return Array.from(e.dataTransfer?.types || []).includes('Files');
|
|
6676
6672
|
}
|
|
6677
|
-
/**
|
|
6678
|
-
|
|
6679
|
-
const
|
|
6673
|
+
/** Lower-cased extension of a filename (without the dot), or '' if none. */
|
|
6674
|
+
_getFileExtension(name) {
|
|
6675
|
+
const dot = name.lastIndexOf('.');
|
|
6676
|
+
if (dot < 0) {
|
|
6677
|
+
return '';
|
|
6678
|
+
}
|
|
6679
|
+
return name.slice(dot + 1).toLowerCase();
|
|
6680
|
+
}
|
|
6681
|
+
/**
|
|
6682
|
+
* Stage a set of files in the composer (held locally until send). `aiAnalyze`
|
|
6683
|
+
* comes from which attach action was used — true for "Attach Files" (the model
|
|
6684
|
+
* reads them), false for "Attach Files for Reference" (tool-only, not read).
|
|
6685
|
+
* Drag-and-drop and paste default to analyze.
|
|
6686
|
+
*/
|
|
6687
|
+
_addFiles(fileList, aiAnalyze = true) {
|
|
6688
|
+
// Only accept what the picker's `accept` filter allows (extension-based, so a
|
|
6689
|
+
// drag/paste can't sneak in a type the picker itself wouldn't offer).
|
|
6690
|
+
const files = Array.from(fileList).filter((f) => ACCEPTED_FILE_EXTENSIONS.includes(this._getFileExtension(f.name)));
|
|
6680
6691
|
if (!files.length) {
|
|
6681
6692
|
return;
|
|
6682
6693
|
}
|
|
6683
6694
|
files.forEach((file) => {
|
|
6695
|
+
// Only images get a local preview thumbnail; documents render an icon.
|
|
6696
|
+
// Prefer the File's MIME, falling back to the extension when it's absent.
|
|
6697
|
+
let previewUrl;
|
|
6698
|
+
if (file.type.startsWith('image/') || IMAGE_EXTENSIONS.includes(this._getFileExtension(file.name))) {
|
|
6699
|
+
previewUrl = URL.createObjectURL(file);
|
|
6700
|
+
}
|
|
6684
6701
|
const pending = {
|
|
6685
6702
|
localId: 'att-' + this._pendingSeq++,
|
|
6686
6703
|
file,
|
|
6687
6704
|
name: file.name,
|
|
6688
|
-
|
|
6705
|
+
mimeType: file.type,
|
|
6706
|
+
previewUrl,
|
|
6707
|
+
aiAnalyze,
|
|
6689
6708
|
};
|
|
6690
6709
|
this._pendingConversationFiles = [...this._pendingConversationFiles, pending];
|
|
6691
6710
|
});
|
|
6692
6711
|
}
|
|
6693
|
-
|
|
6712
|
+
_handlePendingRemove(localId) {
|
|
6694
6713
|
const target = this._pendingConversationFiles.find((p) => p.localId === localId);
|
|
6695
|
-
if (target) {
|
|
6714
|
+
if (target?.previewUrl) {
|
|
6696
6715
|
URL.revokeObjectURL(target.previewUrl);
|
|
6697
6716
|
}
|
|
6698
6717
|
this._pendingConversationFiles = this._pendingConversationFiles.filter((p) => p.localId !== localId);
|
|
6699
6718
|
}
|
|
6700
6719
|
_clearPending() {
|
|
6701
|
-
this._pendingConversationFiles.forEach((p) =>
|
|
6720
|
+
this._pendingConversationFiles.forEach((p) => {
|
|
6721
|
+
if (p.previewUrl) {
|
|
6722
|
+
URL.revokeObjectURL(p.previewUrl);
|
|
6723
|
+
}
|
|
6724
|
+
});
|
|
6702
6725
|
this._pendingConversationFiles = [];
|
|
6703
6726
|
}
|
|
6704
6727
|
/** Open the shared full-screen previewer for a message image. */
|
|
6705
|
-
|
|
6728
|
+
_handleImageOpen(img) {
|
|
6706
6729
|
// Use the local preview if the upload hasn't finished yet; otherwise the
|
|
6707
6730
|
// backend view URL (prefixed with the base URL — a raw fetch of a relative
|
|
6708
6731
|
// path would hit the page origin, e.g. the dev server, not the backend).
|
|
@@ -6719,10 +6742,6 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6719
6742
|
window.open(`${KRHttp.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view?download=true`, '_blank');
|
|
6720
6743
|
});
|
|
6721
6744
|
}
|
|
6722
|
-
/** Whether the send button is active: needs text or at least one image. */
|
|
6723
|
-
_canSend() {
|
|
6724
|
-
return Boolean(this._inputValue.trim()) || this._pendingConversationFiles.length > 0;
|
|
6725
|
-
}
|
|
6726
6745
|
_handleSubmit() {
|
|
6727
6746
|
if (this._isStreaming) {
|
|
6728
6747
|
return;
|
|
@@ -6733,10 +6752,13 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6733
6752
|
if (!messageText && !pending.length) {
|
|
6734
6753
|
return;
|
|
6735
6754
|
}
|
|
6736
|
-
// The
|
|
6737
|
-
// conversation reloads and they come back as stored
|
|
6738
|
-
// just-sent message itself carries text only.
|
|
6739
|
-
const conversationFiles = pending.map((p) =>
|
|
6755
|
+
// The files sent to the backend, each paired with its analyze flag. Sent
|
|
6756
|
+
// files are shown once the conversation reloads and they come back as stored
|
|
6757
|
+
// conversation files; the just-sent message itself carries text only.
|
|
6758
|
+
const conversationFiles = pending.map((p) => ({
|
|
6759
|
+
file: p.file,
|
|
6760
|
+
aiAnalyze: p.aiAnalyze,
|
|
6761
|
+
}));
|
|
6740
6762
|
const submitEvent = new CustomEvent('message-submit', {
|
|
6741
6763
|
detail: {
|
|
6742
6764
|
message: messageText,
|
|
@@ -6768,12 +6790,15 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6768
6790
|
};
|
|
6769
6791
|
// Attach the staged files' local previews to the message so its thumbnails
|
|
6770
6792
|
// show INSTANTLY, without waiting on the upload. Each carries a temporary
|
|
6771
|
-
// previewUrl (
|
|
6772
|
-
//
|
|
6793
|
+
// previewUrl (images only; documents render an icon from mimeType) and
|
|
6794
|
+
// conversationFileID -1 as a placeholder; the built-in send swaps these for
|
|
6795
|
+
// the real server files once the upload returns.
|
|
6773
6796
|
if (this._pendingConversationFiles.length) {
|
|
6774
6797
|
userMessage.conversationFiles = this._pendingConversationFiles.map((p) => ({
|
|
6775
6798
|
conversationFileID: -1,
|
|
6776
6799
|
niceName: p.name,
|
|
6800
|
+
mimeType: p.mimeType,
|
|
6801
|
+
aiAnalyze: p.aiAnalyze,
|
|
6777
6802
|
previewUrl: p.previewUrl,
|
|
6778
6803
|
}));
|
|
6779
6804
|
}
|
|
@@ -6828,11 +6853,17 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
6828
6853
|
// upload is dropped so an undefined id never reaches `fileIds`.
|
|
6829
6854
|
let uploaded = Promise.resolve([]);
|
|
6830
6855
|
if (params.conversationFiles.length) {
|
|
6831
|
-
uploaded = Promise.all(params.conversationFiles.map((file) => {
|
|
6856
|
+
uploaded = Promise.all(params.conversationFiles.map(({ file, aiAnalyze }) => {
|
|
6832
6857
|
const form = new FormData();
|
|
6833
6858
|
form.append('file', file);
|
|
6834
6859
|
form.append('type', 'LIBRARY');
|
|
6835
6860
|
form.append('niceName', file.name);
|
|
6861
|
+
// Reference-only files (from "Attach Files for Reference") aren't read
|
|
6862
|
+
// by the model — the endpoint defaults aiAnalyze=true, so only send
|
|
6863
|
+
// the flag when it's false.
|
|
6864
|
+
if (!aiAnalyze) {
|
|
6865
|
+
form.append('aiAnalyze', 'false');
|
|
6866
|
+
}
|
|
6836
6867
|
return KRHttp.fetch({
|
|
6837
6868
|
url: '/api/system/conversationFile/upload',
|
|
6838
6869
|
method: 'POST',
|
|
@@ -7114,9 +7145,9 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7114
7145
|
// flat keyboard index counts actions first, then skills, so the skills'
|
|
7115
7146
|
// index offset is the number of actions shown.
|
|
7116
7147
|
const debugActions = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
7117
|
-
let actionCount =
|
|
7148
|
+
let actionCount = 3;
|
|
7118
7149
|
if (debugActions) {
|
|
7119
|
-
actionCount =
|
|
7150
|
+
actionCount = 5;
|
|
7120
7151
|
}
|
|
7121
7152
|
return b `
|
|
7122
7153
|
<div class="skill-menu">
|
|
@@ -7126,14 +7157,25 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7126
7157
|
'skill-menu__action': true,
|
|
7127
7158
|
'skill-menu__item--highlighted': this._skillHighlighted === 0,
|
|
7128
7159
|
})}
|
|
7129
|
-
@click=${this._openFilePicker}
|
|
7160
|
+
@click=${() => this._openFilePicker(true)}
|
|
7130
7161
|
>
|
|
7131
|
-
Attach Files
|
|
7162
|
+
<span class="skill-menu__name">Attach Files</span>
|
|
7163
|
+
<span class="skill-menu__desc">The AI reads these files</span>
|
|
7132
7164
|
</button>
|
|
7133
7165
|
<button
|
|
7134
7166
|
class=${e({
|
|
7135
7167
|
'skill-menu__action': true,
|
|
7136
7168
|
'skill-menu__item--highlighted': this._skillHighlighted === 1,
|
|
7169
|
+
})}
|
|
7170
|
+
@click=${() => this._openFilePicker(false)}
|
|
7171
|
+
>
|
|
7172
|
+
<span class="skill-menu__name">Attach Files for Reference</span>
|
|
7173
|
+
<span class="skill-menu__desc">Attached for a tool — not read by the AI</span>
|
|
7174
|
+
</button>
|
|
7175
|
+
<button
|
|
7176
|
+
class=${e({
|
|
7177
|
+
'skill-menu__action': true,
|
|
7178
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 2,
|
|
7137
7179
|
})}
|
|
7138
7180
|
@click=${this._handleSkillBrowseOpen}
|
|
7139
7181
|
>
|
|
@@ -7143,18 +7185,18 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7143
7185
|
<button
|
|
7144
7186
|
class=${e({
|
|
7145
7187
|
'skill-menu__action': true,
|
|
7146
|
-
'skill-menu__item--highlighted': this._skillHighlighted ===
|
|
7188
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 3,
|
|
7147
7189
|
})}
|
|
7148
|
-
@click=${() => this._activateSkillMenuItem(
|
|
7190
|
+
@click=${() => this._activateSkillMenuItem(3)}
|
|
7149
7191
|
>
|
|
7150
7192
|
Test Context Warning
|
|
7151
7193
|
</button>
|
|
7152
7194
|
<button
|
|
7153
7195
|
class=${e({
|
|
7154
7196
|
'skill-menu__action': true,
|
|
7155
|
-
'skill-menu__item--highlighted': this._skillHighlighted ===
|
|
7197
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 4,
|
|
7156
7198
|
})}
|
|
7157
|
-
@click=${() => this._activateSkillMenuItem(
|
|
7199
|
+
@click=${() => this._activateSkillMenuItem(4)}
|
|
7158
7200
|
>
|
|
7159
7201
|
Test Context Limit
|
|
7160
7202
|
</button>
|
|
@@ -7600,19 +7642,41 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7600
7642
|
` : A}
|
|
7601
7643
|
${msg.conversationFiles && msg.conversationFiles.length ? b `
|
|
7602
7644
|
<div class="attachments">
|
|
7603
|
-
${msg.conversationFiles.map((
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7645
|
+
${msg.conversationFiles.map((file) => {
|
|
7646
|
+
const fileName = file.niceName || file.name || 'File';
|
|
7647
|
+
// A stored file is an image by its MIME when it has one, else
|
|
7648
|
+
// by its extension.
|
|
7649
|
+
let isImage = IMAGE_EXTENSIONS.includes(this._getFileExtension(fileName));
|
|
7650
|
+
if (file.mimeType) {
|
|
7651
|
+
isImage = file.mimeType.startsWith('image/');
|
|
7652
|
+
}
|
|
7653
|
+
// Images open in the previewer; documents just show a tile
|
|
7654
|
+
// (the previewer is image-only).
|
|
7655
|
+
if (isImage) {
|
|
7656
|
+
return b `
|
|
7657
|
+
<button
|
|
7658
|
+
class="attachment"
|
|
7659
|
+
type="button"
|
|
7660
|
+
title=${file.niceName || 'View image'}
|
|
7661
|
+
@click=${() => this._handleImageOpen(file)}
|
|
7662
|
+
>
|
|
7663
|
+
<img
|
|
7664
|
+
src=${file.previewUrl || `${KRHttp.getBaseUrl()}/api/system/conversationFile/${file.conversationFileID}/view`}
|
|
7665
|
+
alt=${file.niceName || ''}
|
|
7666
|
+
/>
|
|
7667
|
+
</button>
|
|
7668
|
+
`;
|
|
7669
|
+
}
|
|
7670
|
+
return b `
|
|
7671
|
+
<div
|
|
7672
|
+
class="attachment attachment--doc"
|
|
7673
|
+
title=${fileName}
|
|
7674
|
+
>
|
|
7675
|
+
<span class="attachment__type">${this._getFileExtension(fileName) || 'File'}</span>
|
|
7676
|
+
<span class="attachment__name">${fileName}</span>
|
|
7677
|
+
</div>
|
|
7678
|
+
`;
|
|
7679
|
+
})}
|
|
7616
7680
|
</div>
|
|
7617
7681
|
` : A}
|
|
7618
7682
|
${msg.content ? b `
|
|
@@ -7727,18 +7791,36 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7727
7791
|
${this._pendingConversationFiles.length ? b `
|
|
7728
7792
|
<div class="composer-attachments">
|
|
7729
7793
|
${c(this._pendingConversationFiles, (p) => p.localId, (p) => b `
|
|
7730
|
-
<div
|
|
7731
|
-
|
|
7794
|
+
<div
|
|
7795
|
+
class=${e({
|
|
7796
|
+
chip: true,
|
|
7797
|
+
'chip--doc': !p.previewUrl,
|
|
7798
|
+
})}
|
|
7799
|
+
title=${p.name}
|
|
7800
|
+
>
|
|
7801
|
+
<div
|
|
7802
|
+
class=${e({
|
|
7803
|
+
'chip-body': true,
|
|
7804
|
+
'chip-body--image': Boolean(p.previewUrl),
|
|
7805
|
+
})}
|
|
7806
|
+
>
|
|
7807
|
+
${p.previewUrl
|
|
7808
|
+
? b `<img src=${p.previewUrl} alt=${p.name} />`
|
|
7809
|
+
: b `
|
|
7810
|
+
<span class="chip-type">${this._getFileExtension(p.name) || 'File'}</span>
|
|
7811
|
+
<span class="chip-name">${p.name}</span>
|
|
7812
|
+
`}
|
|
7813
|
+
</div>
|
|
7732
7814
|
<button
|
|
7733
7815
|
class="chip-remove"
|
|
7734
7816
|
title="Remove"
|
|
7735
|
-
@click=${() => this.
|
|
7817
|
+
@click=${() => this._handlePendingRemove(p.localId)}
|
|
7736
7818
|
>
|
|
7737
7819
|
<svg
|
|
7738
7820
|
xmlns="http://www.w3.org/2000/svg"
|
|
7739
7821
|
fill="none"
|
|
7740
7822
|
viewBox="0 0 24 24"
|
|
7741
|
-
stroke-width="
|
|
7823
|
+
stroke-width="1.75"
|
|
7742
7824
|
stroke="currentColor"
|
|
7743
7825
|
>
|
|
7744
7826
|
<path
|
|
@@ -7755,7 +7837,7 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7755
7837
|
<input
|
|
7756
7838
|
class="file-input"
|
|
7757
7839
|
type="file"
|
|
7758
|
-
accept
|
|
7840
|
+
accept=${ACCEPTED_FILE_EXTENSIONS.map((ext) => '.' + ext).join(',')}
|
|
7759
7841
|
multiple
|
|
7760
7842
|
hidden
|
|
7761
7843
|
@change=${this._handleFileInputChange}
|
|
@@ -7800,7 +7882,7 @@ let KRChatbot = class KRChatbot extends i$3 {
|
|
|
7800
7882
|
<button
|
|
7801
7883
|
class=${e({
|
|
7802
7884
|
'send': true,
|
|
7803
|
-
'send--disabled': !this.
|
|
7885
|
+
'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
|
|
7804
7886
|
})}
|
|
7805
7887
|
@click=${this._handleSubmit}
|
|
7806
7888
|
title="Send message"
|
|
@@ -8729,13 +8811,15 @@ KRChatbot.styles = i$6 `
|
|
|
8729
8811
|
margin-bottom: 24px;
|
|
8730
8812
|
}
|
|
8731
8813
|
|
|
8814
|
+
/* A sent attachment is the same tile as the composer chip, so a file looks
|
|
8815
|
+
identical before and after sending. */
|
|
8732
8816
|
.attachment {
|
|
8733
|
-
width:
|
|
8734
|
-
height:
|
|
8735
|
-
border-radius:
|
|
8817
|
+
width: 112px;
|
|
8818
|
+
height: 112px;
|
|
8819
|
+
border-radius: 20px;
|
|
8736
8820
|
overflow: hidden;
|
|
8737
|
-
background: #
|
|
8738
|
-
border:
|
|
8821
|
+
background: #f2f0f0;
|
|
8822
|
+
border: none;
|
|
8739
8823
|
cursor: zoom-in;
|
|
8740
8824
|
flex-shrink: 0;
|
|
8741
8825
|
padding: 0;
|
|
@@ -8748,6 +8832,37 @@ KRChatbot.styles = i$6 `
|
|
|
8748
8832
|
display: block;
|
|
8749
8833
|
}
|
|
8750
8834
|
|
|
8835
|
+
/* Non-image attachment in a sent message — file-type label above the filename
|
|
8836
|
+
(the previewer is image-only, so this isn't clickable). */
|
|
8837
|
+
.attachment--doc {
|
|
8838
|
+
display: flex;
|
|
8839
|
+
flex-direction: column;
|
|
8840
|
+
padding: 16px 14px;
|
|
8841
|
+
box-sizing: border-box;
|
|
8842
|
+
text-align: left;
|
|
8843
|
+
cursor: default;
|
|
8844
|
+
}
|
|
8845
|
+
|
|
8846
|
+
.attachment__type {
|
|
8847
|
+
font-size: 13px;
|
|
8848
|
+
line-height: 1.2;
|
|
8849
|
+
color: #444746;
|
|
8850
|
+
text-transform: uppercase;
|
|
8851
|
+
flex-shrink: 0;
|
|
8852
|
+
}
|
|
8853
|
+
|
|
8854
|
+
.attachment__name {
|
|
8855
|
+
margin-top: auto;
|
|
8856
|
+
font-size: 13px;
|
|
8857
|
+
line-height: 1.25;
|
|
8858
|
+
color: #000;
|
|
8859
|
+
overflow: hidden;
|
|
8860
|
+
display: -webkit-box;
|
|
8861
|
+
-webkit-line-clamp: 3;
|
|
8862
|
+
-webkit-box-orient: vertical;
|
|
8863
|
+
overflow-wrap: anywhere;
|
|
8864
|
+
}
|
|
8865
|
+
|
|
8751
8866
|
/* Skill menu (opened by typing '/'). Fixed + JS-positioned above the
|
|
8752
8867
|
composer, following the krubble select-field dropdown pattern. */
|
|
8753
8868
|
.skill-menu {
|
|
@@ -8822,6 +8937,9 @@ KRChatbot.styles = i$6 `
|
|
|
8822
8937
|
}
|
|
8823
8938
|
|
|
8824
8939
|
.skill-menu__action {
|
|
8940
|
+
display: flex;
|
|
8941
|
+
flex-direction: column;
|
|
8942
|
+
gap: 2px;
|
|
8825
8943
|
width: 100%;
|
|
8826
8944
|
padding: 6px 16px;
|
|
8827
8945
|
background: transparent;
|
|
@@ -8834,6 +8952,13 @@ KRChatbot.styles = i$6 `
|
|
|
8834
8952
|
font-weight: 500;
|
|
8835
8953
|
}
|
|
8836
8954
|
|
|
8955
|
+
/* Secondary line on an attach action (e.g. "reads these files") — muted,
|
|
8956
|
+
not the primary accent, so it reads as a hint under the action label. */
|
|
8957
|
+
.skill-menu__action .skill-menu__desc {
|
|
8958
|
+
font-weight: 400;
|
|
8959
|
+
color: #6b7280;
|
|
8960
|
+
}
|
|
8961
|
+
|
|
8837
8962
|
/* Highlight (keyboard) and hover (mouse) are independent — either shades the
|
|
8838
8963
|
row. Declared after the base rules so it wins the cascade for both the
|
|
8839
8964
|
action buttons and the skill items. */
|
|
@@ -8950,15 +9075,44 @@ KRChatbot.styles = i$6 `
|
|
|
8950
9075
|
padding: 0 6px 10px;
|
|
8951
9076
|
}
|
|
8952
9077
|
|
|
9078
|
+
/* Every attachment — image or document — is the same square tile, so a mixed
|
|
9079
|
+
row reads as one uniform set. */
|
|
8953
9080
|
.chip {
|
|
8954
9081
|
position: relative;
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
9082
|
+
display: flex;
|
|
9083
|
+
flex-direction: column;
|
|
9084
|
+
width: 112px;
|
|
9085
|
+
height: 112px;
|
|
9086
|
+
border-radius: 20px;
|
|
8958
9087
|
overflow: hidden;
|
|
8959
|
-
background: #
|
|
8960
|
-
border: 1px solid var(--kr-chatbot-border);
|
|
9088
|
+
background: #f2f0f0;
|
|
8961
9089
|
flex-shrink: 0;
|
|
9090
|
+
transition: background 0.28s ease;
|
|
9091
|
+
}
|
|
9092
|
+
|
|
9093
|
+
.chip--doc {
|
|
9094
|
+
cursor: pointer;
|
|
9095
|
+
}
|
|
9096
|
+
|
|
9097
|
+
.chip--doc:hover {
|
|
9098
|
+
background: #dfdddd;
|
|
9099
|
+
}
|
|
9100
|
+
|
|
9101
|
+
/* The preview body — an image fills the tile; a document shows its file-type
|
|
9102
|
+
label above the filename, both left-aligned. */
|
|
9103
|
+
.chip-body {
|
|
9104
|
+
flex: 1;
|
|
9105
|
+
display: flex;
|
|
9106
|
+
flex-direction: column;
|
|
9107
|
+
min-height: 0;
|
|
9108
|
+
padding: 16px 14px;
|
|
9109
|
+
box-sizing: border-box;
|
|
9110
|
+
overflow: hidden;
|
|
9111
|
+
}
|
|
9112
|
+
|
|
9113
|
+
/* An image fills the whole tile, so it needs no padding around it. */
|
|
9114
|
+
.chip-body--image {
|
|
9115
|
+
padding: 0;
|
|
8962
9116
|
}
|
|
8963
9117
|
|
|
8964
9118
|
.chip img {
|
|
@@ -8968,31 +9122,65 @@ KRChatbot.styles = i$6 `
|
|
|
8968
9122
|
display: block;
|
|
8969
9123
|
}
|
|
8970
9124
|
|
|
9125
|
+
/* File-type label (PDF, XLSX, …) — sits at the top of the tile. */
|
|
9126
|
+
.chip-type {
|
|
9127
|
+
font-size: 13px;
|
|
9128
|
+
line-height: 1.2;
|
|
9129
|
+
color: #444746;
|
|
9130
|
+
text-transform: uppercase;
|
|
9131
|
+
flex-shrink: 0;
|
|
9132
|
+
}
|
|
9133
|
+
|
|
9134
|
+
/* Filename — pinned to the bottom of the tile, wrapping to at most three
|
|
9135
|
+
lines before it ellipsizes. */
|
|
9136
|
+
.chip-name {
|
|
9137
|
+
margin-top: auto;
|
|
9138
|
+
font-size: 13px;
|
|
9139
|
+
line-height: 1.25;
|
|
9140
|
+
color: #000;
|
|
9141
|
+
overflow: hidden;
|
|
9142
|
+
display: -webkit-box;
|
|
9143
|
+
-webkit-line-clamp: 3;
|
|
9144
|
+
-webkit-box-orient: vertical;
|
|
9145
|
+
overflow-wrap: anywhere;
|
|
9146
|
+
}
|
|
9147
|
+
|
|
9148
|
+
/* Remove button — a bare X in the tile's top-right corner, revealed only when
|
|
9149
|
+
the tile (or the button itself) is hovered or keyboard-focused. */
|
|
8971
9150
|
.chip-remove {
|
|
8972
9151
|
position: absolute;
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
9152
|
+
/* Inset matches .chip-body's padding so the X sits the same distance from
|
|
9153
|
+
the right edge as the file-type label does from the left. */
|
|
9154
|
+
top: 14px;
|
|
9155
|
+
right: 14px;
|
|
9156
|
+
width: 15px;
|
|
9157
|
+
height: 15px;
|
|
8978
9158
|
border: none;
|
|
8979
|
-
background:
|
|
8980
|
-
color: #
|
|
9159
|
+
background: none;
|
|
9160
|
+
color: #444746;
|
|
8981
9161
|
cursor: pointer;
|
|
8982
9162
|
display: flex;
|
|
8983
9163
|
align-items: center;
|
|
8984
9164
|
justify-content: center;
|
|
8985
9165
|
padding: 0;
|
|
8986
9166
|
line-height: 1;
|
|
9167
|
+
flex-shrink: 0;
|
|
9168
|
+
opacity: 0;
|
|
9169
|
+
transition: opacity 0.28s ease;
|
|
9170
|
+
}
|
|
9171
|
+
|
|
9172
|
+
.chip:hover .chip-remove,
|
|
9173
|
+
.chip-remove:focus-visible {
|
|
9174
|
+
opacity: 1;
|
|
8987
9175
|
}
|
|
8988
9176
|
|
|
8989
9177
|
.chip-remove:hover {
|
|
8990
|
-
|
|
9178
|
+
color: #000;
|
|
8991
9179
|
}
|
|
8992
9180
|
|
|
8993
9181
|
.chip-remove svg {
|
|
8994
|
-
width:
|
|
8995
|
-
height:
|
|
9182
|
+
width: 15px;
|
|
9183
|
+
height: 15px;
|
|
8996
9184
|
}
|
|
8997
9185
|
|
|
8998
9186
|
/* Drag-and-drop overlay */
|