@artooi/ag-ui-web-component 0.8.0 → 0.9.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 +36 -1
- package/README.md +45 -7
- package/dist/ag-ui-web-component.bundle.js +147 -27
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +17 -0
- 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/transcribe_audio.d.ts +25 -0
- package/dist/core/transcribe_audio.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +457 -5
- package/dist/index.js.map +4 -4
- 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/ui_strings.d.ts +14 -1
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +33 -0
- package/dist/ui/voice_input.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/core/ag_ui_chat.ts +144 -2
- package/src/core/agui_client.ts +18 -0
- package/src/core/transcribe_audio.ts +62 -0
- package/src/index.ts +5 -0
- package/src/ui/styles.ts +120 -0
- package/src/ui/thoughts_block.ts +83 -0
- package/src/ui/ui_strings.ts +20 -1
- package/src/ui/voice_input.ts +149 -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",
|
|
@@ -3633,6 +3639,20 @@ var STYLES = `
|
|
|
3633
3639
|
padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
3634
3640
|
}
|
|
3635
3641
|
|
|
3642
|
+
/* The rows between the message list and the composer (skill chips, the
|
|
3643
|
+
/-command palette, the missing-placeholder hint, the upload tray) line up
|
|
3644
|
+
with the column too \u2014 chips are padding-based, the palette/hint/tray are
|
|
3645
|
+
margin-based, so each gets its own inline axis nudged by the same gutter. */
|
|
3646
|
+
:host([placement="page"]) .skill-chips,
|
|
3647
|
+
:host([placement="page"]) .attachment-tray {
|
|
3648
|
+
padding-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
:host([placement="page"]) .skill-palette,
|
|
3652
|
+
:host([placement="page"]) .skill-hint {
|
|
3653
|
+
margin-inline: max(12px, calc((100% - var(--ag-ui-content-max-width)) / 2));
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3636
3656
|
/* In the reading column the assistant well uses the full width; the user
|
|
3637
3657
|
message stays a right-aligned pill (its default align-self + max-width). */
|
|
3638
3658
|
:host([placement="page"]) .message--assistant {
|
|
@@ -4004,6 +4024,69 @@ var STYLES = `
|
|
|
4004
4024
|
}
|
|
4005
4025
|
}
|
|
4006
4026
|
|
|
4027
|
+
/* \u2500\u2500 Thoughts region (THINK-1) \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
|
|
4028
|
+
A muted, collapsible chain-of-thought at the top of the answer group: open
|
|
4029
|
+
while the model reasons, folded once the answer text starts. */
|
|
4030
|
+
.thoughts {
|
|
4031
|
+
align-self: stretch;
|
|
4032
|
+
display: flex;
|
|
4033
|
+
flex-direction: column;
|
|
4034
|
+
gap: 4px;
|
|
4035
|
+
font-size: 12px;
|
|
4036
|
+
color: var(--ag-ui-muted);
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
.thoughts-toggle {
|
|
4040
|
+
align-self: flex-start;
|
|
4041
|
+
border: none;
|
|
4042
|
+
padding: 0;
|
|
4043
|
+
background: none;
|
|
4044
|
+
font: inherit;
|
|
4045
|
+
font-size: 12px;
|
|
4046
|
+
font-weight: 600;
|
|
4047
|
+
color: var(--ag-ui-muted);
|
|
4048
|
+
cursor: pointer;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
.thoughts-toggle::before {
|
|
4052
|
+
content: "\u25BE ";
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
.thoughts-toggle[aria-expanded="false"]::before {
|
|
4056
|
+
content: "\u25B8 ";
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
/* A gentle pulse on the label while reasoning is still streaming. */
|
|
4060
|
+
.thoughts[data-streaming] .thoughts-label {
|
|
4061
|
+
animation: ag-ui-thoughts-pulse 1.4s ease-in-out infinite;
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
@keyframes ag-ui-thoughts-pulse {
|
|
4065
|
+
0%, 100% { opacity: 0.55; }
|
|
4066
|
+
50% { opacity: 1; }
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
.thoughts-body {
|
|
4070
|
+
margin: 0;
|
|
4071
|
+
padding: 4px 0 4px 10px;
|
|
4072
|
+
border-left: 2px solid var(--ag-ui-border);
|
|
4073
|
+
max-height: 220px;
|
|
4074
|
+
overflow: auto;
|
|
4075
|
+
white-space: pre-wrap;
|
|
4076
|
+
word-break: break-word;
|
|
4077
|
+
font-family: inherit;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
.thoughts-body[hidden] {
|
|
4081
|
+
display: none;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4085
|
+
.thoughts[data-streaming] .thoughts-label {
|
|
4086
|
+
animation: none;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4007
4090
|
.tool-call {
|
|
4008
4091
|
align-self: flex-start;
|
|
4009
4092
|
max-width: 80%;
|
|
@@ -4212,6 +4295,49 @@ var STYLES = `
|
|
|
4212
4295
|
display: none;
|
|
4213
4296
|
}
|
|
4214
4297
|
|
|
4298
|
+
/* The \u{1F3A4} mic button (VOICE-1); shown only once #wireVoice mounts it. */
|
|
4299
|
+
.voice-slot {
|
|
4300
|
+
display: contents;
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
.voice-btn {
|
|
4304
|
+
border: 1px solid var(--ag-ui-border);
|
|
4305
|
+
border-radius: 8px;
|
|
4306
|
+
padding: 0 10px;
|
|
4307
|
+
background: var(--ag-ui-input-bg);
|
|
4308
|
+
color: inherit;
|
|
4309
|
+
font: inherit;
|
|
4310
|
+
cursor: pointer;
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
.voice-btn:hover {
|
|
4314
|
+
border-color: var(--ag-ui-accent);
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
.voice-btn:disabled {
|
|
4318
|
+
cursor: default;
|
|
4319
|
+
opacity: 0.6;
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
/* Recording: a red tint + a gentle pulse so it's clearly "live". */
|
|
4323
|
+
.voice-btn[data-state="recording"] {
|
|
4324
|
+
border-color: var(--ag-ui-danger);
|
|
4325
|
+
background: var(--ag-ui-danger);
|
|
4326
|
+
color: #ffffff;
|
|
4327
|
+
animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
@keyframes ag-ui-voice-pulse {
|
|
4331
|
+
0%, 100% { opacity: 1; }
|
|
4332
|
+
50% { opacity: 0.6; }
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4336
|
+
.voice-btn[data-state="recording"] {
|
|
4337
|
+
animation: none;
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4215
4341
|
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
4216
4342
|
.attachment-slot {
|
|
4217
4343
|
display: contents;
|
|
@@ -4626,6 +4752,62 @@ var STYLES = `
|
|
|
4626
4752
|
}
|
|
4627
4753
|
`;
|
|
4628
4754
|
|
|
4755
|
+
// src/ui/thoughts_block.ts
|
|
4756
|
+
var ThoughtsBlock = class {
|
|
4757
|
+
/** The block's root element; insert this at the top of the answer group. */
|
|
4758
|
+
element;
|
|
4759
|
+
#label;
|
|
4760
|
+
#body;
|
|
4761
|
+
#toggle;
|
|
4762
|
+
#strings;
|
|
4763
|
+
#collapsed = false;
|
|
4764
|
+
constructor(strings = DEFAULT_UI_STRINGS) {
|
|
4765
|
+
this.#strings = strings;
|
|
4766
|
+
this.element = document.createElement("div");
|
|
4767
|
+
this.element.className = "thoughts";
|
|
4768
|
+
this.element.setAttribute("part", "thoughts");
|
|
4769
|
+
this.element.setAttribute("data-streaming", "");
|
|
4770
|
+
this.#toggle = document.createElement("button");
|
|
4771
|
+
this.#toggle.type = "button";
|
|
4772
|
+
this.#toggle.className = "thoughts-toggle";
|
|
4773
|
+
this.#toggle.setAttribute("part", "thoughts-toggle");
|
|
4774
|
+
this.#toggle.setAttribute("aria-expanded", "true");
|
|
4775
|
+
this.#label = document.createElement("span");
|
|
4776
|
+
this.#label.className = "thoughts-label";
|
|
4777
|
+
this.#label.textContent = strings.thinking;
|
|
4778
|
+
this.#toggle.append(this.#label);
|
|
4779
|
+
this.#body = document.createElement("pre");
|
|
4780
|
+
this.#body.className = "thoughts-body";
|
|
4781
|
+
this.#body.setAttribute("part", "thoughts-body");
|
|
4782
|
+
this.#toggle.addEventListener("click", () => {
|
|
4783
|
+
this.#setCollapsed(!this.#collapsed);
|
|
4784
|
+
});
|
|
4785
|
+
this.element.append(this.#toggle, this.#body);
|
|
4786
|
+
}
|
|
4787
|
+
/** Replace the reasoning body with the running buffer (the full text so far). */
|
|
4788
|
+
stream(buffer) {
|
|
4789
|
+
this.#body.textContent = buffer;
|
|
4790
|
+
}
|
|
4791
|
+
/**
|
|
4792
|
+
* Fold the region away — called when the answer's first text token arrives.
|
|
4793
|
+
* Idempotent (the per-token text handler calls it repeatedly) and flips the
|
|
4794
|
+
* header label from "thinking…" to the settled "Thoughts" affordance.
|
|
4795
|
+
*/
|
|
4796
|
+
collapse() {
|
|
4797
|
+
if (this.#collapsed) {
|
|
4798
|
+
return;
|
|
4799
|
+
}
|
|
4800
|
+
this.element.removeAttribute("data-streaming");
|
|
4801
|
+
this.#label.textContent = this.#strings.thoughts;
|
|
4802
|
+
this.#setCollapsed(true);
|
|
4803
|
+
}
|
|
4804
|
+
#setCollapsed(collapsed) {
|
|
4805
|
+
this.#collapsed = collapsed;
|
|
4806
|
+
this.#body.hidden = collapsed;
|
|
4807
|
+
this.#toggle.setAttribute("aria-expanded", String(!collapsed));
|
|
4808
|
+
}
|
|
4809
|
+
};
|
|
4810
|
+
|
|
4629
4811
|
// src/ui/relative_time.ts
|
|
4630
4812
|
function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
|
|
4631
4813
|
const seconds = Math.round((now - timestamp) / 1e3);
|
|
@@ -4929,6 +5111,115 @@ ${text2}`;
|
|
|
4929
5111
|
}
|
|
4930
5112
|
};
|
|
4931
5113
|
|
|
5114
|
+
// src/ui/voice_input.ts
|
|
5115
|
+
var VoiceInput = class {
|
|
5116
|
+
/** The mic button; mount this in the composer. */
|
|
5117
|
+
element;
|
|
5118
|
+
#transcribe;
|
|
5119
|
+
#onText;
|
|
5120
|
+
#strings;
|
|
5121
|
+
#state = "idle";
|
|
5122
|
+
#recorder = null;
|
|
5123
|
+
#stream = null;
|
|
5124
|
+
#chunks = [];
|
|
5125
|
+
constructor(options) {
|
|
5126
|
+
this.#transcribe = options.transcribe;
|
|
5127
|
+
this.#onText = options.onText;
|
|
5128
|
+
this.#strings = options.strings ?? DEFAULT_UI_STRINGS;
|
|
5129
|
+
this.element = document.createElement("button");
|
|
5130
|
+
this.element.type = "button";
|
|
5131
|
+
this.element.className = "voice-btn";
|
|
5132
|
+
this.element.setAttribute("part", "voice-button");
|
|
5133
|
+
this.element.textContent = "\u{1F3A4}";
|
|
5134
|
+
this.#setState("idle");
|
|
5135
|
+
this.element.addEventListener("click", () => {
|
|
5136
|
+
void this.toggle();
|
|
5137
|
+
});
|
|
5138
|
+
}
|
|
5139
|
+
/** Start recording when idle, stop (and transcribe) when recording. */
|
|
5140
|
+
async toggle() {
|
|
5141
|
+
if (this.#state === "recording") {
|
|
5142
|
+
this.#stop();
|
|
5143
|
+
return;
|
|
5144
|
+
}
|
|
5145
|
+
if (this.#state === "transcribing") {
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
5148
|
+
await this.#start();
|
|
5149
|
+
}
|
|
5150
|
+
async #start() {
|
|
5151
|
+
let stream;
|
|
5152
|
+
try {
|
|
5153
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
5154
|
+
} catch {
|
|
5155
|
+
this.#fail(this.#strings.transcriptionFailed);
|
|
5156
|
+
return;
|
|
5157
|
+
}
|
|
5158
|
+
this.#stream = stream;
|
|
5159
|
+
this.#chunks = [];
|
|
5160
|
+
const recorder = new MediaRecorder(stream);
|
|
5161
|
+
recorder.addEventListener("dataavailable", (event) => {
|
|
5162
|
+
this.#chunks.push(event.data);
|
|
5163
|
+
});
|
|
5164
|
+
recorder.addEventListener("stop", () => {
|
|
5165
|
+
void this.#finish(recorder.mimeType);
|
|
5166
|
+
});
|
|
5167
|
+
this.#recorder = recorder;
|
|
5168
|
+
recorder.start();
|
|
5169
|
+
this.#setState("recording");
|
|
5170
|
+
}
|
|
5171
|
+
#stop() {
|
|
5172
|
+
this.#recorder?.stop();
|
|
5173
|
+
}
|
|
5174
|
+
async #finish(mimeType) {
|
|
5175
|
+
this.#releaseStream();
|
|
5176
|
+
this.#setState("transcribing");
|
|
5177
|
+
const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
|
|
5178
|
+
try {
|
|
5179
|
+
const text2 = await this.#transcribe(audio);
|
|
5180
|
+
this.#setState("idle");
|
|
5181
|
+
if (text2 !== "") {
|
|
5182
|
+
this.#onText(text2);
|
|
5183
|
+
}
|
|
5184
|
+
} catch (error) {
|
|
5185
|
+
this.#fail(error instanceof Error ? error.message : this.#strings.transcriptionFailed);
|
|
5186
|
+
} finally {
|
|
5187
|
+
this.#recorder = null;
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
/** Stop the mic tracks so the browser's recording indicator clears. */
|
|
5191
|
+
#releaseStream() {
|
|
5192
|
+
for (const track of this.#stream?.getTracks() ?? []) {
|
|
5193
|
+
track.stop();
|
|
5194
|
+
}
|
|
5195
|
+
this.#stream = null;
|
|
5196
|
+
}
|
|
5197
|
+
#fail(message) {
|
|
5198
|
+
this.#releaseStream();
|
|
5199
|
+
this.#recorder = null;
|
|
5200
|
+
this.#setState("idle");
|
|
5201
|
+
this.element.title = message;
|
|
5202
|
+
}
|
|
5203
|
+
#setState(state) {
|
|
5204
|
+
this.#state = state;
|
|
5205
|
+
this.element.dataset["state"] = state;
|
|
5206
|
+
const label = this.#labelFor(state);
|
|
5207
|
+
this.element.title = label;
|
|
5208
|
+
this.element.setAttribute("aria-label", label);
|
|
5209
|
+
this.element.setAttribute("aria-pressed", String(state === "recording"));
|
|
5210
|
+
this.element.disabled = state === "transcribing";
|
|
5211
|
+
}
|
|
5212
|
+
#labelFor(state) {
|
|
5213
|
+
if (state === "recording") {
|
|
5214
|
+
return this.#strings.stopRecording;
|
|
5215
|
+
}
|
|
5216
|
+
if (state === "transcribing") {
|
|
5217
|
+
return this.#strings.transcribing;
|
|
5218
|
+
}
|
|
5219
|
+
return this.#strings.recordVoice;
|
|
5220
|
+
}
|
|
5221
|
+
};
|
|
5222
|
+
|
|
4932
5223
|
// src/core/agui_client.ts
|
|
4933
5224
|
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
4934
5225
|
var ConnectionLostError = class extends Error {
|
|
@@ -5100,6 +5391,18 @@ var AgUiClient = class {
|
|
|
5100
5391
|
onToolCallResultEvent({ event }) {
|
|
5101
5392
|
h.onToolResult(event.toolCallId, event.content);
|
|
5102
5393
|
},
|
|
5394
|
+
// Reasoning (THINK-1). `@ag-ui/client` already maps the deprecated
|
|
5395
|
+
// THINKING_* events onto these REASONING_* callbacks, so handling the
|
|
5396
|
+
// reasoning family alone covers both protocol versions.
|
|
5397
|
+
onReasoningStartEvent() {
|
|
5398
|
+
h.onReasoningStart();
|
|
5399
|
+
},
|
|
5400
|
+
onReasoningMessageContentEvent({ reasoningMessageBuffer }) {
|
|
5401
|
+
h.onReasoningDelta(reasoningMessageBuffer);
|
|
5402
|
+
},
|
|
5403
|
+
onReasoningEndEvent() {
|
|
5404
|
+
h.onReasoningEnd();
|
|
5405
|
+
},
|
|
5103
5406
|
onRunErrorEvent({ event }) {
|
|
5104
5407
|
runState.terminal = true;
|
|
5105
5408
|
h.onError(event.message);
|
|
@@ -5376,6 +5679,35 @@ var RemoteConversationStore = class {
|
|
|
5376
5679
|
}
|
|
5377
5680
|
};
|
|
5378
5681
|
|
|
5682
|
+
// src/core/transcribe_audio.ts
|
|
5683
|
+
async function transcribeAudio(audio, options) {
|
|
5684
|
+
const form = new FormData();
|
|
5685
|
+
form.append("audio", audio, "recording.webm");
|
|
5686
|
+
const response = await fetch(options.url, {
|
|
5687
|
+
method: "POST",
|
|
5688
|
+
headers: { ...options.headers ?? {} },
|
|
5689
|
+
body: form
|
|
5690
|
+
});
|
|
5691
|
+
if (!response.ok) {
|
|
5692
|
+
throw new Error(await errorMessage(response));
|
|
5693
|
+
}
|
|
5694
|
+
const body = await response.json();
|
|
5695
|
+
if (typeof body === "object" && body !== null && typeof body.text === "string") {
|
|
5696
|
+
return body.text;
|
|
5697
|
+
}
|
|
5698
|
+
throw new Error("transcription returned an unreadable response");
|
|
5699
|
+
}
|
|
5700
|
+
async function errorMessage(response) {
|
|
5701
|
+
try {
|
|
5702
|
+
const body = await response.json();
|
|
5703
|
+
if (typeof body.error === "string") {
|
|
5704
|
+
return body.error;
|
|
5705
|
+
}
|
|
5706
|
+
} catch {
|
|
5707
|
+
}
|
|
5708
|
+
return `transcription failed (${response.status})`;
|
|
5709
|
+
}
|
|
5710
|
+
|
|
5379
5711
|
// src/core/upload_attachment.ts
|
|
5380
5712
|
function uploadAttachment(file, options) {
|
|
5381
5713
|
return new Promise((resolve, reject) => {
|
|
@@ -5402,7 +5734,7 @@ function uploadAttachment(file, options) {
|
|
|
5402
5734
|
reject(new Error("upload returned an unreadable response"));
|
|
5403
5735
|
}
|
|
5404
5736
|
} else {
|
|
5405
|
-
reject(new Error(
|
|
5737
|
+
reject(new Error(errorMessage2(xhr)));
|
|
5406
5738
|
}
|
|
5407
5739
|
});
|
|
5408
5740
|
xhr.addEventListener("error", () => reject(new Error("upload failed")));
|
|
@@ -5429,7 +5761,7 @@ function parseRef(body) {
|
|
|
5429
5761
|
}
|
|
5430
5762
|
return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
|
|
5431
5763
|
}
|
|
5432
|
-
function
|
|
5764
|
+
function errorMessage2(xhr) {
|
|
5433
5765
|
try {
|
|
5434
5766
|
const body = JSON.parse(xhr.responseText);
|
|
5435
5767
|
if (typeof body.error === "string") {
|
|
@@ -5442,6 +5774,7 @@ function errorMessage(xhr) {
|
|
|
5442
5774
|
|
|
5443
5775
|
// src/core/ag_ui_chat.ts
|
|
5444
5776
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
5777
|
+
var THEME_KEY = "ag-ui-chat:theme";
|
|
5445
5778
|
var AgUiChat = class extends HTMLElement {
|
|
5446
5779
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
5447
5780
|
agentFactory = createHttpAgent;
|
|
@@ -5519,6 +5852,15 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5519
5852
|
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
5520
5853
|
*/
|
|
5521
5854
|
uploadHandler = null;
|
|
5855
|
+
/**
|
|
5856
|
+
* How recorded voice clips are transcribed. `null` (default) POSTs the clip
|
|
5857
|
+
* to `data-transcribe-url` (django-ag-ui's `TranscribeView`). Set a custom
|
|
5858
|
+
* {@link TranscribeHandler} — `(audio: Blob) => Promise<string>` — to swap the
|
|
5859
|
+
* transport (a different STT endpoint, a browser Web Speech adapter) without
|
|
5860
|
+
* touching the mic button. When set, the 🎤 affordance appears even with no
|
|
5861
|
+
* `data-transcribe-url`.
|
|
5862
|
+
*/
|
|
5863
|
+
transcribeHandler = null;
|
|
5522
5864
|
/**
|
|
5523
5865
|
* Builds the tool result a navigating tool resumes with after the page
|
|
5524
5866
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -5587,12 +5929,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5587
5929
|
#attachButton;
|
|
5588
5930
|
#fileInput;
|
|
5589
5931
|
#attachSlot;
|
|
5932
|
+
/** Optional built-in header theme toggle (THEME-1); shown only with `data-theme-toggle`. */
|
|
5933
|
+
#themeToggle;
|
|
5590
5934
|
/** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
|
|
5591
5935
|
#rail;
|
|
5592
5936
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
5593
5937
|
#emptyWrap;
|
|
5594
5938
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5595
5939
|
#attachTray = null;
|
|
5940
|
+
/** Mic button mount point (input row); the control mounts on connect when enabled. */
|
|
5941
|
+
#voiceSlot;
|
|
5942
|
+
/** Voice-input control; created on connect when transcription is available. */
|
|
5943
|
+
#voice = null;
|
|
5596
5944
|
/** Refs attached to the message currently being sent (the context manifest). */
|
|
5597
5945
|
#runAttachments = [];
|
|
5598
5946
|
#client = null;
|
|
@@ -5616,6 +5964,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5616
5964
|
// frontend-tool loop (which is several AG-UI runs), not one run. `null`
|
|
5617
5965
|
// between turns; user bubbles never enter it.
|
|
5618
5966
|
#currentGroup = null;
|
|
5967
|
+
// The current turn's streamed-reasoning region (THINK-1), shown at the top of
|
|
5968
|
+
// the answer group while a reasoning model thinks and collapsed once the
|
|
5969
|
+
// answer's first text token arrives. `null` outside a reasoning turn.
|
|
5970
|
+
#thoughts = null;
|
|
5619
5971
|
#threadId = "";
|
|
5620
5972
|
#initialMessages = [];
|
|
5621
5973
|
// Skill catalog by source; merged backend → embed → client (later wins).
|
|
@@ -5634,6 +5986,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5634
5986
|
this.#attachButton = document.createElement("button");
|
|
5635
5987
|
this.#fileInput = document.createElement("input");
|
|
5636
5988
|
this.#attachSlot = document.createElement("div");
|
|
5989
|
+
this.#voiceSlot = document.createElement("span");
|
|
5990
|
+
this.#themeToggle = document.createElement("button");
|
|
5637
5991
|
this.#rail = document.createElement("button");
|
|
5638
5992
|
this.#emptyWrap = document.createElement("div");
|
|
5639
5993
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
@@ -5759,6 +6113,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5759
6113
|
}
|
|
5760
6114
|
connectedCallback() {
|
|
5761
6115
|
this.#strings = mergeUiStrings({ ...this.#readStringOverrides(), ...this.strings });
|
|
6116
|
+
if (this.getAttribute("data-theme-toggle") !== null) {
|
|
6117
|
+
const saved = sessionStorage.getItem(THEME_KEY);
|
|
6118
|
+
if (saved !== null) {
|
|
6119
|
+
this.setAttribute("theme", saved);
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
5762
6122
|
this.#render();
|
|
5763
6123
|
this.#drawer.setStrings(this.#strings);
|
|
5764
6124
|
if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
|
|
@@ -5769,6 +6129,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5769
6129
|
void this.#fetchToolCatalog();
|
|
5770
6130
|
this.#wireThreadStore();
|
|
5771
6131
|
this.#wireAttachments();
|
|
6132
|
+
this.#wireVoice();
|
|
5772
6133
|
this.#threadId = this.conversationStore.threadId();
|
|
5773
6134
|
void this.#rehydrate();
|
|
5774
6135
|
}
|
|
@@ -5819,6 +6180,40 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5819
6180
|
}
|
|
5820
6181
|
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
5821
6182
|
}
|
|
6183
|
+
/**
|
|
6184
|
+
* Reveal the composer's 🎤 mic button when transcription is possible — either
|
|
6185
|
+
* a custom {@link transcribeHandler} is set or `data-transcribe-url` provides
|
|
6186
|
+
* the built-in POST endpoint. The control records via `MediaRecorder` and
|
|
6187
|
+
* drops the transcript into the composer; with neither configured the mic
|
|
6188
|
+
* stays hidden and the chat is text-only.
|
|
6189
|
+
*/
|
|
6190
|
+
#wireVoice() {
|
|
6191
|
+
const url = this.getAttribute("data-transcribe-url");
|
|
6192
|
+
const transcribe = this.transcribeHandler ?? this.#defaultTranscribeHandler(url);
|
|
6193
|
+
if (transcribe === null) {
|
|
6194
|
+
return;
|
|
6195
|
+
}
|
|
6196
|
+
this.#voice = new VoiceInput({
|
|
6197
|
+
transcribe,
|
|
6198
|
+
onText: (text2) => this.#insertVoiceText(text2),
|
|
6199
|
+
strings: this.#strings
|
|
6200
|
+
});
|
|
6201
|
+
this.#voiceSlot.appendChild(this.#voice.element);
|
|
6202
|
+
}
|
|
6203
|
+
/** The built-in transcription handler for `data-transcribe-url`, or `null`. */
|
|
6204
|
+
#defaultTranscribeHandler(url) {
|
|
6205
|
+
if (url === null) {
|
|
6206
|
+
return null;
|
|
6207
|
+
}
|
|
6208
|
+
return (audio) => transcribeAudio(audio, { url, headers: this.headers });
|
|
6209
|
+
}
|
|
6210
|
+
/** Drop a voice transcript into the composer (appended to any typed text). */
|
|
6211
|
+
#insertVoiceText(text2) {
|
|
6212
|
+
const current = this.#input.value.trim();
|
|
6213
|
+
this.#input.value = current === "" ? text2 : `${current} ${text2}`;
|
|
6214
|
+
this.#onInput();
|
|
6215
|
+
this.#input.focus();
|
|
6216
|
+
}
|
|
5822
6217
|
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
5823
6218
|
#attachmentMaxBytes() {
|
|
5824
6219
|
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
@@ -6001,6 +6396,23 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6001
6396
|
toggleCollapsed() {
|
|
6002
6397
|
this.setCollapsed(!this.collapsed);
|
|
6003
6398
|
}
|
|
6399
|
+
/**
|
|
6400
|
+
* Flip the `theme` attribute between `light` and `dark` and persist the choice
|
|
6401
|
+
* per tab. Bound to the optional built-in header theme toggle
|
|
6402
|
+
* (`data-theme-toggle`); any non-dark theme (incl. `auto` / `code`) flips to
|
|
6403
|
+
* `dark` first.
|
|
6404
|
+
*/
|
|
6405
|
+
toggleTheme() {
|
|
6406
|
+
const next = this.getAttribute("theme") === "dark" ? "light" : "dark";
|
|
6407
|
+
this.setAttribute("theme", next);
|
|
6408
|
+
sessionStorage.setItem(THEME_KEY, next);
|
|
6409
|
+
this.#syncThemeGlyph();
|
|
6410
|
+
}
|
|
6411
|
+
/** Reflect the current theme on the toggle: show the destination's glyph. */
|
|
6412
|
+
#syncThemeGlyph() {
|
|
6413
|
+
const dark = this.getAttribute("theme") === "dark";
|
|
6414
|
+
this.#themeToggle.textContent = dark ? "\u2600\uFE0F" : "\u{1F319}";
|
|
6415
|
+
}
|
|
6004
6416
|
/**
|
|
6005
6417
|
* Start a fresh conversation: forget the persisted history, drop the
|
|
6006
6418
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
@@ -6017,6 +6429,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6017
6429
|
this.#client = null;
|
|
6018
6430
|
this.#streamingBubble = null;
|
|
6019
6431
|
this.#currentGroup = null;
|
|
6432
|
+
this.#thoughts = null;
|
|
6020
6433
|
this.#hidePending();
|
|
6021
6434
|
this.#toolCards.clear();
|
|
6022
6435
|
this.#serverSettled.clear();
|
|
@@ -6216,7 +6629,18 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6216
6629
|
newChat.addEventListener("click", () => this.newChat());
|
|
6217
6630
|
const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
6218
6631
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
6219
|
-
controls.append(history, newChat
|
|
6632
|
+
controls.append(history, newChat);
|
|
6633
|
+
if (this.getAttribute("data-theme-toggle") !== null) {
|
|
6634
|
+
this.#themeToggle.type = "button";
|
|
6635
|
+
this.#themeToggle.className = "header-btn header-btn--theme";
|
|
6636
|
+
this.#themeToggle.setAttribute("part", "header-button theme-toggle");
|
|
6637
|
+
this.#themeToggle.title = this.#strings.toggleTheme;
|
|
6638
|
+
this.#themeToggle.setAttribute("aria-label", this.#strings.toggleTheme);
|
|
6639
|
+
this.#themeToggle.addEventListener("click", () => this.toggleTheme());
|
|
6640
|
+
this.#syncThemeGlyph();
|
|
6641
|
+
controls.append(this.#themeToggle);
|
|
6642
|
+
}
|
|
6643
|
+
controls.append(collapse);
|
|
6220
6644
|
header.append(title, headerActions, controls);
|
|
6221
6645
|
this.#messages.className = "messages";
|
|
6222
6646
|
this.#messages.setAttribute("part", "messages");
|
|
@@ -6268,9 +6692,10 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6268
6692
|
this.#fileInput.hidden = true;
|
|
6269
6693
|
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
6270
6694
|
this.#attachSlot.className = "attachment-slot";
|
|
6695
|
+
this.#voiceSlot.className = "voice-slot";
|
|
6271
6696
|
const footer = document.createElement("slot");
|
|
6272
6697
|
footer.name = "footer";
|
|
6273
|
-
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
6698
|
+
inputRow.append(this.#attachButton, this.#voiceSlot, this.#input, this.#send, this.#fileInput);
|
|
6274
6699
|
this.#chat.append(
|
|
6275
6700
|
header,
|
|
6276
6701
|
this.#messages,
|
|
@@ -6495,8 +6920,18 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6495
6920
|
this.#ensureGroup();
|
|
6496
6921
|
this.#showPending();
|
|
6497
6922
|
},
|
|
6923
|
+
onReasoningStart: () => {
|
|
6924
|
+
this.#hidePending();
|
|
6925
|
+
this.#showThoughts();
|
|
6926
|
+
},
|
|
6927
|
+
onReasoningDelta: (buffer) => {
|
|
6928
|
+
this.#showThoughts().stream(buffer);
|
|
6929
|
+
},
|
|
6930
|
+
onReasoningEnd: () => {
|
|
6931
|
+
},
|
|
6498
6932
|
onTextDelta: (buffer) => {
|
|
6499
6933
|
this.#hidePending();
|
|
6934
|
+
this.#thoughts?.collapse();
|
|
6500
6935
|
this.#streamInto(buffer);
|
|
6501
6936
|
this.#streamDeltas += 1;
|
|
6502
6937
|
},
|
|
@@ -6548,6 +6983,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6548
6983
|
this.#updateEmptyState();
|
|
6549
6984
|
}
|
|
6550
6985
|
this.#currentGroup = null;
|
|
6986
|
+
this.#thoughts = null;
|
|
6551
6987
|
}
|
|
6552
6988
|
};
|
|
6553
6989
|
}
|
|
@@ -6591,6 +7027,21 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6591
7027
|
this.#pending?.remove();
|
|
6592
7028
|
this.#pending = null;
|
|
6593
7029
|
}
|
|
7030
|
+
/**
|
|
7031
|
+
* The current turn's thoughts region, creating it (at the top of the answer
|
|
7032
|
+
* group, above any streamed text or tool cards) on first sight. Idempotent
|
|
7033
|
+
* across a turn's reasoning tokens.
|
|
7034
|
+
*/
|
|
7035
|
+
#showThoughts() {
|
|
7036
|
+
if (this.#thoughts === null) {
|
|
7037
|
+
this.#thoughts = new ThoughtsBlock(this.#strings);
|
|
7038
|
+
const group = this.#ensureGroup();
|
|
7039
|
+
group.insertBefore(this.#thoughts.element, group.firstChild);
|
|
7040
|
+
this.#updateEmptyState();
|
|
7041
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
7042
|
+
}
|
|
7043
|
+
return this.#thoughts;
|
|
7044
|
+
}
|
|
6594
7045
|
#streamInto(buffer) {
|
|
6595
7046
|
if (this.#streamingBubble === null) {
|
|
6596
7047
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
@@ -6662,7 +7113,7 @@ function setControlValue(el, value) {
|
|
|
6662
7113
|
}
|
|
6663
7114
|
|
|
6664
7115
|
// src/version.ts
|
|
6665
|
-
var VERSION = "0.
|
|
7116
|
+
var VERSION = "0.9.0";
|
|
6666
7117
|
export {
|
|
6667
7118
|
AgUiChat,
|
|
6668
7119
|
AgUiClient,
|
|
@@ -6714,6 +7165,7 @@ export {
|
|
|
6714
7165
|
setNativeValue,
|
|
6715
7166
|
toggleCheckbox,
|
|
6716
7167
|
toggleControl,
|
|
7168
|
+
transcribeAudio,
|
|
6717
7169
|
typeInto,
|
|
6718
7170
|
uploadAttachment
|
|
6719
7171
|
};
|