@kodaris/krubble-app-components 1.0.70 → 1.0.71

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,13 +5640,9 @@
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
  */
@@ -5652,13 +5657,23 @@
5652
5657
  * then driven via the `open()` / `close()` / `toggle()` methods.
5653
5658
  */
5654
5659
  this.hideToggle = false;
5660
+ /**
5661
+ * Layout mode.
5662
+ * - `'floating'` (default): the chatbot is a fixed bottom-right widget with a
5663
+ * toggle button, and the panel can be dragged and resized. This is the
5664
+ * traditional chat-widget appearance.
5665
+ * - `'inline'`: the chatbot drops its fixed positioning and fills its parent
5666
+ * container (100% width/height, no border radius/shadow, no toggle, no
5667
+ * drag/resize). The panel is always open. Use this to embed the chat as a
5668
+ * full page or panel region.
5669
+ */
5670
+ this.variant = 'floating';
5655
5671
  // --- @state (internal) ---
5656
5672
  this._inputValue = '';
5657
5673
  this._isStreaming = false;
5658
- this._statusText = '';
5659
5674
  this._error = '';
5660
5675
  /** Images staged in the composer, before the message is sent. */
5661
- this._pendingAttachments = [];
5676
+ this._pendingInternalFiles = [];
5662
5677
  /** True while a file is being dragged over the panel (shows the drop overlay). */
5663
5678
  this._isDraggingFile = false;
