@kodaris/krubble-app-components 1.0.69 → 1.0.70

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.d.ts CHANGED
@@ -1,4 +1,18 @@
1
1
  import { LitElement } from 'lit';
2
+ /**
3
+ /**
4
+ * An image shown on a chat message. `url` is what the thumbnail/lightbox render:
5
+ * an object URL for an image the user just sent, or a server view URL for one
6
+ * loaded from conversation history.
7
+ */
8
+ export interface KRChatbotAttachment {
9
+ /** URL used to display the image (thumbnail + lightbox). */
10
+ url: string;
11
+ /** Original filename, if known. */
12
+ name?: string;
13
+ /** MIME type, if known. */
14
+ mimeType?: string;
15
+ }
2
16
  /**
3
17
  * Chat message interface
4
18
  */
@@ -6,15 +20,19 @@ export interface KRChatbotMessage {
6
20
  role: 'user' | 'assistant';
7
21
  content: string;
8
22
  reasoning?: string;
23
+ /** Images attached to this message (user messages only, for now). */
24
+ attachments?: KRChatbotAttachment[];
9
25
  }
10
26
  /**
11
27
  * Callback function type for sending messages.
12
- * Receives the user's message text and the current conversation history.
13
- * Must return a Response with a streaming body (SSE format).
28
+ * Receives the user's message text, the current conversation history, and the
29
+ * raw image files attached to this message. Must return a Response with a
30
+ * streaming body (SSE format).
14
31
  */
15
32
  export type KRChatbotSend = (params: {
16
33
  message: string;
17
34
  messages: KRChatbotMessage[];
35
+ files: File[];
18
36
  }) => Promise<Response>;
19
37
  /**
20
38
  * AI chatbot component with SSE streaming.
@@ -69,6 +87,10 @@ export declare class KRChatbot extends LitElement {
69
87
  private _dragStartTop;
70
88
  private _resizeStartW;
71
89
  private _resizeStartH;
90
+ /** Counter for depth-aware dragenter/dragleave tracking. */
91
+ private _dragDepth;
92
+ /** Monotonic id source for pending attachments. */
93
+ private _pendingSeq;
72
94
  /**
73
95
  * Callback function for sending messages. Receives the user's message and
74
96
  * current conversation history. Must return a Response with a streaming SSE body.
@@ -105,13 +127,24 @@ export declare class KRChatbot extends LitElement {
105
127
  * Whether the chat panel is open. Only relevant in floating mode.
106
128
  */
107
129
  opened: boolean;
130
+ /**
131
+ * Hide the built-in floating toggle button. Use this when an external
132
+ * control (e.g. the site-editor bar) owns opening the chat — the panel is
133
+ * then driven via the `open()` / `close()` / `toggle()` methods.
134
+ */
135
+ hideToggle: boolean;
108
136
  private _inputValue;
109
137
  private _isStreaming;
110
138
  private _statusText;
111
139
  private _error;
140
+ /** Images staged in the composer, before the message is sent. */
141
+ private _pendingAttachments;
142
+ /** True while a file is being dragged over the panel (shows the drop overlay). */
143
+ private _isDraggingFile;
112
144
  private _panelEl;
113
145
  private _messagesEl;
114
146
  private _inputEl;
147
+ private _fileInputEl;
115
148
  connectedCallback(): void;
116
149
  disconnectedCallback(): void;
117
150
  updated(changed: Map<string, unknown>): void;
