@kodaris/krubble-app-components 1.0.68 → 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 +57 -2
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +400 -26
- package/dist/chatbot.js.map +1 -1
- package/dist/krubble-app.bundled.js +441 -56
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +276 -74
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +440 -55
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +276 -74
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/package.json +1 -1
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
|
|
13
|
-
* Must return a Response with a
|
|
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;
|
package/dist/chatbot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"chatbot.d.ts","sourceRoot":"","sources":["../src/chatbot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAQrD;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,4DAA4D;IAC5D,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,WAAW,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACrC;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,EAAE,IAAI,EAAE,CAAC;CACf,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAoBxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBACa,SAAU,SAAQ,UAAU;IACvC,OAAgB,MAAM,0BA4oBpB;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;IAE1B,4DAA4D;IAC5D,OAAO,CAAC,UAAU,CAAK;IAEvB,mDAAmD;IACnD,OAAO,CAAC,WAAW,CAAK;IAIxB;;;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;IAEf;;;;OAIG;IAMH,UAAU,UAAS;IAKnB,OAAO,CAAC,WAAW,CAAM;IAGzB,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,WAAW,CAAM;IAGzB,OAAO,CAAC,MAAM,CAAM;IAEpB,iEAAiE;IAEjE,OAAO,CAAC,mBAAmB,CAAoC;IAE/D,kFAAkF;IAElF,OAAO,CAAC,eAAe,CAAS;IAKhC,OAAO,CAAC,QAAQ,CAAe;IAG/B,OAAO,CAAC,WAAW,CAAe;IAGlC,OAAO,CAAC,QAAQ,CAAuB;IAGvC,OAAO,CAAC,YAAY,CAAoB;IAI/B,iBAAiB;IAYjB,oBAAoB;IAYpB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAsB9C;;OAEG;IACH,IAAI;IAIJ,2BAA2B;IAC3B,IAAI;IAMJ,4BAA4B;IAC5B,KAAK;IAML,yCAAyC;IACzC,MAAM;IAMN;;;;;;;;;;;;;;;;;;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;IAStB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,YAAY;IAoBpB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,SAAS;IAIjB,4EAA4E;IAC5E,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;IAKrB,+DAA+D;IAC/D,OAAO,CAAC,eAAe;IAOvB,2EAA2E;IAC3E,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,aAAa;IAoHrB,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;CAiUhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}
|
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.
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
601
|
-
|
|
767
|
+
${this.hideToggle ? nothing : html `
|
|
768
|
+
<button
|
|
769
|
+
class=${classMap({
|
|
602
770
|
'toggle': true,
|
|
603
771
|
'toggle--hidden': this.opened,
|
|
604
772
|
})}
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
<
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
|
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.
|
|
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);
|