@kodaris/krubble-app-components 1.0.84 → 1.0.86

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.
@@ -5642,14 +5642,6 @@
5642
5642
  * from the Kodaris Bedrock conversation endpoints. Set this to override.
5643
5643
  */
5644
5644
  this.send = null;
5645
- /**
5646
- * Callback function called when the user clears the conversation.
5647
- * Must return a Promise — messages are only cleared if it resolves.
5648
- *
5649
- * Optional — when not set, the chatbot uses its built-in default that clears
5650
- * the Kodaris Bedrock conversation state. Set this to override.
5651
- */
5652
- this.clear = null;
5653
5645
  /**
5654
5646
  * Callback function called on init to load existing conversation messages.
5655
5647
  * Must return a Promise that resolves with an array of KRChatbotMessage.
@@ -5692,6 +5684,20 @@
5692
5684
  * full page or panel region.
5693
5685
  */
5694
5686
  this.variant = 'floating';
5687
+ /**
5688
+ * The conversation this chatbot's turns target (backend `conversationID`).
5689
+ *
5690
+ * - Set → every turn is sent with this `conversationID`, so the chat continues
5691
+ * that specific conversation.
5692
+ * - Unset → the first turn is sent with `createNew: true` (a fresh
5693
+ * conversation); the component then resolves the new conversation's id (via
5694
+ * `/conversation/last`) and pins it here so the rest of the session stays one
5695
+ * conversation instead of spawning a new one per message.
5696
+ *
5697
+ * Only used by the built-in `send` default; a custom `send` owns its own
5698
+ * targeting.
5699
+ */
5700
+ this.conversationId = null;
5695
5701
  // --- @state (internal) ---
5696
5702
  this._inputValue = '';
5697
5703
  this._isStreaming = false;
@@ -5730,7 +5736,7 @@
5730
5736
  /** True while the user is dragging the debug drawer's resize handle. */
5731
5737
  this._isDebugResizing = false;
5732
5738
  /** Images staged in the composer, before the message is sent. */
5733
- this._pendingInternalFiles = [];
5739
+ this._pendingConversationFiles = [];
5734
5740
  /** True while a file is being dragged over the panel (shows the drop overlay). */
5735
5741
  this._isDraggingFile = false;
5736
5742
  /** Whether the `/` skill menu is open above the composer. */
@@ -5900,7 +5906,7 @@
5900
5906
  document.removeEventListener('mousemove', this._handleMouseMove);
5901
5907
  document.removeEventListener('mouseup', this._handleMouseUp);
5902
5908
  // Release any object URLs still held by staged attachments.
5903
- this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5909
+ this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5904
5910
  }
5905
5911
  updated(changed) {
5906
5912
  if (changed.has('messages')) {
@@ -6233,7 +6239,7 @@
6233
6239
  this._skillMenuOpened = false;
6234
6240
  this._send({
6235
6241
  message: '/test-context-warning',
6236
- internalFiles: [],
6242
+ conversationFiles: [],
6237
6243
  });
6238
6244
  return;
6239
6245
  }
@@ -6241,7 +6247,7 @@
6241
6247
  this._skillMenuOpened = false;
6242
6248
  this._send({
6243
6249
  message: '/test-context-limit',
6244
- internalFiles: [],
6250
+ conversationFiles: [],
6245
6251
  });
6246
6252
  return;
6247
6253
  }
@@ -6558,6 +6564,10 @@
6558
6564
  }
6559
6565
  // --- Attachments ---
6560
6566
  _openFilePicker() {
6567
+ // Close the skill/attachment menu — opening the OS file picker is the end of
6568
+ // the menu interaction, and the picker is a separate window so the menu would
6569
+ // otherwise linger open behind it (and after a file is chosen).
6570
+ this._skillMenuOpened = false;
6561
6571
  this._fileInputEl?.click();
6562
6572
  }
6563
6573
  _handleFileInputChange(e) {
@@ -6639,49 +6649,49 @@
6639
6649
  name: file.name,
6640
6650
  previewUrl: URL.createObjectURL(file),
6641
6651
  };
6642
- this._pendingInternalFiles = [...this._pendingInternalFiles, pending];
6652
+ this._pendingConversationFiles = [...this._pendingConversationFiles, pending];
6643
6653
  });
6644
6654
  }