@@ -119,6 +152,12 @@ export declare class KRChatbot extends LitElement {
119
152
  * Stops the current stream without clearing the conversation.
120
153
  */
121
154
  stop(): void;
155
+ /** Open the chat panel. */
156
+ open(): void;
157
+ /** Close the chat panel. */
158
+ close(): void;
159
+ /** Toggle the chat panel open/closed. */
160
+ toggle(): void;
122
161
  /**
123
162
  * Shows a full-viewport transparent overlay during drag/resize operations.
124
163
  *
@@ -150,6 +189,22 @@ export declare class KRChatbot extends LitElement {
150
189
  private _applyStoredLayout;
151
190
  private _handleInputChange;
152
191
  private _handleKeyDown;
192
+ private _openFilePicker;
193
+ private _handleFileInputChange;
194
+ private _handlePaste;
195
+ private _handleDragEnter;
196
+ private _handleDragOver;
197
+ private _handleDragLeave;
198
+ private _handleDrop;
199
+ private _hasFiles;
200
+ /** Stage a set of image files in the composer (held locally until send). */
201
+ private _addFiles;
202
+ private _removePending;
203
+ private _clearPending;
204
+ /** Open the shared full-screen previewer for an attachment. */
205
+ private _openAttachment;
206
+ /** Whether the send button is active: needs text or at least one image. */
207
+ private _canSend;
153
208
  private _handleSubmit;
154
209
  private _handleStop;
155
210
  private _handleClear;
@@ -1 +1 @@
1
- {"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAMrD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBACa,SAAU,SAAQ,UAAU;IACvC,OAAgB,MAAM,0BA8hBpB;IAIF,OAAO,CAAC,OAAO,CAAwD;IAEvE,OAAO,CAAC,QAAQ,CAAqB;IAErC,OAAO,CAAC,OAAO,CAAM;IAErB,OAAO,CAAC,kBAAkB,CAAS;IAEnC,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,QAAQ,CAA4B;IAE5C,OAAO,CAAC,UAAU,CAAM;IAExB,OAAO,CAAC,WAAW,CAAK;IAExB,OAAO,CAAC,WAAW,CAAK;IAExB,OAAO,CAAC,cAAc,CAAK;IAE3B,OAAO,CAAC,aAAa,CAAK;IAE1B,OAAO,CAAC,aAAa,CAAK;IAE1B,OAAO,CAAC,aAAa,CAAK;IAI1B;;;OAGG;IAEH,IAAI,EAAE,aAAa,GAAG,IAAI,CAAQ;IAElC;;;OAGG;IAEH,KAAK,EAAE,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAQ;IAE9C;;;OAGG;IAEH,IAAI,EAAE,CAAC,MAAM,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC,GAAG,IAAI,CAAQ;IAExD;;;OAGG;IAEH,QAAQ,EAAE,gBAAgB,EAAE,CAAM;IAElC;;OAEG;IAEH,KAAK,SAAS;IAEd;;OAEG;IAEH,QAAQ,SAAM;IAEd;;OAEG;IAEH,WAAW,SAAuB;IAElC;;OAEG;IAKH,MAAM,UAAS;IAKf,OAAO,CAAC,WAAW,CAAM;IAGzB,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,WAAW,CAAM;IAGzB,OAAO,CAAC,MAAM,CAAM;IAKpB,OAAO,CAAC,QAAQ,CAAe;IAG/B,OAAO,CAAC,WAAW,CAAe;IAGlC,OAAO,CAAC,QAAQ,CAAuB;IAI9B,iBAAiB;IAYjB,oBAAoB;IAUpB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAsB9C;;OAEG;IACH,IAAI;IAMJ;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,sBAAsB;IA+B9B,OAAO,CAAC,gBAAgB,CA+CvB;IAED,OAAO,CAAC,cAAc,CAarB;IAED,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,UAAU;IAyBlB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,kBAAkB;IAyB1B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,aAAa;IA2FrB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,gBAAgB;IA2FxB,OAAO,CAAC,kBAAkB;IAsBjB,MAAM;CAgPhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}
1
+ {"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.ts"],"names":[],"mappings":"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"}
package/dist/chatbot.js CHANGED
@@ -8,7 +8,11 @@ import { LitElement, html, css, nothing } from 'lit';
8
8
  import { customElement, property, state, query } from 'lit/decorators.js';
9
9
  import { classMap } from 'lit/directives/class-map.js';
10
10
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
11
+ import { repeat } from 'lit/directives/repeat.js';
11
12
  import { marked } from 'marked';
13
+ import { KRFilePreview } from '@kodaris/krubble-components';
14
+ /** Image MIME types the chatbot accepts (matches what Bedrock can ingest). */
15
+ const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
12
16
  /**
13
17
  * AI chatbot component with SSE streaming.
14
18
  *
@@ -64,6 +68,10 @@ let KRChatbot = class KRChatbot extends LitElement {
64
68
  this._dragStartTop = 0;
65
69
  this._resizeStartW = 0;
66
70
  this._resizeStartH = 0;
71
+ /** Counter for depth-aware dragenter/dragleave tracking. */
72
+ this._dragDepth = 0;
73
+ /** Monotonic id source for pending attachments. */
74
+ this._pendingSeq = 0;
67
75
  // --- @property (public API) ---
68
76
  /**
69
77
  * Callback function for sending messages. Receives the user's message and
@@ -101,11 +109,21 @@ let KRChatbot = class KRChatbot extends LitElement {
101
109
  * Whether the chat panel is open. Only relevant in floating mode.
102
110
  */
103
111
  this.opened = false;
112
+ /**
113
+ * Hide the built-in floating toggle button. Use this when an external
114
+ * control (e.g. the site-editor bar) owns opening the chat — the panel is
115
+ * then driven via the `open()` / `close()` / `toggle()` methods.
116
+ */
117
+ this.hideToggle = false;
104
118
  // --- @state (internal) ---
105
119
  this._inputValue = '';
106
120
  this._isStreaming = false;
107
121
  this._statusText = '';
108
122
  this._error = '';
123
+ /** Images staged in the composer, before the message is sent. */
124
+ this._pendingAttachments = [];
125
+ /** True while a file is being dragged over the panel (shows the drop overlay). */
126
+ this._isDraggingFile = false;
109
127
  this._handleMouseMove = (e) => {
110
128
  if (this._isDragging) {
111
129
  let newLeft = this._dragStartLeft + (e.clientX - this._dragStartX);
@@ -184,6 +202,8 @@ let KRChatbot = class KRChatbot extends LitElement {
184
202
  }
185
203
  document.removeEventListener('mousemove', this._handleMouseMove);
186
204
  document.removeEventListener('mouseup', this._handleMouseUp);
205
+ // Release any object URLs still held by staged attachments.
206
+ this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
187
207
  }
188
208
  updated(changed) {
189
209
  if (changed.has('messages') || changed.has('_statusText')) {
@@ -210,6 +230,22 @@ let KRChatbot = class KRChatbot extends LitElement {
210
230
  stop() {
211
231
  this._handleStop();
212
232
  }
233
+ /** Open the chat panel. */
234
+ open() {
235
+ if (!this.opened) {
236
+ this._handleToggle();
237
+ }
238
+ }
239
+ /** Close the chat panel. */
240
+ close() {
241
+ if (this.opened) {
242
+ this._handleToggle();
243
+ }
244
+ }
245
+ /** Toggle the chat panel open/closed. */
246
+ toggle() {
247
+ this._handleToggle();
248
+ }
213
249
  // --- Private methods ---
214
250
  /**
215
251
  * Shows a full-viewport transparent overlay during drag/resize operations.
@@ -378,14 +414,137 @@ let KRChatbot = class KRChatbot extends LitElement {
378
414
  this._handleSubmit();
379
415
  }
380
416
  }
417
+ // --- Attachments ---
418
+ _openFilePicker() {
419
+ this._fileInputEl?.click();
420
+ }
421
+ _handleFileInputChange(e) {
422
+ const input = e.target;
423
+ if (input.files) {
424
+ this._addFiles(input.files);
425
+ }
426
+ // Reset so picking the same file again re-fires change.
427
+ input.value = '';
428
+ }
429
+ _handlePaste(e) {
430
+ const items = e.clipboardData?.items;
431
+ if (!items) {
432
+ return;
433
+ }
434
+ const files = [];
435
+ for (let i = 0; i < items.length; i++) {
436
+ if (items[i].kind === 'file') {
437
+ const file = items[i].getAsFile();
438
+ if (file) {
439
+ files.push(file);
440
+ }
441
+ }
442
+ }
443
+ if (files.length) {
444
+ e.preventDefault();
445
+ this._addFiles(files);
446
+ }
447
+ }
448
+ _handleDragEnter(e) {
449
+ if (!this._hasFiles(e)) {
450
+ return;
451
+ }
452
+ e.preventDefault();
453
+ this._dragDepth++;
454
+ this._isDraggingFile = true;
455
+ }
456
+ _handleDragOver(e) {
457
+ if (!this._hasFiles(e)) {
458
+ return;
459
+ }
460
+ // Required so the drop event fires.
461
+ e.preventDefault();
462
+ }
463
+ _handleDragLeave(e) {
464
+ if (!this._hasFiles(e)) {
465
+ return;
466
+ }
467
+ this._dragDepth--;
468
+ if (this._dragDepth <= 0) {
469
+ this._dragDepth = 0;
470
+ this._isDraggingFile = false;
471
+ }
472
+ }
473
+ _handleDrop(e) {
474
+ if (!this._hasFiles(e)) {
475
+ return;
476
+ }
477
+ e.preventDefault();
478
+ this._dragDepth = 0;
479
+ this._isDraggingFile = false;
480
+ if (e.dataTransfer?.files) {
481
+ this._addFiles(e.dataTransfer.files);
482
+ }
483
+ }
484
+ _hasFiles(e) {
485
+ return Array.from(e.dataTransfer?.types || []).includes('Files');
486
+ }
487
+ /** Stage a set of image files in the composer (held locally until send). */
488
+ _addFiles(fileList) {
489
+ const files = Array.from(fileList).filter((f) => ACCEPTED_IMAGE_TYPES.includes(f.type));
490
+ if (!files.length) {
491
+ return;
492
+ }
493
+ files.forEach((file) => {
494
+ const pending = {
495
+ localId: 'att-' + this._pendingSeq++,
496
+ file,
497
+ name: file.name,
498
+ previewUrl: URL.createObjectURL(file),
499
+ };
500
+ this._pendingAttachments = [...this._pendingAttachments, pending];
501
+ });
502
+ }
503
+ _removePending(localId) {
504
+ const target = this._pendingAttachments.find((p) => p.localId === localId);
505
+ if (target) {
506
+ URL.revokeObjectURL(target.previewUrl);
507
+ }
508
+ this._pendingAttachments = this._pendingAttachments.filter((p) => p.localId !== localId);
509
+ }
510
+ _clearPending() {
511
+ this._pendingAttachments.forEach((p) => URL.revokeObjectURL(p.previewUrl));
512
+ this._pendingAttachments = [];
513
+ }
514
+ /** Open the shared full-screen previewer for an attachment. */
515
+ _openAttachment(att) {
516
+ KRFilePreview.open({
517
+ src: att.url,
518
+ name: att.name || 'image',
519
+ });
520
+ }
521
+ /** Whether the send button is active: needs text or at least one image. */
522
+ _canSend() {
523
+ return Boolean(this._inputValue.trim()) || this._pendingAttachments.length > 0;
524
+ }
381
525
  _handleSubmit() {
382
- if (!this._inputValue.trim() || !this.send || this._isStreaming) {
526
+ if (!this.send || this._isStreaming) {
383
527
  return;
384
528
  }
385
529
  const messageText = this._inputValue.trim();
530
+ const pending = this._pendingAttachments;
531
+ // Allow sending an image with no text, but require at least one of them.
532
+ if (!messageText && !pending.length) {
533
+ return;
534
+ }
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
+ }));
386
544
  const submitEvent = new CustomEvent('message-submit', {
387
545
  detail: {
388
546
  message: messageText,
547
+ files,
389
548
  },
390
549
  bubbles: true,
391
550
  composed: true,
@@ -395,13 +554,20 @@ let KRChatbot = class KRChatbot extends LitElement {
395
554
  if (submitEvent.defaultPrevented) {
396
555
  return;
397
556
  }
398
- this.messages = [...this.messages, {
399
- role: 'user',
400
- content: messageText,
401
- }];
557
+ const userMessage = {
558
+ role: 'user',
559
+ content: messageText,
560
+ };
561
+ if (attachments.length) {
562
+ userMessage.attachments = attachments;
563
+ }
564
+ this.messages = [...this.messages, userMessage];
402
565
  this._inputValue = '';
403
566
  this._error = '';
404
567
  this._userHasScrolledUp = false;
568
+ // Clear the composer without revoking the object URLs — the sent message now
569
+ // owns them for display.
570
+ this._pendingAttachments = [];
405
571
  // Reset textarea height
406
572
  if (this._inputEl) {
407
573
  this._inputEl.style.height = 'auto';
@@ -437,6 +603,7 @@ let KRChatbot = class KRChatbot extends LitElement {
437
603
  this.send({
438
604
  message: messageText,
439
605
  messages: this.messages.slice(0, -1),
606
+ files,
440
607
  }).then((response) => {
441
608
  if (!response.ok) {
442
609
  this._handleStreamError('Request failed: ' + response.status);
@@ -597,31 +764,57 @@ let KRChatbot = class KRChatbot extends LitElement {
597
764
  // --- Render ---
598
765
  render() {
599
766
  return html `
600
- <button
601
- class=${classMap({
767
+ ${this.hideToggle ? nothing : html `
768
+ <button
769
+ class=${classMap({
602
770
  'toggle': true,
603
771
  'toggle--hidden': this.opened,
604
772
  })}
605
- @click=${this._handleToggle}
606
- title="Open chat"
607
- >
608
- <svg
609
- viewBox="0 0 24 24"
610
- fill="none"
611
- stroke="currentColor"
612
- stroke-width="2"
613
- stroke-linecap="round"
614
- class="toggle-icon"
773
+ @click=${this._handleToggle}
774
+ title="Open chat"
615
775
  >
616
- <path d="M12 2C6.48 2 2 6.03 2 11c0 2.61 1.19 4.94 3.05 6.55L4 22l4.8-2.4C9.82 19.85 10.88 20 12 20c5.52 0 10-4.03 10-9S17.52 2 12 2z"/>
617
- <path d="M8 11h.01M12 11h.01M16 11h.01" stroke-width="2.5"/>
618
- </svg>
619
- </button>
620
-
621
- <div class=${classMap({
776
+ <svg
777
+ viewBox="0 0 24 24"
778
+ fill="none"
779
+ stroke="currentColor"
780
+ stroke-width="2"
781
+ stroke-linecap="round"
782
+ class="toggle-icon"
783
+ >
784
+ <path d="M12 2C6.48 2 2 6.03 2 11c0 2.61 1.19 4.94 3.05 6.55L4 22l4.8-2.4C9.82 19.85 10.88 20 12 20c5.52 0 10-4.03 10-9S17.52 2 12 2z"/>
785
+ <path d="M8 11h.01M12 11h.01M16 11h.01" stroke-width="2.5"/>
786
+ </svg>
787
+ </button>
788
+ `}
789
+
790
+ <div
791
+ class=${classMap({
622
792
  'panel': true,
623
793
  'panel--opened': this.opened,
624
- })}>
794
+ })}
795
+ @dragenter=${this._handleDragEnter}
796
+ @dragover=${this._handleDragOver}
797
+ @dragleave=${this._handleDragLeave}
798
+ @drop=${this._handleDrop}
799
+ >
800
+ ${this._isDraggingFile ? html `
801
+ <div class="drop-overlay">
802
+ <svg
803
+ xmlns="http://www.w3.org/2000/svg"
804
+ fill="none"
805
+ viewBox="0 0 24 24"
806
+ stroke-width="1.5"
807
+ stroke="currentColor"
808
+ >
809
+ <path
810
+ stroke-linecap="round"
811
+ stroke-linejoin="round"
812
+ d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
813
+ />
814
+ </svg>
815
+ <span>Drop images to attach</span>
816
+ </div>
817
+ ` : nothing}
625
818
  <div
626
819
  class="resize resize--n"
627
820
  data-dir="n"
@@ -735,6 +928,20 @@ let KRChatbot = class KRChatbot extends LitElement {
735
928
  <div class="reasoning-content">${msg.reasoning}</div>
736
929
  </details>
737
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}
738
945
  ${msg.content ? html `
739
946
  <div class="message-content">
740
947
  ${msg.role === 'user'
@@ -776,8 +983,48 @@ let KRChatbot = class KRChatbot extends LitElement {
776
983
  ` : nothing}
777
984
 
778
985
  <div class="footer">
986
+ ${this._pendingAttachments.length ? html `
987
+ <div class="composer-attachments">
988
+ ${repeat(this._pendingAttachments, (p) => p.localId, (p) => html `
989
+ <div class="chip">
990
+ <img src=${p.previewUrl} alt=${p.name} />
991
+ <button
992
+ class="chip-remove"
993
+ title="Remove"
994
+ @click=${() => this._removePending(p.localId)}
995
+ >
996
+ <svg
997
+ xmlns="http://www.w3.org/2000/svg"
998
+ fill="none"
999
+ viewBox="0 0 24 24"
1000
+ stroke-width="3"
1001
+ stroke="currentColor"
1002
+ >
1003
+ <path
1004
+ stroke-linecap="round"
1005
+ stroke-linejoin="round"
1006
+ d="M6 18 18 6M6 6l12 12"
1007
+ />
1008
+ </svg>
1009
+ </button>
1010
+ </div>
1011
+ `)}
1012
+ </div>
1013
+ ` : nothing}
1014
+ <input
1015
+ class="file-input"
1016
+ type="file"
1017
+ accept="image/png,image/jpeg,image/gif,image/webp"
1018
+ multiple
1019
+ hidden
1020
+ @change=${this._handleFileInputChange}
1021
+ />
779
1022
  <div class="input-wrapper">
780
- <button class="input-plus" title="Attach">+</button>
1023
+ <button
1024
+ class="input-plus"
1025
+ title="Attach image"
1026
+ @click=${this._openFilePicker}
1027
+ >+</button>
781
1028
  <textarea
782
1029
  class="input"
783
1030
  .value=${this._inputValue}
@@ -785,6 +1032,7 @@ let KRChatbot = class KRChatbot extends LitElement {
785
1032
  rows="1"
786
1033
  @input=${this._handleInputChange}
787
1034
  @keydown=${this._handleKeyDown}
1035
+ @paste=${this._handlePaste}
788
1036
  ></textarea>
789
1037
  ${this._isStreaming ? html `
790
1038
  <button
@@ -811,7 +1059,7 @@ let KRChatbot = class KRChatbot extends LitElement {
811
1059
  <button
812
1060
  class=${classMap({
813
1061
  'send': true,
814
- 'send--disabled': !this._inputValue.trim(),
1062
+ 'send--disabled': !this._canSend(),
815
1063
  })}
816
1064
  @click=${this._handleSubmit}
817
1065
  title="Send message"
@@ -1378,6 +1626,116 @@ KRChatbot.styles = css `
1378
1626
  .resize--sw { bottom: -4px; left: -4px; width: 16px; height: 16px; cursor: sw-resize; }
1379
1627
  .resize--se { bottom: -4px; right: -4px; width: 16px; height: 16px; cursor: se-resize; }
1380
1628
 
1629
+ /* Attachment thumbnails inside a sent message bubble */
1630
+ .attachments {
1631
+ display: flex;
1632
+ flex-wrap: wrap;
1633
+ gap: 8px;
1634
+ }
1635
+
1636
+ .message--user .attachments {
1637
+ justify-content: flex-end;
1638
+ }
1639
+
1640
+ .attachment {
1641
+ width: 120px;
1642
+ height: 120px;
1643
+ border-radius: 12px;
1644
+ overflow: hidden;
1645
+ background: #f2f2f4;
1646
+ border: 1px solid rgba(0, 0, 0, 0.08);
1647
+ cursor: zoom-in;
1648
+ flex-shrink: 0;
1649
+ padding: 0;
1650
+ }
1651
+
1652
+ .attachment img {
1653
+ width: 100%;
1654
+ height: 100%;
1655
+ object-fit: cover;
1656
+ display: block;
1657
+ }
1658
+
1659
+ /* Composer attachment chips (pre-send) */
1660
+ .composer-attachments {
1661
+ display: flex;
1662
+ flex-wrap: wrap;
1663
+ gap: 8px;
1664
+ padding: 0 6px 10px;
1665
+ }
1666
+
1667
+ .chip {
1668
+ position: relative;
1669
+ width: 56px;
1670
+ height: 56px;
1671
+ border-radius: 10px;
1672
+ overflow: hidden;
1673
+ background: #f2f2f4;
1674
+ border: 1px solid var(--kr-chatbot-border);
1675
+ flex-shrink: 0;
1676
+ }
1677
+
1678
+ .chip img {
1679
+ width: 100%;
1680
+ height: 100%;
1681
+ object-fit: cover;
1682
+ display: block;
1683
+ }
1684
+
1685
+ .chip-remove {
1686
+ position: absolute;
1687
+ top: 2px;
1688
+ right: 2px;
1689
+ width: 18px;
1690
+ height: 18px;
1691
+ border-radius: 50%;
1692
+ border: none;
1693
+ background: rgba(0, 0, 0, 0.6);
1694
+ color: #fff;
1695
+ cursor: pointer;
1696
+ display: flex;
1697
+ align-items: center;
1698
+ justify-content: center;
1699
+ padding: 0;
1700
+ line-height: 1;
1701
+ }
1702
+
1703
+ .chip-remove:hover {
1704
+ background: rgba(0, 0, 0, 0.8);
1705
+ }
1706
+
1707
+ .chip-remove svg {
1708
+ width: 10px;
1709
+ height: 10px;
1710
+ }
1711
+
1712
+ /* Drag-and-drop overlay */
1713
+ .drop-overlay {
1714
+ position: absolute;
1715
+ inset: 0;
1716
+ z-index: 20;
1717
+ display: flex;
1718
+ align-items: center;
1719
+ justify-content: center;
1720
+ flex-direction: column;
1721
+ gap: 10px;
1722
+ /* Frosted, mostly-opaque surface so the chat content behind is masked
1723
+ and doesn't clash with the overlay label. */
1724
+ background: rgba(255, 255, 255, 0.92);
1725
+ backdrop-filter: blur(3px);
1726
+ border: 2px dashed var(--kr-chatbot-primary);
1727
+ border-radius: 16px;
1728
+ color: var(--kr-chatbot-primary);
1729
+ font-weight: 600;
1730
+ font-size: 15px;
1731
+ pointer-events: none;
1732
+ }
1733
+
1734
+ .drop-overlay svg {
1735
+ width: 32px;
1736
+ height: 32px;
1737
+ }
1738
+
1381
1739
  `;
1382
1740
  __decorate([
1383
1741
  property({ attribute: false })
@@ -1406,6 +1764,13 @@ __decorate([
1406
1764
  reflect: true
1407
1765
  })
1408
1766
  ], KRChatbot.prototype, "opened", void 0);
1767
+ __decorate([
1768
+ property({
1769
+ type: Boolean,
1770
+ attribute: 'hide-toggle',
1771
+ reflect: true
1772
+ })
1773
+ ], KRChatbot.prototype, "hideToggle", void 0);
1409
1774
  __decorate([
1410
1775
  state()
1411
1776
  ], KRChatbot.prototype, "_inputValue", void 0);
@@ -1418,6 +1783,12 @@ __decorate([
1418
1783
  __decorate([
1419
1784
  state()
1420
1785
  ], KRChatbot.prototype, "_error", void 0);
1786
+ __decorate([
1787
+ state()
1788
+ ], KRChatbot.prototype, "_pendingAttachments", void 0);
1789
+ __decorate([
1790
+ state()
1791
+ ], KRChatbot.prototype, "_isDraggingFile", void 0);
1421
1792
  __decorate([
1422
1793
  query('.panel')
1423
1794
  ], KRChatbot.prototype, "_panelEl", void 0);
@@ -1427,6 +1798,9 @@ __decorate([
1427
1798
  __decorate([
1428
1799
  query('.input')
1429
1800
  ], KRChatbot.prototype, "_inputEl", void 0);
1801
+ __decorate([
1802
+ query('.file-input')
1803
+ ], KRChatbot.prototype, "_fileInputEl", void 0);
1430
1804
  KRChatbot = __decorate([
1431
1805
  customElement('kr-chatbot')
1432
1806
  ], KRChatbot);