@kodaris/krubble-app-components 1.0.92 → 1.0.94

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.
@@ -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
- this.toggleNavItem(item.id);
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
- startEditing() {
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
- resetPref() {
805
+ handlePrefReset() {
817
806
  this.pref.nav = {};
818
807
  }
819
- savePref() {
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.expandNavGroupOnDrag(item.id);
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
- toggleUserMenu() {
1260
+ handleUserMenuToggle() {
1282
1261
  this.isUserMenuOpen = !this.isUserMenuOpen;
1283
1262
  }
1284
- closeUserMenu() {
1285
- this.isUserMenuOpen = false;
1286
- }
1287
1263
  handleCustomize() {
1288
- this.closeUserMenu();
1289
- this.startEditing();
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.toggleUserMenu}>
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.savePref}>
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.resetPref}>
1312
+ <button class="edit-actions__btn" @click=${this.handlePrefReset}>
1337
1313
  Reset
1338
1314
  </button>
1339
- <button class="edit-actions__btn" @click=${this.cancelEditing}>
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=${this.resolveUrl(crumb)}
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 MIME types the chatbot accepts (matches what Bedrock can ingest). */
5568
- const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
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
- /** Images staged in the composer, before the message is sent. */
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._hideOverlay();
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) => URL.revokeObjectURL(p.previewUrl));
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 the two debug test triggers
6227
- // when enabled) come first, then the filtered skills — all one navigable list.
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 = 2;
6213
+ let actionCount = 3;
6231
6214
  if (debugActions) {
6232
- actionCount = 4;
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 = 2;
6247
+ let actionCount = 3;
6265
6248
  if (debugActions) {
6266
- actionCount = 4;
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 === 2) {
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 === 3) {
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
- _openFilePicker() {
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
- /** Stage a set of image files in the composer (held locally until send). */
6678
- _addFiles(fileList) {
6679
- const files = Array.from(fileList).filter((f) => ACCEPTED_IMAGE_TYPES.includes(f.type));
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
- previewUrl: URL.createObjectURL(file),
6705
+ mimeType: file.type,
6706
+ previewUrl,
6707
+ aiAnalyze,
6689
6708
  };
6690
6709
  this._pendingConversationFiles = [...this._pendingConversationFiles, pending];
6691
6710
  });
6692
6711
  }
6693
- _removePending(localId) {
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) => URL.revokeObjectURL(p.previewUrl));
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
- _openImage(img) {
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 raw files sent to the backend. Sent images are shown once the
6737
- // conversation reloads and they come back as stored conversation files; the
6738
- // just-sent message itself carries text only.
6739
- const conversationFiles = pending.map((p) => p.file);
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 (and conversationFileID -1 as a placeholder); the built-in send
6772
- // swaps these for the real server files once the upload returns.
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 = 2;
7148
+ let actionCount = 3;
7118
7149
  if (debugActions) {
7119
- actionCount = 4;
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 === 2,
7188
+ 'skill-menu__item--highlighted': this._skillHighlighted === 3,
7147
7189
  })}
7148
- @click=${() => this._activateSkillMenuItem(2)}
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 === 3,
7197
+ 'skill-menu__item--highlighted': this._skillHighlighted === 4,
7156
7198
  })}
7157
- @click=${() => this._activateSkillMenuItem(3)}
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((img) => b `
7604
- <button
7605
- class="attachment"
7606
- type="button"
7607
- title=${img.niceName || 'View image'}
7608
- @click=${() => this._openImage(img)}
7609
- >
7610
- <img
7611
- src=${img.previewUrl || `${KRHttp.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view`}
7612
- alt=${img.niceName || ''}
7613
- />
7614
- </button>
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 class="chip">
7731
- <img src=${p.previewUrl} alt=${p.name} />
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._removePending(p.localId)}
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="3"
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="image/png,image/jpeg,image/gif,image/webp"
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._canSend(),
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: 120px;
8734
- height: 120px;
8735
- border-radius: 12px;
8817
+ width: 112px;
8818
+ height: 112px;
8819
+ border-radius: 20px;
8736
8820
  overflow: hidden;
8737
- background: #f2f2f4;
8738
- border: 1px solid rgba(0, 0, 0, 0.08);
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
- width: 56px;
8956
- height: 56px;
8957
- border-radius: 10px;
9082
+ display: flex;
9083
+ flex-direction: column;
9084
+ width: 112px;
9085
+ height: 112px;
9086
+ border-radius: 20px;
8958
9087
  overflow: hidden;
8959
- background: #f2f2f4;
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
- top: 2px;
8974
- right: 2px;
8975
- width: 18px;
8976
- height: 18px;
8977
- border-radius: 50%;
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: rgba(0, 0, 0, 0.6);
8980
- color: #fff;
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
- background: rgba(0, 0, 0, 0.8);
9178
+ color: #000;
8991
9179
  }
8992
9180
 
8993
9181
  .chip-remove svg {
8994
- width: 10px;
8995
- height: 10px;
9182
+ width: 15px;
9183
+ height: 15px;
8996
9184
  }
8997
9185
 
8998
9186
  /* Drag-and-drop overlay */