@kodaris/krubble-app-components 1.0.70 → 1.0.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5570,7 +5570,7 @@
5570
5570
  * ## Usage
5571
5571
  * ```html
5572
5572
  * <kr-chatbot
5573
- * title="AI Assistant"
5573
+ * label="AI Assistant"
5574
5574
  * .send=${(params) => {
5575
5575
  * return fetch('/api/ai/chat/stream', {
5576
5576
  * method: 'POST',
@@ -5613,16 +5613,25 @@
5613
5613
  /**
5614
5614
  * Callback function for sending messages. Receives the user's message and
5615
5615
  * current conversation history. Must return a Response with a streaming SSE body.
5616
+ *
5617
+ * Optional — when not set, the chatbot uses its built-in default that streams
5618
+ * from the Kodaris Bedrock conversation endpoints. Set this to override.
5616
5619
  */
5617
5620
  this.send = null;
5618
5621
  /**
5619
5622
  * Callback function called when the user clears the conversation.
5620
5623
  * Must return a Promise — messages are only cleared if it resolves.
5624
+ *
5625
+ * Optional — when not set, the chatbot uses its built-in default that clears
5626
+ * the Kodaris Bedrock conversation state. Set this to override.
5621
5627
  */
5622
5628
  this.clear = null;
5623
5629
  /**
5624
5630
  * Callback function called on init to load existing conversation messages.
5625
5631
  * Must return a Promise that resolves with an array of KRChatbotMessage.
5632
+ *
5633
+ * Optional — when not set, the chatbot uses its built-in default that loads
5634
+ * existing messages from the Kodaris Bedrock conversation. Set this to override.
5626
5635
  */
5627
5636
  this.load = null;
5628
5637
  /**
@@ -5631,17 +5640,19 @@
5631
5640
  */
5632
5641
  this.messages = [];
5633
5642
  /**
5634
- * Header title text.
5643
+ * Header label text.
5635
5644
  */
5636
- this.title = 'Kat';
5637
- /**
5638
- * Header subtitle text (shown below title with a green status dot).
5639
- */
5640
- this.subtitle = '';
5645
+ this.label = 'Kat';
5641
5646
  /**
5642
5647
  * Input placeholder text.
5643
5648
  */
5644
5649
  this.placeholder = 'Type a message...';
5650
+ /**
5651
+ * Skills the user can invoke by typing `/` in the composer. Selecting one
5652
+ * inserts `/name ` into the message. The `/` menu shows the first 15 (filtered
5653
+ * as the user types); "Browse all skills" opens a dialog with the full list.
5654
+ */
5655
+ this.skills = [];
5645
5656
  /**
5646
5657
  * Whether the chat panel is open. Only relevant in floating mode.
5647
5658
  */
@@ -5652,15 +5663,39 @@
5652
5663
  * then driven via the `open()` / `close()` / `toggle()` methods.
5653
5664
  */
5654
5665
  this.hideToggle = false;
5666
+ /**
5667
+ * Layout mode.
5668
+ * - `'floating'` (default): the chatbot is a fixed bottom-right widget with a
5669
+ * toggle button, and the panel can be dragged and resized. This is the
5670
+ * traditional chat-widget appearance.
5671
+ * - `'inline'`: the chatbot drops its fixed positioning and fills its parent
5672
+ * container (100% width/height, no border radius/shadow, no toggle, no
5673
+ * drag/resize). The panel is always open. Use this to embed the chat as a
5674
+ * full page or panel region.
5675
+ */
5676
+ this.variant = 'floating';
5655
5677
  // --- @state (internal) ---
5656
5678
  this._inputValue = '';
5657
5679
  this._isStreaming = false;
5658
- this._statusText = '';
5659
5680
  this._error = '';
5660
5681
  /** Images staged in the composer, before the message is sent. */
5661
- this._pendingAttachments = [];
5682
+ this._pendingInternalFiles = [];
5662
5683
  /** True while a file is being dragged over the panel (shows the drop overlay). */
5663
5684
  this._isDraggingFile = false;
5685
+ /** Whether the `/` skill menu is open above the composer. */
5686
+ this._skillMenuOpened = false;
5687
+ /** Current `/` query (text after the slash), used to filter the skill menu. */
5688
+ this._skillQuery = '';
5689
+ /** Highlighted item in the skill menu for keyboard navigation (-1 = none). */
5690
+ this._skillHighlighted = 0;
5691
+ /** Index of the message whose copy button just flashed "Copied" (-1 = none). */
5692
+ this._copiedIndex = -1;
5693
+ /** Whether the "Browse all skills" dialog is open. */
5694
+ this._skillBrowseOpened = false;
5695
+ /** Active tab in the browse dialog. */
5696
+ this._skillBrowseSource = 'org';
5697
+ /** Search text in the browse dialog. */
5698
+ this._skillBrowseQuery = '';
5664
5699
  this._handleMouseMove = (e) => {
5665
5700
  if (this._isDragging) {
5666
5701
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -5722,12 +5757,27 @@
5722
5757
  // --- Lifecycle ---
5723
5758
  connectedCallback() {
5724
5759
  super.connectedCallback();
5725
- if (this.load) {
5726
- this.load().then((messages) => {
5727
- if (messages && messages.length) {
5728
- this.messages = messages;
5729
- }
5730
- }).catch(() => { });
5760
+ // Default to loading existing messages from the Kodaris Bedrock conversation
5761
+ // when the consumer hasn't provided their own `load`. The endpoint returns
5762
+ // messages already in KRChatbotMessage shape ({ type, content, images }), so
5763
+ // they pass straight through.
5764
+ if (!this.load) {
5765
+ this.load = () => krubbleHttp.Http.fetch({
5766
+ url: '/api/system/integration/aws/bedrock/getConversationMessages',
5767
+ })
5768
+ .then((r) => r.json())
5769
+ .then((res) => res.data || []);
5770
+ }
5771
+ this.load().then((messages) => {
5772
+ if (messages && messages.length) {
5773
+ this.messages = messages;
5774
+ }
5775
+ }).catch(() => { });
5776
+ // Inline mode fills its container and is always open — the floating widget's
5777
+ // toggle/drag/resize state (including any persisted layout) doesn't apply.
5778
+ if (this.variant === 'inline') {
5779
+ this.opened = true;
5780
+ return;
5731
5781
  }
5732
5782
  this._restoreState();
5733
5783
  }
@@ -5740,10 +5790,10 @@
5740
5790
  document.removeEventListener('mousemove', this._handleMouseMove);
5741
5791
  document.removeEventListener('mouseup', this._handleMouseUp);
5742
5792
  // Release any object URLs still held by staged attachments.
5743
- this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5793
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5744
5794
  }
5745
5795
  updated(changed) {
5746
- if (changed.has('messages') || changed.has('_statusText')) {
5796
+ if (changed.has('messages')) {
5747
5797
  if (!this._userHasScrolledUp && !this._isStreaming && this._messagesEl) {
5748
5798
  requestAnimationFrame(() => {
5749
5799
  if (this._messagesEl) {
@@ -5759,6 +5809,40 @@
5759
5809
  }
5760
5810
  }, 200);
5761
5811
  }
5812
+ // Position the fixed skill menu above the composer, following the krubble
5813
+ // select-field pattern (fixed element positioned from the trigger's rect).
5814
+ if (this._skillMenuOpened) {
5815
+ this._positionSkillMenu();
5816
+ }
5817
+ // Keep the keyboard-highlighted skill scrolled into view.
5818
+ if (changed.has('_skillHighlighted') && this._skillMenuOpened) {
5819
+ this._skillMenuEl
5820
+ ?.querySelector('.skill-menu__item--highlighted')
5821
+ ?.scrollIntoView({ block: 'nearest' });
5822
+ }
5823
+ }
5824
+ /** Anchor the fixed skill menu to the composer, opening upward above it. */
5825
+ _positionSkillMenu() {
5826
+ requestAnimationFrame(() => {
5827
+ if (!this._skillMenuEl || !this._inputWrapperEl) {
5828
+ return;
5829
+ }
5830
+ const inputRect = this._inputWrapperEl.getBoundingClientRect();
5831
+ this._skillMenuEl.style.left = inputRect.left + 'px';
5832
+ this._skillMenuEl.style.width = inputRect.width + 'px';
5833
+ // Bound the menu by the chat panel's own top edge so it stays within the
5834
+ // widget. In floating mode that keeps it inside the card; in inline mode the
5835
+ // panel top sits just below the app header/breadcrumb, so it stays clear of
5836
+ // those too. An 8px gap keeps it off both edges.
5837
+ const topLimit = this._panelEl.getBoundingClientRect().top + 8;
5838
+ const spaceAbove = inputRect.top - 8 - topLimit;
5839
+ this._skillMenuEl.style.maxHeight = spaceAbove + 'px';
5840
+ // Measure after the cap is applied so the upward anchor uses the real
5841
+ // height, then clamp the top edge to the panel bound.
5842
+ const menuRect = this._skillMenuEl.getBoundingClientRect();
5843
+ const top = Math.max(topLimit, inputRect.top - menuRect.height - 8);
5844
+ this._skillMenuEl.style.top = top + 'px';
5845
+ });
5762
5846
  }
5763
5847
  // --- Public methods ---
5764
5848
  /**
@@ -5817,6 +5901,10 @@
5817
5901
  }
5818
5902
  }
5819
5903
  _handleHeaderMouseDown(e) {
5904
+ // Inline mode fills its parent — dragging the panel doesn't apply.
5905
+ if (this.variant === 'inline') {
5906
+ return;
5907
+ }
5820
5908
  // Ignore clicks on buttons inside the header
5821
5909
  if (e.target.closest('button')) {
5822
5910
  return;
@@ -5842,6 +5930,10 @@
5842
5930
  document.addEventListener('mouseup', this._handleMouseUp);
5843
5931
  }
5844
5932
  _handleResizeMouseDown(e) {
5933
+ // Inline mode fills its parent — resizing the panel doesn't apply.
5934
+ if (this.variant === 'inline') {
5935
+ return;
5936
+ }
5845
5937
  if (!this._panelEl) {
5846
5938
  return;
5847
5939
  }
@@ -5944,13 +6036,129 @@
5944
6036
  const textarea = e.target;
5945
6037
  textarea.style.height = 'auto';
5946
6038
  textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
6039
+ // Open the skill menu while the message is a `/` command being typed (a
6040
+ // leading slash with no space yet). The text after the slash filters it.
6041
+ const slashMatch = this._inputValue.match(/^\/(\S*)$/);
6042
+ if (slashMatch && this.skills.length) {
6043
+ this._skillMenuOpened = true;
6044
+ this._skillQuery = slashMatch[1];
6045
+ this._skillHighlighted = 0;
6046
+ }
6047
+ else {
6048
+ this._skillMenuOpened = false;
6049
+ }
5947
6050
  }
5948
6051
  _handleKeyDown(e) {
6052
+ // When the skill menu is open, arrow keys / Enter / Escape drive it. The two
6053
+ // top actions (Attach Files, Browse All Skills) are index 0 and 1, then the
6054
+ // filtered skills follow — all navigable as one list.
6055
+ if (this._skillMenuOpened) {
6056
+ const count = 2 + this._filteredSkills().length;
6057
+ if (e.key === 'ArrowDown') {
6058
+ e.preventDefault();
6059
+ this._skillHighlighted = (this._skillHighlighted + 1) % count;
6060
+ return;
6061
+ }
6062
+ if (e.key === 'ArrowUp') {
6063
+ e.preventDefault();
6064
+ this._skillHighlighted = (this._skillHighlighted - 1 + count) % count;
6065
+ return;
6066
+ }
6067
+ if (e.key === 'Enter' && !e.shiftKey) {
6068
+ e.preventDefault();
6069
+ this._activateSkillMenuItem(this._skillHighlighted);
6070
+ return;
6071
+ }
6072
+ if (e.key === 'Escape') {
6073
+ e.preventDefault();
6074
+ this._skillMenuOpened = false;
6075
+ return;
6076
+ }
6077
+ }
5949
6078
  if (e.key === 'Enter' && !e.shiftKey) {
5950
6079
  e.preventDefault();
5951
6080
  this._handleSubmit();
5952
6081
  }
5953
6082
  }
6083
+ /** Run the skill-menu item at the given flat index (0/1 = actions, then skills). */
6084
+ _activateSkillMenuItem(index) {
6085
+ if (index === 0) {
6086
+ this._openFilePicker();
6087
+ return;
6088
+ }
6089
+ if (index === 1) {
6090
+ this._handleSkillBrowseOpen();
6091
+ return;
6092
+ }
6093
+ const skill = this._filteredSkills()[index - 2];
6094
+ if (skill) {
6095
+ this._handleSkillSelect(skill);
6096
+ }
6097
+ }
6098
+ // --- Skills ---
6099
+ /** Skills matching the current `/` query, capped at the first 15. */
6100
+ _filteredSkills() {
6101
+ const query = this._skillQuery.toLowerCase();
6102
+ return this.skills
6103
+ .filter((skill) => skill.name.toLowerCase().includes(query) ||
6104
+ skill.label.toLowerCase().includes(query))
6105
+ .slice(0, 15);
6106
+ }
6107
+ /** The `+` button toggles the skill/attachment menu open (showing all skills). */
6108
+ _handlePlusClick() {
6109
+ if (this._skillMenuOpened) {
6110
+ this._skillMenuOpened = false;
6111
+ return;
6112
+ }
6113
+ this._skillQuery = '';
6114
+ this._skillHighlighted = 0;
6115
+ this._skillMenuOpened = true;
6116
+ }
6117
+ /** Insert `/name ` into the composer and focus it, ready for the message. */
6118
+ _handleSkillSelect(skill) {
6119
+ this._inputValue = `/${skill.name} `;
6120
+ this._skillMenuOpened = false;
6121
+ this._skillBrowseOpened = false;
6122
+ this._inputEl?.focus();
6123
+ }
6124
+ /** Copy a message's text to the clipboard and briefly flash "Copied". */
6125
+ _handleCopyMessage(content, index) {
6126
+ navigator.clipboard.writeText(content).then(() => {
6127
+ this._copiedIndex = index;
6128
+ setTimeout(() => {
6129
+ if (this._copiedIndex === index) {
6130
+ this._copiedIndex = -1;
6131
+ }
6132
+ }, 2000);
6133
+ }).catch(() => { });
6134
+ }
6135
+ _handleSkillBrowseOpen() {
6136
+ this._skillMenuOpened = false;
6137
+ this._skillBrowseQuery = '';
6138
+ this._skillBrowseOpened = true;
6139
+ }
6140
+ _handleSkillBrowseClose() {
6141
+ this._skillBrowseOpened = false;
6142
+ }
6143
+ _handleSkillBrowseSearch(e) {
6144
+ this._skillBrowseQuery = e.target.value;
6145
+ }
6146
+ _handleSkillBrowseTabChange(e) {
6147
+ this._skillBrowseSource = e.detail.activeTabId;
6148
+ }
6149
+ /** Skills for the browse dialog's active tab, filtered by its search box. */
6150
+ _browseSkills() {
6151
+ const query = this._skillBrowseQuery.toLowerCase();
6152
+ return this.skills.filter((skill) => {
6153
+ const source = skill.source || 'org';
6154
+ if (source !== this._skillBrowseSource) {
6155
+ return false;
6156
+ }
6157
+ return skill.name.toLowerCase().includes(query) ||
6158
+ skill.label.toLowerCase().includes(query) ||
6159
+ (skill.description || '').toLowerCase().includes(query);
6160
+ });
6161
+ }
5954
6162
  // --- Attachments ---
5955
6163
  _openFilePicker() {
5956
6164
  this._fileInputEl?.click();
@@ -6034,54 +6242,49 @@
6034
6242
  name: file.name,
6035
6243
  previewUrl: URL.createObjectURL(file),
6036
6244
  };
6037
- this._pendingAttachments = [...this._pendingAttachments, pending];
6245
+ this._pendingInternalFiles = [...this._pendingInternalFiles, pending];
6038
6246
  });
6039
6247
  }
6040
6248
  _removePending(localId) {
6041
- const target = this._pendingAttachments.find((p) => p.localId === localId);
6249
+ const target = this._pendingInternalFiles.find((p) => p.localId === localId);
6042
6250
  if (target) {
6043
6251
  URL.revokeObjectURL(target.previewUrl);
6044
6252
  }
6045
- this._pendingAttachments = this._pendingAttachments.filter((p) => p.localId !== localId);
6253
+ this._pendingInternalFiles = this._pendingInternalFiles.filter((p) => p.localId !== localId);
6046
6254
  }
6047
6255
  _clearPending() {
6048
- this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6049
- this._pendingAttachments = [];
6256
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6257
+ this._pendingInternalFiles = [];
6050
6258
  }
6051
- /** Open the shared full-screen previewer for an attachment. */
6052
- _openAttachment(att) {
6259
+ /** Open the shared full-screen previewer for a message image. */
6260
+ _openImage(img) {
6053
6261
  krubbleComponents.FilePreview.open({
6054
- src: att.url,
6055
- name: att.name || 'image',
6262
+ src: `/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view`,
6263
+ name: img.niceName || 'image',
6056
6264
  });
6057
6265
  }
6058
6266
  /** Whether the send button is active: needs text or at least one image. */
6059
6267
  _canSend() {
6060
- return Boolean(this._inputValue.trim()) || this._pendingAttachments.length > 0;
6268
+ return Boolean(this._inputValue.trim()) || this._pendingInternalFiles.length > 0;
6061
6269
  }
6062
6270
  _handleSubmit() {
6063
- if (!this.send || this._isStreaming) {
6271
+ if (this._isStreaming) {
6064
6272
  return;
6065
6273
  }
6066
6274
  const messageText = this._inputValue.trim();
6067
- const pending = this._pendingAttachments;
6275
+ const pending = this._pendingInternalFiles;
6068
6276
  // Allow sending an image with no text, but require at least one of them.
6069
6277
  if (!messageText && !pending.length) {
6070
6278
  return;
6071
6279
  }
6072
- // The raw files sent to the backend, and the display attachments for the
6073
- // sent message. The message reuses each preview object URL, so ownership of
6074
- // those URLs transfers to the message (we don't revoke them here).
6075
- const files = pending.map((p) => p.file);
6076
- const attachments = pending.map((p) => ({
6077
- url: p.previewUrl,
6078
- name: p.name,
6079
- mimeType: p.file.type,
6080
- }));
6280
+ // The raw files sent to the backend. Sent images are shown once the
6281
+ // conversation reloads and they come back as stored internal files; the
6282
+ // just-sent message itself carries text only.
6283
+ const internalFiles = pending.map((p) => p.file);
6081
6284
  const submitEvent = new CustomEvent('message-submit', {
6082
6285
  detail: {
6083
6286
  message: messageText,
6084
- files,
6287
+ internalFiles,
6085
6288
  },
6086
6289
  bubbles: true,
6087
6290
  composed: true,
@@ -6092,30 +6295,26 @@
6092
6295
  return;
6093
6296
  }
6094
6297
  const userMessage = {
6095
- role: 'user',
6298
+ type: 'user',
6096
6299
  content: messageText,
6097
6300
  };
6098
- if (attachments.length) {
6099
- userMessage.attachments = attachments;
6100
- }
6101
6301
  this.messages = [...this.messages, userMessage];
6102
6302
  this._inputValue = '';
6103
6303
  this._error = '';
6104
6304
  this._userHasScrolledUp = false;
6105
- // Clear the composer without revoking the object URLs — the sent message now
6106
- // owns them for display.
6107
- this._pendingAttachments = [];
6305
+ // Clear the composer and release its preview object URLs.
6306
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6307
+ this._pendingInternalFiles = [];
6108
6308
  // Reset textarea height
6109
6309
  if (this._inputEl) {
6110
6310
  this._inputEl.style.height = 'auto';
6111
6311
  }
6112
6312
  // Add empty assistant message to stream into
6113
6313
  this.messages = [...this.messages, {
6114
- role: 'assistant',
6314
+ type: 'assistant',
6115
6315
  content: '',
6116
6316
  }];
6117
6317
  this._isStreaming = true;
6118
- this._statusText = '';
6119
6318
  // Scroll user's message to the top (Gemini-style).
6120
6319
  // Set min-height on the assistant message = container height so there's enough
6121
6320
  // scroll room below the user message to push it to the top.
@@ -6137,10 +6336,34 @@
6137
6336
  this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
6138
6337
  }
6139
6338
  });
6339
+ // Default to streaming from the Kodaris Bedrock conversation endpoints when
6340
+ // the consumer hasn't provided their own `send`.
6341
+ if (!this.send) {
6342
+ this.send = (params) => {
6343
+ // With images, post text + files as multipart; without, post plain JSON.
6344
+ if (params.internalFiles.length) {
6345
+ const form = new FormData();
6346
+ form.append('userText', params.message);
6347
+ params.internalFiles.forEach((file) => form.append('images', file));
6348
+ return krubbleHttp.Http.fetch({
6349
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithImages',
6350
+ method: 'POST',
6351
+ headers: { Accept: 'text/event-stream' },
6352
+ body: form,
6353
+ });
6354
+ }
6355
+ return krubbleHttp.Http.fetch({
6356
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
6357
+ method: 'POST',
6358
+ headers: { Accept: 'text/event-stream' },
6359
+ body: { userText: params.message },
6360
+ });
6361
+ };
6362
+ }
6140
6363
  this.send({
6141
6364
  message: messageText,
6142
6365
  messages: this.messages.slice(0, -1),
6143
- files,
6366
+ internalFiles,
6144
6367
  }).then((response) => {
6145
6368
  if (!response.ok) {
6146
6369
  this._handleStreamError('Request failed: ' + response.status);
@@ -6166,11 +6389,15 @@
6166
6389
  this._reader = null;
6167
6390
  }
6168
6391
  this._isStreaming = false;
6169
- this._statusText = '';
6170
6392
  }
6171
6393
  _handleClear() {
6394
+ // Default to clearing the Kodaris Bedrock conversation state when the consumer
6395
+ // hasn't provided their own `clear`.
6172
6396
  if (!this.clear) {
6173
- return;
6397
+ this.clear = () => krubbleHttp.Http.fetch({
6398
+ url: '/api/system/integration/aws/bedrock/clearConversationWithState',
6399
+ method: 'POST',
6400
+ });
6174
6401
  }
6175
6402
  this.clear().then(() => {
6176
6403
  if (this._reader) {
@@ -6179,7 +6406,6 @@
6179
6406
  }
6180
6407
  this.messages = [];
6181
6408
  this._isStreaming = false;
6182
- this._statusText = '';
6183
6409
  this._error = '';
6184
6410
  this._inputValue = '';
6185
6411
  this._userHasScrolledUp = false;
@@ -6205,7 +6431,6 @@
6205
6431
  this._reader.read().then((result) => {
6206
6432
  if (result.done) {
6207
6433
  this._isStreaming = false;
6208
- this._statusText = '';
6209
6434
  this._reader = null;
6210
6435
  return;
6211
6436
  }
@@ -6238,15 +6463,10 @@
6238
6463
  bubbles: true,
6239
6464
  composed: true,
6240
6465
  }));
6241
- if (data.type === 'STATUS') {
6242
- if (data.message) {
6243
- this._statusText = String(data.message);
6244
- }
6245
- }
6246
- else if (data.type === 'REASONING_PART') {
6466
+ if (data.type === 'REASONING_PART') {
6247
6467
  if (data.message) {
6248
6468
  const lastIndex = this.messages.length - 1;
6249
- if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
6469
+ if (lastIndex >= 0 && this.messages[lastIndex].type === 'assistant') {
6250
6470
  if (!this.messages[lastIndex].reasoning) {
6251
6471
  this.messages[lastIndex].reasoning = '';
6252
6472
  }
@@ -6257,9 +6477,8 @@
6257
6477
  }
6258
6478
  else if (data.type === 'RESPONSE_PART') {
6259
6479
  if (data.message) {
6260
- this._statusText = '';
6261
6480
  const lastIndex = this.messages.length - 1;
6262
- if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
6481
+ if (lastIndex >= 0 && this.messages[lastIndex].type === 'assistant') {
6263
6482
  this.messages[lastIndex].content += String(data.message);
6264
6483
  this.requestUpdate();
6265
6484
  }
@@ -6270,7 +6489,6 @@
6270
6489
  }
6271
6490
  if (data.finished) {
6272
6491
  this._isStreaming = false;
6273
- this._statusText = '';
6274
6492
  this._reader = null;
6275
6493
  }
6276
6494
  }
@@ -6284,7 +6502,6 @@
6284
6502
  }
6285
6503
  _handleStreamError(message) {
6286
6504
  this._isStreaming = false;
6287
- this._statusText = '';
6288
6505
  this._error = message;
6289
6506
  if (this._reader) {
6290
6507
  this._reader.cancel();
@@ -6293,15 +6510,128 @@
6293
6510
  // Remove the empty assistant message if it has no content
6294
6511
  if (this.messages.length > 0) {
6295
6512
  const lastMessage = this.messages[this.messages.length - 1];
6296
- if (lastMessage.role === 'assistant' && !lastMessage.content) {
6513
+ if (lastMessage.type === 'assistant' && !lastMessage.content) {
6297
6514
  this.messages = this.messages.slice(0, -1);
6298
6515
  }
6299
6516
  }
6300
6517
  }
6301
6518
  // --- Render ---
6519
+ /** The `/` skill menu shown above the composer while typing a slash command. */
6520
+ _renderSkillMenu() {
6521
+ const skills = this._filteredSkills();
6522
+ return b `
6523
+ <div class="skill-menu">
6524
+ <div class="skill-menu__actions">
6525
+ <button
6526
+ class=${e({
6527
+ 'skill-menu__action': true,
6528
+ 'skill-menu__item--highlighted': this._skillHighlighted === 0,
6529
+ })}
6530
+ @click=${this._openFilePicker}
6531
+ >
6532
+ Attach Files
6533
+ </button>
6534
+ <button
6535
+ class=${e({
6536
+ 'skill-menu__action': true,
6537
+ 'skill-menu__item--highlighted': this._skillHighlighted === 1,
6538
+ })}
6539
+ @click=${this._handleSkillBrowseOpen}
6540
+ >
6541
+ Browse All Skills
6542
+ </button>
6543
+ </div>
6544
+ ${skills.length ? b `
6545
+ <ul class="skill-menu__list">
6546
+ ${skills.map((skill, index) => b `
6547
+ <li>
6548
+ <button
6549
+ class=${e({
6550
+ 'skill-menu__item': true,
6551
+ 'skill-menu__item--highlighted': index + 2 === this._skillHighlighted,
6552
+ })}
6553
+ @click=${() => this._handleSkillSelect(skill)}
6554
+ >
6555
+ <span class="skill-menu__name">${skill.label}</span>
6556
+ ${skill.description ? b `
6557
+ <span class="skill-menu__desc">${skill.description}</span>
6558
+ ` : A}
6559
+ </button>
6560
+ </li>
6561
+ `)}
6562
+ </ul>
6563
+ ` : b `
6564
+ <div class="skill-menu__empty">No matching skills</div>
6565
+ `}
6566
+ </div>
6567
+ `;
6568
+ }
6569
+ /** The "Browse all skills" dialog — a searchable grid of Org / Kodaris cards. */
6570
+ _renderSkillBrowse() {
6571
+ return b `
6572
+ <kr-dialog
6573
+ opened
6574
+ label="Browse Skills"
6575
+ width="720px"
6576
+ @close=${this._handleSkillBrowseClose}
6577
+ >
6578
+ <div class="skill-browse">
6579
+ <kr-text-field
6580
+ placeholder="Search skills..."
6581
+ .value=${this._skillBrowseQuery}
6582
+ @input=${this._handleSkillBrowseSearch}
6583
+ ></kr-text-field>
6584
+ <kr-tab-group
6585
+ active-tab-id=${this._skillBrowseSource}
6586
+ @tab-change=${this._handleSkillBrowseTabChange}
6587
+ >
6588
+ <kr-tab id="org" label="Your Org">
6589
+ ${this._renderSkillGrid()}
6590
+ </kr-tab>
6591
+ <kr-tab id="kodaris" label="Kodaris">
6592
+ ${this._renderSkillGrid()}
6593
+ </kr-tab>
6594
+ </kr-tab-group>
6595
+ </div>
6596
+ </kr-dialog>
6597
+ `;
6598
+ }
6599
+ /** The card grid for the browse dialog's active tab. */
6600
+ _renderSkillGrid() {
6601
+ const skills = this._browseSkills();
6602
+ if (!skills.length) {
6603
+ return b `<div class="skill-browse__empty">No matching skills</div>`;
6604
+ }
6605
+ return b `
6606
+ <div class="skill-browse__grid">
6607
+ ${skills.map((skill) => b `
6608
+ <button
6609
+ class="skill-card"
6610
+ @click=${() => this._handleSkillSelect(skill)}
6611
+ >
6612
+ <span class="skill-card__name">${skill.label}</span>
6613
+ ${skill.description ? b `
6614
+ <span class="skill-card__desc">${skill.description}</span>
6615
+ ` : A}
6616
+ </button>
6617
+ `)}
6618
+ </div>
6619
+ `;
6620
+ }
6621
+ /**
6622
+ * Render a user message, highlighting a leading `/skill-name` token when it
6623
+ * matches a known skill (like the way Claude highlights slash commands).
6624
+ */
6625
+ _renderUserContent(content) {
6626
+ const match = content.match(/^\/(\S+)(\s[\s\S]*)?$/);
6627
+ if (match && this.skills.some((skill) => skill.name === match[1])) {
6628
+ return b `<span class="message-skill">/${match[1]}</span>${match[2] || ''}`;
6629
+ }
6630
+ return content;
6631
+ }
6302
6632
  render() {
6303
6633
  return b `