6645
6655
  _removePending(localId) {
6646
- const target = this._pendingInternalFiles.find((p) => p.localId === localId);
6656
+ const target = this._pendingConversationFiles.find((p) => p.localId === localId);
6647
6657
  if (target) {
6648
6658
  URL.revokeObjectURL(target.previewUrl);
6649
6659
  }
6650
- this._pendingInternalFiles = this._pendingInternalFiles.filter((p) => p.localId !== localId);
6660
+ this._pendingConversationFiles = this._pendingConversationFiles.filter((p) => p.localId !== localId);
6651
6661
  }
6652
6662
  _clearPending() {
6653
- this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6654
- this._pendingInternalFiles = [];
6663
+ this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6664
+ this._pendingConversationFiles = [];
6655
6665
  }
6656
6666
  /** Open the shared full-screen previewer for a message image. */
6657
6667
  _openImage(img) {
6658
6668
  krubbleComponents.FilePreview.open({
6659
- src: `/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view`,
6669
+ src: `/api/system/conversationFile/${img.conversationFileID}/view`,
6660
6670
  name: img.niceName || 'image',
6661
6671
  });
6662
6672
  }
6663
6673
  /** Whether the send button is active: needs text or at least one image. */
6664
6674
  _canSend() {
6665
- return Boolean(this._inputValue.trim()) || this._pendingInternalFiles.length > 0;
6675
+ return Boolean(this._inputValue.trim()) || this._pendingConversationFiles.length > 0;
6666
6676
  }
