@artooi/ag-ui-web-component 0.8.1 → 0.10.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 +82 -4
- package/README.md +45 -7
- package/dist/ag-ui-web-component.bundle.js +163 -57
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +27 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +6 -0
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +5 -0
- package/dist/core/attachment.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +8 -0
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/transcribe_audio.d.ts +25 -0
- package/dist/core/transcribe_audio.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +8 -2
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +735 -55
- package/dist/index.js.map +4 -4
- package/dist/ui/attachment_tray.d.ts +7 -1
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -3
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thoughts_block.d.ts +30 -0
- package/dist/ui/thoughts_block.d.ts.map +1 -0
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +14 -1
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +41 -0
- package/dist/ui/voice_input.d.ts.map +1 -0
- package/dist/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +217 -7
- package/src/core/agui_client.ts +31 -2
- package/src/core/attachment.ts +21 -1
- package/src/core/conversation_store.ts +84 -18
- package/src/core/remote_conversation_store.ts +24 -3
- package/src/core/transcribe_audio.ts +62 -0
- package/src/core/upload_attachment.ts +8 -1
- package/src/index.ts +5 -0
- package/src/ui/attachment_tray.ts +42 -5
- package/src/ui/relative_time.ts +8 -3
- package/src/ui/styles.ts +113 -7
- package/src/ui/thoughts_block.ts +83 -0
- package/src/ui/thread_drawer.ts +83 -9
- package/src/ui/tool_call_card.ts +6 -0
- package/src/ui/ui_strings.ts +20 -1
- package/src/ui/voice_input.ts +169 -0
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -506,8 +506,10 @@ var DEFAULT_UI_STRINGS = {
|
|
|
506
506
|
newChat: "New chat",
|
|
507
507
|
collapse: "Collapse",
|
|
508
508
|
expand: "Expand",
|
|
509
|
+
toggleTheme: "Toggle theme",
|
|
509
510
|
conversation: "Conversation",
|
|
510
511
|
thinking: "Assistant is thinking\u2026",
|
|
512
|
+
thoughts: "Thoughts",
|
|
511
513
|
stopped: "\u23F9 Stopped",
|
|
512
514
|
connectionLost: "Connection lost",
|
|
513
515
|
noResult: "No result returned.",
|
|
@@ -519,6 +521,10 @@ var DEFAULT_UI_STRINGS = {
|
|
|
519
521
|
send: "Send",
|
|
520
522
|
stop: "Stop",
|
|
521
523
|
attachFiles: "Attach files",
|
|
524
|
+
recordVoice: "Record voice",
|
|
525
|
+
stopRecording: "Stop recording",
|
|
526
|
+
transcribing: "Transcribing\u2026",
|
|
527
|
+
transcriptionFailed: "Transcription failed",
|
|
522
528
|
toolRunning: "running\u2026",
|
|
523
529
|
toolDone: "\u2713 done",
|
|
524
530
|
toolError: "\u26A0 error",
|
|
@@ -585,7 +591,8 @@ var AttachmentTray = class {
|
|
|
585
591
|
status: ATTACHMENT_STATUS.UPLOADING,
|
|
586
592
|
progress: 0,
|
|
587
593
|
ref: null,
|
|
588
|
-
error: ""
|
|
594
|
+
error: "",
|
|
595
|
+
controller: null
|
|
589
596
|
};
|
|
590
597
|
this.#items.push(item);
|
|
591
598
|
const rejection = this.#reject(file);
|
|
@@ -623,11 +630,24 @@ var AttachmentTray = class {
|
|
|
623
630
|
this.#items = this.#items.filter((item) => item.status === ATTACHMENT_STATUS.UPLOADING);
|
|
624
631
|
this.#render();
|
|
625
632
|
}
|
|
626
|
-
/** Drop every chip (a reset / new-chat). */
|
|
633
|
+
/** Drop every chip (a reset / new-chat), aborting any in-flight upload. */
|
|
627
634
|
clear() {
|
|
635
|
+
for (const item of this.#items) {
|
|
636
|
+
item.controller?.abort();
|
|
637
|
+
}
|
|
628
638
|
this.#items = [];
|
|
629
639
|
this.#render();
|
|
630
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Abort every in-flight upload without touching the rendered chips — the
|
|
643
|
+
* teardown path when the host element is removed mid-upload, so a cancelled
|
|
644
|
+
* transfer doesn't orphan a server-side file.
|
|
645
|
+
*/
|
|
646
|
+
dispose() {
|
|
647
|
+
for (const item of this.#items) {
|
|
648
|
+
item.controller?.abort();
|
|
649
|
+
}
|
|
650
|
+
}
|
|
631
651
|
/** The size/type rejection reason for a file, or `null` when accepted. */
|
|
632
652
|
#reject(file) {
|
|
633
653
|
if (this.#config.maxBytes > 0 && file.size > this.#config.maxBytes) {
|
|
@@ -639,25 +659,41 @@ var AttachmentTray = class {
|
|
|
639
659
|
return null;
|
|
640
660
|
}
|
|
641
661
|
#upload(item) {
|
|
662
|
+
const rejection = this.#reject(item.file);
|
|
663
|
+
if (rejection !== null) {
|
|
664
|
+
item.status = ATTACHMENT_STATUS.ERROR;
|
|
665
|
+
item.error = rejection;
|
|
666
|
+
this.#render();
|
|
667
|
+
this.#config.onChange?.();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
642
670
|
item.status = ATTACHMENT_STATUS.UPLOADING;
|
|
643
671
|
item.progress = 0;
|
|
644
672
|
item.error = "";
|
|
673
|
+
const controller = new AbortController();
|
|
674
|
+
item.controller = controller;
|
|
645
675
|
this.#render();
|
|
646
|
-
this.#config.upload(
|
|
647
|
-
item.
|
|
648
|
-
|
|
649
|
-
|
|
676
|
+
this.#config.upload(
|
|
677
|
+
item.file,
|
|
678
|
+
(fraction) => {
|
|
679
|
+
item.progress = fraction;
|
|
680
|
+
this.#render();
|
|
681
|
+
},
|
|
682
|
+
controller.signal
|
|
683
|
+
).then((ref) => {
|
|
650
684
|
item.status = ATTACHMENT_STATUS.READY;
|
|
651
685
|
item.ref = ref;
|
|
652
686
|
}).catch((error) => {
|
|
653
687
|
item.status = ATTACHMENT_STATUS.ERROR;
|
|
654
688
|
item.error = error instanceof Error ? error.message : this.#strings.uploadFailed;
|
|
655
689
|
}).finally(() => {
|
|
690
|
+
item.controller = null;
|
|
656
691
|
this.#render();
|
|
657
692
|
this.#config.onChange?.();
|
|
658
693
|
});
|
|
659
694
|
}
|
|
660
695
|
#remove(item) {
|
|
696
|
+
item.controller?.abort();
|
|
661
697
|
this.#items = this.#items.filter((other) => other !== item);
|
|
662
698
|
this.#render();
|
|
663
699
|
this.#config.onChange?.();
|
|
@@ -3611,7 +3647,7 @@ var STYLES = `
|
|
|
3611
3647
|
--ag-ui-radius: 0;
|
|
3612
3648
|
}
|
|
3613
3649
|
|
|
3614
|
-
/* Page
|
|
3650
|
+
/* Page: full-bleed background with a centred reading column. Unlike
|
|
3615
3651
|
"full" (edge-to-edge, left-aligned messages) the content sits in a column
|
|
3616
3652
|
capped at --ag-ui-content-max-width. The column is produced by symmetric auto
|
|
3617
3653
|
padding on the scroll area + composer (no per-row wrapper), so user pills
|
|
@@ -3653,7 +3689,7 @@ var STYLES = `
|
|
|
3653
3689
|
max-width: 100%;
|
|
3654
3690
|
}
|
|
3655
3691
|
|
|
3656
|
-
/* Sidebar
|
|
3692
|
+
/* Sidebar: a full-height docked panel that slides open/closed and
|
|
3657
3693
|
collapses to a slim icon rail (not the floating launcher). Docked right by
|
|
3658
3694
|
default; data-side="left" docks it left. Overlay by default \u2014 set
|
|
3659
3695
|
--ag-ui-position: static (and place this element in your own layout) for a
|
|
@@ -3763,7 +3799,7 @@ var STYLES = `
|
|
|
3763
3799
|
white-space: nowrap;
|
|
3764
3800
|
}
|
|
3765
3801
|
|
|
3766
|
-
/* Header / launcher icon holder
|
|
3802
|
+
/* Header / launcher icon holder: a slot, with a data-icon-url <img>
|
|
3767
3803
|
fallback, sized via --ag-ui-icon-size. */
|
|
3768
3804
|
.icon-holder {
|
|
3769
3805
|
display: inline-flex;
|
|
@@ -3837,7 +3873,7 @@ var STYLES = `
|
|
|
3837
3873
|
gap: var(--ag-ui-space);
|
|
3838
3874
|
}
|
|
3839
3875
|
|
|
3840
|
-
/* Empty-state region (
|
|
3876
|
+
/* Empty-state region (slot): centred while it's the only thing in the
|
|
3841
3877
|
list, hidden as soon as a message, card, or pending indicator renders. */
|
|
3842
3878
|
.empty {
|
|
3843
3879
|
margin: auto;
|
|
@@ -3849,7 +3885,7 @@ var STYLES = `
|
|
|
3849
3885
|
display: none;
|
|
3850
3886
|
}
|
|
3851
3887
|
|
|
3852
|
-
/* \u2500\u2500 Answer group / well
|
|
3888
|
+
/* \u2500\u2500 Answer group / well \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
|
|
3853
3889
|
One .answer per assistant turn wraps the streamed text, its tool cards,
|
|
3854
3890
|
and the pending indicator so a whole answer reads (and can be boxed) as one
|
|
3855
3891
|
unit. A flex column on the message-list gap, stretched to the list width so
|
|
@@ -4018,6 +4054,69 @@ var STYLES = `
|
|
|
4018
4054
|
}
|
|
4019
4055
|
}
|
|
4020
4056
|
|
|
4057
|
+
/* \u2500\u2500 Thoughts region \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
|
|
4058
|
+
A muted, collapsible chain-of-thought at the top of the answer group: open
|
|
4059
|
+
while the model reasons, folded once the answer text starts. */
|
|
4060
|
+
.thoughts {
|
|
4061
|
+
align-self: stretch;
|
|
4062
|
+
display: flex;
|
|
4063
|
+
flex-direction: column;
|
|
4064
|
+
gap: 4px;
|
|
4065
|
+
font-size: 12px;
|
|
4066
|
+
color: var(--ag-ui-muted);
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
.thoughts-toggle {
|
|
4070
|
+
align-self: flex-start;
|
|
4071
|
+
border: none;
|
|
4072
|
+
padding: 0;
|
|
4073
|
+
background: none;
|
|
4074
|
+
font: inherit;
|
|
4075
|
+
font-size: 12px;
|
|
4076
|
+
font-weight: 600;
|
|
4077
|
+
color: var(--ag-ui-muted);
|
|
4078
|
+
cursor: pointer;
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
.thoughts-toggle::before {
|
|
4082
|
+
content: "\u25BE ";
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
.thoughts-toggle[aria-expanded="false"]::before {
|
|
4086
|
+
content: "\u25B8 ";
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
/* A gentle pulse on the label while reasoning is still streaming. */
|
|
4090
|
+
.thoughts[data-streaming] .thoughts-label {
|
|
4091
|
+
animation: ag-ui-thoughts-pulse 1.4s ease-in-out infinite;
|
|
4092
|
+
}
|
|
4093
|
+
|
|
4094
|
+
@keyframes ag-ui-thoughts-pulse {
|
|
4095
|
+
0%, 100% { opacity: 0.55; }
|
|
4096
|
+
50% { opacity: 1; }
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
.thoughts-body {
|
|
4100
|
+
margin: 0;
|
|
4101
|
+
padding: 4px 0 4px 10px;
|
|
4102
|
+
border-left: 2px solid var(--ag-ui-border);
|
|
4103
|
+
max-height: 220px;
|
|
4104
|
+
overflow: auto;
|
|
4105
|
+
white-space: pre-wrap;
|
|
4106
|
+
word-break: break-word;
|
|
4107
|
+
font-family: inherit;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
.thoughts-body[hidden] {
|
|
4111
|
+
display: none;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4115
|
+
.thoughts[data-streaming] .thoughts-label {
|
|
4116
|
+
animation: none;
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4021
4120
|
.tool-call {
|
|
4022
4121
|
align-self: flex-start;
|
|
4023
4122
|
max-width: 80%;
|
|
@@ -4048,7 +4147,7 @@ var STYLES = `
|
|
|
4048
4147
|
word-break: break-word;
|
|
4049
4148
|
}
|
|
4050
4149
|
|
|
4051
|
-
/* Leading status icon
|
|
4150
|
+
/* Leading status icon. Empty in the DOM \u2014 the glyph/spinner is drawn
|
|
4052
4151
|
here from the card's data-status, so it stays themeable. */
|
|
4053
4152
|
.tool-call-icon {
|
|
4054
4153
|
flex: none;
|
|
@@ -4096,7 +4195,7 @@ var STYLES = `
|
|
|
4096
4195
|
}
|
|
4097
4196
|
}
|
|
4098
4197
|
|
|
4099
|
-
/* Inline display mode
|
|
4198
|
+
/* Inline display mode: the lightest card \u2014 drop the box chrome so the
|
|
4100
4199
|
status row reads as one line of the answer; the result toggle still expands
|
|
4101
4200
|
below it. */
|
|
4102
4201
|
.tool-call[data-display="inline"] {
|
|
@@ -4226,6 +4325,49 @@ var STYLES = `
|
|
|
4226
4325
|
display: none;
|
|
4227
4326
|
}
|
|
4228
4327
|
|
|
4328
|
+
/* The \u{1F3A4} mic button; shown only once #wireVoice mounts it. */
|
|
4329
|
+
.voice-slot {
|
|
4330
|
+
display: contents;
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
.voice-btn {
|
|
4334
|
+
border: 1px solid var(--ag-ui-border);
|
|
4335
|
+
border-radius: 8px;
|
|
4336
|
+
padding: 0 10px;
|
|
4337
|
+
background: var(--ag-ui-input-bg);
|
|
4338
|
+
color: inherit;
|
|
4339
|
+
font: inherit;
|
|
4340
|
+
cursor: pointer;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
.voice-btn:hover {
|
|
4344
|
+
border-color: var(--ag-ui-accent);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
.voice-btn:disabled {
|
|
4348
|
+
cursor: default;
|
|
4349
|
+
opacity: 0.6;
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
/* Recording: a red tint + a gentle pulse so it's clearly "live". */
|
|
4353
|
+
.voice-btn[data-state="recording"] {
|
|
4354
|
+
border-color: var(--ag-ui-danger);
|
|
4355
|
+
background: var(--ag-ui-danger);
|
|
4356
|
+
color: #ffffff;
|
|
4357
|
+
animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
@keyframes ag-ui-voice-pulse {
|
|
4361
|
+
0%, 100% { opacity: 1; }
|
|
4362
|
+
50% { opacity: 0.6; }
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4366
|
+
.voice-btn[data-state="recording"] {
|
|
4367
|
+
animation: none;
|
|
4368
|
+
}
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4229
4371
|
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
4230
4372
|
.attachment-slot {
|
|
4231
4373
|
display: contents;
|
|
@@ -4640,8 +4782,67 @@ var STYLES = `
|
|
|
4640
4782
|
}
|
|
4641
4783
|
`;
|
|
4642
4784
|
|
|
4785
|
+
// src/ui/thoughts_block.ts
|
|
4786
|
+
var ThoughtsBlock = class {
|
|
4787
|
+
/** The block's root element; insert this at the top of the answer group. */
|
|
4788
|
+
element;
|
|
4789
|
+
#label;
|
|
4790
|
+
#body;
|
|
4791
|
+
#toggle;
|
|
4792
|
+
#strings;
|
|
4793
|
+
#collapsed = false;
|
|
4794
|
+
constructor(strings = DEFAULT_UI_STRINGS) {
|
|
4795
|
+
this.#strings = strings;
|
|
4796
|
+
this.element = document.createElement("div");
|
|
4797
|
+
this.element.className = "thoughts";
|
|
4798
|
+
this.element.setAttribute("part", "thoughts");
|
|
4799
|
+
this.element.setAttribute("data-streaming", "");
|
|
4800
|
+
this.#toggle = document.createElement("button");
|
|
4801
|
+
this.#toggle.type = "button";
|
|
4802
|
+
this.#toggle.className = "thoughts-toggle";
|
|
4803
|
+
this.#toggle.setAttribute("part", "thoughts-toggle");
|
|
4804
|
+
this.#toggle.setAttribute("aria-expanded", "true");
|
|
4805
|
+
this.#label = document.createElement("span");
|
|
4806
|
+
this.#label.className = "thoughts-label";
|
|
4807
|
+
this.#label.textContent = strings.thinking;
|
|
4808
|
+
this.#toggle.append(this.#label);
|
|
4809
|
+
this.#body = document.createElement("pre");
|
|
4810
|
+
this.#body.className = "thoughts-body";
|
|
4811
|
+
this.#body.setAttribute("part", "thoughts-body");
|
|
4812
|
+
this.#toggle.addEventListener("click", () => {
|
|
4813
|
+
this.#setCollapsed(!this.#collapsed);
|
|
4814
|
+
});
|
|
4815
|
+
this.element.append(this.#toggle, this.#body);
|
|
4816
|
+
}
|
|
4817
|
+
/** Replace the reasoning body with the running buffer (the full text so far). */
|
|
4818
|
+
stream(buffer) {
|
|
4819
|
+
this.#body.textContent = buffer;
|
|
4820
|
+
}
|
|
4821
|
+
/**
|
|
4822
|
+
* Fold the region away — called when the answer's first text token arrives.
|
|
4823
|
+
* Idempotent (the per-token text handler calls it repeatedly) and flips the
|
|
4824
|
+
* header label from "thinking…" to the settled "Thoughts" affordance.
|
|
4825
|
+
*/
|
|
4826
|
+
collapse() {
|
|
4827
|
+
if (this.#collapsed) {
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4830
|
+
this.element.removeAttribute("data-streaming");
|
|
4831
|
+
this.#label.textContent = this.#strings.thoughts;
|
|
4832
|
+
this.#setCollapsed(true);
|
|
4833
|
+
}
|
|
4834
|
+
#setCollapsed(collapsed) {
|
|
4835
|
+
this.#collapsed = collapsed;
|
|
4836
|
+
this.#body.hidden = collapsed;
|
|
4837
|
+
this.#toggle.setAttribute("aria-expanded", String(!collapsed));
|
|
4838
|
+
}
|
|
4839
|
+
};
|
|
4840
|
+
|
|
4643
4841
|
// src/ui/relative_time.ts
|
|
4644
4842
|
function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
|
|
4843
|
+
if (!Number.isFinite(timestamp)) {
|
|
4844
|
+
return strings.justNow;
|
|
4845
|
+
}
|
|
4645
4846
|
const seconds = Math.round((now - timestamp) / 1e3);
|
|
4646
4847
|
if (seconds < 60) {
|
|
4647
4848
|
return strings.justNow;
|
|
@@ -4673,6 +4874,8 @@ var ThreadDrawer = class {
|
|
|
4673
4874
|
#strings;
|
|
4674
4875
|
#threads = [];
|
|
4675
4876
|
#activeId = "";
|
|
4877
|
+
/** The element focused before the drawer opened, restored on close. */
|
|
4878
|
+
#lastFocused = null;
|
|
4676
4879
|
constructor(callbacks, strings = DEFAULT_UI_STRINGS) {
|
|
4677
4880
|
this.#callbacks = callbacks;
|
|
4678
4881
|
this.#strings = strings;
|
|
@@ -4688,7 +4891,9 @@ var ThreadDrawer = class {
|
|
|
4688
4891
|
this.#panel.className = "drawer-panel";
|
|
4689
4892
|
this.#panel.setAttribute("part", "drawer-panel");
|
|
4690
4893
|
this.#panel.setAttribute("role", "dialog");
|
|
4894
|
+
this.#panel.setAttribute("aria-modal", "true");
|
|
4691
4895
|
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
4896
|
+
this.#panel.addEventListener("keydown", (event) => this.#onPanelKeydown(event));
|
|
4692
4897
|
const header = document.createElement("div");
|
|
4693
4898
|
header.className = "drawer-header";
|
|
4694
4899
|
header.setAttribute("part", "drawer-header");
|
|
@@ -4724,13 +4929,55 @@ var ThreadDrawer = class {
|
|
|
4724
4929
|
return !this.element.hidden;
|
|
4725
4930
|
}
|
|
4726
4931
|
open() {
|
|
4932
|
+
if (this.isOpen()) {
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
this.#lastFocused = this.#activeElement();
|
|
4727
4936
|
this.element.hidden = false;
|
|
4937
|
+
this.#newButton.focus();
|
|
4728
4938
|
}
|
|
4729
4939
|
close() {
|
|
4940
|
+
if (!this.isOpen()) {
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4730
4943
|
this.element.hidden = true;
|
|
4944
|
+
this.#lastFocused?.focus();
|
|
4945
|
+
this.#lastFocused = null;
|
|
4731
4946
|
}
|
|
4732
4947
|
toggle() {
|
|
4733
|
-
this.
|
|
4948
|
+
if (this.isOpen()) {
|
|
4949
|
+
this.close();
|
|
4950
|
+
} else {
|
|
4951
|
+
this.open();
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
/** The currently-focused element within the drawer's root (shadow-aware). */
|
|
4955
|
+
#activeElement() {
|
|
4956
|
+
return this.element.getRootNode().activeElement;
|
|
4957
|
+
}
|
|
4958
|
+
/** Escape-to-close and a Tab focus trap while the dialog is open. */
|
|
4959
|
+
#onPanelKeydown(event) {
|
|
4960
|
+
if (event.key === "Escape") {
|
|
4961
|
+
event.preventDefault();
|
|
4962
|
+
this.close();
|
|
4963
|
+
return;
|
|
4964
|
+
}
|
|
4965
|
+
if (event.key !== "Tab") {
|
|
4966
|
+
return;
|
|
4967
|
+
}
|
|
4968
|
+
const focusables = Array.from(
|
|
4969
|
+
this.#panel.querySelectorAll("button, input, [tabindex]")
|
|
4970
|
+
).filter((el) => !el.hidden);
|
|
4971
|
+
const first = focusables[0];
|
|
4972
|
+
const last = focusables[focusables.length - 1];
|
|
4973
|
+
const active = this.#activeElement();
|
|
4974
|
+
if (event.shiftKey && active === first) {
|
|
4975
|
+
event.preventDefault();
|
|
4976
|
+
last?.focus();
|
|
4977
|
+
} else if (!event.shiftKey && active === last) {
|
|
4978
|
+
event.preventDefault();
|
|
4979
|
+
first?.focus();
|
|
4980
|
+
}
|
|
4734
4981
|
}
|
|
4735
4982
|
/** Render the rows (or the empty state), highlighting the active thread. */
|
|
4736
4983
|
setThreads(threads, activeId) {
|
|
@@ -4797,24 +5044,43 @@ var ThreadDrawer = class {
|
|
|
4797
5044
|
row.append(select, actions);
|
|
4798
5045
|
return row;
|
|
4799
5046
|
}
|
|
4800
|
-
/** Swap a row for an inline rename input; Enter commits, Escape cancels. */
|
|
5047
|
+
/** Swap a row for an inline rename input; Enter/blur commits, Escape cancels. */
|
|
4801
5048
|
#startRename(row, meta) {
|
|
4802
5049
|
const input = document.createElement("input");
|
|
4803
5050
|
input.type = "text";
|
|
4804
5051
|
input.className = "drawer-rename-input";
|
|
4805
5052
|
input.value = meta.title;
|
|
5053
|
+
let done = false;
|
|
5054
|
+
const commit = () => {
|
|
5055
|
+
if (done) {
|
|
5056
|
+
return;
|
|
5057
|
+
}
|
|
5058
|
+
done = true;
|
|
5059
|
+
const value = input.value.trim();
|
|
5060
|
+
if (value === "" || value === meta.title) {
|
|
5061
|
+
this.#renderList();
|
|
5062
|
+
} else {
|
|
5063
|
+
this.#callbacks.onRename(meta.threadId, value);
|
|
5064
|
+
}
|
|
5065
|
+
};
|
|
5066
|
+
const cancel = () => {
|
|
5067
|
+
if (done) {
|
|
5068
|
+
return;
|
|
5069
|
+
}
|
|
5070
|
+
done = true;
|
|
5071
|
+
this.#renderList();
|
|
5072
|
+
};
|
|
4806
5073
|
input.addEventListener("keydown", (event) => {
|
|
4807
5074
|
if (event.key === "Enter") {
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
this.#renderList();
|
|
4811
|
-
} else {
|
|
4812
|
-
this.#callbacks.onRename(meta.threadId, value);
|
|
4813
|
-
}
|
|
5075
|
+
event.preventDefault();
|
|
5076
|
+
commit();
|
|
4814
5077
|
} else if (event.key === "Escape") {
|
|
4815
|
-
|
|
5078
|
+
event.preventDefault();
|
|
5079
|
+
event.stopPropagation();
|
|
5080
|
+
cancel();
|
|
4816
5081
|
}
|
|
4817
5082
|
});
|
|
5083
|
+
input.addEventListener("blur", () => commit());
|
|
4818
5084
|
row.replaceChildren(input);
|
|
4819
5085
|
input.focus();
|
|
4820
5086
|
input.select();
|
|
@@ -4910,6 +5176,9 @@ var ToolCallCard = class {
|
|
|
4910
5176
|
* `inline`), or the args + result together (`compact`).
|
|
4911
5177
|
*/
|
|
4912
5178
|
settle(status, text2) {
|
|
5179
|
+
if (this.#settled) {
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
4913
5182
|
this.#settled = true;
|
|
4914
5183
|
this.element.setAttribute("data-status", status);
|
|
4915
5184
|
this.#status.textContent = statusLabels(this.#strings)[status];
|
|
@@ -4943,6 +5212,134 @@ ${text2}`;
|
|
|
4943
5212
|
}
|
|
4944
5213
|
};
|
|
4945
5214
|
|
|
5215
|
+
// src/ui/voice_input.ts
|
|
5216
|
+
var VoiceInput = class {
|
|
5217
|
+
/** The mic button; mount this in the composer. */
|
|
5218
|
+
element;
|
|
5219
|
+
#transcribe;
|
|
5220
|
+
#onText;
|
|
5221
|
+
#strings;
|
|
5222
|
+
#state = "idle";
|
|
5223
|
+
#recorder = null;
|
|
5224
|
+
#stream = null;
|
|
5225
|
+
#chunks = [];
|
|
5226
|
+
#disposed = false;
|
|
5227
|
+
constructor(options) {
|
|
5228
|
+
this.#transcribe = options.transcribe;
|
|
5229
|
+
this.#onText = options.onText;
|
|
5230
|
+
this.#strings = options.strings ?? DEFAULT_UI_STRINGS;
|
|
5231
|
+
this.element = document.createElement("button");
|
|
5232
|
+
this.element.type = "button";
|
|
5233
|
+
this.element.className = "voice-btn";
|
|
5234
|
+
this.element.setAttribute("part", "voice-button");
|
|
5235
|
+
this.element.textContent = "\u{1F3A4}";
|
|
5236
|
+
this.#setState("idle");
|
|
5237
|
+
this.element.addEventListener("click", () => {
|
|
5238
|
+
void this.toggle();
|
|
5239
|
+
});
|
|
5240
|
+
}
|
|
5241
|
+
/** Start recording when idle, stop (and transcribe) when recording. */
|
|
5242
|
+
async toggle() {
|
|
5243
|
+
if (this.#state === "recording") {
|
|
5244
|
+
this.#stop();
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5247
|
+
if (this.#state === "transcribing") {
|
|
5248
|
+
return;
|
|
5249
|
+
}
|
|
5250
|
+
await this.#start();
|
|
5251
|
+
}
|
|
5252
|
+
async #start() {
|
|
5253
|
+
let stream;
|
|
5254
|
+
try {
|
|
5255
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
5256
|
+
} catch {
|
|
5257
|
+
this.#fail(this.#strings.transcriptionFailed);
|
|
5258
|
+
return;
|
|
5259
|
+
}
|
|
5260
|
+
this.#stream = stream;
|
|
5261
|
+
this.#chunks = [];
|
|
5262
|
+
const recorder = new MediaRecorder(stream);
|
|
5263
|
+
recorder.addEventListener("dataavailable", (event) => {
|
|
5264
|
+
this.#chunks.push(event.data);
|
|
5265
|
+
});
|
|
5266
|
+
recorder.addEventListener("stop", () => {
|
|
5267
|
+
void this.#finish(recorder.mimeType);
|
|
5268
|
+
});
|
|
5269
|
+
this.#recorder = recorder;
|
|
5270
|
+
recorder.start();
|
|
5271
|
+
this.#setState("recording");
|
|
5272
|
+
}
|
|
5273
|
+
#stop() {
|
|
5274
|
+
this.#recorder?.stop();
|
|
5275
|
+
}
|
|
5276
|
+
/**
|
|
5277
|
+
* Tear the control down — the teardown path when the host element is removed
|
|
5278
|
+
* mid-recording. Stops any live `MediaRecorder`, releases the mic tracks (so
|
|
5279
|
+
* the browser's recording indicator clears), and suppresses the pending
|
|
5280
|
+
* transcription: a disconnected control must not fire `onText` back into a
|
|
5281
|
+
* detached element.
|
|
5282
|
+
*/
|
|
5283
|
+
dispose() {
|
|
5284
|
+
this.#disposed = true;
|
|
5285
|
+
if (this.#recorder !== null && this.#recorder.state !== "inactive") {
|
|
5286
|
+
this.#recorder.stop();
|
|
5287
|
+
}
|
|
5288
|
+
this.#recorder = null;
|
|
5289
|
+
this.#releaseStream();
|
|
5290
|
+
}
|
|
5291
|
+
async #finish(mimeType) {
|
|
5292
|
+
if (this.#disposed) {
|
|
5293
|
+
return;
|
|
5294
|
+
}
|
|
5295
|
+
this.#releaseStream();
|
|
5296
|
+
this.#setState("transcribing");
|
|
5297
|
+
const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
|
|
5298
|
+
try {
|
|
5299
|
+
const text2 = await this.#transcribe(audio);
|
|
5300
|
+
this.#setState("idle");
|
|
5301
|
+
if (text2 !== "") {
|
|
5302
|
+
this.#onText(text2);
|
|
5303
|
+
}
|
|
5304
|
+
} catch (error) {
|
|
5305
|
+
this.#fail(error instanceof Error ? error.message : this.#strings.transcriptionFailed);
|
|
5306
|
+
} finally {
|
|
5307
|
+
this.#recorder = null;
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
/** Stop the mic tracks so the browser's recording indicator clears. */
|
|
5311
|
+
#releaseStream() {
|
|
5312
|
+
for (const track of this.#stream?.getTracks() ?? []) {
|
|
5313
|
+
track.stop();
|
|
5314
|
+
}
|
|
5315
|
+
this.#stream = null;
|
|
5316
|
+
}
|
|
5317
|
+
#fail(message) {
|
|
5318
|
+
this.#releaseStream();
|
|
5319
|
+
this.#recorder = null;
|
|
5320
|
+
this.#setState("idle");
|
|
5321
|
+
this.element.title = message;
|
|
5322
|
+
}
|
|
5323
|
+
#setState(state) {
|
|
5324
|
+
this.#state = state;
|
|
5325
|
+
this.element.dataset["state"] = state;
|
|
5326
|
+
const label = this.#labelFor(state);
|
|
5327
|
+
this.element.title = label;
|
|
5328
|
+
this.element.setAttribute("aria-label", label);
|
|
5329
|
+
this.element.setAttribute("aria-pressed", String(state === "recording"));
|
|
5330
|
+
this.element.disabled = state === "transcribing";
|
|
5331
|
+
}
|
|
5332
|
+
#labelFor(state) {
|
|
5333
|
+
if (state === "recording") {
|
|
5334
|
+
return this.#strings.stopRecording;
|
|
5335
|
+
}
|
|
5336
|
+
if (state === "transcribing") {
|
|
5337
|
+
return this.#strings.transcribing;
|
|
5338
|
+
}
|
|
5339
|
+
return this.#strings.recordVoice;
|
|
5340
|
+
}
|
|
5341
|
+
};
|
|
5342
|
+
|
|
4946
5343
|
// src/core/agui_client.ts
|
|
4947
5344
|
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
4948
5345
|
var ConnectionLostError = class extends Error {
|
|
@@ -5052,7 +5449,7 @@ var AgUiClient = class {
|
|
|
5052
5449
|
return;
|
|
5053
5450
|
}
|
|
5054
5451
|
const pending = [];
|
|
5055
|
-
const runState = { terminal: false };
|
|
5452
|
+
const runState = { terminal: false, errored: false };
|
|
5056
5453
|
await this.#agent.runAgent(
|
|
5057
5454
|
{ tools: this.#getTools(), context: this.#getContext() },
|
|
5058
5455
|
this.#buildSubscriber(pending, runState)
|
|
@@ -5064,6 +5461,9 @@ var AgUiClient = class {
|
|
|
5064
5461
|
if (!runState.terminal) {
|
|
5065
5462
|
throw new ConnectionLostError(this.#connectionLostMessage);
|
|
5066
5463
|
}
|
|
5464
|
+
if (runState.errored) {
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
5067
5467
|
if (this.#executeTool === null || pending.length === 0) {
|
|
5068
5468
|
return;
|
|
5069
5469
|
}
|
|
@@ -5114,8 +5514,21 @@ var AgUiClient = class {
|
|
|
5114
5514
|
onToolCallResultEvent({ event }) {
|
|
5115
5515
|
h.onToolResult(event.toolCallId, event.content);
|
|
5116
5516
|
},
|
|
5517
|
+
// Reasoning. `@ag-ui/client` already maps the deprecated
|
|
5518
|
+
// THINKING_* events onto these REASONING_* callbacks, so handling the
|
|
5519
|
+
// reasoning family alone covers both protocol versions.
|
|
5520
|
+
onReasoningStartEvent() {
|
|
5521
|
+
h.onReasoningStart();
|
|
5522
|
+
},
|
|
5523
|
+
onReasoningMessageContentEvent({ reasoningMessageBuffer }) {
|
|
5524
|
+
h.onReasoningDelta(reasoningMessageBuffer);
|
|
5525
|
+
},
|
|
5526
|
+
onReasoningEndEvent() {
|
|
5527
|
+
h.onReasoningEnd();
|
|
5528
|
+
},
|
|
5117
5529
|
onRunErrorEvent({ event }) {
|
|
5118
5530
|
runState.terminal = true;
|
|
5531
|
+
runState.errored = true;
|
|
5119
5532
|
h.onError(event.message);
|
|
5120
5533
|
},
|
|
5121
5534
|
onRunFinalized() {
|
|
@@ -5132,40 +5545,56 @@ function isAbortError(error) {
|
|
|
5132
5545
|
// src/core/attachment.ts
|
|
5133
5546
|
function messageAttachments(message) {
|
|
5134
5547
|
const refs = message.attachments;
|
|
5135
|
-
return Array.isArray(refs) ? refs : [];
|
|
5548
|
+
return Array.isArray(refs) ? refs.filter(isAttachmentRef) : [];
|
|
5549
|
+
}
|
|
5550
|
+
function isAttachmentRef(value) {
|
|
5551
|
+
if (typeof value !== "object" || value === null) {
|
|
5552
|
+
return false;
|
|
5553
|
+
}
|
|
5554
|
+
const ref = value;
|
|
5555
|
+
return typeof ref["id"] === "string" && typeof ref["name"] === "string" && typeof ref["mime"] === "string" && typeof ref["size"] === "number" && (ref["url"] === void 0 || typeof ref["url"] === "string");
|
|
5136
5556
|
}
|
|
5137
5557
|
|
|
5138
5558
|
// src/core/conversation_store.ts
|
|
5139
5559
|
import { randomUUID as randomUUID3 } from "@ag-ui/client";
|
|
5140
|
-
var
|
|
5141
|
-
var
|
|
5142
|
-
var
|
|
5143
|
-
var
|
|
5560
|
+
var KEY_ROOT = "ag-ui-chat";
|
|
5561
|
+
var THREAD_SUFFIX = "thread";
|
|
5562
|
+
var THREADS_SUFFIX = "threads";
|
|
5563
|
+
var MESSAGES_SUFFIX = "messages:";
|
|
5564
|
+
var CHECKPOINT_SUFFIX = "checkpoint:";
|
|
5144
5565
|
var TITLE_LIMIT = 60;
|
|
5145
5566
|
var PREVIEW_LIMIT = 100;
|
|
5146
5567
|
var DEFAULT_TITLE = "New conversation";
|
|
5147
5568
|
var SessionStorageStore = class {
|
|
5569
|
+
#root;
|
|
5570
|
+
constructor(namespace = "") {
|
|
5571
|
+
this.#root = namespace === "" ? KEY_ROOT : `${KEY_ROOT}@${namespace}`;
|
|
5572
|
+
if (namespace !== "") {
|
|
5573
|
+
this.#migrateLegacyKeys();
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5148
5576
|
threadId() {
|
|
5149
|
-
const
|
|
5577
|
+
const key = this.#key(THREAD_SUFFIX);
|
|
5578
|
+
const existing = sessionStorage.getItem(key);
|
|
5150
5579
|
if (existing !== null) {
|
|
5151
5580
|
return existing;
|
|
5152
5581
|
}
|
|
5153
5582
|
const id = randomUUID3();
|
|
5154
|
-
sessionStorage.setItem(
|
|
5583
|
+
sessionStorage.setItem(key, id);
|
|
5155
5584
|
return id;
|
|
5156
5585
|
}
|
|
5157
5586
|
loadMessages(threadId) {
|
|
5158
|
-
return Promise.resolve(this.#readJson(
|
|
5587
|
+
return Promise.resolve(this.#readJson(this.#key(MESSAGES_SUFFIX + threadId)));
|
|
5159
5588
|
}
|
|
5160
5589
|
saveMessages(threadId, messages) {
|
|
5161
|
-
sessionStorage.setItem(
|
|
5590
|
+
sessionStorage.setItem(this.#key(MESSAGES_SUFFIX + threadId), JSON.stringify(messages));
|
|
5162
5591
|
this.#touchThread(threadId, messages);
|
|
5163
5592
|
}
|
|
5164
5593
|
loadCheckpoint(threadId) {
|
|
5165
|
-
return this.#readJson(
|
|
5594
|
+
return this.#readJson(this.#key(CHECKPOINT_SUFFIX + threadId));
|
|
5166
5595
|
}
|
|
5167
5596
|
saveCheckpoint(threadId, checkpoint) {
|
|
5168
|
-
const key =
|
|
5597
|
+
const key = this.#key(CHECKPOINT_SUFFIX + threadId);
|
|
5169
5598
|
if (checkpoint === null) {
|
|
5170
5599
|
sessionStorage.removeItem(key);
|
|
5171
5600
|
return;
|
|
@@ -5173,11 +5602,11 @@ var SessionStorageStore = class {
|
|
|
5173
5602
|
sessionStorage.setItem(key, JSON.stringify(checkpoint));
|
|
5174
5603
|
}
|
|
5175
5604
|
clear(threadId) {
|
|
5176
|
-
sessionStorage.removeItem(
|
|
5177
|
-
sessionStorage.removeItem(
|
|
5605
|
+
sessionStorage.removeItem(this.#key(MESSAGES_SUFFIX + threadId));
|
|
5606
|
+
sessionStorage.removeItem(this.#key(CHECKPOINT_SUFFIX + threadId));
|
|
5178
5607
|
this.#writeThreads(this.#readThreads().filter((thread) => thread.threadId !== threadId));
|
|
5179
|
-
if (sessionStorage.getItem(
|
|
5180
|
-
sessionStorage.removeItem(
|
|
5608
|
+
if (sessionStorage.getItem(this.#key(THREAD_SUFFIX)) === threadId) {
|
|
5609
|
+
sessionStorage.removeItem(this.#key(THREAD_SUFFIX));
|
|
5181
5610
|
}
|
|
5182
5611
|
}
|
|
5183
5612
|
listThreads() {
|
|
@@ -5185,7 +5614,7 @@ var SessionStorageStore = class {
|
|
|
5185
5614
|
return Promise.resolve(metas);
|
|
5186
5615
|
}
|
|
5187
5616
|
setActiveThread(threadId) {
|
|
5188
|
-
sessionStorage.setItem(
|
|
5617
|
+
sessionStorage.setItem(this.#key(THREAD_SUFFIX), threadId);
|
|
5189
5618
|
}
|
|
5190
5619
|
renameThread(threadId, title) {
|
|
5191
5620
|
const threads = this.#readThreads();
|
|
@@ -5221,14 +5650,48 @@ var SessionStorageStore = class {
|
|
|
5221
5650
|
this.#writeThreads(threads);
|
|
5222
5651
|
}
|
|
5223
5652
|
#readThreads() {
|
|
5224
|
-
return this.#readJson(
|
|
5653
|
+
return this.#readJson(this.#key(THREADS_SUFFIX)) ?? [];
|
|
5225
5654
|
}
|
|
5226
5655
|
#writeThreads(threads) {
|
|
5656
|
+
const key = this.#key(THREADS_SUFFIX);
|
|
5227
5657
|
if (threads.length === 0) {
|
|
5228
|
-
sessionStorage.removeItem(
|
|
5658
|
+
sessionStorage.removeItem(key);
|
|
5229
5659
|
return;
|
|
5230
5660
|
}
|
|
5231
|
-
sessionStorage.setItem(
|
|
5661
|
+
sessionStorage.setItem(key, JSON.stringify(threads));
|
|
5662
|
+
}
|
|
5663
|
+
/** This store's fully-qualified key for a suffix (namespaced when set). */
|
|
5664
|
+
#key(suffix) {
|
|
5665
|
+
return `${this.#root}:${suffix}`;
|
|
5666
|
+
}
|
|
5667
|
+
/**
|
|
5668
|
+
* One-time move of pre-namespacing (`ag-ui-chat:*`) keys into this instance's
|
|
5669
|
+
* namespace, so an existing conversation isn't orphaned by the upgrade. Only
|
|
5670
|
+
* this store's own keys move (thread pointer, drawer index, per-thread
|
|
5671
|
+
* messages/checkpoints) — the element's `collapsed`/`theme` keys are left
|
|
5672
|
+
* alone. The first namespaced instance to mount adopts the legacy data; a
|
|
5673
|
+
* second namespace finds it gone and starts fresh.
|
|
5674
|
+
*/
|
|
5675
|
+
#migrateLegacyKeys() {
|
|
5676
|
+
const legacyRoot = `${KEY_ROOT}:`;
|
|
5677
|
+
const moves = [];
|
|
5678
|
+
for (let i = 0; i < sessionStorage.length; i += 1) {
|
|
5679
|
+
const key = sessionStorage.key(i);
|
|
5680
|
+
if (key === null || !key.startsWith(legacyRoot)) {
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
const suffix = key.slice(legacyRoot.length);
|
|
5684
|
+
if (isOwnedSuffix(suffix)) {
|
|
5685
|
+
moves.push([key, this.#key(suffix)]);
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
for (const [from, to] of moves) {
|
|
5689
|
+
const value = sessionStorage.getItem(from);
|
|
5690
|
+
if (value !== null && sessionStorage.getItem(to) === null) {
|
|
5691
|
+
sessionStorage.setItem(to, value);
|
|
5692
|
+
}
|
|
5693
|
+
sessionStorage.removeItem(from);
|
|
5694
|
+
}
|
|
5232
5695
|
}
|
|
5233
5696
|
/** Parse a stored JSON value, returning `null` when absent or corrupt. */
|
|
5234
5697
|
#readJson(key) {
|
|
@@ -5243,6 +5706,9 @@ var SessionStorageStore = class {
|
|
|
5243
5706
|
}
|
|
5244
5707
|
}
|
|
5245
5708
|
};
|
|
5709
|
+
function isOwnedSuffix(suffix) {
|
|
5710
|
+
return suffix === THREAD_SUFFIX || suffix === THREADS_SUFFIX || suffix.startsWith(MESSAGES_SUFFIX) || suffix.startsWith(CHECKPOINT_SUFFIX);
|
|
5711
|
+
}
|
|
5246
5712
|
function deriveTitle(messages) {
|
|
5247
5713
|
for (const message of messages) {
|
|
5248
5714
|
if (message.role === "user") {
|
|
@@ -5349,7 +5815,10 @@ var RemoteConversationStore = class {
|
|
|
5349
5815
|
if (response === null || !response.ok) {
|
|
5350
5816
|
return this.#local.loadMessages(threadId);
|
|
5351
5817
|
}
|
|
5352
|
-
const body = await response
|
|
5818
|
+
const body = await this.#readJson(response);
|
|
5819
|
+
if (body === null) {
|
|
5820
|
+
return this.#local.loadMessages(threadId);
|
|
5821
|
+
}
|
|
5353
5822
|
return body.messages ?? null;
|
|
5354
5823
|
}
|
|
5355
5824
|
async #fetchThreads() {
|
|
@@ -5357,14 +5826,28 @@ var RemoteConversationStore = class {
|
|
|
5357
5826
|
if (response === null || !response.ok) {
|
|
5358
5827
|
return null;
|
|
5359
5828
|
}
|
|
5360
|
-
const body = await response
|
|
5829
|
+
const body = await this.#readJson(response);
|
|
5830
|
+
if (body === null) {
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5361
5833
|
return body.threads ?? [];
|
|
5362
5834
|
}
|
|
5835
|
+
/** Parse a `Response` body as JSON, or `null` when it isn't valid JSON. */
|
|
5836
|
+
async #readJson(response) {
|
|
5837
|
+
try {
|
|
5838
|
+
return await response.json();
|
|
5839
|
+
} catch {
|
|
5840
|
+
return null;
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5363
5843
|
#toMeta(row) {
|
|
5364
5844
|
return {
|
|
5365
5845
|
threadId: row.thread_id,
|
|
5366
5846
|
title: this.#renamed.get(row.thread_id) ?? row.title,
|
|
5367
|
-
|
|
5847
|
+
// `null` or an unparseable date both become `NaN` (Date.parse's own
|
|
5848
|
+
// signal), which `relativeTime` renders as a neutral label rather than
|
|
5849
|
+
// "~2950w ago" (epoch 0) or "NaNw ago".
|
|
5850
|
+
updatedAt: row.updated_at === null ? Number.NaN : Date.parse(row.updated_at),
|
|
5368
5851
|
preview: row.preview
|
|
5369
5852
|
};
|
|
5370
5853
|
}
|
|
@@ -5390,6 +5873,35 @@ var RemoteConversationStore = class {
|
|
|
5390
5873
|
}
|
|
5391
5874
|
};
|
|
5392
5875
|
|
|
5876
|
+
// src/core/transcribe_audio.ts
|
|
5877
|
+
async function transcribeAudio(audio, options) {
|
|
5878
|
+
const form = new FormData();
|
|
5879
|
+
form.append("audio", audio, "recording.webm");
|
|
5880
|
+
const response = await fetch(options.url, {
|
|
5881
|
+
method: "POST",
|
|
5882
|
+
headers: { ...options.headers ?? {} },
|
|
5883
|
+
body: form
|
|
5884
|
+
});
|
|
5885
|
+
if (!response.ok) {
|
|
5886
|
+
throw new Error(await errorMessage(response));
|
|
5887
|
+
}
|
|
5888
|
+
const body = await response.json();
|
|
5889
|
+
if (typeof body === "object" && body !== null && typeof body.text === "string") {
|
|
5890
|
+
return body.text;
|
|
5891
|
+
}
|
|
5892
|
+
throw new Error("transcription returned an unreadable response");
|
|
5893
|
+
}
|
|
5894
|
+
async function errorMessage(response) {
|
|
5895
|
+
try {
|
|
5896
|
+
const body = await response.json();
|
|
5897
|
+
if (typeof body.error === "string") {
|
|
5898
|
+
return body.error;
|
|
5899
|
+
}
|
|
5900
|
+
} catch {
|
|
5901
|
+
}
|
|
5902
|
+
return `transcription failed (${response.status})`;
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5393
5905
|
// src/core/upload_attachment.ts
|
|
5394
5906
|
function uploadAttachment(file, options) {
|
|
5395
5907
|
return new Promise((resolve, reject) => {
|
|
@@ -5416,7 +5928,7 @@ function uploadAttachment(file, options) {
|
|
|
5416
5928
|
reject(new Error("upload returned an unreadable response"));
|
|
5417
5929
|
}
|
|
5418
5930
|
} else {
|
|
5419
|
-
reject(new Error(
|
|
5931
|
+
reject(new Error(errorMessage2(xhr)));
|
|
5420
5932
|
}
|
|
5421
5933
|
});
|
|
5422
5934
|
xhr.addEventListener("error", () => reject(new Error("upload failed")));
|
|
@@ -5443,7 +5955,7 @@ function parseRef(body) {
|
|
|
5443
5955
|
}
|
|
5444
5956
|
return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
|
|
5445
5957
|
}
|
|
5446
|
-
function
|
|
5958
|
+
function errorMessage2(xhr) {
|
|
5447
5959
|
try {
|
|
5448
5960
|
const body = JSON.parse(xhr.responseText);
|
|
5449
5961
|
if (typeof body.error === "string") {
|
|
@@ -5456,6 +5968,7 @@ function errorMessage(xhr) {
|
|
|
5456
5968
|
|
|
5457
5969
|
// src/core/ag_ui_chat.ts
|
|
5458
5970
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
5971
|
+
var THEME_KEY = "ag-ui-chat:theme";
|
|
5459
5972
|
var AgUiChat = class extends HTMLElement {
|
|
5460
5973
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
5461
5974
|
agentFactory = createHttpAgent;
|
|
@@ -5533,6 +6046,15 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5533
6046
|
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
5534
6047
|
*/
|
|
5535
6048
|
uploadHandler = null;
|
|
6049
|
+
/**
|
|
6050
|
+
* How recorded voice clips are transcribed. `null` (default) POSTs the clip
|
|
6051
|
+
* to `data-transcribe-url` (django-ag-ui's `TranscribeView`). Set a custom
|
|
6052
|
+
* {@link TranscribeHandler} — `(audio: Blob) => Promise<string>` — to swap the
|
|
6053
|
+
* transport (a different STT endpoint, a browser Web Speech adapter) without
|
|
6054
|
+
* touching the mic button. When set, the 🎤 affordance appears even with no
|
|
6055
|
+
* `data-transcribe-url`.
|
|
6056
|
+
*/
|
|
6057
|
+
transcribeHandler = null;
|
|
5536
6058
|
/**
|
|
5537
6059
|
* Builds the tool result a navigating tool resumes with after the page
|
|
5538
6060
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -5601,12 +6123,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5601
6123
|
#attachButton;
|
|
5602
6124
|
#fileInput;
|
|
5603
6125
|
#attachSlot;
|
|
6126
|
+
/** Optional built-in header theme toggle; shown only with `data-theme-toggle`. */
|
|
6127
|
+
#themeToggle;
|
|
5604
6128
|
/** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
|
|
5605
6129
|
#rail;
|
|
5606
6130
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
5607
6131
|
#emptyWrap;
|
|
5608
6132
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5609
6133
|
#attachTray = null;
|
|
6134
|
+
/** Mic button mount point (input row); the control mounts on connect when enabled. */
|
|
6135
|
+
#voiceSlot;
|
|
6136
|
+
/** Voice-input control; created on connect when transcription is available. */
|
|
6137
|
+
#voice = null;
|
|
5610
6138
|
/** Refs attached to the message currently being sent (the context manifest). */
|
|
5611
6139
|
#runAttachments = [];
|
|
5612
6140
|
#client = null;
|
|
@@ -5623,14 +6151,25 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5623
6151
|
// it; ≤1 ⇒ it arrived at once and the word reveal is appropriate.
|
|
5624
6152
|
#streamDeltas = 0;
|
|
5625
6153
|
#pending = null;
|
|
5626
|
-
// The current assistant turn's grouping container
|
|
6154
|
+
// The current assistant turn's grouping container. One `.answer`
|
|
5627
6155
|
// wraps everything a single answer produces — streamed text, tool cards, the
|
|
5628
6156
|
// pending indicator — so it can be boxed as one "well" by CSS. Opened on the
|
|
5629
6157
|
// turn's first run start, closed at settle, so it spans the whole multi-round
|
|
5630
6158
|
// frontend-tool loop (which is several AG-UI runs), not one run. `null`
|
|
5631
6159
|
// between turns; user bubbles never enter it.
|
|
5632
6160
|
#currentGroup = null;
|
|
6161
|
+
// The current turn's streamed-reasoning region, shown at the top of
|
|
6162
|
+
// the answer group while a reasoning model thinks and collapsed once the
|
|
6163
|
+
// answer's first text token arrives. `null` outside a reasoning turn.
|
|
6164
|
+
#thoughts = null;
|
|
5633
6165
|
#threadId = "";
|
|
6166
|
+
// Per-instance suffix for the origin-scoped storage keys (collapsed / theme /
|
|
6167
|
+
// active thread), so two instances on one origin don't clobber each other.
|
|
6168
|
+
// Empty ⇒ the pre-namespacing global keys (back-compat). Resolved on connect.
|
|
6169
|
+
#storageNs = "";
|
|
6170
|
+
// Bumped on every #rehydrate; a replay whose generation is stale (a newer
|
|
6171
|
+
// thread switch started while it awaited a slow store) drops its result.
|
|
6172
|
+
#rehydrateGeneration = 0;
|
|
5634
6173
|
#initialMessages = [];
|
|
5635
6174
|
// Skill catalog by source; merged backend → embed → client (later wins).
|
|
5636
6175
|
#backendSkills = [];
|
|
@@ -5648,6 +6187,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5648
6187
|
this.#attachButton = document.createElement("button");
|
|
5649
6188
|
this.#fileInput = document.createElement("input");
|
|
5650
6189
|
this.#attachSlot = document.createElement("div");
|
|
6190
|
+
this.#voiceSlot = document.createElement("span");
|
|
6191
|
+
this.#themeToggle = document.createElement("button");
|
|
5651
6192
|
this.#rail = document.createElement("button");
|
|
5652
6193
|
this.#emptyWrap = document.createElement("div");
|
|
5653
6194
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
@@ -5772,20 +6313,44 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5772
6313
|
this.setAttribute("data-tool-display", value);
|
|
5773
6314
|
}
|
|
5774
6315
|
connectedCallback() {
|
|
6316
|
+
this.#storageNs = this.id !== "" ? this.id : this.endpoint;
|
|
5775
6317
|
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
6318
|
+
if (this.getAttribute("data-theme-toggle") !== null) {
|
|
6319
|
+
const saved = this.#readScopedItem(THEME_KEY);
|
|
6320
|
+
if (saved !== null) {
|
|
6321
|
+
this.setAttribute("theme", saved);
|
|
6322
|
+
}
|
|
6323
|
+
}
|
|
5776
6324
|
this.#render();
|
|
5777
6325
|
this.#drawer.setStrings(this.#strings);
|
|
5778
|
-
if (
|
|
6326
|
+
if (this.#readScopedItem(COLLAPSED_KEY) === "1") {
|
|
5779
6327
|
this.setAttribute("collapsed", "");
|
|
5780
6328
|
}
|
|
5781
6329
|
this.#syncRail();
|
|
5782
6330
|
this.#initSkills();
|
|
5783
6331
|
void this.#fetchToolCatalog();
|
|
6332
|
+
if (this.#storageNs !== "" && this.conversationStore instanceof SessionStorageStore) {
|
|
6333
|
+
this.conversationStore = new SessionStorageStore(this.#storageNs);
|
|
6334
|
+
}
|
|
5784
6335
|
this.#wireThreadStore();
|
|
5785
6336
|
this.#wireAttachments();
|
|
6337
|
+
this.#wireVoice();
|
|
5786
6338
|
this.#threadId = this.conversationStore.threadId();
|
|
5787
6339
|
void this.#rehydrate();
|
|
5788
6340
|
}
|
|
6341
|
+
/**
|
|
6342
|
+
* Tear down live resources when the element leaves the DOM (a removed node, a
|
|
6343
|
+
* client-side route swap): cancel the in-flight run so its SSE stream closes,
|
|
6344
|
+
* abort any in-flight uploads so they don't orphan server-side files, and
|
|
6345
|
+
* release the mic so the browser's recording indicator clears. Without this a
|
|
6346
|
+
* removed `<ag-ui-chat>` leaks a streaming request, uploads, and a live
|
|
6347
|
+
* `MediaRecorder`.
|
|
6348
|
+
*/
|
|
6349
|
+
disconnectedCallback() {
|
|
6350
|
+
this.#cancelRun();
|
|
6351
|
+
this.#attachTray?.dispose();
|
|
6352
|
+
this.#voice?.dispose();
|
|
6353
|
+
}
|
|
5789
6354
|
/** Parse the inline `data-strings` JSON overrides (empty when absent/malformed). */
|
|
5790
6355
|
#readStringOverrides() {
|
|
5791
6356
|
const raw = this.getAttribute("data-strings");
|
|
@@ -5831,7 +6396,41 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5831
6396
|
if (url === null) {
|
|
5832
6397
|
return null;
|
|
5833
6398
|
}
|
|
5834
|
-
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
6399
|
+
return (file, onProgress, signal) => uploadAttachment(file, { url, headers: this.headers, onProgress, signal });
|
|
6400
|
+
}
|
|
6401
|
+
/**
|
|
6402
|
+
* Reveal the composer's 🎤 mic button when transcription is possible — either
|
|
6403
|
+
* a custom {@link transcribeHandler} is set or `data-transcribe-url` provides
|
|
6404
|
+
* the built-in POST endpoint. The control records via `MediaRecorder` and
|
|
6405
|
+
* drops the transcript into the composer; with neither configured the mic
|
|
6406
|
+
* stays hidden and the chat is text-only.
|
|
6407
|
+
*/
|
|
6408
|
+
#wireVoice() {
|
|
6409
|
+
const url = this.getAttribute("data-transcribe-url");
|
|
6410
|
+
const transcribe = this.transcribeHandler ?? this.#defaultTranscribeHandler(url);
|
|
6411
|
+
if (transcribe === null) {
|
|
6412
|
+
return;
|
|
6413
|
+
}
|
|
6414
|
+
this.#voice = new VoiceInput({
|
|
6415
|
+
transcribe,
|
|
6416
|
+
onText: (text2) => this.#insertVoiceText(text2),
|
|
6417
|
+
strings: this.#strings
|
|
6418
|
+
});
|
|
6419
|
+
this.#voiceSlot.appendChild(this.#voice.element);
|
|
6420
|
+
}
|
|
6421
|
+
/** The built-in transcription handler for `data-transcribe-url`, or `null`. */
|
|
6422
|
+
#defaultTranscribeHandler(url) {
|
|
6423
|
+
if (url === null) {
|
|
6424
|
+
return null;
|
|
6425
|
+
}
|
|
6426
|
+
return (audio) => transcribeAudio(audio, { url, headers: this.headers });
|
|
6427
|
+
}
|
|
6428
|
+
/** Drop a voice transcript into the composer (appended to any typed text). */
|
|
6429
|
+
#insertVoiceText(text2) {
|
|
6430
|
+
const current = this.#input.value.trim();
|
|
6431
|
+
this.#input.value = current === "" ? text2 : `${current} ${text2}`;
|
|
6432
|
+
this.#onInput();
|
|
6433
|
+
this.#input.focus();
|
|
5835
6434
|
}
|
|
5836
6435
|
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
5837
6436
|
#attachmentMaxBytes() {
|
|
@@ -6001,7 +6600,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6001
6600
|
} else {
|
|
6002
6601
|
this.removeAttribute("collapsed");
|
|
6003
6602
|
}
|
|
6004
|
-
sessionStorage.setItem(COLLAPSED_KEY, collapsed ? "1" : "0");
|
|
6603
|
+
sessionStorage.setItem(this.#storageKey(COLLAPSED_KEY), collapsed ? "1" : "0");
|
|
6005
6604
|
this.#syncRail();
|
|
6006
6605
|
this.dispatchEvent(
|
|
6007
6606
|
new CustomEvent(TOGGLE_EVENT, {
|
|
@@ -6015,6 +6614,39 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6015
6614
|
toggleCollapsed() {
|
|
6016
6615
|
this.setCollapsed(!this.collapsed);
|
|
6017
6616
|
}
|
|
6617
|
+
/**
|
|
6618
|
+
* Flip the `theme` attribute between `light` and `dark` and persist the choice
|
|
6619
|
+
* per tab. Bound to the optional built-in header theme toggle
|
|
6620
|
+
* (`data-theme-toggle`); any non-dark theme (incl. `auto` / `code`) flips to
|
|
6621
|
+
* `dark` first.
|
|
6622
|
+
*/
|
|
6623
|
+
toggleTheme() {
|
|
6624
|
+
const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
|
|
6625
|
+
this.setAttribute("theme", next);
|
|
6626
|
+
sessionStorage.setItem(this.#storageKey(THEME_KEY), next);
|
|
6627
|
+
this.#syncThemeGlyph();
|
|
6628
|
+
}
|
|
6629
|
+
/** This instance's namespaced form of an origin-scoped storage key. */
|
|
6630
|
+
#storageKey(base) {
|
|
6631
|
+
return this.#storageNs === "" ? base : `${base}:${this.#storageNs}`;
|
|
6632
|
+
}
|
|
6633
|
+
/**
|
|
6634
|
+
* Read a namespaced origin-scoped value, falling back once to the legacy
|
|
6635
|
+
* pre-namespacing global key (left in place) so an existing collapsed/theme
|
|
6636
|
+
* preference survives the upgrade.
|
|
6637
|
+
*/
|
|
6638
|
+
#readScopedItem(base) {
|
|
6639
|
+
const scoped = sessionStorage.getItem(this.#storageKey(base));
|
|
6640
|
+
if (scoped !== null || this.#storageNs === "") {
|
|
6641
|
+
return scoped;
|
|
6642
|
+
}
|
|
6643
|
+
return sessionStorage.getItem(base);
|
|
6644
|
+
}
|
|
6645
|
+
/** Reflect the current theme on the toggle: show the destination's glyph. */
|
|
6646
|
+
#syncThemeGlyph() {
|
|
6647
|
+
const dark = this.getAttribute("theme") === "dark";
|
|
6648
|
+
this.#themeToggle.textContent = dark ? "\u2600\uFE0F" : "\u{1F319}";
|
|
6649
|
+
}
|
|
6018
6650
|
/**
|
|
6019
6651
|
* Start a fresh conversation: forget the persisted history, drop the
|
|
6020
6652
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
@@ -6031,6 +6663,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6031
6663
|
this.#client = null;
|
|
6032
6664
|
this.#streamingBubble = null;
|
|
6033
6665
|
this.#currentGroup = null;
|
|
6666
|
+
this.#thoughts = null;
|
|
6034
6667
|
this.#hidePending();
|
|
6035
6668
|
this.#toolCards.clear();
|
|
6036
6669
|
this.#serverSettled.clear();
|
|
@@ -6076,7 +6709,12 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6076
6709
|
* result from the page we landed on.
|
|
6077
6710
|
*/
|
|
6078
6711
|
async #rehydrate() {
|
|
6712
|
+
this.#rehydrateGeneration += 1;
|
|
6713
|
+
const generation = this.#rehydrateGeneration;
|
|
6079
6714
|
const messages = await this.conversationStore.loadMessages(this.#threadId);
|
|
6715
|
+
if (generation !== this.#rehydrateGeneration) {
|
|
6716
|
+
return;
|
|
6717
|
+
}
|
|
6080
6718
|
if (messages !== null) {
|
|
6081
6719
|
this.#initialMessages = messages;
|
|
6082
6720
|
for (const message of messages) {
|
|
@@ -6163,7 +6801,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6163
6801
|
* stays literal text (no need to parse what the user typed, and it avoids
|
|
6164
6802
|
* rendering user-authored markup).
|
|
6165
6803
|
*
|
|
6166
|
-
* Assistant bubbles land in the current answer group
|
|
6804
|
+
* Assistant bubbles land in the current answer group, opening one if
|
|
6167
6805
|
* needed; a user bubble closes the prior group and sits directly in the list
|
|
6168
6806
|
* (the well wraps the *assistant* turn, the user message precedes it).
|
|
6169
6807
|
*/
|
|
@@ -6230,7 +6868,18 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6230
6868
|
newChat.addEventListener("click", () => this.newChat());
|
|
6231
6869
|
const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
6232
6870
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
6233
|
-
controls.append(history, newChat
|
|
6871
|
+
controls.append(history, newChat);
|
|
6872
|
+
if (this.getAttribute("data-theme-toggle") !== null) {
|
|
6873
|
+
this.#themeToggle.type = "button";
|
|
6874
|
+
this.#themeToggle.className = "header-btn header-btn--theme";
|
|
6875
|
+
this.#themeToggle.setAttribute("part", "header-button theme-toggle");
|
|
6876
|
+
this.#themeToggle.title = this.#strings.toggleTheme;
|
|
6877
|
+
this.#themeToggle.setAttribute("aria-label", this.#strings.toggleTheme);
|
|
6878
|
+
this.#themeToggle.addEventListener("click", () => this.toggleTheme());
|
|
6879
|
+
this.#syncThemeGlyph();
|
|
6880
|
+
controls.append(this.#themeToggle);
|
|
6881
|
+
}
|
|
6882
|
+
controls.append(collapse);
|
|
6234
6883
|
header.append(title, headerActions, controls);
|
|
6235
6884
|
this.#messages.className = "messages";
|
|
6236
6885
|
this.#messages.setAttribute("part", "messages");
|
|
@@ -6282,9 +6931,10 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6282
6931
|
this.#fileInput.hidden = true;
|
|
6283
6932
|
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
6284
6933
|
this.#attachSlot.className = "attachment-slot";
|
|
6934
|
+
this.#voiceSlot.className = "voice-slot";
|
|
6285
6935
|
const footer = document.createElement("slot");
|
|
6286
6936
|
footer.name = "footer";
|
|
6287
|
-
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
6937
|
+
inputRow.append(this.#attachButton, this.#voiceSlot, this.#input, this.#send, this.#fileInput);
|
|
6288
6938
|
this.#chat.append(
|
|
6289
6939
|
header,
|
|
6290
6940
|
this.#messages,
|
|
@@ -6385,6 +7035,9 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6385
7035
|
this.#send.dataset["state"] = running ? "running" : "idle";
|
|
6386
7036
|
}
|
|
6387
7037
|
async #submit() {
|
|
7038
|
+
if (this.#running) {
|
|
7039
|
+
return;
|
|
7040
|
+
}
|
|
6388
7041
|
const content = this.#input.value.trim();
|
|
6389
7042
|
const attachments = this.#attachTray?.readyRefs() ?? [];
|
|
6390
7043
|
if (content === "" && attachments.length === 0) {
|
|
@@ -6509,8 +7162,18 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6509
7162
|
this.#ensureGroup();
|
|
6510
7163
|
this.#showPending();
|
|
6511
7164
|
},
|
|
7165
|
+
onReasoningStart: () => {
|
|
7166
|
+
this.#hidePending();
|
|
7167
|
+
this.#showThoughts();
|
|
7168
|
+
},
|
|
7169
|
+
onReasoningDelta: (buffer) => {
|
|
7170
|
+
this.#showThoughts().stream(buffer);
|
|
7171
|
+
},
|
|
7172
|
+
onReasoningEnd: () => {
|
|
7173
|
+
},
|
|
6512
7174
|
onTextDelta: (buffer) => {
|
|
6513
7175
|
this.#hidePending();
|
|
7176
|
+
this.#thoughts?.collapse();
|
|
6514
7177
|
this.#streamInto(buffer);
|
|
6515
7178
|
this.#streamDeltas += 1;
|
|
6516
7179
|
},
|
|
@@ -6562,6 +7225,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6562
7225
|
this.#updateEmptyState();
|
|
6563
7226
|
}
|
|
6564
7227
|
this.#currentGroup = null;
|
|
7228
|
+
this.#thoughts = null;
|
|
6565
7229
|
}
|
|
6566
7230
|
};
|
|
6567
7231
|
}
|
|
@@ -6605,6 +7269,21 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6605
7269
|
this.#pending?.remove();
|
|
6606
7270
|
this.#pending = null;
|
|
6607
7271
|
}
|
|
7272
|
+
/**
|
|
7273
|
+
* The current turn's thoughts region, creating it (at the top of the answer
|
|
7274
|
+
* group, above any streamed text or tool cards) on first sight. Idempotent
|
|
7275
|
+
* across a turn's reasoning tokens.
|
|
7276
|
+
*/
|
|
7277
|
+
#showThoughts() {
|
|
7278
|
+
if (this.#thoughts === null) {
|
|
7279
|
+
this.#thoughts = new ThoughtsBlock(this.#strings);
|
|
7280
|
+
const group = this.#ensureGroup();
|
|
7281
|
+
group.insertBefore(this.#thoughts.element, group.firstChild);
|
|
7282
|
+
this.#updateEmptyState();
|
|
7283
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
7284
|
+
}
|
|
7285
|
+
return this.#thoughts;
|
|
7286
|
+
}
|
|
6608
7287
|
#streamInto(buffer) {
|
|
6609
7288
|
if (this.#streamingBubble === null) {
|
|
6610
7289
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
@@ -6676,7 +7355,7 @@ function setControlValue(el, value) {
|
|
|
6676
7355
|
}
|
|
6677
7356
|
|
|
6678
7357
|
// src/version.ts
|
|
6679
|
-
var VERSION = "0.
|
|
7358
|
+
var VERSION = "0.10.0";
|
|
6680
7359
|
export {
|
|
6681
7360
|
AgUiChat,
|
|
6682
7361
|
AgUiClient,
|
|
@@ -6728,6 +7407,7 @@ export {
|
|
|
6728
7407
|
setNativeValue,
|
|
6729
7408
|
toggleCheckbox,
|
|
6730
7409
|
toggleControl,
|
|
7410
|
+
transcribeAudio,
|
|
6731
7411
|
typeInto,
|
|
6732
7412
|
uploadAttachment
|
|
6733
7413
|
};
|