@kodaris/krubble-app-components 1.0.85 → 1.0.87

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. */
@@ -5856,16 +5862,32 @@
5856
5862
  // --- Lifecycle ---
5857
5863
  connectedCallback() {
5858
5864
  super.connectedCallback();
5859
- // Default to loading existing messages from the Kodaris Bedrock conversation
5860
- // when the consumer hasn't provided their own `load`. The endpoint returns
5861
- // messages already in KRChatbotMessage shape ({ type, content, images }), so
5862
- // they pass straight through.
5865
+ // Default to loading a specific conversation's messages when the consumer
5866
+ // hasn't provided their own `load`. With `conversationId` set we rebuild that
5867
+ // conversation from `/conversation/{id}/messages`; with none set there's
5868
+ // nothing to resume, so start empty. Rows already match KRChatbotMessage
5869
+ // ({ type, content }); we only parse each row's `conversationFileIDs` (a JSON
5870
+ // array string of ids) into the `conversationFiles` the render iterates.
5863
5871
  if (!this.load) {
5864
- this.load = () => krubbleHttp.Http.fetch({
5865
- url: '/api/system/integration/aws/bedrock/getConversationMessages',
5866
- })
5867
- .then((r) => r.json())
5868
- .then((res) => res.data || []);
5872
+ this.load = () => {
5873
+ if (this.conversationId == null) {
5874
+ return Promise.resolve([]);
5875
+ }
5876
+ return krubbleHttp.Http.fetch({
5877
+ url: `/api/system/conversation/${this.conversationId}/messages`,
5878
+ })
5879
+ .then((r) => r.json())
5880
+ .then((res) => (res?.data || []).map((msg) => {
5881
+ try {
5882
+ const ids = JSON.parse(msg.conversationFileIDs || '[]');
5883
+ if (ids.length) {
5884
+ msg.conversationFiles = ids.map((id) => ({ conversationFileID: id }));
5885
+ }
5886
+ }
5887
+ catch { /* no/invalid attachments — leave empty */ }
5888
+ return msg;
5889
+ }));
5890
+ };
5869
5891
  }
5870
5892
  this.load().then((messages) => {
5871
5893
  if (messages && messages.length) {
@@ -5900,7 +5922,7 @@
5900
5922
  document.removeEventListener('mousemove', this._handleMouseMove);
5901
5923
  document.removeEventListener('mouseup', this._handleMouseUp);
5902
5924
  // Release any object URLs still held by staged attachments.
5903
- this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5925
+ this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
5904
5926
  }
5905
5927
  updated(changed) {
5906
5928
  if (changed.has('messages')) {
@@ -6233,7 +6255,7 @@
6233
6255
  this._skillMenuOpened = false;
6234
6256
  this._send({
6235
6257
  message: '/test-context-warning',
6236
- internalFiles: [],
6258
+ conversationFiles: [],
6237
6259
  });
6238
6260
  return;
6239
6261
  }
@@ -6241,7 +6263,7 @@
6241
6263
  this._skillMenuOpened = false;
6242
6264
  this._send({
6243
6265
  message: '/test-context-limit',
6244
- internalFiles: [],
6266
+ conversationFiles: [],
6245
6267
  });
6246
6268
  return;
6247
6269
  }
@@ -6558,6 +6580,10 @@
6558
6580
  }
6559
6581
  // --- Attachments ---
6560
6582
  _openFilePicker() {
6583
+ // Close the skill/attachment menu — opening the OS file picker is the end of
6584
+ // the menu interaction, and the picker is a separate window so the menu would
6585
+ // otherwise linger open behind it (and after a file is chosen).
6586
+ this._skillMenuOpened = false;
6561
6587
  this._fileInputEl?.click();
6562
6588
  }
6563
6589
  _handleFileInputChange(e) {
@@ -6639,49 +6665,49 @@
6639
6665
  name: file.name,
6640
6666
  previewUrl: URL.createObjectURL(file),
6641
6667
  };
6642
- this._pendingInternalFiles = [...this._pendingInternalFiles, pending];
6668
+ this._pendingConversationFiles = [...this._pendingConversationFiles, pending];
6643
6669
  });
6644
6670
  }
6645
6671
  _removePending(localId) {
6646
- const target = this._pendingInternalFiles.find((p) => p.localId === localId);
6672
+ const target = this._pendingConversationFiles.find((p) => p.localId === localId);
6647
6673
  if (target) {
6648
6674
  URL.revokeObjectURL(target.previewUrl);
6649
6675
  }
6650
- this._pendingInternalFiles = this._pendingInternalFiles.filter((p) => p.localId !== localId);
6676
+ this._pendingConversationFiles = this._pendingConversationFiles.filter((p) => p.localId !== localId);
6651
6677
  }
6652
6678
  _clearPending() {
6653
- this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6654
- this._pendingInternalFiles = [];
6679
+ this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6680
+ this._pendingConversationFiles = [];
6655
6681
  }
