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