@artooi/ag-ui-web-component 0.8.1 → 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 +25 -1
- package/README.md +45 -7
- package/dist/ag-ui-web-component.bundle.js +133 -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 +443 -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 +106 -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",
|
|
@@ -4018,6 +4024,69 @@ var STYLES = `
|
|
|
4018
4024
|
}
|
|
4019
4025
|
}
|
|
4020
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
|
+
|
|
4021
4090
|
.tool-call {
|
|
4022
4091
|
align-self: flex-start;
|
|
4023
4092
|
max-width: 80%;
|
|
@@ -4226,6 +4295,49 @@ var STYLES = `
|
|
|
4226
4295
|
display: none;
|
|
4227
4296
|
}
|
|
4228
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
|
+
|
|
4229
4341
|
/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */
|
|
4230
4342
|
.attachment-slot {
|
|
4231
4343
|
display: contents;
|
|
@@ -4640,6 +4752,62 @@ var STYLES = `
|
|
|
4640
4752
|
}
|
|
4641
4753
|
`;
|
|
4642
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
|
+
|
|
4643
4811
|
// src/ui/relative_time.ts
|
|
4644
4812
|
function relativeTime(timestamp, now = Date.now(), strings = DEFAULT_UI_STRINGS) {
|
|
4645
4813
|
const seconds = Math.round((now - timestamp) / 1e3);
|
|
@@ -4943,6 +5111,115 @@ ${text2}`;
|
|
|
4943
5111
|
}
|
|
4944
5112
|
};
|
|
4945
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
|
+
|
|
4946
5223
|
// src/core/agui_client.ts
|
|
4947
5224
|
import { randomUUID as randomUUID2 } from "@ag-ui/client";
|
|
4948
5225
|
var ConnectionLostError = class extends Error {
|
|
@@ -5114,6 +5391,18 @@ var AgUiClient = class {
|
|
|
5114
5391
|
onToolCallResultEvent({ event }) {
|
|
5115
5392
|
h.onToolResult(event.toolCallId, event.content);
|
|
5116
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
|
+
},
|
|
5117
5406
|
onRunErrorEvent({ event }) {
|
|
5118
5407
|
runState.terminal = true;
|
|
5119
5408
|
h.onError(event.message);
|
|
@@ -5390,6 +5679,35 @@ var RemoteConversationStore = class {
|
|
|
5390
5679
|
}
|
|
5391
5680
|
};
|
|
5392
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
|
+
|
|
5393
5711
|
// src/core/upload_attachment.ts
|
|
5394
5712
|
function uploadAttachment(file, options) {
|
|
5395
5713
|
return new Promise((resolve, reject) => {
|
|
@@ -5416,7 +5734,7 @@ function uploadAttachment(file, options) {
|
|
|
5416
5734
|
reject(new Error("upload returned an unreadable response"));
|
|
5417
5735
|
}
|
|
5418
5736
|
} else {
|
|
5419
|
-
reject(new Error(
|
|
5737
|
+
reject(new Error(errorMessage2(xhr)));
|
|
5420
5738
|
}
|
|
5421
5739
|
});
|
|
5422
5740
|
xhr.addEventListener("error", () => reject(new Error("upload failed")));
|
|
@@ -5443,7 +5761,7 @@ function parseRef(body) {
|
|
|
5443
5761
|
}
|
|
5444
5762
|
return typeof url === "string" ? { id, name, mime, size, url } : { id, name, mime, size };
|
|
5445
5763
|
}
|
|
5446
|
-
function
|
|
5764
|
+
function errorMessage2(xhr) {
|
|
5447
5765
|
try {
|
|
5448
5766
|
const body = JSON.parse(xhr.responseText);
|
|
5449
5767
|
if (typeof body.error === "string") {
|
|
@@ -5456,6 +5774,7 @@ function errorMessage(xhr) {
|
|
|
5456
5774
|
|
|
5457
5775
|
// src/core/ag_ui_chat.ts
|
|
5458
5776
|
var COLLAPSED_KEY = "ag-ui-chat:collapsed";
|
|
5777
|
+
var THEME_KEY = "ag-ui-chat:theme";
|
|
5459
5778
|
var AgUiChat = class extends HTMLElement {
|
|
5460
5779
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
5461
5780
|
agentFactory = createHttpAgent;
|
|
@@ -5533,6 +5852,15 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5533
5852
|
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
5534
5853
|
*/
|
|
5535
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;
|
|
5536
5864
|
/**
|
|
5537
5865
|
* Builds the tool result a navigating tool resumes with after the page
|
|
5538
5866
|
* reloads. Defaults to the landed URL; a host (e.g. the admin package) can
|
|
@@ -5601,12 +5929,18 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5601
5929
|
#attachButton;
|
|
5602
5930
|
#fileInput;
|
|
5603
5931
|
#attachSlot;
|
|
5932
|
+
/** Optional built-in header theme toggle (THEME-1); shown only with `data-theme-toggle`. */
|
|
5933
|
+
#themeToggle;
|
|
5604
5934
|
/** The collapsed-sidebar rail (an expand affordance; shown only for `placement="sidebar"`). */
|
|
5605
5935
|
#rail;
|
|
5606
5936
|
/** Empty-state region at the top of the message list; hidden once anything renders. */
|
|
5607
5937
|
#emptyWrap;
|
|
5608
5938
|
/** Upload tray; created on connect only when `data-attachments-url` is set. */
|
|
5609
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;
|
|
5610
5944
|
/** Refs attached to the message currently being sent (the context manifest). */
|
|
5611
5945
|
#runAttachments = [];
|
|
5612
5946
|
#client = null;
|
|
@@ -5630,6 +5964,10 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5630
5964
|
// frontend-tool loop (which is several AG-UI runs), not one run. `null`
|
|
5631
5965
|
// between turns; user bubbles never enter it.
|
|
5632
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;
|
|
5633
5971
|
#threadId = "";
|
|
5634
5972
|
#initialMessages = [];
|
|
5635
5973
|
// Skill catalog by source; merged backend → embed → client (later wins).
|
|
@@ -5648,6 +5986,8 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5648
5986
|
this.#attachButton = document.createElement("button");
|
|
5649
5987
|
this.#fileInput = document.createElement("input");
|
|
5650
5988
|
this.#attachSlot = document.createElement("div");
|
|
5989
|
+
this.#voiceSlot = document.createElement("span");
|
|
5990
|
+
this.#themeToggle = document.createElement("button");
|
|
5651
5991
|
this.#rail = document.createElement("button");
|
|
5652
5992
|
this.#emptyWrap = document.createElement("div");
|
|
5653
5993
|
this.#skillsMenu = new SkillsMenu((skill) => this.#applySkill(skill));
|
|
@@ -5773,6 +6113,12 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5773
6113
|
}
|
|
5774
6114
|
connectedCallback() {
|
|
5775
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
|
+
}
|
|
5776
6122
|
this.#render();
|
|
5777
6123
|
this.#drawer.setStrings(this.#strings);
|
|
5778
6124
|
if (sessionStorage.getItem(COLLAPSED_KEY) === "1") {
|
|
@@ -5783,6 +6129,7 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5783
6129
|
void this.#fetchToolCatalog();
|
|
5784
6130
|
this.#wireThreadStore();
|
|
5785
6131
|
this.#wireAttachments();
|
|
6132
|
+
this.#wireVoice();
|
|
5786
6133
|
this.#threadId = this.conversationStore.threadId();
|
|
5787
6134
|
void this.#rehydrate();
|
|
5788
6135
|
}
|
|
@@ -5833,6 +6180,40 @@ var AgUiChat = class extends HTMLElement {
|
|
|
5833
6180
|
}
|
|
5834
6181
|
return (file, onProgress) => uploadAttachment(file, { url, headers: this.headers, onProgress });
|
|
5835
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
|
+
}
|
|
5836
6217
|
/** The client-side upload size cap from `data-attachment-max-bytes`. */
|
|
5837
6218
|
#attachmentMaxBytes() {
|
|
5838
6219
|
const attr = this.getAttribute("data-attachment-max-bytes");
|
|
@@ -6015,6 +6396,23 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6015
6396
|
toggleCollapsed() {
|
|
6016
6397
|
this.setCollapsed(!this.collapsed);
|
|
6017
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
|
+
}
|
|
6018
6416
|
/**
|
|
6019
6417
|
* Start a fresh conversation: forget the persisted history, drop the
|
|
6020
6418
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
@@ -6031,6 +6429,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6031
6429
|
this.#client = null;
|
|
6032
6430
|
this.#streamingBubble = null;
|
|
6033
6431
|
this.#currentGroup = null;
|
|
6432
|
+
this.#thoughts = null;
|
|
6034
6433
|
this.#hidePending();
|
|
6035
6434
|
this.#toolCards.clear();
|
|
6036
6435
|
this.#serverSettled.clear();
|
|
@@ -6230,7 +6629,18 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6230
6629
|
newChat.addEventListener("click", () => this.newChat());
|
|
6231
6630
|
const collapse = this.#headerButton("collapse", this.#strings.collapse, "\u2014");
|
|
6232
6631
|
collapse.addEventListener("click", () => this.toggleCollapsed());
|
|
6233
|
-
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);
|
|
6234
6644
|
header.append(title, headerActions, controls);
|
|
6235
6645
|
this.#messages.className = "messages";
|
|
6236
6646
|
this.#messages.setAttribute("part", "messages");
|
|
@@ -6282,9 +6692,10 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6282
6692
|
this.#fileInput.hidden = true;
|
|
6283
6693
|
this.#fileInput.addEventListener("change", () => this.#onFilesPicked());
|
|
6284
6694
|
this.#attachSlot.className = "attachment-slot";
|
|
6695
|
+
this.#voiceSlot.className = "voice-slot";
|
|
6285
6696
|
const footer = document.createElement("slot");
|
|
6286
6697
|
footer.name = "footer";
|
|
6287
|
-
inputRow.append(this.#attachButton, this.#input, this.#send, this.#fileInput);
|
|
6698
|
+
inputRow.append(this.#attachButton, this.#voiceSlot, this.#input, this.#send, this.#fileInput);
|
|
6288
6699
|
this.#chat.append(
|
|
6289
6700
|
header,
|
|
6290
6701
|
this.#messages,
|
|
@@ -6509,8 +6920,18 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6509
6920
|
this.#ensureGroup();
|
|
6510
6921
|
this.#showPending();
|
|
6511
6922
|
},
|
|
6923
|
+
onReasoningStart: () => {
|
|
6924
|
+
this.#hidePending();
|
|
6925
|
+
this.#showThoughts();
|
|
6926
|
+
},
|
|
6927
|
+
onReasoningDelta: (buffer) => {
|
|
6928
|
+
this.#showThoughts().stream(buffer);
|
|
6929
|
+
},
|
|
6930
|
+
onReasoningEnd: () => {
|
|
6931
|
+
},
|
|
6512
6932
|
onTextDelta: (buffer) => {
|
|
6513
6933
|
this.#hidePending();
|
|
6934
|
+
this.#thoughts?.collapse();
|
|
6514
6935
|
this.#streamInto(buffer);
|
|
6515
6936
|
this.#streamDeltas += 1;
|
|
6516
6937
|
},
|
|
@@ -6562,6 +6983,7 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6562
6983
|
this.#updateEmptyState();
|
|
6563
6984
|
}
|
|
6564
6985
|
this.#currentGroup = null;
|
|
6986
|
+
this.#thoughts = null;
|
|
6565
6987
|
}
|
|
6566
6988
|
};
|
|
6567
6989
|
}
|
|
@@ -6605,6 +7027,21 @@ Use the read_attachment tool with an id to read a file's contents.`
|
|
|
6605
7027
|
this.#pending?.remove();
|
|
6606
7028
|
this.#pending = null;
|
|
6607
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
|
+
}
|
|
6608
7045
|
#streamInto(buffer) {
|
|
6609
7046
|
if (this.#streamingBubble === null) {
|
|
6610
7047
|
this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
|
|
@@ -6676,7 +7113,7 @@ function setControlValue(el, value) {
|
|
|
6676
7113
|
}
|
|
6677
7114
|
|
|
6678
7115
|
// src/version.ts
|
|
6679
|
-
var VERSION = "0.
|
|
7116
|
+
var VERSION = "0.9.0";
|
|
6680
7117
|
export {
|
|
6681
7118
|
AgUiChat,
|
|
6682
7119
|
AgUiClient,
|
|
@@ -6728,6 +7165,7 @@ export {
|
|
|
6728
7165
|
setNativeValue,
|
|
6729
7166
|
toggleCheckbox,
|
|
6730
7167
|
toggleControl,
|
|
7168
|
+
transcribeAudio,
|
|
6731
7169
|
typeInto,
|
|
6732
7170
|
uploadAttachment
|
|
6733
7171
|
};
|