@kodaris/krubble-app-components 1.0.93 → 1.0.95
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 +45 -10
- package/dist/chatbot.d.ts.map +1 -1
- package/dist/chatbot.js +311 -87
- package/dist/chatbot.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/krubble-app.bundled.js +381 -193
- package/dist/krubble-app.bundled.js.map +1 -1
- package/dist/krubble-app.bundled.min.js +195 -63
- package/dist/krubble-app.bundled.min.js.map +1 -1
- package/dist/krubble-app.umd.js +381 -193
- package/dist/krubble-app.umd.js.map +1 -1
- package/dist/krubble-app.umd.min.js +188 -56
- package/dist/krubble-app.umd.min.js.map +1 -1
- package/dist/scaffold.d.ts +4 -25
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +52 -76
- package/dist/scaffold.js.map +1 -1
- package/dist/subbar.d.ts +0 -15
- package/dist/subbar.d.ts.map +1 -1
- package/dist/subbar.js +18 -30
- package/dist/subbar.js.map +1 -1
- package/package.json +1 -1
package/dist/chatbot.js
CHANGED
|
@@ -15,8 +15,18 @@ import { marked } from 'marked';
|
|
|
15
15
|
// (kr-dialog, kr-tab-group, kr-tab, kr-text-field) used by the browse dialog.
|
|
16
16
|
import { KRFilePreview } from '@kodaris/krubble-components';
|
|
17
17
|
import { KRHttp } from '@kodaris/krubble-http';
|
|
18
|
-
/** Image
|
|
19
|
-
const
|
|
18
|
+
/** Image extensions the chatbot renders as thumbnails (used when MIME is absent). */
|
|
19
|
+
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
20
|
+
/**
|
|
21
|
+
* File extensions the picker accepts. Mirrors what the backend can read: the
|
|
22
|
+
* document/spreadsheet family Bedrock ingests, the plain-text family read as
|
|
23
|
+
* fallback, and the image formats. This is a UX convenience — the backend
|
|
24
|
+
* accepts anything and just tells the model when a type can't be read.
|
|
25
|
+
*/
|
|
26
|
+
const ACCEPTED_FILE_EXTENSIONS = [
|
|
27
|
+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'html', 'md', 'txt', 'json', 'xml',
|
|
28
|
+
...IMAGE_EXTENSIONS,
|
|
29
|
+
];
|
|
20
30
|
/**
|
|
21
31
|
* AI chatbot component with SSE streaming.
|
|
22
32
|
*
|
|
@@ -81,6 +91,13 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
81
91
|
this._dragDepth = 0;
|
|
82
92
|
/** Monotonic id source for pending attachments. */
|
|
83
93
|
this._pendingSeq = 0;
|
|
94
|
+
/**
|
|
95
|
+
* The `aiAnalyze` mode for the next picked files — set when the attach action
|
|
96
|
+
* opens the picker (true = "Attach Files", false = "Attach Files for
|
|
97
|
+
* Reference"), read by the file-input change handler. Not reactive: it's read
|
|
98
|
+
* synchronously in the change handler, never rendered.
|
|
99
|
+
*/
|
|
100
|
+
this._pendingAttachAnalyze = true;
|
|
84
101
|
/**
|
|
85
102
|
* Request-id guards for skill searches — bumped on each search so out-of-order
|
|
86
103
|
* responses are dropped (mirrors the kr-combo-box `_requestId` pattern). Org and
|
|
@@ -197,7 +214,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
197
214
|
this._debugDrawerWidth = 430;
|
|
198
215
|
/** True while the user is dragging the debug drawer's resize handle. */
|
|
199
216
|
this._isDebugResizing = false;
|
|
200
|
-
/**
|
|
217
|
+
/** Files staged in the composer, before the message is sent. */
|
|
201
218
|
this._pendingConversationFiles = [];
|
|
202
219
|
/** True while a file is being dragged over the panel (shows the drop overlay). */
|
|
203
220
|
this._isDraggingFile = false;
|
|
@@ -296,7 +313,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
296
313
|
this._isResizing = false;
|
|
297
314
|
this._panelEl.classList.remove('panel--resizing');
|
|
298
315
|
}
|
|
299
|
-
this.
|
|
316
|
+
if (this._overlay) {
|
|
317
|
+
this._overlay.style.display = 'none';
|
|
318
|
+
}
|
|
300
319
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
|
301
320
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
302
321
|
this._saveState();
|
|
@@ -395,7 +414,11 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
395
414
|
document.removeEventListener('mousemove', this._handleMouseMove);
|
|
396
415
|
document.removeEventListener('mouseup', this._handleMouseUp);
|
|
397
416
|
// Release any object URLs still held by staged attachments.
|
|
398
|
-
this._pendingConversationFiles.forEach((p) =>
|
|
417
|
+
this._pendingConversationFiles.forEach((p) => {
|
|
418
|
+
if (p.previewUrl) {
|
|
419
|
+
URL.revokeObjectURL(p.previewUrl);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
399
422
|
}
|
|
400
423
|
updated(changed) {
|
|
401
424
|
if (changed.has('messages')) {
|
|
@@ -500,11 +523,6 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
500
523
|
}
|
|
501
524
|
this._overlay.style.display = 'block';
|
|
502
525
|
}
|
|
503
|
-
_hideOverlay() {
|
|
504
|
-
if (this._overlay) {
|
|
505
|
-
this._overlay.style.display = 'none';
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
526
|
_handleHeaderMouseDown(e) {
|
|
509
527
|
// Inline mode fills its parent — dragging the panel doesn't apply.
|
|
510
528
|
if (this.variant === 'inline') {
|
|
@@ -674,13 +692,14 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
674
692
|
}
|
|
675
693
|
_handleKeyDown(e) {
|
|
676
694
|
// When the skill menu is open, arrow keys / Enter / Escape drive it. The top
|
|
677
|
-
// actions (Attach Files, Browse All Skills, plus
|
|
678
|
-
// when enabled) come first, then the filtered
|
|
695
|
+
// actions (Attach Files, Attach Files for Reference, Browse All Skills, plus
|
|
696
|
+
// the two debug test triggers when enabled) come first, then the filtered
|
|
697
|
+
// skills — all one navigable list.
|
|
679
698
|
if (this._skillMenuOpened) {
|
|
680
699
|
const debugActions = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
681
|
-
let actionCount =
|
|
700
|
+
let actionCount = 3;
|
|
682
701
|
if (debugActions) {
|
|
683
|
-
actionCount =
|
|
702
|
+
actionCount = 5;
|
|
684
703
|
}
|
|
685
704
|
const count = actionCount + this._invocableSkills.length;
|
|
686
705
|
if (e.key === 'ArrowDown') {
|
|
@@ -712,19 +731,23 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
712
731
|
/** Run the skill-menu item at the given flat index (actions first, then skills). */
|
|
713
732
|
_activateSkillMenuItem(index) {
|
|
714
733
|
const debugActions = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
715
|
-
let actionCount =
|
|
734
|
+
let actionCount = 3;
|
|
716
735
|
if (debugActions) {
|
|
717
|
-
actionCount =
|
|
736
|
+
actionCount = 5;
|
|
718
737
|
}
|
|
719
738
|
if (index === 0) {
|
|
720
|
-
this._openFilePicker();
|
|
739
|
+
this._openFilePicker(true);
|
|
721
740
|
return;
|
|
722
741
|
}
|
|
723
742
|
if (index === 1) {
|
|
743
|
+
this._openFilePicker(false);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (index === 2) {
|
|
724
747
|
this._handleSkillBrowseOpen();
|
|
725
748
|
return;
|
|
726
749
|
}
|
|
727
|
-
if (debugActions && index ===
|
|
750
|
+
if (debugActions && index === 3) {
|
|
728
751
|
this._skillMenuOpened = false;
|
|
729
752
|
this._send({
|
|
730
753
|
message: '/test-context-warning',
|
|
@@ -732,7 +755,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
732
755
|
});
|
|
733
756
|
return;
|
|
734
757
|
}
|
|
735
|
-
if (debugActions && index ===
|
|
758
|
+
if (debugActions && index === 4) {
|
|
736
759
|
this._skillMenuOpened = false;
|
|
737
760
|
this._send({
|
|
738
761
|
message: '/test-context-limit',
|
|
@@ -1052,17 +1075,26 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1052
1075
|
this._skillBrowseSource = e.detail.activeTabId;
|
|
1053
1076
|
}
|
|
1054
1077
|
// --- Attachments ---
|
|
1055
|
-
|
|
1078
|
+
/**
|
|
1079
|
+
* Open the OS file picker. `aiAnalyze` records which attach action was used so
|
|
1080
|
+
* the change handler stages the picked files in the right mode: true for
|
|
1081
|
+
* "Attach Files" (the model reads them), false for "Attach Files for Reference"
|
|
1082
|
+
* (attached for a tool to act on — the model is told about them but not asked
|
|
1083
|
+
* to read them). Chosen once at the picker, so selecting 30 files never means
|
|
1084
|
+
* toggling 30 chips.
|
|
1085
|
+
*/
|
|
1086
|
+
_openFilePicker(aiAnalyze = true) {
|
|
1056
1087
|
// Close the skill/attachment menu — opening the OS file picker is the end of
|
|
1057
1088
|
// the menu interaction, and the picker is a separate window so the menu would
|
|
1058
1089
|
// otherwise linger open behind it (and after a file is chosen).
|
|
1059
1090
|
this._skillMenuOpened = false;
|
|
1091
|
+
this._pendingAttachAnalyze = aiAnalyze;
|
|
1060
1092
|
this._fileInputEl?.click();
|
|
1061
1093
|
}
|
|
1062
1094
|
_handleFileInputChange(e) {
|
|
1063
1095
|
const input = e.target;
|
|
1064
1096
|
if (input.files) {
|
|
1065
|
-
this._addFiles(input.files);
|
|
1097
|
+
this._addFiles(input.files, this._pendingAttachAnalyze);
|
|
1066
1098
|
}
|
|
1067
1099
|
// Reset so picking the same file again re-fires change.
|
|
1068
1100
|
input.value = '';
|
|
@@ -1125,35 +1157,62 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1125
1157
|
_hasFiles(e) {
|
|
1126
1158
|
return Array.from(e.dataTransfer?.types || []).includes('Files');
|
|
1127
1159
|
}
|
|
1128
|
-
/**
|
|
1129
|
-
|
|
1130
|
-
const
|
|
1160
|
+
/** Lower-cased extension of a filename (without the dot), or '' if none. */
|
|
1161
|
+
_getFileExtension(name) {
|
|
1162
|
+
const dot = name.lastIndexOf('.');
|
|
1163
|
+
if (dot < 0) {
|
|
1164
|
+
return '';
|
|
1165
|
+
}
|
|
1166
|
+
return name.slice(dot + 1).toLowerCase();
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Stage a set of files in the composer (held locally until send). `aiAnalyze`
|
|
1170
|
+
* comes from which attach action was used — true for "Attach Files" (the model
|
|
1171
|
+
* reads them), false for "Attach Files for Reference" (tool-only, not read).
|
|
1172
|
+
* Drag-and-drop and paste default to analyze.
|
|
1173
|
+
*/
|
|
1174
|
+
_addFiles(fileList, aiAnalyze = true) {
|
|
1175
|
+
// Only accept what the picker's `accept` filter allows (extension-based, so a
|
|
1176
|
+
// drag/paste can't sneak in a type the picker itself wouldn't offer).
|
|
1177
|
+
const files = Array.from(fileList).filter((f) => ACCEPTED_FILE_EXTENSIONS.includes(this._getFileExtension(f.name)));
|
|
1131
1178
|
if (!files.length) {
|
|
1132
1179
|
return;
|
|
1133
1180
|
}
|
|
1134
1181
|
files.forEach((file) => {
|
|
1182
|
+
// Only images get a local preview thumbnail; documents render an icon.
|
|
1183
|
+
// Prefer the File's MIME, falling back to the extension when it's absent.
|
|
1184
|
+
let previewUrl;
|
|
1185
|
+
if (file.type.startsWith('image/') || IMAGE_EXTENSIONS.includes(this._getFileExtension(file.name))) {
|
|
1186
|
+
previewUrl = URL.createObjectURL(file);
|
|
1187
|
+
}
|
|
1135
1188
|
const pending = {
|
|
1136
1189
|
localId: 'att-' + this._pendingSeq++,
|
|
1137
1190
|
file,
|
|
1138
1191
|
name: file.name,
|
|
1139
|
-
|
|
1192
|
+
mimeType: file.type,
|
|
1193
|
+
previewUrl,
|
|
1194
|
+
aiAnalyze,
|
|
1140
1195
|
};
|
|
1141
1196
|
this._pendingConversationFiles = [...this._pendingConversationFiles, pending];
|
|
1142
1197
|
});
|
|
1143
1198
|
}
|
|
1144
|
-
|
|
1199
|
+
_handlePendingRemove(localId) {
|
|
1145
1200
|
const target = this._pendingConversationFiles.find((p) => p.localId === localId);
|
|
1146
|
-
if (target) {
|
|
1201
|
+
if (target?.previewUrl) {
|
|
1147
1202
|
URL.revokeObjectURL(target.previewUrl);
|
|
1148
1203
|
}
|
|
1149
1204
|
this._pendingConversationFiles = this._pendingConversationFiles.filter((p) => p.localId !== localId);
|
|
1150
1205
|
}
|
|
1151
1206
|
_clearPending() {
|
|
1152
|
-
this._pendingConversationFiles.forEach((p) =>
|
|
1207
|
+
this._pendingConversationFiles.forEach((p) => {
|
|
1208
|
+
if (p.previewUrl) {
|
|
1209
|
+
URL.revokeObjectURL(p.previewUrl);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1153
1212
|
this._pendingConversationFiles = [];
|
|
1154
1213
|
}
|
|
1155
1214
|
/** Open the shared full-screen previewer for a message image. */
|
|
1156
|
-
|
|
1215
|
+
_handleImageOpen(img) {
|
|
1157
1216
|
// Use the local preview if the upload hasn't finished yet; otherwise the
|
|
1158
1217
|
// backend view URL (prefixed with the base URL — a raw fetch of a relative
|
|
1159
1218
|
// path would hit the page origin, e.g. the dev server, not the backend).
|
|
@@ -1170,10 +1229,6 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1170
1229
|
window.open(`${KRHttp.getBaseUrl()}/api/system/conversationFile/${img.conversationFileID}/view?download=true`, '_blank');
|
|
1171
1230
|
});
|
|
1172
1231
|
}
|
|
1173
|
-
/** Whether the send button is active: needs text or at least one image. */
|
|
1174
|
-
_canSend() {
|
|
1175
|
-
return Boolean(this._inputValue.trim()) || this._pendingConversationFiles.length > 0;
|
|
1176
|
-
}
|
|
1177
1232
|
_handleSubmit() {
|
|
1178
1233
|
if (this._isStreaming) {
|
|
1179
1234
|
return;
|
|
@@ -1184,10 +1239,13 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1184
1239
|
if (!messageText && !pending.length) {
|
|
1185
1240
|
return;
|
|
1186
1241
|
}
|
|
1187
|
-
// The
|
|
1188
|
-
// conversation reloads and they come back as stored
|
|
1189
|
-
// just-sent message itself carries text only.
|
|
1190
|
-
const conversationFiles = pending.map((p) =>
|
|
1242
|
+
// The files sent to the backend, each paired with its analyze flag. Sent
|
|
1243
|
+
// files are shown once the conversation reloads and they come back as stored
|
|
1244
|
+
// conversation files; the just-sent message itself carries text only.
|
|
1245
|
+
const conversationFiles = pending.map((p) => ({
|
|
1246
|
+
file: p.file,
|
|
1247
|
+
aiAnalyze: p.aiAnalyze,
|
|
1248
|
+
}));
|
|
1191
1249
|
const submitEvent = new CustomEvent('message-submit', {
|
|
1192
1250
|
detail: {
|
|
1193
1251
|
message: messageText,
|
|
@@ -1219,12 +1277,15 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1219
1277
|
};
|
|
1220
1278
|
// Attach the staged files' local previews to the message so its thumbnails
|
|
1221
1279
|
// show INSTANTLY, without waiting on the upload. Each carries a temporary
|
|
1222
|
-
// previewUrl (
|
|
1223
|
-
//
|
|
1280
|
+
// previewUrl (images only; documents render an icon from mimeType) and
|
|
1281
|
+
// conversationFileID -1 as a placeholder; the built-in send swaps these for
|
|
1282
|
+
// the real server files once the upload returns.
|
|
1224
1283
|
if (this._pendingConversationFiles.length) {
|
|
1225
1284
|
userMessage.conversationFiles = this._pendingConversationFiles.map((p) => ({
|
|
1226
1285
|
conversationFileID: -1,
|
|
1227
1286
|
niceName: p.name,
|
|
1287
|
+
mimeType: p.mimeType,
|
|
1288
|
+
aiAnalyze: p.aiAnalyze,
|
|
1228
1289
|
previewUrl: p.previewUrl,
|
|
1229
1290
|
}));
|
|
1230
1291
|
}
|
|
@@ -1279,11 +1340,17 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1279
1340
|
// upload is dropped so an undefined id never reaches `fileIds`.
|
|
1280
1341
|
let uploaded = Promise.resolve([]);
|
|
1281
1342
|
if (params.conversationFiles.length) {
|
|
1282
|
-
uploaded = Promise.all(params.conversationFiles.map((file) => {
|
|
1343
|
+
uploaded = Promise.all(params.conversationFiles.map(({ file, aiAnalyze }) => {
|
|
1283
1344
|
const form = new FormData();
|
|
1284
1345
|
form.append('file', file);
|
|
1285
1346
|
form.append('type', 'LIBRARY');
|
|
1286
1347
|
form.append('niceName', file.name);
|
|
1348
|
+
// Reference-only files (from "Attach Files for Reference") aren't read
|
|
1349
|
+
// by the model — the endpoint defaults aiAnalyze=true, so only send
|
|
1350
|
+
// the flag when it's false.
|
|
1351
|
+
if (!aiAnalyze) {
|
|
1352
|
+
form.append('aiAnalyze', 'false');
|
|
1353
|
+
}
|
|
1287
1354
|
return KRHttp.fetch({
|
|
1288
1355
|
url: '/api/system/conversationFile/upload',
|
|
1289
1356
|
method: 'POST',
|
|
@@ -1565,9 +1632,9 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1565
1632
|
// flat keyboard index counts actions first, then skills, so the skills'
|
|
1566
1633
|
// index offset is the number of actions shown.
|
|
1567
1634
|
const debugActions = this.variant === 'inline' && this._aiFrameworkConfig.debugTelemetryEnabled === true;
|
|
1568
|
-
let actionCount =
|
|
1635
|
+
let actionCount = 3;
|
|
1569
1636
|
if (debugActions) {
|
|
1570
|
-
actionCount =
|
|
1637
|
+
actionCount = 5;
|
|
1571
1638
|
}
|
|
1572
1639
|
return html `
|
|
1573
1640
|
<div class="skill-menu">
|
|
@@ -1577,14 +1644,25 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1577
1644
|
'skill-menu__action': true,
|
|
1578
1645
|
'skill-menu__item--highlighted': this._skillHighlighted === 0,
|
|
1579
1646
|
})}
|
|
1580
|
-
@click=${this._openFilePicker}
|
|
1647
|
+
@click=${() => this._openFilePicker(true)}
|
|
1581
1648
|
>
|
|
1582
|
-
Attach Files
|
|
1649
|
+
<span class="skill-menu__name">Attach Files</span>
|
|
1650
|
+
<span class="skill-menu__desc">The AI reads these files</span>
|
|
1583
1651
|
</button>
|
|
1584
1652
|
<button
|
|
1585
1653
|
class=${classMap({
|
|
1586
1654
|
'skill-menu__action': true,
|
|
1587
1655
|
'skill-menu__item--highlighted': this._skillHighlighted === 1,
|
|
1656
|
+
})}
|
|
1657
|
+
@click=${() => this._openFilePicker(false)}
|
|
1658
|
+
>
|
|
1659
|
+
<span class="skill-menu__name">Attach Files for Reference</span>
|
|
1660
|
+
<span class="skill-menu__desc">Attached for a tool — not read by the AI</span>
|
|
1661
|
+
</button>
|
|
1662
|
+
<button
|
|
1663
|
+
class=${classMap({
|
|
1664
|
+
'skill-menu__action': true,
|
|
1665
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 2,
|
|
1588
1666
|
})}
|
|
1589
1667
|
@click=${this._handleSkillBrowseOpen}
|
|
1590
1668
|
>
|
|
@@ -1594,18 +1672,18 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
1594
1672
|
<button
|
|
1595
1673
|
class=${classMap({
|
|
1596
1674
|
'skill-menu__action': true,
|
|
1597
|
-
'skill-menu__item--highlighted': this._skillHighlighted ===
|
|
1675
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 3,
|
|
1598
1676
|
})}
|
|
1599
|
-
@click=${() => this._activateSkillMenuItem(
|
|
1677
|
+
@click=${() => this._activateSkillMenuItem(3)}
|
|
1600
1678
|
>
|
|
1601
1679
|
Test Context Warning
|
|
1602
1680
|
</button>
|
|
1603
1681
|
<button
|
|
1604
1682
|
class=${classMap({
|
|
1605
1683
|
'skill-menu__action': true,
|
|
1606
|
-
'skill-menu__item--highlighted': this._skillHighlighted ===
|
|
1684
|
+
'skill-menu__item--highlighted': this._skillHighlighted === 4,
|
|
1607
1685
|
})}
|
|
1608
|
-
@click=${() => this._activateSkillMenuItem(
|
|
1686
|
+
@click=${() => this._activateSkillMenuItem(4)}
|
|
1609
1687
|
>
|
|
1610
1688
|
Test Context Limit
|
|
1611
1689
|
</button>
|
|
@@ -2051,19 +2129,41 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2051
2129
|
` : nothing}
|
|
2052
2130
|
${msg.conversationFiles && msg.conversationFiles.length ? html `
|
|
2053
2131
|
<div class="attachments">
|
|
2054
|
-
${msg.conversationFiles.map((
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2132
|
+
${msg.conversationFiles.map((file) => {
|
|
2133
|
+
const fileName = file.niceName || file.name || 'File';
|
|
2134
|
+
// A stored file is an image by its MIME when it has one, else
|
|
2135
|
+
// by its extension.
|
|
2136
|
+
let isImage = IMAGE_EXTENSIONS.includes(this._getFileExtension(fileName));
|
|
2137
|
+
if (file.mimeType) {
|
|
2138
|
+
isImage = file.mimeType.startsWith('image/');
|
|
2139
|
+
}
|
|
2140
|
+
// Images open in the previewer; documents just show a tile
|
|
2141
|
+
// (the previewer is image-only).
|
|
2142
|
+
if (isImage) {
|
|
2143
|
+
return html `
|
|
2144
|
+
<button
|
|
2145
|
+
class="attachment"
|
|
2146
|
+
type="button"
|
|
2147
|
+
title=${file.niceName || 'View image'}
|
|
2148
|
+
@click=${() => this._handleImageOpen(file)}
|
|
2149
|
+
>
|
|
2150
|
+
<img
|
|
2151
|
+
src=${file.previewUrl || `${KRHttp.getBaseUrl()}/api/system/conversationFile/${file.conversationFileID}/view`}
|
|
2152
|
+
alt=${file.niceName || ''}
|
|
2153
|
+
/>
|
|
2154
|
+
</button>
|
|
2155
|
+
`;
|
|
2156
|
+
}
|
|
2157
|
+
return html `
|
|
2158
|
+
<div
|
|
2159
|
+
class="attachment attachment--doc"
|
|
2160
|
+
title=${fileName}
|
|
2161
|
+
>
|
|
2162
|
+
<span class="attachment__type">${this._getFileExtension(fileName) || 'File'}</span>
|
|
2163
|
+
<span class="attachment__name">${fileName}</span>
|
|
2164
|
+
</div>
|
|
2165
|
+
`;
|
|
2166
|
+
})}
|
|
2067
2167
|
</div>
|
|
2068
2168
|
` : nothing}
|
|
2069
2169
|
${msg.content ? html `
|
|
@@ -2178,18 +2278,36 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2178
2278
|
${this._pendingConversationFiles.length ? html `
|
|
2179
2279
|
<div class="composer-attachments">
|
|
2180
2280
|
${repeat(this._pendingConversationFiles, (p) => p.localId, (p) => html `
|
|
2181
|
-
<div
|
|
2182
|
-
|
|
2281
|
+
<div
|
|
2282
|
+
class=${classMap({
|
|
2283
|
+
chip: true,
|
|
2284
|
+
'chip--doc': !p.previewUrl,
|
|
2285
|
+
})}
|
|
2286
|
+
title=${p.name}
|
|
2287
|
+
>
|
|
2288
|
+
<div
|
|
2289
|
+
class=${classMap({
|
|
2290
|
+
'chip-body': true,
|
|
2291
|
+
'chip-body--image': Boolean(p.previewUrl),
|
|
2292
|
+
})}
|
|
2293
|
+
>
|
|
2294
|
+
${p.previewUrl
|
|
2295
|
+
? html `<img src=${p.previewUrl} alt=${p.name} />`
|
|
2296
|
+
: html `
|
|
2297
|
+
<span class="chip-type">${this._getFileExtension(p.name) || 'File'}</span>
|
|
2298
|
+
<span class="chip-name">${p.name}</span>
|
|
2299
|
+
`}
|
|
2300
|
+
</div>
|
|
2183
2301
|
<button
|
|
2184
2302
|
class="chip-remove"
|
|
2185
2303
|
title="Remove"
|
|
2186
|
-
@click=${() => this.
|
|
2304
|
+
@click=${() => this._handlePendingRemove(p.localId)}
|
|
2187
2305
|
>
|
|
2188
2306
|
<svg
|
|
2189
2307
|
xmlns="http://www.w3.org/2000/svg"
|
|
2190
2308
|
fill="none"
|
|
2191
2309
|
viewBox="0 0 24 24"
|
|
2192
|
-
stroke-width="
|
|
2310
|
+
stroke-width="1.75"
|
|
2193
2311
|
stroke="currentColor"
|
|
2194
2312
|
>
|
|
2195
2313
|
<path
|
|
@@ -2206,7 +2324,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2206
2324
|
<input
|
|
2207
2325
|
class="file-input"
|
|
2208
2326
|
type="file"
|
|
2209
|
-
accept
|
|
2327
|
+
accept=${ACCEPTED_FILE_EXTENSIONS.map((ext) => '.' + ext).join(',')}
|
|
2210
2328
|
multiple
|
|
2211
2329
|
hidden
|
|
2212
2330
|
@change=${this._handleFileInputChange}
|
|
@@ -2251,7 +2369,7 @@ let KRChatbot = class KRChatbot extends LitElement {
|
|
|
2251
2369
|
<button
|
|
2252
2370
|
class=${classMap({
|
|
2253
2371
|
'send': true,
|
|
2254
|
-
'send--disabled': !this.
|
|
2372
|
+
'send--disabled': !this._inputValue.trim() && !this._pendingConversationFiles.length,
|
|
2255
2373
|
})}
|
|
2256
2374
|
@click=${this._handleSubmit}
|
|
2257
2375
|
title="Send message"
|
|
@@ -3180,13 +3298,15 @@ KRChatbot.styles = css `
|
|
|
3180
3298
|
margin-bottom: 24px;
|
|
3181
3299
|
}
|
|
3182
3300
|
|
|
3301
|
+
/* A sent attachment is the same tile as the composer chip, so a file looks
|
|
3302
|
+
identical before and after sending. */
|
|
3183
3303
|
.attachment {
|
|
3184
|
-
width:
|
|
3185
|
-
height:
|
|
3186
|
-
border-radius:
|
|
3304
|
+
width: 112px;
|
|
3305
|
+
height: 112px;
|
|
3306
|
+
border-radius: 20px;
|
|
3187
3307
|
overflow: hidden;
|
|
3188
|
-
background: #
|
|
3189
|
-
border:
|
|
3308
|
+
background: #f2f0f0;
|
|
3309
|
+
border: none;
|
|
3190
3310
|
cursor: zoom-in;
|
|
3191
3311
|
flex-shrink: 0;
|
|
3192
3312
|
padding: 0;
|
|
@@ -3199,6 +3319,37 @@ KRChatbot.styles = css `
|
|
|
3199
3319
|
display: block;
|
|
3200
3320
|
}
|
|
3201
3321
|
|
|
3322
|
+
/* Non-image attachment in a sent message — file-type label above the filename
|
|
3323
|
+
(the previewer is image-only, so this isn't clickable). */
|
|
3324
|
+
.attachment--doc {
|
|
3325
|
+
display: flex;
|
|
3326
|
+
flex-direction: column;
|
|
3327
|
+
padding: 16px 14px;
|
|
3328
|
+
box-sizing: border-box;
|
|
3329
|
+
text-align: left;
|
|
3330
|
+
cursor: default;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
.attachment__type {
|
|
3334
|
+
font-size: 13px;
|
|
3335
|
+
line-height: 1.2;
|
|
3336
|
+
color: #444746;
|
|
3337
|
+
text-transform: uppercase;
|
|
3338
|
+
flex-shrink: 0;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
.attachment__name {
|
|
3342
|
+
margin-top: auto;
|
|
3343
|
+
font-size: 13px;
|
|
3344
|
+
line-height: 1.25;
|
|
3345
|
+
color: #000;
|
|
3346
|
+
overflow: hidden;
|
|
3347
|
+
display: -webkit-box;
|
|
3348
|
+
-webkit-line-clamp: 3;
|
|
3349
|
+
-webkit-box-orient: vertical;
|
|
3350
|
+
overflow-wrap: anywhere;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3202
3353
|
/* Skill menu (opened by typing '/'). Fixed + JS-positioned above the
|
|
3203
3354
|
composer, following the krubble select-field dropdown pattern. */
|
|
3204
3355
|
.skill-menu {
|
|
@@ -3273,6 +3424,9 @@ KRChatbot.styles = css `
|
|
|
3273
3424
|
}
|
|
3274
3425
|
|
|
3275
3426
|
.skill-menu__action {
|
|
3427
|
+
display: flex;
|
|
3428
|
+
flex-direction: column;
|
|
3429
|
+
gap: 2px;
|
|
3276
3430
|
width: 100%;
|
|
3277
3431
|
padding: 6px 16px;
|
|
3278
3432
|
background: transparent;
|
|
@@ -3285,6 +3439,13 @@ KRChatbot.styles = css `
|
|
|
3285
3439
|
font-weight: 500;
|
|
3286
3440
|
}
|
|
3287
3441
|
|
|
3442
|
+
/* Secondary line on an attach action (e.g. "reads these files") — muted,
|
|
3443
|
+
not the primary accent, so it reads as a hint under the action label. */
|
|
3444
|
+
.skill-menu__action .skill-menu__desc {
|
|
3445
|
+
font-weight: 400;
|
|
3446
|
+
color: #6b7280;
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3288
3449
|
/* Highlight (keyboard) and hover (mouse) are independent — either shades the
|
|
3289
3450
|
row. Declared after the base rules so it wins the cascade for both the
|
|
3290
3451
|
action buttons and the skill items. */
|
|
@@ -3401,15 +3562,44 @@ KRChatbot.styles = css `
|
|
|
3401
3562
|
padding: 0 6px 10px;
|
|
3402
3563
|
}
|
|
3403
3564
|
|
|
3565
|
+
/* Every attachment — image or document — is the same square tile, so a mixed
|
|
3566
|
+
row reads as one uniform set. */
|
|
3404
3567
|
.chip {
|
|
3405
3568
|
position: relative;
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3569
|
+
display: flex;
|
|
3570
|
+
flex-direction: column;
|
|
3571
|
+
width: 112px;
|
|
3572
|
+
height: 112px;
|
|
3573
|
+
border-radius: 20px;
|
|
3409
3574
|
overflow: hidden;
|
|
3410
|
-
background: #
|
|
3411
|
-
border: 1px solid var(--kr-chatbot-border);
|
|
3575
|
+
background: #f2f0f0;
|
|
3412
3576
|
flex-shrink: 0;
|
|
3577
|
+
transition: background 0.28s ease;
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
.chip--doc {
|
|
3581
|
+
cursor: pointer;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
.chip--doc:hover {
|
|
3585
|
+
background: #dfdddd;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
/* The preview body — an image fills the tile; a document shows its file-type
|
|
3589
|
+
label above the filename, both left-aligned. */
|
|
3590
|
+
.chip-body {
|
|
3591
|
+
flex: 1;
|
|
3592
|
+
display: flex;
|
|
3593
|
+
flex-direction: column;
|
|
3594
|
+
min-height: 0;
|
|
3595
|
+
padding: 16px 14px;
|
|
3596
|
+
box-sizing: border-box;
|
|
3597
|
+
overflow: hidden;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
/* An image fills the whole tile, so it needs no padding around it. */
|
|
3601
|
+
.chip-body--image {
|
|
3602
|
+
padding: 0;
|
|
3413
3603
|
}
|
|
3414
3604
|
|
|
3415
3605
|
.chip img {
|
|
@@ -3419,31 +3609,65 @@ KRChatbot.styles = css `
|
|
|
3419
3609
|
display: block;
|
|
3420
3610
|
}
|
|
3421
3611
|
|
|
3612
|
+
/* File-type label (PDF, XLSX, …) — sits at the top of the tile. */
|
|
3613
|
+
.chip-type {
|
|
3614
|
+
font-size: 13px;
|
|
3615
|
+
line-height: 1.2;
|
|
3616
|
+
color: #444746;
|
|
3617
|
+
text-transform: uppercase;
|
|
3618
|
+
flex-shrink: 0;
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
/* Filename — pinned to the bottom of the tile, wrapping to at most three
|
|
3622
|
+
lines before it ellipsizes. */
|
|
3623
|
+
.chip-name {
|
|
3624
|
+
margin-top: auto;
|
|
3625
|
+
font-size: 13px;
|
|
3626
|
+
line-height: 1.25;
|
|
3627
|
+
color: #000;
|
|
3628
|
+
overflow: hidden;
|
|
3629
|
+
display: -webkit-box;
|
|
3630
|
+
-webkit-line-clamp: 3;
|
|
3631
|
+
-webkit-box-orient: vertical;
|
|
3632
|
+
overflow-wrap: anywhere;
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
/* Remove button — a bare X in the tile's top-right corner, revealed only when
|
|
3636
|
+
the tile (or the button itself) is hovered or keyboard-focused. */
|
|
3422
3637
|
.chip-remove {
|
|
3423
3638
|
position: absolute;
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3639
|
+
/* Inset matches .chip-body's padding so the X sits the same distance from
|
|
3640
|
+
the right edge as the file-type label does from the left. */
|
|
3641
|
+
top: 14px;
|
|
3642
|
+
right: 14px;
|
|
3643
|
+
width: 15px;
|
|
3644
|
+
height: 15px;
|
|
3429
3645
|
border: none;
|
|
3430
|
-
background:
|
|
3431
|
-
color: #
|
|
3646
|
+
background: none;
|
|
3647
|
+
color: #444746;
|
|
3432
3648
|
cursor: pointer;
|
|
3433
3649
|
display: flex;
|
|
3434
3650
|
align-items: center;
|
|
3435
3651
|
justify-content: center;
|
|
3436
3652
|
padding: 0;
|
|
3437
3653
|
line-height: 1;
|
|
3654
|
+
flex-shrink: 0;
|
|
3655
|
+
opacity: 0;
|
|
3656
|
+
transition: opacity 0.28s ease;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
.chip:hover .chip-remove,
|
|
3660
|
+
.chip-remove:focus-visible {
|
|
3661
|
+
opacity: 1;
|
|
3438
3662
|
}
|
|
3439
3663
|
|
|
3440
3664
|
.chip-remove:hover {
|
|
3441
|
-
|
|
3665
|
+
color: #000;
|
|
3442
3666
|
}
|
|
3443
3667
|
|
|
3444
3668
|
.chip-remove svg {
|
|
3445
|
-
width:
|
|
3446
|
-
height:
|
|
3669
|
+
width: 15px;
|
|
3670
|
+
height: 15px;
|
|
3447
3671
|
}
|
|
3448
3672
|
|
|
3449
3673
|
/* Drag-and-drop overlay */
|