6667
6677
  _handleSubmit() {
6668
6678
  if (this._isStreaming) {
6669
6679
  return;
6670
6680
  }
6671
6681
  const messageText = this._inputValue.trim();
6672
- const pending = this._pendingInternalFiles;
6682
+ const pending = this._pendingConversationFiles;
6673
6683
  // Allow sending an image with no text, but require at least one of them.
6674
6684
  if (!messageText && !pending.length) {
6675
6685
  return;
6676
6686
  }
6677
6687
  // The raw files sent to the backend. Sent images are shown once the
6678
- // conversation reloads and they come back as stored internal files; the
6688
+ // conversation reloads and they come back as stored conversation files; the
6679
6689
  // just-sent message itself carries text only.
6680
- const internalFiles = pending.map((p) => p.file);
6690
+ const conversationFiles = pending.map((p) => p.file);
6681
6691
  const submitEvent = new CustomEvent('message-submit', {
6682
6692
  detail: {
6683
6693
  message: messageText,
6684
- internalFiles,
6694
+ conversationFiles,
6685
6695
  },
6686
6696
  bubbles: true,
6687
6697
  composed: true,
@@ -6693,7 +6703,7 @@
6693
6703
  }
6694
6704
  this._send({
6695
6705
  message: messageText,
6696
- internalFiles
6706
+ conversationFiles
6697
6707
  });
6698
6708
  }
6699
6709
  /**
@@ -6713,8 +6723,8 @@
6713
6723
  this._error = '';
6714
6724
  this._userHasScrolledUp = false;
6715
6725
  // Clear the composer and release its preview object URLs.
6716
- this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6717
- this._pendingInternalFiles = [];
6726
+ this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6727
+ this._pendingConversationFiles = [];
6718
6728
  // Reset textarea height
6719
6729
  if (this._inputEl) {
6720
6730
  this._inputEl.style.height = 'auto';
@@ -6750,30 +6760,74 @@
6750
6760
  // the consumer hasn't provided their own `send`.
6751
6761
  if (!this.send) {
6752
6762
  this.send = (params) => {
6753
- // With images, post text + files as multipart; without, post plain JSON.
6754
- if (params.internalFiles.length) {
6755
- const form = new FormData();
6756
- form.append('userText', params.message);
6757
- params.internalFiles.forEach((file) => form.append('images', file));
6763
+ // Images are upload-first: each file is stored as a conversation file
6764
+ // (type LIBRARY — the format lives in mimeType), then the resulting
6765
+ // conversationFileIDs are attached to the message via `fileIds` on the
6766
+ // normal JSON stream call. There is no multipart chat endpoint. With no
6767
+ // images the upload resolves to an empty list immediately; a failed
6768
+ // upload is dropped so an undefined id never reaches `fileIds`.
6769
+ let uploaded = Promise.resolve([]);
6770
+ if (params.conversationFiles.length) {
6771
+ uploaded = Promise.all(params.conversationFiles.map((file) => {
6772
+ const form = new FormData();
6773
+ form.append('file', file);
6774
+ form.append('type', 'LIBRARY');
6775
+ form.append('niceName', file.name);
6776
+ return krubbleHttp.Http.fetch({
6777
+ url: '/api/system/conversationFile/upload',
6778
+ method: 'POST',
6779
+ body: form,
6780
+ })
6781
+ .then((r) => r.json())
6782
+ .then((res) => res?.data?.conversationFileID);
6783
+ })).then((ids) => ids.filter((id) => typeof id === 'number'));
6784
+ }
6785
+ return uploaded.then((fileIds) => {
6786
+ // Conversation targeting: continue `conversationId` when set, else
6787
+ // start a fresh conversation (createNew) for this turn. The pin below
6788
+ // means createNew only fires on the very first untargeted turn.
6789
+ const body = {
6790
+ userText: params.message,
6791
+ fileIds,
6792
+ };
6793
+ if (this.conversationId != null) {
6794
+ body.conversationID = this.conversationId;
6795
+ }
6796
+ else {
6797
+ body.createNew = true;
6798
+ }
6758
6799
  return krubbleHttp.Http.fetch({
6759
- url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithImages',
6800
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
6760
6801
  method: 'POST',
6761
6802
  headers: { Accept: 'text/event-stream' },
6762
- body: form,
6803
+ body,
6804
+ }).then((response) => {
6805
+ // Pin the session to the conversation this turn used. The stream
6806
+ // doesn't echo the resolved id, so when we let the server create one
6807
+ // (no conversationId set) we resolve it via `/last` and keep it, so
6808
+ // subsequent turns continue this conversation instead of each sending
6809
+ // createNew and spawning a new one. Best-effort, fire-and-forget: a
6810
+ // failure leaves it unpinned and the next turn resolves server-side.
6811
+ if (this.conversationId == null) {
6812
+ krubbleHttp.Http.fetch({ url: '/api/system/conversation/last' })
6813
+ .then((r) => r.json())
6814
+ .then((res) => {
6815
+ const id = res?.data?.conversationID;
6816
+ if (typeof id === 'number') {
6817
+ this.conversationId = id;
6818
+ }
6819
+ })
6820
+ .catch(() => { });
6821
+ }
6822
+ return response;
6763
6823
  });
6764
- }
6765
- return krubbleHttp.Http.fetch({
6766
- url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
6767
- method: 'POST',
6768
- headers: { Accept: 'text/event-stream' },
6769
- body: { userText: params.message },
6770
6824
  });
6771
6825
  };
6772
6826
  }
6773
6827
  this.send({
6774
6828
  message: params.message,
6775
6829
  messages: this.messages.slice(0, -1),
6776
- internalFiles: params.internalFiles,
6830
+ conversationFiles: params.conversationFiles,
6777
6831
  }).then((response) => {
6778
6832
  if (!response.ok) {
6779
6833
  this._handleStreamError('Request failed: ' + response.status);
@@ -6800,35 +6854,6 @@
6800
6854
  }
6801
6855
  this._isStreaming = false;
6802
6856
  }
6803
- _handleClear() {
6804
- // Default to clearing the Kodaris Bedrock conversation state when the consumer
6805
- // hasn't provided their own `clear`.
6806
- if (!this.clear) {
6807
- this.clear = () => krubbleHttp.Http.fetch({
6808
- url: '/api/system/integration/aws/bedrock/clearConversationWithState',
6809
- method: 'POST',
6810
- });
6811
- }
6812
- this.clear().then(() => {
6813
- if (this._reader) {
6814
- this._reader.cancel();
6815
- this._reader = null;
6816
- }
6817
- this.messages = [];
6818
- this._isStreaming = false;
6819
- this._error = '';
6820
- this._inputValue = '';
6821
- this._userHasScrolledUp = false;
6822
- // Reset conversation-scoped context + telemetry state for the new convo.
6823
- this._contextWarning = '';
6824
- this._contextLimitReached = false;
6825
- this._contextLimitMessage = '';
6826
- this._telemetry = null;
6827
- this._iterationLog = [];
6828
- this._cacheCheckpoints = {};
6829
- this._saveState();
6830
- });
6831
- }
6832
6857
  _handleMessagesScroll(e) {
6833
6858
  const container = e.target;
6834
6859
  const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
@@ -7438,28 +7463,6 @@
7438
7463
  </svg>
7439
7464
  </button>
7440
7465
  ` : A}
7441
- ${this.messages.length > 0 ? b `
7442
- <button
7443
- class="header-action"
7444
- @click=${this._handleClear}
7445
- title="Clear conversation"
7446
- >
7447
- <svg
7448
- xmlns="http://www.w3.org/2000/svg"
7449
- fill="none"
7450
- viewBox="0 0 24 24"
7451
- stroke-width="1.5"
7452
- stroke="currentColor"
7453
- class="header-action-icon"
7454
- >
7455
- <path
7456
- stroke-linecap="round"
7457
- stroke-linejoin="round"
7458
- d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
7459
- />
7460
- </svg>
7461
- </button>
7462
- ` : A}
7463
7466
  ${this.variant === 'inline' ? A : b `
7464
7467
  <button
7465
7468
  class="header-action"
@@ -7504,9 +7507,9 @@
7504
7507
  <div class="reasoning-content">${msg.reasoning}</div>
7505
7508
  </details>
7506
7509
  ` : A}
7507
- ${msg.internalFiles && msg.internalFiles.length ? b `
7510
+ ${msg.conversationFiles && msg.conversationFiles.length ? b `
7508
7511
  <div class="attachments">
7509
- ${msg.internalFiles.map((img) => b `
7512
+ ${msg.conversationFiles.map((img) => b `
7510
7513
  <button
7511
7514
  class="attachment"
7512
7515
  type="button"
@@ -7514,7 +7517,7 @@
7514
7517
  @click=${() => this._openImage(img)}
7515
7518
  >
7516
7519
  <img
7517
- src="/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view"
7520
+ src="/api/system/conversationFile/${img.conversationFileID}/view"
7518
7521
  alt=${img.niceName || ''}
7519
7522
  />
7520
7523
  </button>
@@ -7632,9 +7635,9 @@
7632
7635
  ` : A}
7633
7636
 
7634
7637
  <div class="footer">
7635
- ${this._pendingInternalFiles.length ? b `
7638
+ ${this._pendingConversationFiles.length ? b `
7636
7639
  <div class="composer-attachments">
7637
- ${c(this._pendingInternalFiles, (p) => p.localId, (p) => b `
7640
+ ${c(this._pendingConversationFiles, (p) => p.localId, (p) => b `
7638
7641
  <div class="chip">
7639
7642
  <img src=${p.previewUrl} alt=${p.name} />
7640
7643
  <button
@@ -8924,9 +8927,6 @@
8924
8927
  __decorate([
8925
8928
  n$1({ attribute: false })
8926
8929
  ], exports.Chatbot.prototype, "send", void 0);
8927
- __decorate([
8928
- n$1({ attribute: false })
8929
- ], exports.Chatbot.prototype, "clear", void 0);
8930
8930
  __decorate([
8931
8931
  n$1({ attribute: false })
8932
8932
  ], exports.Chatbot.prototype, "load", void 0);
@@ -8958,6 +8958,13 @@
8958
8958
  reflect: true
8959
8959
  })
8960
8960
  ], exports.Chatbot.prototype, "variant", void 0);
8961
+ __decorate([
8962
+ n$1({
8963
+ type: Number,
8964
+ attribute: 'conversation-id',
8965
+ reflect: true
8966
+ })
8967
+ ], exports.Chatbot.prototype, "conversationId", void 0);
8961
8968
  __decorate([
8962
8969
  r()
8963
8970
  ], exports.Chatbot.prototype, "_inputValue", void 0);
@@ -8996,7 +9003,7 @@
8996
9003
  ], exports.Chatbot.prototype, "_isDebugResizing", void 0);
8997
9004
  __decorate([
8998
9005
  r()
8999
- ], exports.Chatbot.prototype, "_pendingInternalFiles", void 0);
9006
+ ], exports.Chatbot.prototype, "_pendingConversationFiles", void 0);
9000
9007
  __decorate([
9001
9008
  r()
9002
9009
  ], exports.Chatbot.prototype, "_isDraggingFile", void 0);