6656
6682
  /** Open the shared full-screen previewer for a message image. */
6657
6683
  _openImage(img) {
6658
6684
  krubbleComponents.FilePreview.open({
6659
- src: `/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view`,
6685
+ src: `/api/system/conversationFile/${img.conversationFileID}/view`,
6660
6686
  name: img.niceName || 'image',
6661
6687
  });
6662
6688
  }
6663
6689
  /** Whether the send button is active: needs text or at least one image. */
6664
6690
  _canSend() {
6665
- return Boolean(this._inputValue.trim()) || this._pendingInternalFiles.length > 0;
6691
+ return Boolean(this._inputValue.trim()) || this._pendingConversationFiles.length > 0;
6666
6692
  }
6667
6693
  _handleSubmit() {
6668
6694
  if (this._isStreaming) {
6669
6695
  return;
6670
6696
  }
6671
6697
  const messageText = this._inputValue.trim();
6672
- const pending = this._pendingInternalFiles;
6698
+ const pending = this._pendingConversationFiles;
6673
6699
  // Allow sending an image with no text, but require at least one of them.
6674
6700
  if (!messageText && !pending.length) {
6675
6701
  return;
6676
6702
  }
6677
6703
  // 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
6704
+ // conversation reloads and they come back as stored conversation files; the
6679
6705
  // just-sent message itself carries text only.
6680
- const internalFiles = pending.map((p) => p.file);
6706
+ const conversationFiles = pending.map((p) => p.file);
6681
6707
  const submitEvent = new CustomEvent('message-submit', {
6682
6708
  detail: {
6683
6709
  message: messageText,
6684
- internalFiles,
6710
+ conversationFiles,
6685
6711
  },
6686
6712
  bubbles: true,
6687
6713
  composed: true,
@@ -6693,7 +6719,7 @@
6693
6719
  }
6694
6720
  this._send({
6695
6721
  message: messageText,
6696
- internalFiles
6722
+ conversationFiles
6697
6723
  });
6698
6724
  }
6699
6725
  /**
@@ -6713,8 +6739,8 @@
6713
6739
  this._error = '';
6714
6740
  this._userHasScrolledUp = false;
6715
6741
  // Clear the composer and release its preview object URLs.
6716
- this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6717
- this._pendingInternalFiles = [];
6742
+ this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6743
+ this._pendingConversationFiles = [];
6718
6744
  // Reset textarea height
6719
6745
  if (this._inputEl) {
6720
6746
  this._inputEl.style.height = 'auto';
@@ -6750,30 +6776,74 @@
6750
6776
  // the consumer hasn't provided their own `send`.
6751
6777
  if (!this.send) {
6752
6778
  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));
6779
+ // Images are upload-first: each file is stored as a conversation file
6780
+ // (type LIBRARY — the format lives in mimeType), then the resulting
6781
+ // conversationFileIDs are attached to the message via `fileIds` on the
6782
+ // normal JSON stream call. There is no multipart chat endpoint. With no
6783
+ // images the upload resolves to an empty list immediately; a failed
6784
+ // upload is dropped so an undefined id never reaches `fileIds`.
6785
+ let uploaded = Promise.resolve([]);
6786
+ if (params.conversationFiles.length) {
6787
+ uploaded = Promise.all(params.conversationFiles.map((file) => {
6788
+ const form = new FormData();
6789
+ form.append('file', file);
6790
+ form.append('type', 'LIBRARY');
6791
+ form.append('niceName', file.name);
6792
+ return krubbleHttp.Http.fetch({
6793
+ url: '/api/system/conversationFile/upload',
6794
+ method: 'POST',
6795
+ body: form,
6796
+ })
6797
+ .then((r) => r.json())
6798
+ .then((res) => res?.data?.conversationFileID);
6799
+ })).then((ids) => ids.filter((id) => typeof id === 'number'));
6800
+ }
6801
+ return uploaded.then((fileIds) => {
6802
+ // Conversation targeting: continue `conversationId` when set, else
6803
+ // start a fresh conversation (createNew) for this turn. The pin below
6804
+ // means createNew only fires on the very first untargeted turn.
6805
+ const body = {
6806
+ userText: params.message,
6807
+ fileIds,
6808
+ };
6809
+ if (this.conversationId != null) {
6810
+ body.conversationID = this.conversationId;
6811
+ }
6812
+ else {
6813
+ body.createNew = true;
6814
+ }
6758
6815
  return krubbleHttp.Http.fetch({
6759
- url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithImages',
6816
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
6760
6817
  method: 'POST',
6761
6818
  headers: { Accept: 'text/event-stream' },
6762
- body: form,
6819
+ body,
6820
+ }).then((response) => {
6821
+ // Pin the session to the conversation this turn used. The stream
6822
+ // doesn't echo the resolved id, so when we let the server create one
6823
+ // (no conversationId set) we resolve it via `/last` and keep it, so
6824
+ // subsequent turns continue this conversation instead of each sending
6825
+ // createNew and spawning a new one. Best-effort, fire-and-forget: a
6826
+ // failure leaves it unpinned and the next turn resolves server-side.
6827
+ if (this.conversationId == null) {
6828
+ krubbleHttp.Http.fetch({ url: '/api/system/conversation/last' })
6829
+ .then((r) => r.json())
6830
+ .then((res) => {
6831
+ const id = res?.data?.conversationID;
6832
+ if (typeof id === 'number') {
6833
+ this.conversationId = id;
6834
+ }
6835
+ })
6836
+ .catch(() => { });
6837
+ }
6838
+ return response;
6763
6839
  });
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
6840
  });
6771
6841
  };
6772
6842
  }
6773
6843
  this.send({
6774
6844
  message: params.message,
6775
6845
  messages: this.messages.slice(0, -1),
6776
- internalFiles: params.internalFiles,
6846
+ conversationFiles: params.conversationFiles,
6777
6847
  }).then((response) => {
6778
6848
  if (!response.ok) {
6779
6849
  this._handleStreamError('Request failed: ' + response.status);
@@ -6800,35 +6870,6 @@
6800
6870
  }
6801
6871
  this._isStreaming = false;
6802
6872
  }
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
6873
  _handleMessagesScroll(e) {
6833
6874
  const container = e.target;
6834
6875
  const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
@@ -7438,28 +7479,6 @@
7438
7479
  </svg>
7439
7480
  </button>
7440
7481
  ` : 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
7482
  ${this.variant === 'inline' ? A : b `
7464
7483
  <button
7465
7484
  class="header-action"
@@ -7504,9 +7523,9 @@
7504
7523
  <div class="reasoning-content">${msg.reasoning}</div>
7505
7524
  </details>
7506
7525
  ` : A}
7507
- ${msg.internalFiles && msg.internalFiles.length ? b `
7526
+ ${msg.conversationFiles && msg.conversationFiles.length ? b `
7508
7527
  <div class="attachments">
7509
- ${msg.internalFiles.map((img) => b `
7528
+ ${msg.conversationFiles.map((img) => b `
7510
7529
  <button
7511
7530
  class="attachment"
7512
7531
  type="button"
@@ -7514,7 +7533,7 @@
7514
7533
  @click=${() => this._openImage(img)}
7515
7534
  >
7516
7535
  <img
7517
- src="/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view"
7536
+ src="/api/system/conversationFile/${img.conversationFileID}/view"
7518
7537
  alt=${img.niceName || ''}
7519
7538
  />
7520
7539
  </button>
@@ -7632,9 +7651,9 @@
7632
7651
  ` : A}
7633
7652
 
7634
7653
  <div class="footer">
7635
- ${this._pendingInternalFiles.length ? b `
7654
+ ${this._pendingConversationFiles.length ? b `
7636
7655
  <div class="composer-attachments">
7637
- ${c(this._pendingInternalFiles, (p) => p.localId, (p) => b `
7656
+ ${c(this._pendingConversationFiles, (p) => p.localId, (p) => b `
7638
7657
  <div class="chip">
7639
7658
  <img src=${p.previewUrl} alt=${p.name} />
7640
7659
  <button
@@ -8924,9 +8943,6 @@
8924
8943
  __decorate([
8925
8944
  n$1({ attribute: false })
8926
8945
  ], exports.Chatbot.prototype, "send", void 0);
8927
- __decorate([
8928
- n$1({ attribute: false })
8929
- ], exports.Chatbot.prototype, "clear", void 0);
8930
8946
  __decorate([
8931
8947
  n$1({ attribute: false })
8932
8948
  ], exports.Chatbot.prototype, "load", void 0);
@@ -8958,6 +8974,13 @@
8958
8974
  reflect: true
8959
8975
  })
8960
8976
  ], exports.Chatbot.prototype, "variant", void 0);
8977
+ __decorate([
8978
+ n$1({
8979
+ type: Number,
8980
+ attribute: 'conversation-id',
8981
+ reflect: true
8982
+ })
8983
+ ], exports.Chatbot.prototype, "conversationId", void 0);
8961
8984
  __decorate([
8962
8985
  r()
8963
8986
  ], exports.Chatbot.prototype, "_inputValue", void 0);
@@ -8996,7 +9019,7 @@
8996
9019
  ], exports.Chatbot.prototype, "_isDebugResizing", void 0);
8997
9020
  __decorate([
8998
9021
  r()
8999
- ], exports.Chatbot.prototype, "_pendingInternalFiles", void 0);
9022
+ ], exports.Chatbot.prototype, "_pendingConversationFiles", void 0);
9000
9023
  __decorate([
9001
9024
  r()
9002
9025
  ], exports.Chatbot.prototype, "_isDraggingFile", void 0);