@kodaris/krubble-app-components 1.0.87 → 1.0.88

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.
@@ -5862,31 +5862,54 @@
5862
5862
  // --- Lifecycle ---
5863
5863
  connectedCallback() {
5864
5864
  super.connectedCallback();
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.
5865
+ // Default to loading a conversation's messages when the consumer hasn't
5866
+ // provided their own `load`. With `conversationId` set we rebuild that
5867
+ // conversation; with none set we resume the caller's most recent one via
5868
+ // `/conversation/last` (KRHttp rejects its 404 when they have none yet →
5869
+ // stays null → empty chat) and pin it so this session continues it. Rows
5870
+ // already match KRChatbotMessage ({ type, content }); we only parse each row's
5871
+ // `conversationFileIDs` (a JSON array string of ids) into `conversationFiles`.
5871
5872
  if (!this.load) {
5872
5873
  this.load = () => {
5874
+ let resolve = Promise.resolve();
5873
5875
  if (this.conversationId == null) {
5874
- return Promise.resolve([]);
5876
+ resolve = krubbleHttp.Http.fetch({ url: '/api/system/conversation/last' })
5877
+ .then((r) => r.json())
5878
+ .then((res) => {
5879
+ this.conversationId = res?.data?.conversationID ?? null;
5880
+ })
5881
+ .catch(() => { });
5875
5882
  }
5876
- return krubbleHttp.Http.fetch({
5877
- url: `/api/system/conversation/${this.conversationId}/messages`,
5883
+ return resolve
5884
+ .then(() => {
5885
+ if (this.conversationId == null) {
5886
+ return { data: [] };
5887
+ }
5888
+ return krubbleHttp.Http.fetch({ url: `/api/system/conversation/${this.conversationId}/messages` }).then((r) => r.json());
5878
5889
  })
5879
- .then((r) => r.json())
5880
- .then((res) => (res?.data || []).map((msg) => {
5890
+ .then((res) => Promise.all((res?.data || []).map((msg) => {
5891
+ // Resolve this message's `conversationFileIDs` (a JSON array string of
5892
+ // ids) into full file objects — each fetch response IS the
5893
+ // KRChatbotConversationFile (id + niceName + mimeType), and niceName
5894
+ // drives the previewer's file-type detection. A failed fetch drops
5895
+ // that file rather than rendering a broken thumbnail.
5896
+ let ids = [];
5881
5897
  try {
5882
- const ids = JSON.parse(msg.conversationFileIDs || '[]');
5883
- if (ids.length) {
5884
- msg.conversationFiles = ids.map((id) => ({ conversationFileID: id }));
5885
- }
5898
+ ids = JSON.parse(msg.conversationFileIDs || '[]');
5886
5899
  }
5887
5900
  catch { /* no/invalid attachments — leave empty */ }
5888
- return msg;
5889
- }));
5901
+ if (!ids.length) {
5902
+ return Promise.resolve(msg);
5903
+ }
5904
+ return Promise.all(ids.map((id) => krubbleHttp.Http.fetch({ url: `/api/system/conversationFile/${id}` })
5905
+ .then((r) => r.json())
5906
+ .then((meta) => meta?.data)
5907
+ .catch(() => null)))
5908
+ .then((files) => {
5909
+ msg.conversationFiles = files.filter((f) => f != null);
5910
+ return msg;
5911
+ });
5912
+ })));
5890
5913
  };
5891
5914
  }
5892
5915
  this.load().then((messages) => {
@@ -6681,10 +6704,18 @@
6681
6704
  }
6682
6705
  /** Open the shared full-screen previewer for a message image. */
6683
6706
  _openImage(img) {
6684
- krubbleComponents.FilePreview.open({
6685
- src: `/api/system/conversationFile/${img.conversationFileID}/view`,
6707
+ // Prefix the configured backend base URL — a raw fetch of a relative path
6708
+ // would hit the page origin (e.g. the dev server), not the backend.
6709
+ const preview = krubbleComponents.FilePreview.open({
6710
+ src: `${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view`,
6686
6711
  name: img.niceName || 'image',
6687
6712
  });
6713
+ // The previewer's download button only fires a `download` event — the
6714
+ // consumer performs the actual download. Hit the view endpoint with
6715
+ // `?download=true` (Content-Disposition: attachment) in a new tab.
6716
+ preview.addEventListener('download', () => {
6717
+ window.open(`${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view?download=true`, '_blank');
6718
+ });
6688
6719
  }
6689
6720
  /** Whether the send button is active: needs text or at least one image. */
6690
6721
  _canSend() {
@@ -7533,7 +7564,7 @@
7533
7564
  @click=${() => this._openImage(img)}
7534
7565
  >
7535
7566
  <img
7536
- src="/api/system/conversationFile/${img.conversationFileID}/view"
7567
+ src="${krubbleHttp.Http.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view"
7537
7568
  alt=${img.niceName || ''}
7538
7569
  />
7539
7570
  </button>
@@ -7557,7 +7588,7 @@
7557
7588
  ${msg.content ? b `
7558
7589
  <button
7559
7590
  class="message-copy"
7560
- title="Copy"
7591
+ title=${this._copiedIndex === index ? 'Copied' : 'Copy'}
7561
7592
  @click=${() => this._handleCopyMessage(msg.content, index)}
7562
7593
  >
7563
7594
  ${this._copiedIndex === index ? b `
@@ -7571,7 +7602,6 @@
7571
7602
  >
7572
7603
  <path d="M20 6 9 17l-5-5" />
7573
7604
  </svg>
7574
- Copied
7575
7605
  ` : b `
7576
7606
  <svg
7577
7607
  viewBox="0 0 24 24"
@@ -7590,7 +7620,6 @@
7590
7620
  />
7591
7621
  <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
7592
7622
  </svg>
7593
- Copy
7594
7623
  `}
7595
7624
  </button>
7596
7625
  ` : A}
@@ -7957,7 +7986,7 @@
7957
7986
  flex: 1;
7958
7987
  display: flex;
7959
7988
  flex-direction: column;
7960
- gap: 24px;
7989
+ gap: 36px;
7961
7990
  padding: 32px;
7962
7991
  width: 100%;
7963
7992
  max-width: 768px;
@@ -7986,6 +8015,17 @@
7986
8015
  flex-direction: column;
7987
8016
  max-width: 100%;
7988
8017
  animation: chatbot-msg-in 0.3s ease-out;
8018
+ /* Reserve room for the copy button INSIDE the message box (so the button is
8019
+ part of the message's hover region — no dead zone to cross), then cancel
8020
+ that space for layout so message spacing is unchanged. */
8021
+ padding-bottom: 30px;
8022
+ margin-bottom: -30px;
8023
+ }
8024
+
8025
+ /* The reserved padding overhangs into the next message; lift the hovered one
8026
+ so its copy button isn't covered by (or stealing hover from) the sibling. */
8027
+ .message:hover {
8028
+ z-index: 1;
7989
8029
  }
7990
8030
 
7991
8031
  @keyframes chatbot-msg-in {
@@ -8034,43 +8074,41 @@
8034
8074
  color: #b45309;
8035
8075
  }
8036
8076
 
8037
- /* Per-message copy button. Kept in flow with a fixed height that is always
8038
- reserved (via a negative bottom margin that cancels it) so message spacing
8039
- is identical whether or not it's shown; only its opacity toggles on hover. */
8077
+ /* Per-message copy button. Sits in the message's reserved bottom padding —
8078
+ inside the hover box, so moving the cursor down onto it never leaves the
8079
+ message. Height is reserved (via the message padding) so spacing doesn't
8080
+ jump; only opacity toggles on hover. */
8040
8081
  .message-copy {
8041
- align-self: flex-start;
8042
- margin-top: 4px;
8043
- margin-bottom: -27px;
8082
+ position: absolute;
8083
+ bottom: 0;
8084
+ left: 0;
8044
8085
  display: inline-flex;
8045
8086
  align-items: center;
8046
- gap: 4px;
8047
- height: 23px;
8048
- padding: 4px 8px;
8087
+ justify-content: center;
8088
+ width: 26px;
8089
+ height: 26px;
8090
+ padding: 0;
8049
8091
  background: none;
8050
8092
  border: none;
8051
8093
  border-radius: 6px;
8052
- font: inherit;
8053
- font-size: 12px;
8054
- color: #64668b;
8094
+ color: #000000;
8055
8095
  cursor: pointer;
8056
8096
  opacity: 0;
8057
- pointer-events: none;
8058
8097
  transition: opacity 0.15s, background-color 0.15s, color 0.15s;
8059
8098
  }
8060
8099
 
8061
8100
  .message--user .message-copy {
8062
- align-self: flex-end;
8101
+ left: auto;
8102
+ right: 0;
8063
8103
  }
8064
8104
 
8065
8105
  .message:hover .message-copy,
8066
8106
  .message-copy:focus-visible {
8067
8107
  opacity: 1;
8068
- pointer-events: auto;
8069
8108
  }
8070
8109
 
8071
8110
  .message-copy:hover {
8072
8111
  background: rgba(0, 0, 0, 0.06);
8073
- color: #1a1a2e;
8074
8112
  }
8075
8113
 
8076
8114
  .message-copy svg {
@@ -8644,6 +8682,7 @@
8644
8682
 
8645
8683
  .message--user .attachments {
8646
8684
  justify-content: flex-end;
8685
+ margin-bottom: 24px;
8647
8686
  }
8648
8687
 
8649
8688
  .attachment {