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