@kodaris/krubble-app-components 1.0.90 → 1.0.92

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.
@@ -685,6 +685,19 @@
685
685
  *
686
686
  * @param itemId - The unique identifier of the nav group to toggle
687
687
  */
688
+ /**
689
+ * Expand a nav group by id. Public so consumers can keep a group open after
690
+ * adding it dynamically — the scaffold only auto-expands groups once at startup,
691
+ * so a group added later (e.g. an async "Recent" list) would otherwise stay
692
+ * collapsed. Idempotent; a no-op if the group is already expanded.
693
+ */
694
+ expandNavGroup(groupId) {
695
+ if (this.navItemsExpanded.has(groupId)) {
696
+ return;
697
+ }
698
+ this.navItemsExpanded.add(groupId);
699
+ this.requestUpdate();
700
+ }
688
701
  toggleNavItem(itemId) {
689
702
  const navItem = this.shadowRoot?.querySelector(`.nav-item[data-id="${itemId}"]`);
690
703
  const groupEl = navItem?.nextElementSibling;
@@ -6693,10 +6706,13 @@
6693
6706
  }
6694
6707
  /** Open the shared full-screen previewer for a message image. */
6695
6708
  _openImage(img) {
6696
- // Prefix the configured backend base URL — a raw fetch of a relative path
6697
- // would hit the page origin (e.g. the dev server), not the backend.
6709
+ // Use the local preview if the upload hasn't finished yet; otherwise the
6710
+ // backend view URL (prefixed with the base URL — a raw fetch of a relative
6711
+ // path would hit the page origin, e.g. the dev server, not the backend).
6712
+ const src = img.previewUrl
6713
+ || `${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view`;
6698
6714
  const preview = krubbleComponents.FilePreview.open({
6699
- src: `${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view`,
6715
+ src,
6700
6716
  name: img.niceName || 'image',
6701
6717
  });
6702
6718
  // The previewer's download button only fires a `download` event — the
@@ -6753,13 +6769,24 @@
6753
6769
  type: 'user',
6754
6770
  content: params.message,
6755
6771
  };
6772
+ // Attach the staged files' local previews to the message so its thumbnails
6773
+ // show INSTANTLY, without waiting on the upload. Each carries a temporary
6774
+ // previewUrl (and conversationFileID -1 as a placeholder); the built-in send
6775
+ // swaps these for the real server files once the upload returns.
6776
+ if (this._pendingConversationFiles.length) {
6777
+ userMessage.conversationFiles = this._pendingConversationFiles.map((p) => ({
6778
+ conversationFileID: -1,
6779
+ niceName: p.name,
6780
+ previewUrl: p.previewUrl,
6781
+ }));
6782
+ }
6756
6783
  this.messages = [...this.messages, userMessage];
6757
6784
  this._inputValue = '';
6758
6785
  this._saveState();
6759
6786
  this._error = '';
6760
6787
  this._userHasScrolledUp = false;
6761
- // Clear the composer and release its preview object URLs.
6762
- this._pendingConversationFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
6788
+ // Clear the composer, but DON'T revoke the preview object URLs — the sent
6789
+ // message now renders them until the upload's real files replace them.
6763
6790
  this._pendingConversationFiles = [];
6764
6791
  // Reset textarea height
6765
6792
  if (this._inputEl) {
@@ -6815,16 +6842,37 @@
6815
6842
  body: form,
6816
6843
  })
6817
6844
  .then((r) => r.json())
6818
- .then((res) => res?.data?.conversationFileID);
6819
- })).then((ids) => ids.filter((id) => typeof id === 'number'));
6845
+ // Keep the whole file DTO the upload returns (id + niceName +
6846
+ // mimeType + …), not just the id — it's attached to the sent message
6847
+ // below so the attachment shows immediately.
6848
+ .then((res) => res.data);
6849
+ }));
6820
6850
  }
6821
- return uploaded.then((fileIds) => {
6851
+ return uploaded.then((files) => {
6852
+ // Replace the just-sent user message's local previews with the real
6853
+ // uploaded files (backed by the stored file's view URL). The last user
6854
+ // message is the one _send appended before the trailing empty assistant
6855
+ // message; revoke its now-defunct preview object URLs first.
6856
+ if (files.length) {
6857
+ for (let i = this.messages.length - 1; i >= 0; i--) {
6858
+ if (this.messages[i].type === 'user') {
6859
+ (this.messages[i].conversationFiles || []).forEach((f) => {
6860
+ if (f.previewUrl) {
6861
+ URL.revokeObjectURL(f.previewUrl);
6862
+ }
6863
+ });
6864
+ this.messages[i].conversationFiles = files;
6865
+ this.requestUpdate();
6866
+ break;
6867
+ }
6868
+ }
6869
+ }
6822
6870
  // Conversation targeting: continue `conversationId` when set, else
6823
6871
  // start a fresh conversation (createNew) for this turn. The pin below
6824
6872
  // means createNew only fires on the very first untargeted turn.
6825
6873
  const body = {
6826
6874
  userText: params.message,
6827
- fileIds,
6875
+ fileIds: files.map((f) => f.conversationFileID),
6828
6876
  };
6829
6877
  if (this.conversationId != null) {
6830
6878
  body.conversationID = this.conversationId;
@@ -7563,7 +7611,7 @@
7563
7611
  @click=${() => this._openImage(img)}
7564
7612
  >
7565
7613
  <img
7566
- src="${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view"
7614
+ src=${img.previewUrl || `${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view`}
7567
7615
  alt=${img.niceName || ''}
7568
7616
  />
7569
7617
  </button>