@artooi/ag-ui-web-component 0.5.0 → 0.6.0
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/CHANGELOG.md +30 -1
- package/README.md +69 -2
- package/dist/ag-ui-web-component.bundle.js +155 -43
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +17 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +7 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +35 -0
- package/dist/core/attachment.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +32 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +578 -20
- package/dist/index.js.map +4 -4
- package/dist/ui/attachment_chips.d.ts +13 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -0
- package/dist/ui/attachment_tray.d.ts +42 -0
- package/dist/ui/attachment_tray.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +18 -0
- package/src/core/ag_ui_chat.ts +178 -12
- package/src/core/agui_client.ts +15 -2
- package/src/core/attachment.ts +39 -0
- package/src/core/upload_attachment.ts +113 -0
- package/src/index.ts +6 -0
- package/src/ui/attachment_chips.ts +68 -0
- package/src/ui/attachment_tray.ts +237 -0
- package/src/ui/styles.ts +110 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,12 @@ var TOOL_CALL_STATUS = {
|
|
|
17
17
|
ERROR: "error",
|
|
18
18
|
DECLINED: "declined"
|
|
19
19
|
};
|
|
20
|
+
var ATTACHMENT_STATUS = {
|
|
21
|
+
UPLOADING: "uploading",
|
|
22
|
+
READY: "ready",
|
|
23
|
+
ERROR: "error"
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
20
26
|
var TOOL_DISPLAY = {
|
|
21
27
|
MINIMAL: "minimal",
|
|
22
28
|
COMPACT: "compact",
|
|
@@ -231,6 +237,227 @@ function createStateHookTools(hook) {
|
|
|
231
237
|
return tools;
|
|
232
238
|
}
|
|
233
239
|
|
|
240
|
+
// src/ui/attachment_chips.ts
|
|
241
|
+
function renderAttachmentChips(refs) {
|
|
242
|
+
const list = document.createElement("div");
|
|
243
|
+
list.className = "attachment-chips";
|
|
244
|
+
for (const ref of refs) {
|
|
245
|
+
list.appendChild(renderChip(ref));
|
|
246
|
+
}
|
|
247
|
+
return list;
|
|
248
|
+
}
|
|
249
|
+
function renderChip(ref) {
|
|
250
|
+
const chip = document.createElement("div");
|
|
251
|
+
chip.className = "attachment-chip attachment-chip--ready";
|
|
252
|
+
const icon = document.createElement("span");
|
|
253
|
+
icon.className = "attachment-chip-icon";
|
|
254
|
+
icon.textContent = iconFor(ref.mime);
|
|
255
|
+
icon.setAttribute("aria-hidden", "true");
|
|
256
|
+
const name = document.createElement("span");
|
|
257
|
+
name.className = "attachment-chip-name";
|
|
258
|
+
name.textContent = ref.name;
|
|
259
|
+
name.title = ref.name;
|
|
260
|
+
const size = document.createElement("span");
|
|
261
|
+
size.className = "attachment-chip-size";
|
|
262
|
+
size.textContent = formatBytes(ref.size);
|
|
263
|
+
chip.append(icon, name, size);
|
|
264
|
+
return chip;
|
|
265
|
+
}
|
|
266
|
+
function iconFor(mime) {
|
|
267
|
+
if (mime.startsWith("image/")) {
|
|
268
|
+
return "\u{1F5BC}";
|
|
269
|
+
}
|
|
270
|
+
if (mime === "application/pdf") {
|
|
271
|
+
return "\u{1F4D5}";
|
|
272
|
+
}
|
|
273
|
+
if (mime.startsWith("text/")) {
|
|
274
|
+
return "\u{1F4C4}";
|
|
275
|
+
}
|
|
276
|
+
return "\u{1F4CE}";
|
|
277
|
+
}
|
|
278
|
+
function formatBytes(bytes) {
|
|
279
|
+
if (bytes < 1024) {
|
|
280
|
+
return `${bytes} B`;
|
|
281
|
+
}
|
|
282
|
+
const units = ["KB", "MB", "GB"];
|
|
283
|
+
let value = bytes / 1024;
|
|
284
|
+
let unit = 0;
|
|
285
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
286
|
+
value /= 1024;
|
|
287
|
+
unit += 1;
|
|
288
|
+
}
|
|
289
|
+
const rounded = value < 10 ? Math.round(value * 10) / 10 : Math.round(value);
|
|
290
|
+
return `${rounded} ${units[unit]}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/ui/attachment_tray.ts
|
|
294
|
+
import { randomUUID } from "@ag-ui/client";
|
|
295
|
+
var AttachmentTray = class {
|
|
296
|
+
/** The tray root; append above the input row. Hidden while empty. */
|
|
297
|
+
element;
|
|
298
|
+
#config;
|
|
299
|
+
#items = [];
|
|
300
|
+
constructor(config) {
|
|
301
|
+
this.#config = config;
|
|
302
|
+
this.element = document.createElement("div");
|
|
303
|
+
this.element.className = "attachment-tray";
|
|
304
|
+
this.element.hidden = true;
|
|
305
|
+
}
|
|
306
|
+
/** Queue a file: reject oversize/disallowed into an error chip, else upload. */
|
|
307
|
+
add(file) {
|
|
308
|
+
const item = {
|
|
309
|
+
localId: randomUUID(),
|
|
310
|
+
file,
|
|
311
|
+
status: ATTACHMENT_STATUS.UPLOADING,
|
|
312
|
+
progress: 0,
|
|
313
|
+
ref: null,
|
|
314
|
+
error: ""
|
|
315
|
+
};
|
|
316
|
+
this.#items.push(item);
|
|
317
|
+
const rejection = this.#reject(file);
|
|
318
|
+
if (rejection !== null) {
|
|
319
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
320
|
+
item.error = rejection;
|
|
321
|
+
this.#render();
|
|
322
|
+
this.#config.onChange?.();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
this.#render();
|
|
326
|
+
this.#config.onChange?.();
|
|
327
|
+
this.#upload(item);
|
|
328
|
+
}
|
|
329
|
+
/** The durable refs of every chip that finished uploading. */
|
|
330
|
+
readyRefs() {
|
|
331
|
+
const refs = [];
|
|
332
|
+
for (const item of this.#items) {
|
|
333
|
+
if (item.ref !== null) {
|
|
334
|
+
refs.push(item.ref);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return refs;
|
|
338
|
+
}
|
|
339
|
+
/** Whether any chip is still uploading (a send would drop nothing if false). */
|
|
340
|
+
hasPending() {
|
|
341
|
+
return this.#items.some((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
342
|
+
}
|
|
343
|
+
/** Whether the tray holds no chips. */
|
|
344
|
+
isEmpty() {
|
|
345
|
+
return this.#items.length === 0;
|
|
346
|
+
}
|
|
347
|
+
/** Drop the settled (ready / error) chips, leaving any still uploading. */
|
|
348
|
+
clearReady() {
|
|
349
|
+
this.#items = this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
350
|
+
this.#render();
|
|
351
|
+
}
|
|
352
|
+
/** Drop every chip (a reset / new-chat). */
|
|
353
|
+
clear() {
|
|
354
|
+
this.#items = [];
|
|
355
|
+
this.#render();
|
|
356
|
+
}
|
|
357
|
+
/** The size/type rejection reason for a file, or `null` when accepted. */
|
|
358
|
+
#reject(file) {
|
|
359
|
+
if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
|
|
360
|
+
return `Too large (max ${formatBytes(this.#config.maxBytes)})`;
|
|
361
|
+
}
|
|
362
|
+
if (!accepts(this.#config.accept, file)) {
|
|
363
|
+
return "File type not allowed";
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
#upload(item) {
|
|
368
|
+
item.status = ATTACHMENT_STATUS.UPLOADING;
|
|
369
|
+
item.progress = 0;
|
|
370
|
+
item.error = "";
|
|
371
|
+
this.#render();
|
|
372
|
+
this.#config.upload(item.file, (fraction) => {
|
|
373
|
+
item.progress = fraction;
|
|
374
|
+
this.#render();
|
|
375
|
+
}).then((ref) => {
|
|
376
|
+
item.status = ATTACHMENT_STATUS.READY;
|
|
377
|
+
item.ref = ref;
|
|
378
|
+
}).catch((error) => {
|
|
379
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
380
|
+
item.error = error instanceof Error ? error.message : "upload failed";
|
|
381
|
+
}).finally(() => {
|
|
382
|
+
this.#render();
|
|
383
|
+
this.#config.onChange?.();
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
#remove(item) {
|
|
387
|
+
this.#items = this.#items.filter((other) => other !== item);
|
|
388
|
+
this.#render();
|
|
389
|
+
this.#config.onChange?.();
|
|
390
|
+
}
|
|
391
|
+
#render() {
|
|
392
|
+
this.element.replaceChildren();
|
|
393
|
+
this.element.hidden = this.#items.length === 0;
|
|
394
|
+
for (const item of this.#items) {
|
|
395
|
+
this.element.appendChild(this.#renderChip(item));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
#renderChip(item) {
|
|
399
|
+
const chip = document.createElement("div");
|
|
400
|
+
chip.className = `attachment-chip attachment-chip--${item.status}`;
|
|
401
|
+
const icon = document.createElement("span");
|
|
402
|
+
icon.className = "attachment-chip-icon";
|
|
403
|
+
icon.textContent = iconFor(item.file.type);
|
|
404
|
+
icon.setAttribute("aria-hidden", "true");
|
|
405
|
+
const name = document.createElement("span");
|
|
406
|
+
name.className = "attachment-chip-name";
|
|
407
|
+
name.textContent = item.file.name;
|
|
408
|
+
name.title = item.file.name;
|
|
409
|
+
const meta = document.createElement("span");
|
|
410
|
+
meta.className = "attachment-chip-size";
|
|
411
|
+
meta.textContent = item.status === ATTACHMENT_STATUS.ERROR ? item.error : formatBytes(item.file.size);
|
|
412
|
+
chip.append(icon, name, meta);
|
|
413
|
+
if (item.status === ATTACHMENT_STATUS.UPLOADING) {
|
|
414
|
+
const bar = document.createElement("div");
|
|
415
|
+
bar.className = "attachment-chip-bar";
|
|
416
|
+
const fill = document.createElement("div");
|
|
417
|
+
fill.className = "attachment-chip-bar-fill";
|
|
418
|
+
fill.style.width = `${Math.round(item.progress * 100)}%`;
|
|
419
|
+
bar.appendChild(fill);
|
|
420
|
+
chip.appendChild(bar);
|
|
421
|
+
}
|
|
422
|
+
if (item.status === ATTACHMENT_STATUS.ERROR) {
|
|
423
|
+
const retry = document.createElement("button");
|
|
424
|
+
retry.type = "button";
|
|
425
|
+
retry.className = "attachment-chip-retry";
|
|
426
|
+
retry.title = "Retry";
|
|
427
|
+
retry.setAttribute("aria-label", "Retry upload");
|
|
428
|
+
retry.textContent = "\u21BB";
|
|
429
|
+
retry.addEventListener("click", () => this.#upload(item));
|
|
430
|
+
chip.appendChild(retry);
|
|
431
|
+
}
|
|
432
|
+
const remove = document.createElement("button");
|
|
433
|
+
remove.type = "button";
|
|
434
|
+
remove.className = "attachment-chip-remove";
|
|
435
|
+
remove.title = "Remove";
|
|
436
|
+
remove.setAttribute("aria-label", "Remove attachment");
|
|
437
|
+
remove.textContent = "\u2715";
|
|
438
|
+
remove.addEventListener("click", () => this.#remove(item));
|
|
439
|
+
chip.appendChild(remove);
|
|
440
|
+
return chip;
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
function accepts(accept, file) {
|
|
444
|
+
const tokens = accept.split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "");
|
|
445
|
+
if (tokens.length === 0) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
const mime = file.type.toLowerCase();
|
|
449
|
+
const name = file.name.toLowerCase();
|
|
450
|
+
return tokens.some((token) => {
|
|
451
|
+
if (token.startsWith(".")) {
|
|
452
|
+
return name.endsWith(token);
|
|
453
|
+
}
|
|
454
|
+
if (token.endsWith("/*")) {
|
|
455
|
+
return mime.startsWith(token.slice(0, -1));
|
|
456
|
+
}
|
|
457
|
+
return mime === token;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
234
461
|
// src/ui/confirmation_card.ts
|
|
235
462
|
function actionButton(modifier, label) {
|
|
236
463
|
const button = document.createElement("button");
|
|
@@ -3464,6 +3691,116 @@ var STYLES = `
|
|
|
3464
3691
|
background: var(--ag-ui-muted);
|
|
3465
3692
|
}
|
|
3466
3693
|
|
|
3694
|
+
/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3695
|
+
/* The \u{1F4CE} picker button sits left of the input; hidden until data-attachments-url. */
|
|
3696
|
+
.attach-btn {
|
|
3697
|
+
border: 1px solid var(--ag-ui-border);
|
|
3698
|
+
border-radius: 8px;
|
|
3699
|
+
padding: 0 10px;
|
|
3700
|
+
background: var(--ag-ui-input-bg);
|
|
3701
|
+
color: inherit;
|
|
3702
|
+
font: inherit;
|
|
3703
|
+
cursor: pointer;
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
.attach-btn:hover {
|
|
3707
|
+
border-color: var(--ag-ui-accent);
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
.attach-input {
|
|
3711
|
+
display: none;
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
3715
|
+
.attachment-slot {
|
|
3716
|
+
display: contents;
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
.attachment-tray {
|
|
3720
|
+
display: flex;
|
|
3721
|
+
flex-wrap: wrap;
|
|
3722
|
+
gap: 6px;
|
|
3723
|
+
padding: 8px 12px 0;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
.attachment-chips {
|
|
3727
|
+
display: flex;
|
|
3728
|
+
flex-wrap: wrap;
|
|
3729
|
+
gap: 6px;
|
|
3730
|
+
margin-top: 6px;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
.attachment-chip {
|
|
3734
|
+
display: inline-flex;
|
|
3735
|
+
align-items: center;
|
|
3736
|
+
gap: 6px;
|
|
3737
|
+
max-width: 100%;
|
|
3738
|
+
padding: 4px 8px;
|
|
3739
|
+
border: 1px solid var(--ag-ui-border);
|
|
3740
|
+
border-radius: 999px;
|
|
3741
|
+
background: var(--ag-ui-assistant-bg);
|
|
3742
|
+
font-size: 0.85em;
|
|
3743
|
+
position: relative;
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
.attachment-chip--error {
|
|
3747
|
+
border-color: var(--ag-ui-danger);
|
|
3748
|
+
color: var(--ag-ui-danger);
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
.attachment-chip-name {
|
|
3752
|
+
overflow: hidden;
|
|
3753
|
+
text-overflow: ellipsis;
|
|
3754
|
+
white-space: nowrap;
|
|
3755
|
+
max-width: 14ch;
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
.attachment-chip-size {
|
|
3759
|
+
color: var(--ag-ui-muted);
|
|
3760
|
+
white-space: nowrap;
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
.attachment-chip--error .attachment-chip-size {
|
|
3764
|
+
color: var(--ag-ui-danger);
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
/* The progress bar fills as the file uploads. */
|
|
3768
|
+
.attachment-chip-bar {
|
|
3769
|
+
flex-basis: 100%;
|
|
3770
|
+
height: 3px;
|
|
3771
|
+
border-radius: 2px;
|
|
3772
|
+
background: var(--ag-ui-border);
|
|
3773
|
+
overflow: hidden;
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
.attachment-chip-bar-fill {
|
|
3777
|
+
height: 100%;
|
|
3778
|
+
background: var(--ag-ui-accent);
|
|
3779
|
+
transition: width 0.15s ease;
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
.attachment-chip-remove,
|
|
3783
|
+
.attachment-chip-retry {
|
|
3784
|
+
border: none;
|
|
3785
|
+
background: none;
|
|
3786
|
+
color: inherit;
|
|
3787
|
+
cursor: pointer;
|
|
3788
|
+
padding: 0;
|
|
3789
|
+
line-height: 1;
|
|
3790
|
+
opacity: 0.7;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
.attachment-chip-remove:hover,
|
|
3794
|
+
.attachment-chip-retry:hover {
|
|
3795
|
+
opacity: 1;
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
/* A subtle outline while a file is dragged over the shell. */
|
|
3799
|
+
.chat--dragover {
|
|
3800
|
+
outline: 2px dashed var(--ag-ui-accent);
|
|
3801
|
+
outline-offset: -4px;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3467
3804
|
/* Muted "\u23F9 Stopped" line after a cancelled run \u2014 a note, not an error bubble. */
|
|
3468
3805
|
.stopped-note {
|
|
3469
3806
|
align-self: flex-start;
|
|
@@ -4046,7 +4383,7 @@ ${text2}`;
|
|
|
4046
4383
|
};
|
|
4047
4384
|
|
|
4048
4385
|
// src/core/agui_client.ts
|
|
4049
|
-
import { randomUUID } from "@ag-ui/client";
|
|
4386
|
+
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
4050
4387
|
var AgUiClient = class {
|
|
4051
4388
|
#agent;
|
|
4052
4389
|
#handlers;
|
|
@@ -4080,9 +4417,18 @@ var AgUiClient = class {
|
|
|
4080
4417
|
* When the agent calls frontend tools, this executes them and re-runs the
|
|
4081
4418
|
* agent with the results, looping until the agent stops calling frontend
|
|
4082
4419
|
* tools (bounded by {@link MAX_TOOL_ROUNDS}).
|
|
4420
|
+
*
|
|
4421
|
+
* `attachments` ride on the user message as a non-standard field so the
|
|
4422
|
+
* default client store round-trips them for history replay; the agent learns
|
|
4423
|
+
* the ids from the run context (the server's strict validation ignores the
|
|
4424
|
+
* unknown message field), then reads bytes via the `read_attachment` tool.
|
|
4083
4425
|
*/
|
|
4084
|
-
async send(content) {
|
|
4085
|
-
|
|
4426
|
+
async send(content, attachments = []) {
|
|
4427
|
+
const message = { id: randomUUID2(), role: "user", content };
|
|
4428
|
+
if (attachments.length > 0) {
|
|
4429
|
+
message.attachments = attachments;
|
|
4430
|
+
}
|
|
4431
|
+
this.#agent.addMessage(message);
|
|
4086
4432
|
this.#onPersist(this.#agent.messages);
|
|
4087
4433
|
await this.#run();
|
|
4088
4434
|
}
|
|
@@ -4096,7 +4442,7 @@ var AgUiClient = class {
|
|
|
4096
4442
|
}
|
|
4097
4443
|
/** Append a frontend tool result to history (used by the resume path). */
|
|
4098
4444
|
addToolResult(toolCallId, content) {
|
|
4099
|
-
this.#agent.addMessage({ id:
|
|
4445
|
+
this.#agent.addMessage({ id: randomUUID2(), role: "tool", content, toolCallId });
|
|
4100
4446
|
this.#onPersist(this.#agent.messages);
|
|
4101
4447
|
}
|
|
4102
4448
|
/**
|
|
@@ -4158,7 +4504,7 @@ var AgUiClient = class {
|
|
|
4158
4504
|
return;
|
|
4159
4505
|
}
|
|
4160
4506
|
this.#agent.addMessage({
|
|
4161
|
-
id:
|
|
4507
|
+
id: randomUUID2(),
|
|
4162
4508
|
role: "tool",
|
|
4163
4509
|
content: result.content,
|
|
4164
4510
|
toolCallId: call.id
|
|
@@ -4208,8 +4554,14 @@ function isAbortError(error) {
|
|
|
4208
4554
|
return error instanceof Error && error.name === "AbortError";
|
|
4209
4555
|
}
|
|
4210
4556
|
|
|
4557
|
+
// src/core/attachment.ts
|
|
4558
|
+
function messageAttachments(message) {
|
|
4559
|
+
const refs = message.attachments;
|
|
4560
|
+
return Array.isArray(refs) ? refs : [];
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4211
4563
|
// src/core/conversation_store.ts
|
|
4212
|
-
import { randomUUID as
|
|
4564
|
+
import { randomUUID as randomUUID3 } from "@ag-ui/client";
|
|
4213
4565
|
var THREAD_KEY = "ag-ui-chat:thread";
|
|
4214
4566
|
var THREADS_KEY = "ag-ui-chat:threads";
|
|
4215
4567
|
var MESSAGES_PREFIX = "ag-ui-chat:messages:";
|
|
@@ -4223,7 +4575,7 @@ var SessionStorageStore = class {
|
|
|
4223
4575
|
if (existing !== null) {
|
|
4224
4576
|
return existing;
|
|
4225
4577
|
}
|
|
4226
|
-
const id =
|
|
4578
|
+
const id = randomUUID3();
|
|
4227
4579
|
sessionStorage.setItem(THREAD_KEY, id);
|
|
4228
4580
|
return id;
|
|
4229
4581
|
}
|
|
@@ -4463,6 +4815,70 @@ var RemoteConversationStore = class {
|
|
|
4463
4815
|
}
|
|
4464
4816
|
};
|
|
4465
4817
|
|
|
4818
|
+
// src/core/upload_attachment.ts
|
|
4819
|
+
function uploadAttachment(file, options) {
|
|
4820
|
+
return new Promise((resolve, reject) => {
|
|
4821
|
+
const form = new FormData();
|
|
4822
|
+
form.append("file", file);
|
|
4823
|
+
const xhr = new XMLHttpRequest();
|
|
4824
|
+
xhr.open("POST", options.url);
|
|
4825
|
+
for (const [key, value] of Object.entries(options.headers ?? {})) {
|
|
4826
|
+
xhr.setRequestHeader(key, value);
|
|
4827
|
+
}
|
|
4828
|
+
const onProgress = options.onProgress;
|
|
4829
|
+
if (onProgress !== void 0) {
|
|
4830
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
4831
|
+
if (event.lengthComputable) {
|
|
4832
|
+
onProgress(event.total === 0 ? 0 : event.loaded / event.total);
|
|
4833
|
+
}
|
|
4834
|
+
});
|
|
4835
|
+
}
|
|
4836
|
+
xhr.addEventListener("load", () => {
|
|
4837
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
4838
|
+
try {
|
|
4839
|
+
resolve(parseRef(JSON.parse(xhr.responseText)));
|
|
4840
|
+
} catch {
|
|
4841
|
+
reject(new Error("upload returned an unreadable response"));
|
|
4842
|
+
}
|
|
4843
|
+
} else {
|
|
4844
|
+
reject(new Error(errorMessage(xhr)));
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
xhr.addEventListener("error", () => reject(new Error("upload failed")));
|
|
4848
|
+
xhr.addEventListener("abort", () => reject(new Error("upload cancelled")));
|
|
4849
|
+
const signal = options.signal;
|
|
4850
|
+
if (signal !== void 0) {
|
|
4851
|
+
signal.addEventListener("abort", () => xhr.abort());
|
|
4852
|
+
}
|
|
4853
|
+
xhr.send(form);
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
function parseRef(body) {
|
|
4857
|
+
if (typeof body !== "object" || body === null) {
|
|
4858
|
+
throw new Error("not an object");
|
|
4859
|
+
}
|
|
4860
|
+
const o = body;
|
|
4861
|
+
const id = o["id"];
|
|
4862
|
+
const name = o["name"];
|
|
4863
|
+
const mime = o["mime"];
|
|
4864
|
+
const size = o["size"];
|
|
4865
|
+
const url = o["url"];
|
|
4866
|
+
if (typeof id !== "string" || typeof name !== "string" || typeof mime !== "string" || typeof size !== "number") {
|
|
4867
|
+
throw new Error("missing fields");
|
|
4868
|
+
}
|
|
4869
|
+
return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
|
|
4870
|
+
}
|
|
4871
|
+
function errorMessage(xhr) {
|
|
4872
|
+
try {
|
|
4873
|
+
const body = JSON.parse(xhr.responseText);
|
|
4874
|
+
if (typeof body.error === "string") {
|
|
4875
|
+
return body.error;
|
|
4876
|
+
}
|
|
4877
|
+
} catch {
|
|
4878
|
+
}
|
|
4879
|
+
return `upload failed (${xhr.status})`;
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4466
4882
|
// src/core/ag_ui_chat.ts
|
|
4467
4883
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
4468
4884
|
var AgUiChat = class extends HTMLElement {
|
|
@@ -4503,9 +4919,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4503
4919
|
];
|
|
4504
4920
|
/**
|
|
4505
4921
|
* Per-run context provider. Defaults to the compact page map (when a
|
|
4506
|
-
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
4922
|
+
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
|
|
4923
|
+
* plus a one-line manifest of the files attached to the message being sent,
|
|
4924
|
+
* so the agent knows which `read_attachment` ids are available.
|
|
4507
4925
|
*/
|
|
4508
|
-
getContext = () =>
|
|
4926
|
+
getContext = () => [
|
|
4927
|
+
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
4928
|
+
...this.#attachmentContext()
|
|
4929
|
+
];
|
|
4509
4930
|
/**
|
|
4510
4931
|
* Navigable routes the agent can jump to via the built-in `route.*` tools.
|
|
4511
4932
|
* A compact summary also rides in each run's context.
|
|
@@ -4527,6 +4948,16 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4527
4948
|
* server-backed store for cross-tab/device durability.
|
|
4528
4949
|
*/
|
|
4529
4950
|
conversationStore = new SessionStorageStore();
|
|
4951
|
+
/**
|
|
4952
|
+
* How attached files are uploaded. `null` (default) uses the built-in
|
|
4953
|
+
* multipart `POST` to `data-attachments-url`. Set a custom
|
|
4954
|
+
* {@link UploadHandler} — `(file, onProgress) => Promise<AttachmentRef>` — to
|
|
4955
|
+
* swap the transport (e.g. a `tus-js-client` resumable adapter or
|
|
4956
|
+
* direct-to-S3 multipart) without changing the tray, the chips, or the AG-UI
|
|
4957
|
+
* wire (refs are transport-agnostic). When set, the 📎 affordance appears even
|
|
4958
|
+
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
4959
|
+
*/
|
|
4960
|
+
uploadHandler = null;
|
|
4530
4961
|
/**
|
|
4531
4962
|
* Builds the tool result a navigating tool resumes with after the page
|
|
4532
4963
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -4575,6 +5006,14 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4575
5006
|
#skillsMenu;
|
|
4576
5007
|
#drawer;
|
|
4577
5008
|
#skillHint;
|
|
5009
|
+
/** File-picker button + hidden input + tray slot; the tray mounts on connect. */
|
|
5010
|
+
#attachButton;
|
|
5011
|
+
#fileInput;
|
|
5012
|
+
#attachSlot;
|
|
5013
|
+
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5014
|
+
#attachTray = null;
|
|
5015
|
+
/** Refs attached to the message currently being sent (the context manifest). */
|
|
5016
|
+
#runAttachments = [];
|
|
4578
5017
|
#client = null;
|
|
4579
5018
|
// Whether an interaction is in flight (first onRunStart → onSettled). Drives
|
|
4580
5019
|
// the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
|
|
@@ -4604,6 +5043,9 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4604
5043
|
this.#send = document.createElement("button");
|
|
4605
5044
|
this.#title = document.createElement("span");
|
|
4606
5045
|
this.#skillHint = document.createElement("div");
|
|
5046
|
+
this.#attachButton = document.createElement("button");
|
|
5047
|
+
this.#fileInput = document.createElement("input");
|
|
5048
|
+
this.#attachSlot = document.createElement("div");
|
|
4607
5049
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
4608
5050
|
this.#drawer = new ThreadDrawer({
|
|
4609
5051
|
onSelect: (threadId) => {
|
|
@@ -4717,9 +5159,96 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4717
5159
|
this.#initSkills();
|
|
4718
5160
|
void this.#fetchToolCatalog();
|
|
4719
5161
|
this.#wireThreadStore();
|
|
5162
|
+
this.#wireAttachments();
|
|
4720
5163
|
this.#threadId = this.conversationStore.threadId();
|
|
4721
5164
|
void this.#rehydrate();
|
|
4722
5165
|
}
|
|
5166
|
+
/**
|
|
5167
|
+
* Enable the composer's file-upload tray when uploads are possible — either a
|
|
5168
|
+
* custom {@link uploadHandler} is set or `data-attachments-url` provides the
|
|
5169
|
+
* built-in multipart endpoint: reveal the 📎 button, wire the hidden file
|
|
5170
|
+
* input + drag-and-drop, and mount the tray. With neither, the affordance
|
|
5171
|
+
* stays hidden and the chat degrades to text-only.
|
|
5172
|
+
*/
|
|
5173
|
+
#wireAttachments() {
|
|
5174
|
+
const url = this.getAttribute("data-attachments-url");
|
|
5175
|
+
const upload = this.uploadHandler ?? this.#defaultUploadHandler(url);
|
|
5176
|
+
if (upload === null) {
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
const accept = this.getAttribute("data-attachment-accept") ?? "";
|
|
5180
|
+
this.#attachTray = new AttachmentTray({
|
|
5181
|
+
upload,
|
|
5182
|
+
maxBytes: this.#attachmentMaxBytes(),
|
|
5183
|
+
accept
|
|
5184
|
+
});
|
|
5185
|
+
this.#attachSlot.appendChild(this.#attachTray.element);
|
|
5186
|
+
this.#fileInput.accept = accept;
|
|
5187
|
+
this.#attachButton.hidden = false;
|
|
5188
|
+
this.#enableDragAndDrop();
|
|
5189
|
+
}
|
|
5190
|
+
/** The built-in multipart upload handler for `data-attachments-url`, or `null`. */
|
|
5191
|
+
#defaultUploadHandler(url) {
|
|
5192
|
+
if (url === null) {
|
|
5193
|
+
return null;
|
|
5194
|
+
}
|
|
5195
|
+
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
5196
|
+
}
|
|
5197
|
+
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
5198
|
+
#attachmentMaxBytes() {
|
|
5199
|
+
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
5200
|
+
if (attr === null) {
|
|
5201
|
+
return DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
5202
|
+
}
|
|
5203
|
+
const parsed = Number.parseInt(attr, 10);
|
|
5204
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_ATTACHMENT_MAX_BYTES;
|
|
5205
|
+
}
|
|
5206
|
+
/** Queue every file from the picker into the tray, then reset the input. */
|
|
5207
|
+
#onFilesPicked() {
|
|
5208
|
+
const files = this.#fileInput.files;
|
|
5209
|
+
if (files !== null) {
|
|
5210
|
+
for (const file of Array.from(files)) {
|
|
5211
|
+
this.#attachTray?.add(file);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
this.#fileInput.value = "";
|
|
5215
|
+
}
|
|
5216
|
+
/** Accept files dropped anywhere on the chat shell into the tray. */
|
|
5217
|
+
#enableDragAndDrop() {
|
|
5218
|
+
this.#chat.addEventListener("dragover", (event) => {
|
|
5219
|
+
event.preventDefault();
|
|
5220
|
+
this.#chat.classList.add("chat--dragover");
|
|
5221
|
+
});
|
|
5222
|
+
this.#chat.addEventListener("dragleave", () => {
|
|
5223
|
+
this.#chat.classList.remove("chat--dragover");
|
|
5224
|
+
});
|
|
5225
|
+
this.#chat.addEventListener("drop", (event) => {
|
|
5226
|
+
event.preventDefault();
|
|
5227
|
+
this.#chat.classList.remove("chat--dragover");
|
|
5228
|
+
const files = event.dataTransfer?.files;
|
|
5229
|
+
if (files !== void 0) {
|
|
5230
|
+
for (const file of Array.from(files)) {
|
|
5231
|
+
this.#attachTray?.add(file);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
/** The one-line manifest of the message's attachments, for the run context. */
|
|
5237
|
+
#attachmentContext() {
|
|
5238
|
+
if (this.#runAttachments.length === 0) {
|
|
5239
|
+
return [];
|
|
5240
|
+
}
|
|
5241
|
+
const lines = this.#runAttachments.map(
|
|
5242
|
+
(ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`
|
|
5243
|
+
);
|
|
5244
|
+
return [
|
|
5245
|
+
{
|
|
5246
|
+
description: "Files the user attached to this message",
|
|
5247
|
+
value: `${lines.join("\n")}
|
|
5248
|
+
Use the read_attachment tool with an id to read a file's contents.`
|
|
5249
|
+
}
|
|
5250
|
+
];
|
|
5251
|
+
}
|
|
4723
5252
|
/**
|
|
4724
5253
|
* When `data-threads-url` is set, route thread enumeration / load / rename /
|
|
4725
5254
|
* delete through that server endpoint (wrapping the current store as the
|
|
@@ -4865,6 +5394,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4865
5394
|
this.#toolCards.clear();
|
|
4866
5395
|
this.#serverSettled.clear();
|
|
4867
5396
|
this.#initialMessages = [];
|
|
5397
|
+
this.#runAttachments = [];
|
|
5398
|
+
this.#attachTray?.clear();
|
|
4868
5399
|
this.#messages.replaceChildren();
|
|
4869
5400
|
}
|
|
4870
5401
|
/** Switch the active conversation to an existing thread and replay it. */
|
|
@@ -4924,8 +5455,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
4924
5455
|
#renderHistoricMessage(message) {
|
|
4925
5456
|
const text2 = typeof message.content === "string" ? message.content : "";
|
|
4926
5457
|
if (message.role === MESSAGE_ROLE.USER) {
|
|
4927
|
-
|
|
4928
|
-
|
|
5458
|
+
const attachments = messageAttachments(message);
|
|
5459
|
+
if (text2 !== "" || attachments.length > 0) {
|
|
5460
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, text2);
|
|
5461
|
+
if (attachments.length > 0) {
|
|
5462
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
5463
|
+
}
|
|
4929
5464
|
}
|
|
4930
5465
|
return;
|
|
4931
5466
|
}
|
|
@@ -5061,13 +5596,27 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5061
5596
|
});
|
|
5062
5597
|
this.#skillHint.className = "skill-hint";
|
|
5063
5598
|
this.#skillHint.hidden = true;
|
|
5064
|
-
|
|
5599
|
+
this.#attachButton.className = "attach-btn";
|
|
5600
|
+
this.#attachButton.type = "button";
|
|
5601
|
+
this.#attachButton.textContent = "\u{1F4CE}";
|
|
5602
|
+
this.#attachButton.title = "Attach files";
|
|
5603
|
+
this.#attachButton.setAttribute("aria-label", "Attach files");
|
|
5604
|
+
this.#attachButton.hidden = true;
|
|
5605
|
+
this.#attachButton.addEventListener("click", () => this.#fileInput.click());
|
|
5606
|
+
this.#fileInput.className = "attach-input";
|
|
5607
|
+
this.#fileInput.type = "file";
|
|
5608
|
+
this.#fileInput.multiple = true;
|
|
5609
|
+
this.#fileInput.hidden = true;
|
|
5610
|
+
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
5611
|
+
this.#attachSlot.className = "attachment-slot";
|
|
5612
|
+
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
5065
5613
|
this.#chat.append(
|
|
5066
5614
|
header,
|
|
5067
5615
|
this.#messages,
|
|
5068
5616
|
this.#skillsMenu.palette,
|
|
5069
5617
|
this.#skillsMenu.chips,
|
|
5070
5618
|
this.#skillHint,
|
|
5619
|
+
this.#attachSlot,
|
|
5071
5620
|
inputRow,
|
|
5072
5621
|
this.#drawer.element
|
|
5073
5622
|
);
|
|
@@ -5113,25 +5662,31 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5113
5662
|
}
|
|
5114
5663
|
async #submit() {
|
|
5115
5664
|
const content = this.#input.value.trim();
|
|
5116
|
-
|
|
5665
|
+
const attachments = this.#attachTray?.readyRefs() ?? [];
|
|
5666
|
+
if (content === "" && attachments.length === 0) {
|
|
5117
5667
|
return;
|
|
5118
5668
|
}
|
|
5119
|
-
this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
5669
|
+
const bubble = this.appendMessage(MESSAGE_ROLE.USER, content);
|
|
5670
|
+
if (attachments.length > 0) {
|
|
5671
|
+
bubble.appendChild(renderAttachmentChips(attachments));
|
|
5672
|
+
}
|
|
5120
5673
|
this.#input.value = "";
|
|
5674
|
+
this.#attachTray?.clearReady();
|
|
5675
|
+
this.#runAttachments = attachments;
|
|
5121
5676
|
this.dispatchEvent(
|
|
5122
5677
|
new CustomEvent(SUBMIT_EVENT, {
|
|
5123
|
-
detail: { content },
|
|
5678
|
+
detail: { content, attachments },
|
|
5124
5679
|
bubbles: true,
|
|
5125
5680
|
composed: true
|
|
5126
5681
|
})
|
|
5127
5682
|
);
|
|
5128
|
-
await this.#client_send(content);
|
|
5683
|
+
await this.#client_send(content, attachments);
|
|
5129
5684
|
}
|
|
5130
|
-
async #client_send(content) {
|
|
5685
|
+
async #client_send(content, attachments) {
|
|
5131
5686
|
if (this.endpoint === "") {
|
|
5132
5687
|
return;
|
|
5133
5688
|
}
|
|
5134
|
-
await this.#ensureClient().send(content);
|
|
5689
|
+
await this.#ensureClient().send(content, attachments);
|
|
5135
5690
|
}
|
|
5136
5691
|
#ensureClient() {
|
|
5137
5692
|
if (this.#client === null) {
|
|
@@ -5268,6 +5823,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5268
5823
|
this.#hidePending();
|
|
5269
5824
|
this.#setRunning(false);
|
|
5270
5825
|
this.#streamingBubble = null;
|
|
5826
|
+
this.#runAttachments = [];
|
|
5271
5827
|
}
|
|
5272
5828
|
};
|
|
5273
5829
|
}
|
|
@@ -5501,7 +6057,7 @@ function setControlValue(el, value) {
|
|
|
5501
6057
|
}
|
|
5502
6058
|
|
|
5503
6059
|
// src/version.ts
|
|
5504
|
-
var VERSION = "0.
|
|
6060
|
+
var VERSION = "0.6.0";
|
|
5505
6061
|
export {
|
|
5506
6062
|
AgUiChat,
|
|
5507
6063
|
AgUiClient,
|
|
@@ -5532,6 +6088,7 @@ export {
|
|
|
5532
6088
|
highlightThenClick,
|
|
5533
6089
|
isDestructive,
|
|
5534
6090
|
isNavigates,
|
|
6091
|
+
messageAttachments,
|
|
5535
6092
|
parseToolCatalog,
|
|
5536
6093
|
prefersReducedMotion,
|
|
5537
6094
|
pressButton,
|
|
@@ -5547,7 +6104,8 @@ export {
|
|
|
5547
6104
|
setNativeValue,
|
|
5548
6105
|
toggleCheckbox,
|
|
5549
6106
|
toggleControl,
|
|
5550
|
-
typeInto
|
|
6107
|
+
typeInto,
|
|
6108
|
+
uploadAttachment
|
|
5551
6109
|
};
|
|
5552
6110
|
/*! Bundled license information:
|
|
5553
6111
|
|