@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.
package/dist/chatbot.js CHANGED
@@ -11,6 +11,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
11
11
  import { repeat } from 'lit/directives/repeat.js';
12
12
  import { marked } from 'marked';
13
13
  import { KRFilePreview } from '@kodaris/krubble-components';
14
+ import { KRHttp } from '@kodaris/krubble-http';
14
15
  /** Image MIME types the chatbot accepts (matches what Bedrock can ingest). */
15
16
  const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
16
17
  /**
@@ -33,7 +34,7 @@ const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/web
33
34
  * ## Usage
34
35
  * ```html
35
36
  * <kr-chatbot
36
- * title="AI Assistant"
37
+ * label="AI Assistant"
37
38
  * .send=${(params) => {
38
39
  * return fetch('/api/ai/chat/stream', {
39
40
  * method: 'POST',
@@ -76,16 +77,25 @@ let KRChatbot = class KRChatbot extends LitElement {
76
77
  /**
77
78
  * Callback function for sending messages. Receives the user's message and
78
79
  * current conversation history. Must return a Response with a streaming SSE body.
80
+ *
81
+ * Optional — when not set, the chatbot uses its built-in default that streams
82
+ * from the Kodaris Bedrock conversation endpoints. Set this to override.
79
83
  */
80
84
  this.send = null;
81
85
  /**
82
86
  * Callback function called when the user clears the conversation.
83
87
  * Must return a Promise — messages are only cleared if it resolves.
88
+ *
89
+ * Optional — when not set, the chatbot uses its built-in default that clears
90
+ * the Kodaris Bedrock conversation state. Set this to override.
84
91
  */
85
92
  this.clear = null;
86
93
  /**
87
94
  * Callback function called on init to load existing conversation messages.
88
95
  * Must return a Promise that resolves with an array of KRChatbotMessage.
96
+ *
97
+ * Optional — when not set, the chatbot uses its built-in default that loads
98
+ * existing messages from the Kodaris Bedrock conversation. Set this to override.
89
99
  */
90
100
  this.load = null;
91
101
  /**
@@ -94,13 +104,9 @@ let KRChatbot = class KRChatbot extends LitElement {
94
104
  */
95
105
  this.messages = [];
96
106
  /**
97
- * Header title text.
107
+ * Header label text.
98
108
  */
99
- this.title = 'Kat';
100
- /**
101
- * Header subtitle text (shown below title with a green status dot).
102
- */
103
- this.subtitle = '';
109
+ this.label = 'Kat';
104
110
  /**
105
111
  * Input placeholder text.
106
112
  */