6304
- ${this.hideToggle ? A : b `
6634
+ ${(this.hideToggle || this.variant === 'inline') ? A : b `
6305
6635
  <button
6306
6636
  class=${e({
6307
6637
  'toggle': true,
@@ -6352,53 +6682,52 @@
6352
6682
  <span>Drop images to attach</span>
6353
6683
  </div>
6354
6684
  ` : A}
6355
- <div
6356
- class="resize resize--n"
6357
- data-dir="n"
6358
- @mousedown=${this._handleResizeMouseDown}
6359
- ></div>
6360
- <div
6361
- class="resize resize--s"
6362
- data-dir="s"
6363
- @mousedown=${this._handleResizeMouseDown}
6364
- ></div>
6365
- <div
6366
- class="resize resize--w"
6367
- data-dir="w"
6368
- @mousedown=${this._handleResizeMouseDown}
6369
- ></div>
6370
- <div
6371
- class="resize resize--e"
6372
- data-dir="e"
6373
- @mousedown=${this._handleResizeMouseDown}
6374
- ></div>
6375
- <div
6376
- class="resize resize--nw"
6377
- data-dir="nw"
6378
- @mousedown=${this._handleResizeMouseDown}
6379
- ></div>
6380
- <div
6381
- class="resize resize--ne"
6382
- data-dir="ne"
6383
- @mousedown=${this._handleResizeMouseDown}
6384
- ></div>
6385
- <div
6386
- class="resize resize--sw"
6387
- data-dir="sw"
6388
- @mousedown=${this._handleResizeMouseDown}
6389
- ></div>
6390
- <div
6391
- class="resize resize--se"
6392
- data-dir="se"
6393
- @mousedown=${this._handleResizeMouseDown}
6394
- ></div>
6685
+ ${this.variant === 'inline' ? A : b `
6686
+ <div
6687
+ class="resize resize--n"
6688
+ data-dir="n"
6689
+ @mousedown=${this._handleResizeMouseDown}
6690
+ ></div>
6691
+ <div
6692
+ class="resize resize--s"
6693
+ data-dir="s"
6694
+ @mousedown=${this._handleResizeMouseDown}
6695
+ ></div>
6696
+ <div
6697
+ class="resize resize--w"
6698
+ data-dir="w"
6699
+ @mousedown=${this._handleResizeMouseDown}
6700
+ ></div>
6701
+ <div
6702
+ class="resize resize--e"
6703
+ data-dir="e"
6704
+ @mousedown=${this._handleResizeMouseDown}
6705
+ ></div>
6706
+ <div
6707
+ class="resize resize--nw"
6708
+ data-dir="nw"
6709
+ @mousedown=${this._handleResizeMouseDown}
6710
+ ></div>
6711
+ <div
6712
+ class="resize resize--ne"
6713
+ data-dir="ne"
6714
+ @mousedown=${this._handleResizeMouseDown}
6715
+ ></div>
6716
+ <div
6717
+ class="resize resize--sw"
6718
+ data-dir="sw"
6719
+ @mousedown=${this._handleResizeMouseDown}
6720
+ ></div>
6721
+ <div
6722
+ class="resize resize--se"
6723
+ data-dir="se"
6724
+ @mousedown=${this._handleResizeMouseDown}
6725
+ ></div>
6726
+ `}
6395
6727
  <div class="header" @mousedown=${this._handleHeaderMouseDown}>
6396
6728
  <div class="header-left">
6397
6729
  <div class="header-info">
6398
- <span class="header-title">${this.title}</span>
6399
- ${this.subtitle ? b `
6400
- <span class="header-subtitle">${this.subtitle}</span>
6401
- ` : A}
6730
+ <span class="header-title">${this.label}</span>
6402
6731
  </div>
6403
6732
  </div>
6404
6733
  <div class="header-actions">
@@ -6424,77 +6753,124 @@
6424
6753
  </svg>
6425
6754
  </button>
6426
6755
  ` : A}
6427
- <button
6428
- class="header-action"
6429
- @click=${this._handleToggle}
6430
- title="Close"
6431
- >
6432
- <svg
6433
- xmlns="http://www.w3.org/2000/svg"
6434
- fill="none"
6435
- viewBox="0 0 24 24"
6436
- stroke-width="2"
6437
- stroke="currentColor"
6438
- class="header-action-icon"
6756
+ ${this.variant === 'inline' ? A : b `
6757
+ <button
6758
+ class="header-action"
6759
+ @click=${this._handleToggle}
6760
+ title="Close"
6439
6761
  >
6440
- <path
6441
- stroke-linecap="round"
6442
- stroke-linejoin="round"
6443
- d="M6 18 18 6M6 6l12 12"
6444
- />
6445
- </svg>
6446
- </button>
6762
+ <svg
6763
+ xmlns="http://www.w3.org/2000/svg"
6764
+ fill="none"
6765
+ viewBox="0 0 24 24"
6766
+ stroke-width="2"
6767
+ stroke="currentColor"
6768
+ class="header-action-icon"
6769
+ >
6770
+ <path
6771
+ stroke-linecap="round"
6772
+ stroke-linejoin="round"
6773
+ d="M6 18 18 6M6 6l12 12"
6774
+ />
6775
+ </svg>
6776
+ </button>
6777
+ `}
6447
6778
  </div>
6448
6779
  </div>
6449
6780
 
6450
6781
  <div class="messages" @scroll=${this._handleMessagesScroll}>
6451
- ${this.messages.length === 0 ? b `
6452
- <div class="empty">
6453
- <span class="empty-text">What can I help you with?</span>
6454
- </div>
6455
- ` : A}
6456
- ${this.messages.map((msg) => b `
6457
- <div class=${e({
6782
+ <div class="messages-inner">
6783
+ ${this.messages.length === 0 ? b `
6784
+ <div class="empty">
6785
+ <span class="empty-text">What can I help you with?</span>
6786
+ </div>
6787
+ ` : A}
6788
+ ${this.messages.map((msg, index) => b `
6789
+ <div class=${e({
6458
6790
  'message': true,
6459
- 'message--user': msg.role === 'user',
6460
- 'message--assistant': msg.role === 'assistant',
6791
+ 'message--user': msg.type === 'user',
6792
+ 'message--assistant': msg.type === 'assistant',
6461
6793
  })}>
6462
- ${msg.reasoning ? b `
6463
- <details class="reasoning">
6464
- <summary>Thinking</summary>
6465
- <div class="reasoning-content">${msg.reasoning}</div>
6466
- </details>
6467
- ` : A}
6468
- ${msg.attachments && msg.attachments.length ? b `
6469
- <div class="attachments">
6470
- ${msg.attachments.map((att) => b `
6471
- <button
6472
- class="attachment"
6473
- type="button"
6474
- title=${att.name || 'View image'}
6475
- @click=${() => this._openAttachment(att)}
6476
- >
6477
- <img src=${att.url} alt=${att.name || ''} />
6478
- </button>
6479
- `)}
6480
- </div>
6481
- ` : A}
6482
- ${msg.content ? b `
6483
- <div class="message-content">
6484
- ${msg.role === 'user'
6485
- ? msg.content
6794
+ ${msg.reasoning ? b `
6795
+ <details class="reasoning">
6796
+ <summary>Thinking</summary>
6797
+ <div class="reasoning-content">${msg.reasoning}</div>
6798
+ </details>
6799
+ ` : A}
6800
+ ${msg.internalFiles && msg.internalFiles.length ? b `
6801
+ <div class="attachments">
6802
+ ${msg.internalFiles.map((img) => b `
6803
+ <button
6804
+ class="attachment"
6805
+ type="button"
6806
+ title=${img.niceName || 'View image'}
6807
+ @click=${() => this._openImage(img)}
6808
+ >
6809
+ <img
6810
+ src="/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view"
6811
+ alt=${img.niceName || ''}
6812
+ />
6813
+ </button>
6814
+ `)}
6815
+ </div>
6816
+ ` : A}
6817
+ ${msg.content ? b `
6818
+ <div class="message-content">
6819
+ ${msg.type === 'user'
6820
+ ? this._renderUserContent(msg.content)
6486
6821
  : o(marked.parse(msg.content, { breaks: true }))}
6487
- </div>
6488
- ` : A}
6489
- ${msg.role === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? b `
6490
- <div class="typing">
6491
- <span></span>
6492
- <span></span>
6493
- <span></span>
6494
- </div>
6495
- ` : A}
6496
- </div>
6497
- `)}
6822
+ </div>
6823
+ ` : A}
6824
+ ${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? b `
6825
+ <div class="typing">
6826
+ <span></span>
6827
+ <span></span>
6828
+ <span></span>
6829
+ </div>
6830
+ ` : A}
6831
+ ${msg.content ? b `
6832
+ <button
6833
+ class="message-copy"
6834
+ title="Copy"
6835
+ @click=${() => this._handleCopyMessage(msg.content, index)}
6836
+ >
6837
+ ${this._copiedIndex === index ? b `
6838
+ <svg
6839
+ viewBox="0 0 24 24"
6840
+ fill="none"
6841
+ stroke="currentColor"
6842
+ stroke-width="1.5"
6843
+ stroke-linecap="round"
6844
+ stroke-linejoin="round"
6845
+ >
6846
+ <path d="M20 6 9 17l-5-5" />
6847
+ </svg>
6848
+ Copied
6849
+ ` : b `
6850
+ <svg
6851
+ viewBox="0 0 24 24"
6852
+ fill="none"
6853
+ stroke="currentColor"
6854
+ stroke-width="1.5"
6855
+ stroke-linecap="round"
6856
+ stroke-linejoin="round"
6857
+ >
6858
+ <rect
6859
+ x="9"
6860
+ y="9"
6861
+ width="13"
6862
+ height="13"
6863
+ rx="2"
6864
+ />
6865
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
6866
+ </svg>
6867
+ Copy
6868
+ `}
6869
+ </button>
6870
+ ` : A}
6871
+ </div>
6872
+ `)}
6873
+ </div>
6498
6874
  </div>
6499
6875
 
6500
6876
  ${this._error ? b `
@@ -6520,9 +6896,9 @@
6520
6896
  ` : A}
6521
6897
 
6522
6898
  <div class="footer">
6523
- ${this._pendingAttachments.length ? b `
6899
+ ${this._pendingInternalFiles.length ? b `
6524
6900
  <div class="composer-attachments">
6525
- ${c(this._pendingAttachments, (p) => p.localId, (p) => b `
6901
+ ${c(this._pendingInternalFiles, (p) => p.localId, (p) => b `
6526
6902
  <div class="chip">
6527
6903
  <img src=${p.previewUrl} alt=${p.name} />
6528
6904
  <button
@@ -6556,11 +6932,11 @@
6556
6932
  hidden
6557
6933
  @change=${this._handleFileInputChange}
6558
6934
  />
6935
+ ${this._skillMenuOpened ? this._renderSkillMenu() : A}
6559
6936
  <div class="input-wrapper">
6560
6937
  <button
6561
6938
  class="input-plus"
6562
- title="Attach image"
6563
- @click=${this._openFilePicker}
6939
+ @click=${this._handlePlusClick}
6564
6940
  >+</button>
6565
6941
  <textarea
6566
6942
  class="input"
@@ -6617,6 +6993,7 @@
6617
6993
  `}
6618
6994
  </div>
6619
6995
  </div>
6996
+ ${this._skillBrowseOpened ? this._renderSkillBrowse() : A}
6620
6997
  </div>
6621
6998
  `;
6622
6999
  }
@@ -6711,6 +7088,28 @@
6711
7088
  display: flex;
6712
7089
  }
6713
7090
 
7091
+ /* Inline mode: fill the parent container instead of floating bottom-right. */
7092
+ :host([variant='inline']) {
7093
+ position: static;
7094
+ bottom: auto;
7095
+ right: auto;
7096
+ z-index: auto;
7097
+ height: 100%;
7098
+ }
7099
+
7100
+ :host([variant='inline']) .panel {
7101
+ position: relative;
7102
+ bottom: auto;
7103
+ right: auto;
7104
+ width: 100%;
7105
+ height: 100%;
7106
+ max-height: none;
7107
+ border-radius: 0;
7108
+ box-shadow: none;
7109
+ border: none;
7110
+ animation: none;
7111
+ }
7112
+
6714
7113
  @keyframes chatbot-panel-in {
6715
7114
  from {
6716
7115
  opacity: 0;
@@ -6741,7 +7140,7 @@
6741
7140
  gap: 10px;
6742
7141
  }
6743
7142
 
6744
- .header-info {
7143
+ .header-info {
6745
7144
  display: flex;
6746
7145
  flex-direction: column;
6747
7146
  }
@@ -6751,22 +7150,6 @@
6751
7150
  font-weight: 600;
6752
7151
  }
6753
7152
 
6754
- .header-subtitle {
6755
- color: rgba(255, 255, 255, 0.5);
6756
- font-size: 11px;
6757
- display: flex;
6758
- align-items: center;
6759
- gap: 4px;
6760
- }
6761
-
6762
- .header-subtitle::before {
6763
- content: '';
6764
- width: 5px;
6765
- height: 5px;
6766
- background: var(--kr-chatbot-success);
6767
- border-radius: 50%;
6768
- }
6769
-
6770
7153
  .header-actions {
6771
7154
  display: flex;
6772
7155
  align-items: center;
@@ -6798,17 +7181,29 @@
6798
7181
  }
6799
7182
 
6800
7183
  /* Messages */
7184
+ /* Full-width scroll container — keeps the scrollbar at the panel edge. */
6801
7185
  .messages {
6802
7186
  flex: 1;
6803
7187
  overflow-y: auto;
6804
- padding: 32px;
6805
7188
  display: flex;
6806
7189
  flex-direction: column;
6807
- gap: 24px;
6808
7190
  scroll-behavior: smooth;
6809
7191
  background: white;
6810
7192
  }
6811
7193
 
7194
+ /* Centered, width-capped column that actually holds the messages. */
7195
+ .messages-inner {
7196
+ flex: 1;
7197
+ display: flex;
7198
+ flex-direction: column;
7199
+ gap: 24px;
7200
+ padding: 32px;
7201
+ width: 100%;
7202
+ max-width: 768px;
7203
+ margin-left: auto;
7204
+ margin-right: auto;
7205
+ }
7206
+
6812
7207
  .messages::-webkit-scrollbar {
6813
7208
  width: 4px;
6814
7209
  }
@@ -6832,12 +7227,13 @@
6832
7227
  gap: 8px;
6833
7228
  }
6834
7229
 
6835
- .empty-text {
7230
+ .empty-text {
6836
7231
  font-size: 14px;
6837
7232
  color: rgb(0 0 0 / 80%);
6838
7233
  }
6839
7234
 
6840
7235
  .message {
7236
+ position: relative;
6841
7237
  display: flex;
6842
7238
  flex-direction: column;
6843
7239
  max-width: 100%;
@@ -6882,6 +7278,58 @@
6882
7278
  font-size: 14px;
6883
7279
  }
6884
7280
 
7281
+ /* Highlighted leading /skill token in a sent user message. Monospace + a
7282
+ stronger color makes it read as a distinct command without a pill. */
7283
+ .message-skill {
7284
+ font-family: 'SF Mono', SFMono-Regular, ui-monospace, Menlo, Consolas, monospace;
7285
+ font-weight: 600;
7286
+ color: #b45309;
7287
+ }
7288
+
7289
+ /* Per-message copy button. Kept in flow with a fixed height that is always
7290
+ reserved (via a negative bottom margin that cancels it) so message spacing
7291
+ is identical whether or not it's shown; only its opacity toggles on hover. */
7292
+ .message-copy {
7293
+ align-self: flex-start;
7294
+ margin-top: 4px;
7295
+ margin-bottom: -27px;
7296
+ display: inline-flex;
7297
+ align-items: center;
7298
+ gap: 4px;
7299
+ height: 23px;
7300
+ padding: 4px 8px;
7301
+ background: none;
7302
+ border: none;
7303
+ border-radius: 6px;
7304
+ font: inherit;
7305
+ font-size: 12px;
7306
+ color: #64668b;
7307
+ cursor: pointer;
7308
+ opacity: 0;
7309
+ pointer-events: none;
7310
+ transition: opacity 0.15s, background-color 0.15s, color 0.15s;
7311
+ }
7312
+
7313
+ .message--user .message-copy {
7314
+ align-self: flex-end;
7315
+ }
7316
+
7317
+ .message:hover .message-copy,
7318
+ .message-copy:focus-visible {
7319
+ opacity: 1;
7320
+ pointer-events: auto;
7321
+ }
7322
+
7323
+ .message-copy:hover {
7324
+ background: rgba(0, 0, 0, 0.06);
7325
+ color: #1a1a2e;
7326
+ }
7327
+
7328
+ .message-copy svg {
7329
+ width: 14px;
7330
+ height: 14px;
7331
+ }
7332
+
6885
7333
  .message--assistant .message-content {
6886
7334
  background: transparent;
6887
7335
  color: #000000;
@@ -6944,7 +7392,7 @@
6944
7392
  word-wrap: break-word;
6945
7393
  }
6946
7394
 
6947
- .typing {
7395
+ .typing {
6948
7396
  display: flex;
6949
7397
  gap: 4px;
6950
7398
  padding: 4px 0;
@@ -7012,6 +7460,15 @@
7012
7460
  padding: 16px 16px 16px;
7013
7461
  flex-shrink: 0;
7014
7462
  background: white;
7463
+ width: 100%;
7464
+ max-width: 768px;
7465
+ margin-left: auto;
7466
+ margin-right: auto;
7467
+ }
7468
+
7469
+ /* Inline mode: keep the composer clear of the bottom edge. */
7470
+ :host([variant='inline']) .footer {
7471
+ padding-bottom: 40px;
7015
7472
  }
7016
7473
 
7017
7474
  .input-wrapper {
@@ -7030,6 +7487,15 @@
7030
7487
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
7031
7488
  }
7032
7489
 
7490
+ /* Inline mode: keep the border and add a soft floating shadow (ChatGPT/Gemini style). */
7491
+ :host([variant='inline']) .input-wrapper {
7492
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.1);
7493
+ }
7494
+
7495
+ :host([variant='inline']) .input-wrapper:focus-within {
7496
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.14);
7497
+ }
7498
+
7033
7499
  .input-plus {
7034
7500
  width: 34px;
7035
7501
  height: 34px;
@@ -7137,6 +7603,12 @@
7137
7603
  cursor: grabbing;
7138
7604
  }
7139
7605
 
7606
+ /* Inline mode isn't draggable — don't show the grab/hand cursor. */
7607
+ :host([variant='inline']) .header,
7608
+ :host([variant='inline']) .header:active {
7609
+ cursor: default;
7610
+ }
7611
+
7140
7612
  /* Drag / Resize states */
7141
7613
  .panel--dragging,
7142
7614
  .panel--resizing {
@@ -7193,6 +7665,176 @@
7193
7665
  display: block;
7194
7666
  }
7195
7667
 
7668
+ /* Skill menu (opened by typing '/'). Fixed + JS-positioned above the
7669
+ composer, following the krubble select-field dropdown pattern. */
7670
+ .skill-menu {
7671
+ position: fixed;
7672
+ z-index: 10000;
7673
+ background: white;
7674
+ border: 1px solid #9ba7b6;
7675
+ border-radius: 8px;
7676
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
7677
+ overflow: hidden;
7678
+ display: flex;
7679
+ flex-direction: column;
7680
+ /* max-height is set dynamically to the space above the composer. */
7681
+ }
7682
+
7683
+ /* Floating mode is a compact card — use a lighter shadow and softer border. */
7684
+ :host(:not([variant='inline'])) .skill-menu {
7685
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
7686
+ border-color: #b0b0b0;
7687
+ }
7688
+
7689
+ .skill-menu__list {
7690
+ list-style: none;
7691
+ margin: 0;
7692
+ padding: 8px 0;
7693
+ overflow-y: auto;
7694
+ }
7695
+
7696
+ .skill-menu__item {
7697
+ display: flex;
7698
+ flex-direction: column;
7699
+ gap: 2px;
7700
+ width: 100%;
7701
+ padding: 8px 16px;
7702
+ background: none;
7703
+ border: none;
7704
+ text-align: left;
7705
+ cursor: pointer;
7706
+ font: inherit;
7707
+ color: #000000;
7708
+ }
7709
+
7710
+
7711
+ /* Floating mode is a compact card — tighten name/description spacing. */
7712
+ :host(:not([variant='inline'])) .skill-menu__item {
7713
+ gap: 1px;
7714
+ }
7715
+
7716
+ .skill-menu__name {
7717
+ font-weight: 500;
7718
+ }
7719
+
7720
+ /* Floating mode is a compact card — shrink the skill name a touch. */
7721
+ :host(:not([variant='inline'])) .skill-menu__name {
7722
+ font-size: 13px;
7723
+ }
7724
+
7725
+ .skill-menu__desc {
7726
+ font-size: 12px;
7727
+ color: #000000;
7728
+ overflow: hidden;
7729
+ text-overflow: ellipsis;
7730
+ white-space: nowrap;
7731
+ }
7732
+
7733
+ .skill-menu__empty {
7734
+ padding: 16px;
7735
+ text-align: center;
7736
+ color: #000000;
7737
+ }
7738
+
7739
+ .skill-menu__actions {
7740
+ display: flex;
7741
+ flex-direction: column;
7742
+ padding: 8px 0;
7743
+ border-bottom: 1px solid #eee;
7744
+ flex-shrink: 0;
7745
+ }
7746
+
7747
+ .skill-menu__action {
7748
+ width: 100%;
7749
+ padding: 6px 16px;
7750
+ background: transparent;
7751
+ border: none;
7752
+ text-align: left;
7753
+ cursor: pointer;
7754
+ font: inherit;
7755
+ font-size: 13px;
7756
+ color: var(--kr-chatbot-primary);
7757
+ font-weight: 500;
7758
+ }
7759
+
7760
+ /* Highlight (keyboard) and hover (mouse) are independent — either shades the
7761
+ row. Declared after the base rules so it wins the cascade for both the
7762
+ action buttons and the skill items. */
7763
+ .skill-menu__action.skill-menu__item--highlighted,
7764
+ .skill-menu__item.skill-menu__item--highlighted,
7765
+ .skill-menu__action:hover,
7766
+ .skill-menu__item:hover {
7767
+ background: #f3f4f6;
7768
+ }
7769
+
7770
+ /* Browse-all dialog body. Owns the height so the dialog is the same size on
7771
+ every tab; the search is fixed and the card grid flexes/scrolls to fill. */
7772
+ .skill-browse {
7773
+ display: flex;
7774
+ flex-direction: column;
7775
+ height: 70vh;
7776
+ }
7777
+
7778
+ .skill-browse kr-text-field {
7779
+ display: block;
7780
+ margin: 20px 20px 16px;
7781
+ flex-shrink: 0;
7782
+ }
7783
+
7784
+ .skill-browse kr-tab-group {
7785
+ flex: 1;
7786
+ min-height: 0;
7787
+ }
7788
+
7789
+ /* The grid is the scroller and spans the full width, so its scrollbar sits
7790
+ flush against the dialog edge; padding-right insets the cards from it. */
7791
+ .skill-browse__grid {
7792
+ height: 100%;
7793
+ overflow-y: auto;
7794
+ display: grid;
7795
+ grid-template-columns: repeat(3, 1fr);
7796
+ grid-auto-rows: min-content;
7797
+ align-content: start;
7798
+ gap: 12px;
7799
+ padding: 16px 20px 20px;
7800
+ }
7801
+
7802
+ .skill-card {
7803
+ display: flex;
7804
+ flex-direction: column;
7805
+ gap: 6px;
7806
+ padding: 14px;
7807
+ background: white;
7808
+ border: 1px solid #e5e7eb;
7809
+ border-radius: 10px;
7810
+ text-align: left;
7811
+ cursor: pointer;
7812
+ font: inherit;
7813
+ color: #000000;
7814
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
7815
+ }
7816
+
7817
+ .skill-card:hover {
7818
+ border-color: var(--kr-chatbot-primary);
7819
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
7820
+ }
7821
+
7822
+ .skill-card__name {
7823
+ font-weight: 600;
7824
+ }
7825
+
7826
+ .skill-card__desc {
7827
+ font-size: 12px;
7828
+ color: #000000;
7829
+ }
7830
+
7831
+ .skill-browse__empty {
7832
+ height: 100%;
7833
+ padding: 32px;
7834
+ text-align: center;
7835
+ color: #000000;
7836
+ }
7837
+
7196
7838
  /* Composer attachment chips (pre-send) */
7197
7839
  .composer-attachments {
7198
7840
  display: flex;
@@ -7288,13 +7930,13 @@
7288
7930
  ], exports.Chatbot.prototype, "messages", void 0);
7289
7931
  __decorate([
7290
7932
  n({ type: String })
7291
- ], exports.Chatbot.prototype, "title", void 0);
7292
- __decorate([
7293
- n({ type: String })
7294
- ], exports.Chatbot.prototype, "subtitle", void 0);
7933
+ ], exports.Chatbot.prototype, "label", void 0);
7295
7934
  __decorate([
7296
7935
  n({ type: String })
7297
7936
  ], exports.Chatbot.prototype, "placeholder", void 0);