5664
5679
  this._handleMouseMove = (e) => {
@@ -5722,12 +5737,27 @@
5722
5737
  // --- Lifecycle ---
5723
5738
  connectedCallback() {
5724
5739
  super.connectedCallback();
5725
- if (this.load) {
5726
- this.load().then((messages) => {
5727
- if (messages && messages.length) {
5728
- this.messages = messages;
5729
- }
5730
- }).catch(() => { });
5740
+ // Default to loading existing messages from the Kodaris Bedrock conversation
5741
+ // when the consumer hasn't provided their own `load`. The endpoint returns
5742
+ // messages already in KRChatbotMessage shape ({ type, content, images }), so
5743
+ // they pass straight through.
5744
+ if (!this.load) {
5745
+ this.load = () => krubbleHttp.Http.fetch({
5746
+ url: '/api/system/integration/aws/bedrock/getConversationMessages',
5747
+ })
5748
+ .then((r) => r.json())
5749
+ .then((res) => res.data || []);
5750
+ }
5751
+ this.load().then((messages) => {
5752
+ if (messages && messages.length) {
5753
+ this.messages = messages;
5754
+ }
5755
+ }).catch(() => { });
5756
+ // Inline mode fills its container and is always open — the floating widget's
5757
+ // toggle/drag/resize state (including any persisted layout) doesn't apply.
5758
+ if (this.variant === 'inline') {
5759
+ this.opened = true;
5760
+ return;
5731
5761
  }
5732
5762
  this._restoreState();
5733
5763
  }
@@ -5740,10 +5770,10 @@
5740
5770
  document.removeEventListener('mousemove', this._handleMouseMove);
5741
5771
  document.removeEventListener('mouseup', this._handleMouseUp);
5742
5772
  // Release any object URLs still held by staged attachments.
5743
- this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5773
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5744
5774
  }
5745
5775
  updated(changed) {
5746
- if (changed.has('messages') || changed.has('_statusText')) {
5776
+ if (changed.has('messages')) {
5747
5777
  if (!this._userHasScrolledUp && !this._isStreaming && this._messagesEl) {
5748
5778
  requestAnimationFrame(() => {
5749
5779
  if (this._messagesEl) {
@@ -5817,6 +5847,10 @@
5817
5847
  }
5818
5848
  }
5819
5849
  _handleHeaderMouseDown(e) {
5850
+ // Inline mode fills its parent — dragging the panel doesn't apply.
5851
+ if (this.variant === 'inline') {
5852
+ return;
5853
+ }
5820
5854
  // Ignore clicks on buttons inside the header
5821
5855
  if (e.target.closest('button')) {
5822
5856
  return;
@@ -5842,6 +5876,10 @@
5842
5876
  document.addEventListener('mouseup', this._handleMouseUp);
5843
5877
  }
5844
5878
  _handleResizeMouseDown(e) {
5879
+ // Inline mode fills its parent — resizing the panel doesn't apply.
5880
+ if (this.variant === 'inline') {
5881
+ return;
5882
+ }
5845
5883
  if (!this._panelEl) {
5846
5884
  return;
5847
5885
  }
@@ -6034,54 +6072,49 @@
6034
6072
  name: file.name,
6035
6073
  previewUrl: URL.createObjectURL(file),
6036
6074
  };
6037
- this._pendingAttachments = [...this._pendingAttachments, pending];
6075
+ this._pendingInternalFiles = [...this._pendingInternalFiles, pending];
6038
6076
  });
6039
6077
  }
6040
6078
  _removePending(localId) {
6041
- const target = this._pendingAttachments.find((p) => p.localId === localId);
6079
+ const target = this._pendingInternalFiles.find((p) => p.localId === localId);
6042
6080
  if (target) {
6043
6081
  URL.revokeObjectURL(target.previewUrl);
6044
6082
  }
6045
- this._pendingAttachments = this._pendingAttachments.filter((p) => p.localId !== localId);
6083
+ this._pendingInternalFiles = this._pendingInternalFiles.filter((p) => p.localId !== localId);
6046
6084
  }
6047
6085
  _clearPending() {
6048
- this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6049
- this._pendingAttachments = [];
6086
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6087
+ this._pendingInternalFiles = [];
6050
6088
  }
6051
- /** Open the shared full-screen previewer for an attachment. */
6052
- _openAttachment(att) {
6089
+ /** Open the shared full-screen previewer for a message image. */
6090
+ _openImage(img) {
6053
6091
  krubbleComponents.FilePreview.open({
6054
- src: att.url,
6055
- name: att.name || 'image',
6092
+ src: `/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view`,
6093
+ name: img.niceName || 'image',
6056
6094
  });
6057
6095
  }
6058
6096
  /** Whether the send button is active: needs text or at least one image. */
6059
6097
  _canSend() {
6060
- return Boolean(this._inputValue.trim()) || this._pendingAttachments.length > 0;
6098
+ return Boolean(this._inputValue.trim()) || this._pendingInternalFiles.length > 0;
6061
6099
  }
6062
6100
  _handleSubmit() {
6063
- if (!this.send || this._isStreaming) {
6101
+ if (this._isStreaming) {
6064
6102
  return;
6065
6103
  }
6066
6104
  const messageText = this._inputValue.trim();
6067
- const pending = this._pendingAttachments;
6105
+ const pending = this._pendingInternalFiles;
6068
6106
  // Allow sending an image with no text, but require at least one of them.
6069
6107
  if (!messageText && !pending.length) {
6070
6108
  return;
6071
6109
  }
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
- }));
6110
+ // The raw files sent to the backend. Sent images are shown once the
6111
+ // conversation reloads and they come back as stored internal files; the
6112
+ // just-sent message itself carries text only.
6113
+ const internalFiles = pending.map((p) => p.file);
6081
6114
  const submitEvent = new CustomEvent('message-submit', {
6082
6115
  detail: {
6083
6116
  message: messageText,
6084
- files,
6117
+ internalFiles,
6085
6118
  },
6086
6119
  bubbles: true,
6087
6120
  composed: true,
@@ -6092,30 +6125,26 @@
6092
6125
  return;
6093
6126
  }
6094
6127
  const userMessage = {
6095
- role: 'user',
6128
+ type: 'user',
6096
6129
  content: messageText,
6097
6130
  };
6098
- if (attachments.length) {
6099
- userMessage.attachments = attachments;
6100
- }
6101
6131
  this.messages = [...this.messages, userMessage];
6102
6132
  this._inputValue = '';
6103
6133
  this._error = '';
6104
6134
  this._userHasScrolledUp = false;
6105
- // Clear the composer without revoking the object URLs — the sent message now
6106
- // owns them for display.
6107
- this._pendingAttachments = [];
6135
+ // Clear the composer and release its preview object URLs.
6136
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6137
+ this._pendingInternalFiles = [];
6108
6138
  // Reset textarea height
6109
6139
  if (this._inputEl) {
6110
6140
  this._inputEl.style.height = 'auto';
6111
6141
  }
6112
6142
  // Add empty assistant message to stream into
6113
6143
  this.messages = [...this.messages, {
6114
- role: 'assistant',
6144
+ type: 'assistant',
6115
6145
  content: '',
6116
6146
  }];
6117
6147
  this._isStreaming = true;
6118
- this._statusText = '';
6119
6148
  // Scroll user's message to the top (Gemini-style).
6120
6149
  // Set min-height on the assistant message = container height so there's enough
6121
6150
  // scroll room below the user message to push it to the top.
@@ -6137,10 +6166,34 @@
6137
6166
  this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
6138
6167
  }
6139
6168
  });
6169
+ // Default to streaming from the Kodaris Bedrock conversation endpoints when
6170
+ // the consumer hasn't provided their own `send`.
6171
+ if (!this.send) {
6172
+ this.send = (params) => {
6173
+ // With images, post text + files as multipart; without, post plain JSON.
6174
+ if (params.internalFiles.length) {
6175
+ const form = new FormData();
6176
+ form.append('userText', params.message);
6177
+ params.internalFiles.forEach((file) => form.append('images', file));
6178
+ return krubbleHttp.Http.fetch({
6179
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithImages',
6180
+ method: 'POST',
6181
+ headers: { Accept: 'text/event-stream' },
6182
+ body: form,
6183
+ });
6184
+ }
6185
+ return krubbleHttp.Http.fetch({
6186
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
6187
+ method: 'POST',
6188
+ headers: { Accept: 'text/event-stream' },
6189
+ body: { userText: params.message },
6190
+ });
6191
+ };
6192
+ }
6140
6193
  this.send({
6141
6194
  message: messageText,
6142
6195
  messages: this.messages.slice(0, -1),
6143
- files,
6196
+ internalFiles,
6144
6197
  }).then((response) => {
6145
6198
  if (!response.ok) {
6146
6199
  this._handleStreamError('Request failed: ' + response.status);
@@ -6166,11 +6219,15 @@
6166
6219
  this._reader = null;
6167
6220
  }
6168
6221
  this._isStreaming = false;
6169
- this._statusText = '';
6170
6222
  }
6171
6223
  _handleClear() {
6224
+ // Default to clearing the Kodaris Bedrock conversation state when the consumer
6225
+ // hasn't provided their own `clear`.
6172
6226
  if (!this.clear) {
6173
- return;
6227
+ this.clear = () => krubbleHttp.Http.fetch({
6228
+ url: '/api/system/integration/aws/bedrock/clearConversationWithState',
6229
+ method: 'POST',
6230
+ });
6174
6231
  }
6175
6232
  this.clear().then(() => {
6176
6233
  if (this._reader) {
@@ -6179,7 +6236,6 @@
6179
6236
  }
6180
6237
  this.messages = [];
6181
6238
  this._isStreaming = false;
6182
- this._statusText = '';
6183
6239
  this._error = '';
6184
6240
  this._inputValue = '';
6185
6241
  this._userHasScrolledUp = false;
@@ -6205,7 +6261,6 @@
6205
6261
  this._reader.read().then((result) => {
6206
6262
  if (result.done) {
6207
6263
  this._isStreaming = false;
6208
- this._statusText = '';
6209
6264
  this._reader = null;
6210
6265
  return;
6211
6266
  }
@@ -6238,15 +6293,10 @@
6238
6293
  bubbles: true,
6239
6294
  composed: true,
6240
6295
  }));
6241
- if (data.type === 'STATUS') {
6242
- if (data.message) {
6243
- this._statusText = String(data.message);
6244
- }
6245
- }
6246
- else if (data.type === 'REASONING_PART') {
6296
+ if (data.type === 'REASONING_PART') {
6247
6297
  if (data.message) {
6248
6298
  const lastIndex = this.messages.length - 1;
6249
- if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
6299
+ if (lastIndex >= 0 && this.messages[lastIndex].type === 'assistant') {
6250
6300
  if (!this.messages[lastIndex].reasoning) {
6251
6301
  this.messages[lastIndex].reasoning = '';
6252
6302
  }
@@ -6257,9 +6307,8 @@
6257
6307
  }
6258
6308
  else if (data.type === 'RESPONSE_PART') {
6259
6309
  if (data.message) {
6260
- this._statusText = '';
6261
6310
  const lastIndex = this.messages.length - 1;
6262
- if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
6311
+ if (lastIndex >= 0 && this.messages[lastIndex].type === 'assistant') {
6263
6312
  this.messages[lastIndex].content += String(data.message);
6264
6313
  this.requestUpdate();
6265
6314
  }
@@ -6270,7 +6319,6 @@
6270
6319
  }
6271
6320
  if (data.finished) {
6272
6321
  this._isStreaming = false;
6273
- this._statusText = '';
6274
6322
  this._reader = null;
6275
6323
  }
6276
6324
  }
@@ -6284,7 +6332,6 @@
6284
6332
  }
6285
6333
  _handleStreamError(message) {
6286
6334
  this._isStreaming = false;
6287
- this._statusText = '';
6288
6335
  this._error = message;
6289
6336
  if (this._reader) {
6290
6337
  this._reader.cancel();
@@ -6293,7 +6340,7 @@
6293
6340
  // Remove the empty assistant message if it has no content
6294
6341
  if (this.messages.length > 0) {
6295
6342
  const lastMessage = this.messages[this.messages.length - 1];
6296
- if (lastMessage.role === 'assistant' && !lastMessage.content) {
6343
+ if (lastMessage.type === 'assistant' && !lastMessage.content) {
6297
6344
  this.messages = this.messages.slice(0, -1);
6298
6345
  }
6299
6346
  }
@@ -6301,7 +6348,7 @@
6301
6348
  // --- Render ---
6302
6349
  render() {
6303
6350
  return b `
6304
- ${this.hideToggle ? A : b `
6351
+ ${(this.hideToggle || this.variant === 'inline') ? A : b `
6305
6352
  <button
6306
6353
  class=${e({
6307
6354
  'toggle': true,
@@ -6352,53 +6399,52 @@
6352
6399
  <span>Drop images to attach</span>
6353
6400
  </div>
6354
6401
  ` : 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>
6402
+ ${this.variant === 'inline' ? A : b `
6403
+ <div
6404
+ class="resize resize--n"
6405
+ data-dir="n"
6406
+ @mousedown=${this._handleResizeMouseDown}
6407
+ ></div>
6408
+ <div
6409
+ class="resize resize--s"
6410
+ data-dir="s"
6411
+ @mousedown=${this._handleResizeMouseDown}
6412
+ ></div>
6413
+ <div
6414
+ class="resize resize--w"
6415
+ data-dir="w"
6416
+ @mousedown=${this._handleResizeMouseDown}
6417
+ ></div>
6418
+ <div
6419
+ class="resize resize--e"
6420
+ data-dir="e"
6421
+ @mousedown=${this._handleResizeMouseDown}
6422
+ ></div>
6423
+ <div
6424
+ class="resize resize--nw"
6425
+ data-dir="nw"
6426
+ @mousedown=${this._handleResizeMouseDown}
6427
+ ></div>
6428
+ <div
6429
+ class="resize resize--ne"
6430
+ data-dir="ne"
6431
+ @mousedown=${this._handleResizeMouseDown}
6432
+ ></div>
6433
+ <div
6434
+ class="resize resize--sw"
6435
+ data-dir="sw"
6436
+ @mousedown=${this._handleResizeMouseDown}
6437
+ ></div>
6438
+ <div
6439
+ class="resize resize--se"
6440
+ data-dir="se"
6441
+ @mousedown=${this._handleResizeMouseDown}
6442
+ ></div>
6443
+ `}
6395
6444
  <div class="header" @mousedown=${this._handleHeaderMouseDown}>
6396
6445
  <div class="header-left">
6397
6446
  <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}
6447
+ <span class="header-title">${this.label}</span>
6402
6448
  </div>
6403
6449
  </div>
6404
6450
  <div class="header-actions">
@@ -6424,77 +6470,84 @@
6424
6470
  </svg>
6425
6471
  </button>
6426
6472
  ` : 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"
6473
+ ${this.variant === 'inline' ? A : b `
6474
+ <button
6475
+ class="header-action"
6476
+ @click=${this._handleToggle}
6477
+ title="Close"
6439
6478
  >
6440
- <path
6441
- stroke-linecap="round"
6442
- stroke-linejoin="round"
6443
- d="M6 18 18 6M6 6l12 12"
6444
- />
6445
- </svg>
6446
- </button>
6479
+ <svg
6480
+ xmlns="http://www.w3.org/2000/svg"
6481
+ fill="none"
6482
+ viewBox="0 0 24 24"
6483
+ stroke-width="2"
6484
+ stroke="currentColor"
6485
+ class="header-action-icon"
6486
+ >
6487
+ <path
6488
+ stroke-linecap="round"
6489
+ stroke-linejoin="round"
6490
+ d="M6 18 18 6M6 6l12 12"
6491
+ />
6492
+ </svg>
6493
+ </button>
6494
+ `}
6447
6495
  </div>
6448
6496
  </div>
6449
6497
 
6450
6498
  <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({
6499
+ <div class="messages-inner">
6500
+ ${this.messages.length === 0 ? b `
6501
+ <div class="empty">
6502
+ <span class="empty-text">What can I help you with?</span>
6503
+ </div>
6504
+ ` : A}
6505
+ ${this.messages.map((msg) => b `
6506
+ <div class=${e({
6458
6507
  'message': true,
6459
- 'message--user': msg.role === 'user',
6460
- 'message--assistant': msg.role === 'assistant',
6508
+ 'message--user': msg.type === 'user',
6509
+ 'message--assistant': msg.type === 'assistant',
6461
6510
  })}>
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'
6511
+ ${msg.reasoning ? b `
6512
+ <details class="reasoning">
6513
+ <summary>Thinking</summary>
6514
+ <div class="reasoning-content">${msg.reasoning}</div>
6515
+ </details>
6516
+ ` : A}
6517
+ ${msg.internalFiles && msg.internalFiles.length ? b `
6518
+ <div class="attachments">
6519
+ ${msg.internalFiles.map((img) => b `
6520
+ <button
6521
+ class="attachment"
6522
+ type="button"
6523
+ title=${img.niceName || 'View image'}
6524
+ @click=${() => this._openImage(img)}
6525
+ >
6526
+ <img
6527
+ src="/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view"
6528
+ alt=${img.niceName || ''}
6529
+ />
6530
+ </button>
6531
+ `)}
6532
+ </div>
6533
+ ` : A}
6534
+ ${msg.content ? b `
6535
+ <div class="message-content">
6536
+ ${msg.type === 'user'
6485
6537
  ? msg.content
6486
6538
  : 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
- `)}
6539
+ </div>
6540
+ ` : A}
6541
+ ${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? b `
6542
+ <div class="typing">
6543
+ <span></span>
6544
+ <span></span>
6545
+ <span></span>
6546
+ </div>
6547
+ ` : A}
6548
+ </div>
6549
+ `)}
6550
+ </div>
6498
6551
  </div>
6499
6552
 
6500
6553
  ${this._error ? b `
@@ -6520,9 +6573,9 @@
6520
6573
  ` : A}
6521
6574
 
6522
6575
  <div class="footer">
6523
- ${this._pendingAttachments.length ? b `
6576
+ ${this._pendingInternalFiles.length ? b `
6524
6577
  <div class="composer-attachments">
6525
- ${c(this._pendingAttachments, (p) => p.localId, (p) => b `
6578
+ ${c(this._pendingInternalFiles, (p) => p.localId, (p) => b `
6526
6579
  <div class="chip">
6527
6580
  <img src=${p.previewUrl} alt=${p.name} />
6528
6581
  <button
@@ -6711,6 +6764,28 @@
6711
6764
  display: flex;
6712
6765
  }
6713
6766
 
6767
+ /* Inline mode: fill the parent container instead of floating bottom-right. */
6768
+ :host([variant='inline']) {
6769
+ position: static;
6770
+ bottom: auto;
6771
+ right: auto;
6772
+ z-index: auto;
6773
+ height: 100%;
6774
+ }
6775
+
6776
+ :host([variant='inline']) .panel {
6777
+ position: relative;
6778
+ bottom: auto;
6779
+ right: auto;
6780
+ width: 100%;
6781
+ height: 100%;
6782
+ max-height: none;
6783
+ border-radius: 0;
6784
+ box-shadow: none;
6785
+ border: none;
6786
+ animation: none;
6787
+ }
6788
+
6714
6789
  @keyframes chatbot-panel-in {
6715
6790
  from {
6716
6791
  opacity: 0;
@@ -6741,7 +6816,7 @@
6741
6816
  gap: 10px;
6742
6817
  }
6743
6818
 
6744
- .header-info {
6819
+ .header-info {
6745
6820
  display: flex;
6746
6821
  flex-direction: column;
6747
6822
  }
@@ -6751,22 +6826,6 @@
6751
6826
  font-weight: 600;
6752
6827
  }
6753
6828
 
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
6829
  .header-actions {
6771
6830
  display: flex;
6772
6831
  align-items: center;
@@ -6798,17 +6857,29 @@
6798
6857
  }
6799
6858
 
6800
6859
  /* Messages */
6860
+ /* Full-width scroll container — keeps the scrollbar at the panel edge. */
6801
6861
  .messages {
6802
6862
  flex: 1;
6803
6863
  overflow-y: auto;
6804
- padding: 32px;
6805
6864
  display: flex;
6806
6865
  flex-direction: column;
6807
- gap: 24px;
6808
6866
  scroll-behavior: smooth;
6809
6867
  background: white;
6810
6868
  }
6811
6869
 
6870
+ /* Centered, width-capped column that actually holds the messages. */
6871
+ .messages-inner {
6872
+ flex: 1;
6873
+ display: flex;
6874
+ flex-direction: column;
6875
+ gap: 24px;
6876
+ padding: 32px;
6877
+ width: 100%;
6878
+ max-width: 768px;
6879
+ margin-left: auto;
6880
+ margin-right: auto;
6881
+ }
6882
+
6812
6883
  .messages::-webkit-scrollbar {
6813
6884
  width: 4px;
6814
6885
  }
@@ -6832,7 +6903,7 @@
6832
6903
  gap: 8px;
6833
6904
  }
6834
6905
 
6835
- .empty-text {
6906
+ .empty-text {
6836
6907
  font-size: 14px;
6837
6908
  color: rgb(0 0 0 / 80%);
6838
6909
  }
@@ -6944,7 +7015,7 @@
6944
7015
  word-wrap: break-word;
6945
7016
  }
6946
7017
 
6947
- .typing {
7018
+ .typing {
6948
7019
  display: flex;
6949
7020
  gap: 4px;
6950
7021
  padding: 4px 0;
@@ -7012,6 +7083,15 @@
7012
7083
  padding: 16px 16px 16px;
7013
7084
  flex-shrink: 0;
7014
7085
  background: white;
7086
+ width: 100%;
7087
+ max-width: 768px;
7088
+ margin-left: auto;
7089
+ margin-right: auto;
7090
+ }
7091
+
7092
+ /* Inline mode: keep the composer clear of the bottom edge. */
7093
+ :host([variant='inline']) .footer {
7094
+ padding-bottom: 40px;
7015
7095
  }
7016
7096
 
7017
7097
  .input-wrapper {
@@ -7030,6 +7110,15 @@
7030
7110
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
7031
7111
  }
7032
7112
 
7113
+ /* Inline mode: keep the border and add a soft floating shadow (ChatGPT/Gemini style). */
7114
+ :host([variant='inline']) .input-wrapper {
7115
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.1);
7116
+ }
7117
+
7118
+ :host([variant='inline']) .input-wrapper:focus-within {
7119
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.14);
7120
+ }
7121
+
7033
7122
  .input-plus {
7034
7123
  width: 34px;
7035
7124
  height: 34px;
@@ -7137,6 +7226,12 @@
7137
7226
  cursor: grabbing;
7138
7227
  }
7139
7228
 
7229
+ /* Inline mode isn't draggable — don't show the grab/hand cursor. */
7230
+ :host([variant='inline']) .header,
7231
+ :host([variant='inline']) .header:active {
7232
+ cursor: default;
7233
+ }
7234
+
7140
7235
  /* Drag / Resize states */
7141
7236
  .panel--dragging,
7142
7237
  .panel--resizing {
@@ -7288,10 +7383,7 @@
7288
7383
  ], exports.Chatbot.prototype, "messages", void 0);
7289
7384
  __decorate([
7290
7385
  n({ type: String })
7291
- ], exports.Chatbot.prototype, "title", void 0);
7292
- __decorate([
7293
- n({ type: String })
7294
- ], exports.Chatbot.prototype, "subtitle", void 0);
7386
+ ], exports.Chatbot.prototype, "label", void 0);
7295
7387
  __decorate([
7296
7388
  n({ type: String })
7297
7389
  ], exports.Chatbot.prototype, "placeholder", void 0);
@@ -7308,21 +7400,24 @@
7308
7400
  reflect: true
7309
7401
  })
7310
7402
  ], exports.Chatbot.prototype, "hideToggle", void 0);
7403
+ __decorate([
7404
+ n({
7405
+ type: String,
7406
+ reflect: true
7407
+ })
7408
+ ], exports.Chatbot.prototype, "variant", void 0);
7311
7409
  __decorate([
7312
7410
  r()
7313
7411
  ], exports.Chatbot.prototype, "_inputValue", void 0);
7314
7412
  __decorate([
7315
7413
  r()
7316
7414
  ], exports.Chatbot.prototype, "_isStreaming", void 0);
7317
- __decorate([
7318
- r()
7319
- ], exports.Chatbot.prototype, "_statusText", void 0);
7320
7415
  __decorate([
7321
7416
  r()
7322
7417
  ], exports.Chatbot.prototype, "_error", void 0);
7323
7418
  __decorate([
7324
7419
  r()
7325
- ], exports.Chatbot.prototype, "_pendingAttachments", void 0);
7420
+ ], exports.Chatbot.prototype, "_pendingInternalFiles", void 0);
7326
7421
  __decorate([
7327
7422
  r()
7328
7423
  ], exports.Chatbot.prototype, "_isDraggingFile", void 0);