@@ -115,13 +121,23 @@ let KRChatbot = class KRChatbot extends LitElement {
115
121
  * then driven via the `open()` / `close()` / `toggle()` methods.
116
122
  */
117
123
  this.hideToggle = false;
124
+ /**
125
+ * Layout mode.
126
+ * - `'floating'` (default): the chatbot is a fixed bottom-right widget with a
127
+ * toggle button, and the panel can be dragged and resized. This is the
128
+ * traditional chat-widget appearance.
129
+ * - `'inline'`: the chatbot drops its fixed positioning and fills its parent
130
+ * container (100% width/height, no border radius/shadow, no toggle, no
131
+ * drag/resize). The panel is always open. Use this to embed the chat as a
132
+ * full page or panel region.
133
+ */
134
+ this.variant = 'floating';
118
135
  // --- @state (internal) ---
119
136
  this._inputValue = '';
120
137
  this._isStreaming = false;
121
- this._statusText = '';
122
138
  this._error = '';
123
139
  /** Images staged in the composer, before the message is sent. */
124
- this._pendingAttachments = [];
140
+ this._pendingInternalFiles = [];
125
141
  /** True while a file is being dragged over the panel (shows the drop overlay). */
126
142
  this._isDraggingFile = false;
127
143
  this._handleMouseMove = (e) => {
@@ -185,12 +201,27 @@ let KRChatbot = class KRChatbot extends LitElement {
185
201
  // --- Lifecycle ---
186
202
  connectedCallback() {
187
203
  super.connectedCallback();
188
- if (this.load) {
189
- this.load().then((messages) => {
190
- if (messages && messages.length) {
191
- this.messages = messages;
192
- }
193
- }).catch(() => { });
204
+ // Default to loading existing messages from the Kodaris Bedrock conversation
205
+ // when the consumer hasn't provided their own `load`. The endpoint returns
206
+ // messages already in KRChatbotMessage shape ({ type, content, images }), so
207
+ // they pass straight through.
208
+ if (!this.load) {
209
+ this.load = () => KRHttp.fetch({
210
+ url: '/api/system/integration/aws/bedrock/getConversationMessages',
211
+ })
212
+ .then((r) => r.json())
213
+ .then((res) => res.data || []);
214
+ }
215
+ this.load().then((messages) => {
216
+ if (messages && messages.length) {
217
+ this.messages = messages;
218
+ }
219
+ }).catch(() => { });
220
+ // Inline mode fills its container and is always open — the floating widget's
221
+ // toggle/drag/resize state (including any persisted layout) doesn't apply.
222
+ if (this.variant === 'inline') {
223
+ this.opened = true;
224
+ return;
194
225
  }
195
226
  this._restoreState();
196
227
  }
@@ -203,10 +234,10 @@ let KRChatbot = class KRChatbot extends LitElement {
203
234
  document.removeEventListener('mousemove', this._handleMouseMove);
204
235
  document.removeEventListener('mouseup', this._handleMouseUp);
205
236
  // Release any object URLs still held by staged attachments.
206
- this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
237
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
207
238
  }
208
239
  updated(changed) {
209
- if (changed.has('messages') || changed.has('_statusText')) {
240
+ if (changed.has('messages')) {
210
241
  if (!this._userHasScrolledUp && !this._isStreaming && this._messagesEl) {
211
242
  requestAnimationFrame(() => {
212
243
  if (this._messagesEl) {
@@ -280,6 +311,10 @@ let KRChatbot = class KRChatbot extends LitElement {
280
311
  }
281
312
  }
282
313
  _handleHeaderMouseDown(e) {
314
+ // Inline mode fills its parent — dragging the panel doesn't apply.
315
+ if (this.variant === 'inline') {
316
+ return;
317
+ }
283
318
  // Ignore clicks on buttons inside the header
284
319
  if (e.target.closest('button')) {
285
320
  return;
@@ -305,6 +340,10 @@ let KRChatbot = class KRChatbot extends LitElement {
305
340
  document.addEventListener('mouseup', this._handleMouseUp);
306
341
  }
307
342
  _handleResizeMouseDown(e) {
343
+ // Inline mode fills its parent — resizing the panel doesn't apply.
344
+ if (this.variant === 'inline') {
345
+ return;
346
+ }
308
347
  if (!this._panelEl) {
309
348
  return;
310
349
  }
@@ -497,54 +536,49 @@ let KRChatbot = class KRChatbot extends LitElement {
497
536
  name: file.name,
498
537
  previewUrl: URL.createObjectURL(file),
499
538
  };
500
- this._pendingAttachments = [...this._pendingAttachments, pending];
539
+ this._pendingInternalFiles = [...this._pendingInternalFiles, pending];
501
540
  });
502
541
  }
503
542
  _removePending(localId) {
504
- const target = this._pendingAttachments.find((p) => p.localId === localId);
543
+ const target = this._pendingInternalFiles.find((p) => p.localId === localId);
505
544
  if (target) {
506
545
  URL.revokeObjectURL(target.previewUrl);
507
546
  }
508
- this._pendingAttachments = this._pendingAttachments.filter((p) => p.localId !== localId);
547
+ this._pendingInternalFiles = this._pendingInternalFiles.filter((p) => p.localId !== localId);
509
548
  }
510
549
  _clearPending() {
511
- this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
512
- this._pendingAttachments = [];
550
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
551
+ this._pendingInternalFiles = [];
513
552
  }
514
- /** Open the shared full-screen previewer for an attachment. */
515
- _openAttachment(att) {
553
+ /** Open the shared full-screen previewer for a message image. */
554
+ _openImage(img) {
516
555
  KRFilePreview.open({
517
- src: att.url,
518
- name: att.name || 'image',
556
+ src: `/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view`,
557
+ name: img.niceName || 'image',
519
558
  });
520
559
  }
521
560
  /** Whether the send button is active: needs text or at least one image. */
522
561
  _canSend() {
523
- return Boolean(this._inputValue.trim()) || this._pendingAttachments.length > 0;
562
+ return Boolean(this._inputValue.trim()) || this._pendingInternalFiles.length > 0;
524
563
  }
525
564
  _handleSubmit() {
526
- if (!this.send || this._isStreaming) {
565
+ if (this._isStreaming) {
527
566
  return;
528
567
  }
529
568
  const messageText = this._inputValue.trim();
530
- const pending = this._pendingAttachments;
569
+ const pending = this._pendingInternalFiles;
531
570
  // Allow sending an image with no text, but require at least one of them.
532
571
  if (!messageText && !pending.length) {
533
572
  return;
534
573
  }
535
- // The raw files sent to the backend, and the display attachments for the
536
- // sent message. The message reuses each preview object URL, so ownership of
537
- // those URLs transfers to the message (we don't revoke them here).
538
- const files = pending.map((p) => p.file);
539
- const attachments = pending.map((p) => ({
540
- url: p.previewUrl,
541
- name: p.name,
542
- mimeType: p.file.type,
543
- }));
574
+ // The raw files sent to the backend. Sent images are shown once the
575
+ // conversation reloads and they come back as stored internal files; the
576
+ // just-sent message itself carries text only.
577
+ const internalFiles = pending.map((p) => p.file);
544
578
  const submitEvent = new CustomEvent('message-submit', {
545
579
  detail: {
546
580
  message: messageText,
547
- files,
581
+ internalFiles,
548
582
  },
549
583
  bubbles: true,
550
584
  composed: true,
@@ -555,30 +589,26 @@ let KRChatbot = class KRChatbot extends LitElement {
555
589
  return;
556
590
  }
557
591
  const userMessage = {
558
- role: 'user',
592
+ type: 'user',
559
593
  content: messageText,
560
594
  };
561
- if (attachments.length) {
562
- userMessage.attachments = attachments;
563
- }
564
595
  this.messages = [...this.messages, userMessage];
565
596
  this._inputValue = '';
566
597
  this._error = '';
567
598
  this._userHasScrolledUp = false;
568
- // Clear the composer without revoking the object URLs — the sent message now
569
- // owns them for display.
570
- this._pendingAttachments = [];
599
+ // Clear the composer and release its preview object URLs.
600
+ this._pendingInternalFiles.forEach((p) => URL.revokeObjectURL(p.previewUrl));
601
+ this._pendingInternalFiles = [];
571
602
  // Reset textarea height
572
603
  if (this._inputEl) {
573
604
  this._inputEl.style.height = 'auto';
574
605
  }
575
606
  // Add empty assistant message to stream into
576
607
  this.messages = [...this.messages, {
577
- role: 'assistant',
608
+ type: 'assistant',
578
609
  content: '',
579
610
  }];
580
611
  this._isStreaming = true;
581
- this._statusText = '';
582
612
  // Scroll user's message to the top (Gemini-style).
583
613
  // Set min-height on the assistant message = container height so there's enough
584
614
  // scroll room below the user message to push it to the top.
@@ -600,10 +630,34 @@ let KRChatbot = class KRChatbot extends LitElement {
600
630
  this._messagesEl.scrollTop = userMsgEl.offsetTop - this._messagesEl.offsetTop - 24;
601
631
  }
602
632
  });
633
+ // Default to streaming from the Kodaris Bedrock conversation endpoints when
634
+ // the consumer hasn't provided their own `send`.
635
+ if (!this.send) {
636
+ this.send = (params) => {
637
+ // With images, post text + files as multipart; without, post plain JSON.
638
+ if (params.internalFiles.length) {
639
+ const form = new FormData();
640
+ form.append('userText', params.message);
641
+ params.internalFiles.forEach((file) => form.append('images', file));
642
+ return KRHttp.fetch({
643
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithImages',
644
+ method: 'POST',
645
+ headers: { Accept: 'text/event-stream' },
646
+ body: form,
647
+ });
648
+ }
649
+ return KRHttp.fetch({
650
+ url: '/api/system/integration/aws/bedrock/stream/invokeModelForConversationWithState',
651
+ method: 'POST',
652
+ headers: { Accept: 'text/event-stream' },
653
+ body: { userText: params.message },
654
+ });
655
+ };
656
+ }
603
657
  this.send({
604
658
  message: messageText,
605
659
  messages: this.messages.slice(0, -1),
606
- files,
660
+ internalFiles,
607
661
  }).then((response) => {
608
662
  if (!response.ok) {
609
663
  this._handleStreamError('Request failed: ' + response.status);
@@ -629,11 +683,15 @@ let KRChatbot = class KRChatbot extends LitElement {
629
683
  this._reader = null;
630
684
  }
631
685
  this._isStreaming = false;
632
- this._statusText = '';
633
686
  }
634
687
  _handleClear() {
688
+ // Default to clearing the Kodaris Bedrock conversation state when the consumer
689
+ // hasn't provided their own `clear`.
635
690
  if (!this.clear) {
636
- return;
691
+ this.clear = () => KRHttp.fetch({
692
+ url: '/api/system/integration/aws/bedrock/clearConversationWithState',
693
+ method: 'POST',
694
+ });
637
695
  }
638
696
  this.clear().then(() => {
639
697
  if (this._reader) {
@@ -642,7 +700,6 @@ let KRChatbot = class KRChatbot extends LitElement {
642
700
  }
643
701
  this.messages = [];
644
702
  this._isStreaming = false;
645
- this._statusText = '';
646
703
  this._error = '';
647
704
  this._inputValue = '';
648
705
  this._userHasScrolledUp = false;
@@ -668,7 +725,6 @@ let KRChatbot = class KRChatbot extends LitElement {
668
725
  this._reader.read().then((result) => {
669
726
  if (result.done) {
670
727
  this._isStreaming = false;
671
- this._statusText = '';
672
728
  this._reader = null;
673
729
  return;
674
730
  }
@@ -701,15 +757,10 @@ let KRChatbot = class KRChatbot extends LitElement {
701
757
  bubbles: true,
702
758
  composed: true,
703
759
  }));
704
- if (data.type === 'STATUS') {
705
- if (data.message) {
706
- this._statusText = String(data.message);
707
- }
708
- }
709
- else if (data.type === 'REASONING_PART') {
760
+ if (data.type === 'REASONING_PART') {
710
761
  if (data.message) {
711
762
  const lastIndex = this.messages.length - 1;
712
- if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
763
+ if (lastIndex >= 0 && this.messages[lastIndex].type === 'assistant') {
713
764
  if (!this.messages[lastIndex].reasoning) {
714
765
  this.messages[lastIndex].reasoning = '';
715
766
  }
@@ -720,9 +771,8 @@ let KRChatbot = class KRChatbot extends LitElement {
720
771
  }
721
772
  else if (data.type === 'RESPONSE_PART') {
722
773
  if (data.message) {
723
- this._statusText = '';
724
774
  const lastIndex = this.messages.length - 1;
725
- if (lastIndex >= 0 && this.messages[lastIndex].role === 'assistant') {
775
+ if (lastIndex >= 0 && this.messages[lastIndex].type === 'assistant') {
726
776
  this.messages[lastIndex].content += String(data.message);
727
777
  this.requestUpdate();
728
778
  }
@@ -733,7 +783,6 @@ let KRChatbot = class KRChatbot extends LitElement {
733
783
  }
734
784
  if (data.finished) {
735
785
  this._isStreaming = false;
736
- this._statusText = '';
737
786
  this._reader = null;
738
787
  }
739
788
  }
@@ -747,7 +796,6 @@ let KRChatbot = class KRChatbot extends LitElement {
747
796
  }
748
797
  _handleStreamError(message) {
749
798
  this._isStreaming = false;
750
- this._statusText = '';
751
799
  this._error = message;
752
800
  if (this._reader) {
753
801
  this._reader.cancel();
@@ -756,7 +804,7 @@ let KRChatbot = class KRChatbot extends LitElement {
756
804
  // Remove the empty assistant message if it has no content
757
805
  if (this.messages.length > 0) {
758
806
  const lastMessage = this.messages[this.messages.length - 1];
759
- if (lastMessage.role === 'assistant' && !lastMessage.content) {
807
+ if (lastMessage.type === 'assistant' && !lastMessage.content) {
760
808
  this.messages = this.messages.slice(0, -1);
761
809
  }
762
810
  }
@@ -764,7 +812,7 @@ let KRChatbot = class KRChatbot extends LitElement {
764
812
  // --- Render ---
765
813
  render() {
766
814
  return html `
767
- ${this.hideToggle ? nothing : html `
815
+ ${(this.hideToggle || this.variant === 'inline') ? nothing : html `
768
816
  <button
769
817
  class=${classMap({
770
818
  'toggle': true,
@@ -815,53 +863,52 @@ let KRChatbot = class KRChatbot extends LitElement {
815
863
  <span>Drop images to attach</span>
816
864
  </div>
817
865
  ` : nothing}
818
- <div
819
- class="resize resize--n"
820
- data-dir="n"
821
- @mousedown=${this._handleResizeMouseDown}
822
- ></div>
823
- <div
824
- class="resize resize--s"
825
- data-dir="s"
826
- @mousedown=${this._handleResizeMouseDown}
827
- ></div>
828
- <div
829
- class="resize resize--w"
830
- data-dir="w"
831
- @mousedown=${this._handleResizeMouseDown}
832
- ></div>
833
- <div
834
- class="resize resize--e"
835
- data-dir="e"
836
- @mousedown=${this._handleResizeMouseDown}
837
- ></div>
838
- <div
839
- class="resize resize--nw"
840
- data-dir="nw"
841
- @mousedown=${this._handleResizeMouseDown}
842
- ></div>
843
- <div
844
- class="resize resize--ne"
845
- data-dir="ne"
846
- @mousedown=${this._handleResizeMouseDown}
847
- ></div>
848
- <div
849
- class="resize resize--sw"
850
- data-dir="sw"
851
- @mousedown=${this._handleResizeMouseDown}
852
- ></div>
853
- <div
854
- class="resize resize--se"
855
- data-dir="se"
856
- @mousedown=${this._handleResizeMouseDown}
857
- ></div>
866
+ ${this.variant === 'inline' ? nothing : html `
867
+ <div
868
+ class="resize resize--n"
869
+ data-dir="n"
870
+ @mousedown=${this._handleResizeMouseDown}
871
+ ></div>
872
+ <div
873
+ class="resize resize--s"
874
+ data-dir="s"
875
+ @mousedown=${this._handleResizeMouseDown}
876
+ ></div>
877
+ <div
878
+ class="resize resize--w"
879
+ data-dir="w"
880
+ @mousedown=${this._handleResizeMouseDown}
881
+ ></div>
882
+ <div
883
+ class="resize resize--e"
884
+ data-dir="e"
885
+ @mousedown=${this._handleResizeMouseDown}
886
+ ></div>
887
+ <div
888
+ class="resize resize--nw"
889
+ data-dir="nw"
890
+ @mousedown=${this._handleResizeMouseDown}
891
+ ></div>
892
+ <div
893
+ class="resize resize--ne"
894
+ data-dir="ne"
895
+ @mousedown=${this._handleResizeMouseDown}
896
+ ></div>
897
+ <div
898
+ class="resize resize--sw"
899
+ data-dir="sw"
900
+ @mousedown=${this._handleResizeMouseDown}
901
+ ></div>
902
+ <div
903
+ class="resize resize--se"
904
+ data-dir="se"
905
+ @mousedown=${this._handleResizeMouseDown}
906
+ ></div>
907
+ `}
858
908
  <div class="header" @mousedown=${this._handleHeaderMouseDown}>
859
909
  <div class="header-left">
860
910
  <div class="header-info">
861
- <span class="header-title">${this.title}</span>
862
- ${this.subtitle ? html `
863
- <span class="header-subtitle">${this.subtitle}</span>
864
- ` : nothing}
911
+ <span class="header-title">${this.label}</span>
865
912
  </div>
866
913
  </div>
867
914
  <div class="header-actions">
@@ -887,77 +934,84 @@ let KRChatbot = class KRChatbot extends LitElement {
887
934
  </svg>
888
935
  </button>
889
936
  ` : nothing}
890
- <button
891
- class="header-action"
892
- @click=${this._handleToggle}
893
- title="Close"
894
- >
895
- <svg
896
- xmlns="http://www.w3.org/2000/svg"
897
- fill="none"
898
- viewBox="0 0 24 24"
899
- stroke-width="2"
900
- stroke="currentColor"
901
- class="header-action-icon"
937
+ ${this.variant === 'inline' ? nothing : html `
938
+ <button
939
+ class="header-action"
940
+ @click=${this._handleToggle}
941
+ title="Close"
902
942
  >
903
- <path
904
- stroke-linecap="round"
905
- stroke-linejoin="round"
906
- d="M6 18 18 6M6 6l12 12"
907
- />
908
- </svg>
909
- </button>
943
+ <svg
944
+ xmlns="http://www.w3.org/2000/svg"
945
+ fill="none"
946
+ viewBox="0 0 24 24"
947
+ stroke-width="2"
948
+ stroke="currentColor"
949
+ class="header-action-icon"
950
+ >
951
+ <path
952
+ stroke-linecap="round"
953
+ stroke-linejoin="round"
954
+ d="M6 18 18 6M6 6l12 12"
955
+ />
956
+ </svg>
957
+ </button>
958
+ `}
910
959
  </div>
911
960
  </div>
912
961
 
913
962
  <div class="messages" @scroll=${this._handleMessagesScroll}>
914
- ${this.messages.length === 0 ? html `
915
- <div class="empty">
916
- <span class="empty-text">What can I help you with?</span>
917
- </div>
918
- ` : nothing}
919
- ${this.messages.map((msg) => html `
920
- <div class=${classMap({
963
+ <div class="messages-inner">
964
+ ${this.messages.length === 0 ? html `
965
+ <div class="empty">
966
+ <span class="empty-text">What can I help you with?</span>
967
+ </div>
968
+ ` : nothing}
969
+ ${this.messages.map((msg) => html `
970
+ <div class=${classMap({
921
971
  'message': true,
922
- 'message--user': msg.role === 'user',
923
- 'message--assistant': msg.role === 'assistant',
972
+ 'message--user': msg.type === 'user',
973
+ 'message--assistant': msg.type === 'assistant',
924
974
  })}>
925
- ${msg.reasoning ? html `
926
- <details class="reasoning">
927
- <summary>Thinking</summary>
928
- <div class="reasoning-content">${msg.reasoning}</div>
929
- </details>
930
- ` : nothing}
931
- ${msg.attachments && msg.attachments.length ? html `
932
- <div class="attachments">
933
- ${msg.attachments.map((att) => html `
934
- <button
935
- class="attachment"
936
- type="button"
937
- title=${att.name || 'View image'}
938
- @click=${() => this._openAttachment(att)}
939
- >
940
- <img src=${att.url} alt=${att.name || ''} />
941
- </button>
942
- `)}
943
- </div>
944
- ` : nothing}
945
- ${msg.content ? html `
946
- <div class="message-content">
947
- ${msg.role === 'user'
975
+ ${msg.reasoning ? html `
976
+ <details class="reasoning">
977
+ <summary>Thinking</summary>
978
+ <div class="reasoning-content">${msg.reasoning}</div>
979
+ </details>
980
+ ` : nothing}
981
+ ${msg.internalFiles && msg.internalFiles.length ? html `
982
+ <div class="attachments">
983
+ ${msg.internalFiles.map((img) => html `
984
+ <button
985
+ class="attachment"
986
+ type="button"
987
+ title=${img.niceName || 'View image'}
988
+ @click=${() => this._openImage(img)}
989
+ >
990
+ <img
991
+ src="/api/system/integration/aws/bedrock/conversationImage/${img.internalFileID}/view"
992
+ alt=${img.niceName || ''}
993
+ />
994
+ </button>
995
+ `)}
996
+ </div>
997
+ ` : nothing}
998
+ ${msg.content ? html `
999
+ <div class="message-content">
1000
+ ${msg.type === 'user'
948
1001
  ? msg.content
949
1002
  : unsafeHTML(marked.parse(msg.content, { breaks: true }))}
950
- </div>
951
- ` : nothing}
952
- ${msg.role === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? html `
953
- <div class="typing">
954
- <span></span>
955
- <span></span>
956
- <span></span>
957
- </div>
958
- ` : nothing}
959
- </div>
960
- `)}
1003
+ </div>
1004
+ ` : nothing}
1005
+ ${msg.type === 'assistant' && this._isStreaming && msg === this.messages[this.messages.length - 1] ? html `
1006
+ <div class="typing">
1007
+ <span></span>
1008
+ <span></span>
1009
+ <span></span>
1010
+ </div>
1011
+ ` : nothing}
1012
+ </div>
1013
+ `)}
1014
+ </div>
961
1015
  </div>
962
1016
 
963
1017
  ${this._error ? html `
@@ -983,9 +1037,9 @@ let KRChatbot = class KRChatbot extends LitElement {
983
1037
  ` : nothing}
984
1038
 
985
1039
  <div class="footer">
986
- ${this._pendingAttachments.length ? html `
1040
+ ${this._pendingInternalFiles.length ? html `
987
1041
  <div class="composer-attachments">
988
- ${repeat(this._pendingAttachments, (p) => p.localId, (p) => html `
1042
+ ${repeat(this._pendingInternalFiles, (p) => p.localId, (p) => html `
989
1043
  <div class="chip">
990
1044
  <img src=${p.previewUrl} alt=${p.name} />
991
1045
  <button
@@ -1174,6 +1228,28 @@ KRChatbot.styles = css `
1174
1228
  display: flex;
1175
1229
  }
1176
1230
 
1231
+ /* Inline mode: fill the parent container instead of floating bottom-right. */
1232
+ :host([variant='inline']) {
1233
+ position: static;
1234
+ bottom: auto;
1235
+ right: auto;
1236
+ z-index: auto;
1237
+ height: 100%;
1238
+ }
1239
+
1240
+ :host([variant='inline']) .panel {
1241
+ position: relative;
1242
+ bottom: auto;
1243
+ right: auto;
1244
+ width: 100%;
1245
+ height: 100%;
1246
+ max-height: none;
1247
+ border-radius: 0;
1248
+ box-shadow: none;
1249
+ border: none;
1250
+ animation: none;
1251
+ }
1252
+
1177
1253
  @keyframes chatbot-panel-in {
1178
1254
  from {
1179
1255
  opacity: 0;
@@ -1204,7 +1280,7 @@ KRChatbot.styles = css `
1204
1280
  gap: 10px;
1205
1281
  }
1206
1282
 
1207
- .header-info {
1283
+ .header-info {
1208
1284
  display: flex;
1209
1285
  flex-direction: column;
1210
1286
  }
@@ -1214,22 +1290,6 @@ KRChatbot.styles = css `
1214
1290
  font-weight: 600;
1215
1291
  }
1216
1292
 
1217
- .header-subtitle {
1218
- color: rgba(255, 255, 255, 0.5);
1219
- font-size: 11px;
1220
- display: flex;
1221
- align-items: center;
1222
- gap: 4px;
1223
- }
1224
-
1225
- .header-subtitle::before {
1226
- content: '';
1227
- width: 5px;
1228
- height: 5px;
1229
- background: var(--kr-chatbot-success);
1230
- border-radius: 50%;
1231
- }
1232
-
1233
1293
  .header-actions {
1234
1294
  display: flex;
1235
1295
  align-items: center;
@@ -1261,17 +1321,29 @@ KRChatbot.styles = css `
1261
1321
  }
1262
1322
 
1263
1323
  /* Messages */
1324
+ /* Full-width scroll container — keeps the scrollbar at the panel edge. */
1264
1325
  .messages {
1265
1326
  flex: 1;
1266
1327
  overflow-y: auto;
1267
- padding: 32px;
1268
1328
  display: flex;
1269
1329
  flex-direction: column;
1270
- gap: 24px;
1271
1330
  scroll-behavior: smooth;
1272
1331
  background: white;
1273
1332
  }
1274
1333
 
1334
+ /* Centered, width-capped column that actually holds the messages. */
1335
+ .messages-inner {
1336
+ flex: 1;
1337
+ display: flex;
1338
+ flex-direction: column;
1339
+ gap: 24px;
1340
+ padding: 32px;
1341
+ width: 100%;
1342
+ max-width: 768px;
1343
+ margin-left: auto;
1344
+ margin-right: auto;
1345
+ }
1346
+
1275
1347
  .messages::-webkit-scrollbar {
1276
1348
  width: 4px;
1277
1349
  }
@@ -1295,7 +1367,7 @@ KRChatbot.styles = css `
1295
1367
  gap: 8px;
1296
1368
  }
1297
1369
 
1298
- .empty-text {
1370
+ .empty-text {
1299
1371
  font-size: 14px;
1300
1372
  color: rgb(0 0 0 / 80%);
1301
1373
  }
@@ -1407,7 +1479,7 @@ KRChatbot.styles = css `
1407
1479
  word-wrap: break-word;
1408
1480
  }
1409
1481
 
1410
- .typing {
1482
+ .typing {
1411
1483
  display: flex;
1412
1484
  gap: 4px;
1413
1485
  padding: 4px 0;
@@ -1475,6 +1547,15 @@ KRChatbot.styles = css `
1475
1547
  padding: 16px 16px 16px;
1476
1548
  flex-shrink: 0;
1477
1549
  background: white;
1550
+ width: 100%;
1551
+ max-width: 768px;
1552
+ margin-left: auto;
1553
+ margin-right: auto;
1554
+ }
1555
+
1556
+ /* Inline mode: keep the composer clear of the bottom edge. */
1557
+ :host([variant='inline']) .footer {
1558
+ padding-bottom: 40px;
1478
1559
  }
1479
1560
 
1480
1561
  .input-wrapper {
@@ -1493,6 +1574,15 @@ KRChatbot.styles = css `
1493
1574
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04);
1494
1575
  }
1495
1576
 
1577
+ /* Inline mode: keep the border and add a soft floating shadow (ChatGPT/Gemini style). */
1578
+ :host([variant='inline']) .input-wrapper {
1579
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06), 0 8px 24px rgba(0, 0, 0, 0.1);
1580
+ }
1581
+
1582
+ :host([variant='inline']) .input-wrapper:focus-within {
1583
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 12px 32px rgba(0, 0, 0, 0.14);
1584
+ }
1585
+
1496
1586
  .input-plus {
1497
1587
  width: 34px;
1498
1588
  height: 34px;
@@ -1600,6 +1690,12 @@ KRChatbot.styles = css `
1600
1690
  cursor: grabbing;
1601
1691
  }
1602
1692
 
1693
+ /* Inline mode isn't draggable — don't show the grab/hand cursor. */
1694
+ :host([variant='inline']) .header,
1695
+ :host([variant='inline']) .header:active {
1696
+ cursor: default;
1697
+ }
1698
+
1603
1699
  /* Drag / Resize states */
1604
1700
  .panel--dragging,
1605
1701
  .panel--resizing {
@@ -1751,10 +1847,7 @@ __decorate([
1751
1847
  ], KRChatbot.prototype, "messages", void 0);
1752
1848
  __decorate([
1753
1849
  property({ type: String })
1754
- ], KRChatbot.prototype, "title", void 0);
1755
- __decorate([
1756
- property({ type: String })
1757
- ], KRChatbot.prototype, "subtitle", void 0);
1850
+ ], KRChatbot.prototype, "label", void 0);
1758
1851
  __decorate([
1759
1852
  property({ type: String })
1760
1853
  ], KRChatbot.prototype, "placeholder", void 0);
@@ -1771,21 +1864,24 @@ __decorate([
1771
1864
  reflect: true
1772
1865
  })
1773
1866
  ], KRChatbot.prototype, "hideToggle", void 0);
1867
+ __decorate([
1868
+ property({
1869
+ type: String,
1870
+ reflect: true
1871
+ })
1872
+ ], KRChatbot.prototype, "variant", void 0);
1774
1873
  __decorate([
1775
1874
  state()
1776
1875
  ], KRChatbot.prototype, "_inputValue", void 0);
1777
1876
  __decorate([
1778
1877
  state()
1779
1878
  ], KRChatbot.prototype, "_isStreaming", void 0);
1780
- __decorate([
1781
- state()
1782
- ], KRChatbot.prototype, "_statusText", void 0);
1783
1879
  __decorate([
1784
1880
  state()
1785
1881
  ], KRChatbot.prototype, "_error", void 0);
1786
1882
  __decorate([
1787
1883
  state()
1788
- ], KRChatbot.prototype, "_pendingAttachments", void 0);
1884
+ ], KRChatbot.prototype, "_pendingInternalFiles", void 0);
1789
1885
  __decorate([
1790
1886
  state()
1791
1887
  ], KRChatbot.prototype, "_isDraggingFile", void 0);