7937
+ __decorate([
7938
+ n({ attribute: false })
7939
+ ], exports.Chatbot.prototype, "skills", void 0);
7298
7940
  __decorate([
7299
7941
  n({
7300
7942
  type: Boolean,
@@ -7308,24 +7950,48 @@
7308
7950
  reflect: true
7309
7951
  })
7310
7952
  ], exports.Chatbot.prototype, "hideToggle", void 0);
7953
+ __decorate([
7954
+ n({
7955
+ type: String,
7956
+ reflect: true
7957
+ })
7958
+ ], exports.Chatbot.prototype, "variant", void 0);
7311
7959
  __decorate([
7312
7960
  r()
7313
7961
  ], exports.Chatbot.prototype, "_inputValue", void 0);
7314
7962
  __decorate([
7315
7963
  r()
7316
7964
  ], exports.Chatbot.prototype, "_isStreaming", void 0);
7317
- __decorate([
7318
- r()
7319
- ], exports.Chatbot.prototype, "_statusText", void 0);
7320
7965
  __decorate([
7321
7966
  r()
7322
7967
  ], exports.Chatbot.prototype, "_error", void 0);
7323
7968
  __decorate([
7324
7969
  r()
7325
- ], exports.Chatbot.prototype, "_pendingAttachments", void 0);
7970
+ ], exports.Chatbot.prototype, "_pendingInternalFiles", void 0);
7326
7971
  __decorate([
7327
7972
  r()
7328
7973
  ], exports.Chatbot.prototype, "_isDraggingFile", void 0);
7974
+ __decorate([
7975
+ r()
7976
+ ], exports.Chatbot.prototype, "_skillMenuOpened", void 0);
7977
+ __decorate([
7978
+ r()
7979
+ ], exports.Chatbot.prototype, "_skillQuery", void 0);
7980
+ __decorate([
7981
+ r()
7982
+ ], exports.Chatbot.prototype, "_skillHighlighted", void 0);
7983
+ __decorate([
7984
+ r()
7985
+ ], exports.Chatbot.prototype, "_copiedIndex", void 0);
7986
+ __decorate([
7987
+ r()
7988
+ ], exports.Chatbot.prototype, "_skillBrowseOpened", void 0);
7989
+ __decorate([
7990
+ r()
7991
+ ], exports.Chatbot.prototype, "_skillBrowseSource", void 0);
7992
+ __decorate([
7993
+ r()
7994
+ ], exports.Chatbot.prototype, "_skillBrowseQuery", void 0);
7329
7995
  __decorate([
7330
7996
  e$3('.panel')
7331
7997
  ], exports.Chatbot.prototype, "_panelEl", void 0);
@@ -7338,6 +8004,12 @@
7338
8004
  __decorate([
7339
8005
  e$3('.file-input')
7340
8006
  ], exports.Chatbot.prototype, "_fileInputEl", void 0);
8007
+ __decorate([
8008
+ e$3('.input-wrapper')
8009
+ ], exports.Chatbot.prototype, "_inputWrapperEl", void 0);
8010
+ __decorate([
8011
+ e$3('.skill-menu')
8012
+ ], exports.Chatbot.prototype, "_skillMenuEl", void 0);
7341
8013
  exports.Chatbot = __decorate([
7342
8014
  t$2('kr-chatbot')
7343
8015
  ], exports.Chatbot);