@artooi/ag-ui-web-component 0.9.0 → 0.11.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 +104 -7
- package/README.md +89 -7
- package/dist/ag-ui-web-component.bundle.js +168 -47
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +39 -1
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +28 -1
- 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/upload_attachment.d.ts +8 -2
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +887 -128
- package/dist/index.js.map +4 -4
- package/dist/ui/approval_card.d.ts +51 -0
- package/dist/ui/approval_card.d.ts.map +1 -0
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts +7 -1
- package/dist/ui/attachment_tray.d.ts.map +1 -1
- package/dist/ui/question_card.d.ts +52 -0
- package/dist/ui/question_card.d.ts.map +1 -0
- package/dist/ui/relative_time.d.ts +5 -3
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/skills_menu.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 +2 -2
- package/dist/ui/thoughts_block.d.ts.map +1 -1
- 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 +16 -0
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +9 -1
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/ag_ui_chat.ts +251 -14
- package/src/core/agui_client.ts +95 -9
- 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/upload_attachment.ts +8 -1
- package/src/index.ts +14 -0
- package/src/ui/approval_card.ts +119 -0
- package/src/ui/attachment_chips.ts +5 -0
- package/src/ui/attachment_tray.ts +50 -5
- package/src/ui/question_card.ts +216 -0
- package/src/ui/relative_time.ts +8 -3
- package/src/ui/skills_menu.ts +6 -0
- package/src/ui/styles.ts +130 -9
- package/src/ui/thoughts_block.ts +3 -2
- package/src/ui/thread_drawer.ts +94 -9
- package/src/ui/tool_call_card.ts +6 -0
- package/src/ui/ui_strings.ts +30 -0
- package/src/ui/voice_input.ts +21 -1
- package/src/version.ts +1 -1
package/src/ui/thread_drawer.ts
CHANGED
|
@@ -37,6 +37,8 @@ export class ThreadDrawer {
|
|
|
37
37
|
#strings: UiStrings;
|
|
38
38
|
#threads: readonly ThreadMeta[] = [];
|
|
39
39
|
#activeId = "";
|
|
40
|
+
/** The element focused before the drawer opened, restored on close. */
|
|
41
|
+
#lastFocused: HTMLElement | null = null;
|
|
40
42
|
|
|
41
43
|
constructor(callbacks: ThreadDrawerCallbacks, strings: UiStrings = DEFAULT_UI_STRINGS) {
|
|
42
44
|
this.#callbacks = callbacks;
|
|
@@ -56,7 +58,10 @@ export class ThreadDrawer {
|
|
|
56
58
|
this.#panel.className = "drawer-panel";
|
|
57
59
|
this.#panel.setAttribute("part", "drawer-panel");
|
|
58
60
|
this.#panel.setAttribute("role", "dialog");
|
|
61
|
+
this.#panel.setAttribute("aria-modal", "true");
|
|
59
62
|
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
63
|
+
// Escape closes the drawer; Tab is trapped within the panel while it's open.
|
|
64
|
+
this.#panel.addEventListener("keydown", (event) => this.#onPanelKeydown(event));
|
|
60
65
|
|
|
61
66
|
const header = document.createElement("div");
|
|
62
67
|
header.className = "drawer-header";
|
|
@@ -98,15 +103,61 @@ export class ThreadDrawer {
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
open(): void {
|
|
106
|
+
if (this.isOpen()) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Remember what had focus so it's restored on close, then move focus into
|
|
110
|
+
// the panel (its first control) so keyboard users land inside the dialog.
|
|
111
|
+
this.#lastFocused = this.#activeElement() as HTMLElement | null;
|
|
101
112
|
this.element.hidden = false;
|
|
113
|
+
this.#newButton.focus();
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
close(): void {
|
|
117
|
+
if (!this.isOpen()) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
105
120
|
this.element.hidden = true;
|
|
121
|
+
this.#lastFocused?.focus();
|
|
122
|
+
this.#lastFocused = null;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
toggle(): void {
|
|
109
|
-
this.
|
|
126
|
+
if (this.isOpen()) {
|
|
127
|
+
this.close();
|
|
128
|
+
} else {
|
|
129
|
+
this.open();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The currently-focused element within the drawer's root (shadow-aware). */
|
|
134
|
+
#activeElement(): Element | null {
|
|
135
|
+
return (this.element.getRootNode() as Document | ShadowRoot).activeElement;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Escape-to-close and a Tab focus trap while the dialog is open. */
|
|
139
|
+
#onPanelKeydown(event: KeyboardEvent): void {
|
|
140
|
+
if (event.key === "Escape") {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
this.close();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (event.key !== "Tab") {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const focusables = Array.from(
|
|
149
|
+
this.#panel.querySelectorAll<HTMLElement>("button, input, [tabindex]"),
|
|
150
|
+
).filter((el) => !el.hidden);
|
|
151
|
+
const first = focusables[0];
|
|
152
|
+
const last = focusables[focusables.length - 1];
|
|
153
|
+
const active = this.#activeElement();
|
|
154
|
+
if (event.shiftKey && active === first) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
last?.focus();
|
|
157
|
+
} else if (!event.shiftKey && active === last) {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
first?.focus();
|
|
160
|
+
}
|
|
110
161
|
}
|
|
111
162
|
|
|
112
163
|
/** Render the rows (or the empty state), highlighting the active thread. */
|
|
@@ -145,12 +196,15 @@ export class ThreadDrawer {
|
|
|
145
196
|
select.setAttribute("part", "drawer-row-select");
|
|
146
197
|
const title = document.createElement("span");
|
|
147
198
|
title.className = "drawer-row-title";
|
|
199
|
+
title.setAttribute("part", "drawer-row-title");
|
|
148
200
|
title.textContent = meta.title;
|
|
149
201
|
const time = document.createElement("span");
|
|
150
202
|
time.className = "drawer-row-time";
|
|
203
|
+
time.setAttribute("part", "drawer-row-time");
|
|
151
204
|
time.textContent = relativeTime(meta.updatedAt, undefined, this.#strings);
|
|
152
205
|
const preview = document.createElement("span");
|
|
153
206
|
preview.className = "drawer-row-preview";
|
|
207
|
+
preview.setAttribute("part", "drawer-row-preview");
|
|
154
208
|
preview.textContent = meta.preview;
|
|
155
209
|
select.append(title, time, preview);
|
|
156
210
|
select.addEventListener("click", () => {
|
|
@@ -161,6 +215,7 @@ export class ThreadDrawer {
|
|
|
161
215
|
const rename = document.createElement("button");
|
|
162
216
|
rename.type = "button";
|
|
163
217
|
rename.className = "drawer-row-rename";
|
|
218
|
+
rename.setAttribute("part", "drawer-row-rename");
|
|
164
219
|
rename.title = this.#strings.rename;
|
|
165
220
|
rename.setAttribute("aria-label", this.#strings.renameConversation);
|
|
166
221
|
rename.textContent = "✎";
|
|
@@ -169,6 +224,7 @@ export class ThreadDrawer {
|
|
|
169
224
|
const remove = document.createElement("button");
|
|
170
225
|
remove.type = "button";
|
|
171
226
|
remove.className = "drawer-row-delete";
|
|
227
|
+
remove.setAttribute("part", "drawer-row-delete");
|
|
172
228
|
remove.title = this.#strings.delete;
|
|
173
229
|
remove.setAttribute("aria-label", this.#strings.deleteConversation);
|
|
174
230
|
remove.textContent = "🗑";
|
|
@@ -176,30 +232,55 @@ export class ThreadDrawer {
|
|
|
176
232
|
|
|
177
233
|
const actions = document.createElement("div");
|
|
178
234
|
actions.className = "drawer-row-actions";
|
|
235
|
+
actions.setAttribute("part", "drawer-row-actions");
|
|
179
236
|
actions.append(rename, remove);
|
|
180
237
|
|
|
181
238
|
row.append(select, actions);
|
|
182
239
|
return row;
|
|
183
240
|
}
|
|
184
241
|
|
|
185
|
-
/** Swap a row for an inline rename input; Enter commits, Escape cancels. */
|
|
242
|
+
/** Swap a row for an inline rename input; Enter/blur commits, Escape cancels. */
|
|
186
243
|
#startRename(row: HTMLDivElement, meta: ThreadMeta): void {
|
|
187
244
|
const input = document.createElement("input");
|
|
188
245
|
input.type = "text";
|
|
189
246
|
input.className = "drawer-rename-input";
|
|
247
|
+
input.setAttribute("part", "drawer-rename-input");
|
|
190
248
|
input.value = meta.title;
|
|
249
|
+
// One-shot: Enter, Escape, and blur can all fire for a single edit (Enter
|
|
250
|
+
// commits and re-renders, which blurs the detached input); the flag makes
|
|
251
|
+
// the later events no-ops so a rename isn't submitted twice.
|
|
252
|
+
let done = false;
|
|
253
|
+
const commit = (): void => {
|
|
254
|
+
if (done) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
done = true;
|
|
258
|
+
const value = input.value.trim();
|
|
259
|
+
if (value === "" || value === meta.title) {
|
|
260
|
+
this.#renderList();
|
|
261
|
+
} else {
|
|
262
|
+
this.#callbacks.onRename(meta.threadId, value);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const cancel = (): void => {
|
|
266
|
+
if (done) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
done = true;
|
|
270
|
+
this.#renderList();
|
|
271
|
+
};
|
|
191
272
|
input.addEventListener("keydown", (event) => {
|
|
192
273
|
if (event.key === "Enter") {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.#renderList();
|
|
196
|
-
} else {
|
|
197
|
-
this.#callbacks.onRename(meta.threadId, value);
|
|
198
|
-
}
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
commit();
|
|
199
276
|
} else if (event.key === "Escape") {
|
|
200
|
-
|
|
277
|
+
// Stop the panel's Escape handler from also closing the drawer.
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
event.stopPropagation();
|
|
280
|
+
cancel();
|
|
201
281
|
}
|
|
202
282
|
});
|
|
283
|
+
input.addEventListener("blur", () => commit());
|
|
203
284
|
row.replaceChildren(input);
|
|
204
285
|
input.focus();
|
|
205
286
|
input.select();
|
|
@@ -209,17 +290,21 @@ export class ThreadDrawer {
|
|
|
209
290
|
#confirmDelete(row: HTMLDivElement, meta: ThreadMeta): void {
|
|
210
291
|
const confirm = document.createElement("div");
|
|
211
292
|
confirm.className = "drawer-confirm";
|
|
293
|
+
confirm.setAttribute("part", "drawer-confirm");
|
|
212
294
|
const label = document.createElement("span");
|
|
213
295
|
label.className = "drawer-confirm-label";
|
|
296
|
+
label.setAttribute("part", "drawer-confirm-label");
|
|
214
297
|
label.textContent = this.#strings.deletePrompt;
|
|
215
298
|
const yes = document.createElement("button");
|
|
216
299
|
yes.type = "button";
|
|
217
300
|
yes.className = "drawer-confirm-yes";
|
|
301
|
+
yes.setAttribute("part", "drawer-confirm-yes");
|
|
218
302
|
yes.textContent = this.#strings.delete;
|
|
219
303
|
yes.addEventListener("click", () => this.#callbacks.onDelete(meta.threadId));
|
|
220
304
|
const no = document.createElement("button");
|
|
221
305
|
no.type = "button";
|
|
222
306
|
no.className = "drawer-confirm-no";
|
|
307
|
+
no.setAttribute("part", "drawer-confirm-no");
|
|
223
308
|
no.textContent = this.#strings.cancel;
|
|
224
309
|
no.addEventListener("click", () => this.#renderList());
|
|
225
310
|
confirm.append(label, yes, no);
|
package/src/ui/tool_call_card.ts
CHANGED
|
@@ -126,6 +126,12 @@ export class ToolCallCard {
|
|
|
126
126
|
* `inline`), or the args + result together (`compact`).
|
|
127
127
|
*/
|
|
128
128
|
settle(status: SettledStatus, text: string): void {
|
|
129
|
+
// Idempotent: a duplicate `TOOL_CALL_RESULT`, or a replayed tool message
|
|
130
|
+
// for an already-settled card, must not append a second toggle+body. The
|
|
131
|
+
// first settle wins; later calls are ignored.
|
|
132
|
+
if (this.#settled) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
129
135
|
this.#settled = true;
|
|
130
136
|
this.element.setAttribute("data-status", status);
|
|
131
137
|
this.#status.textContent = statusLabels(this.#strings)[status];
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -98,6 +98,26 @@ export interface UiStrings {
|
|
|
98
98
|
/** Cancel button (confirmation + delete confirm). */
|
|
99
99
|
cancel: string;
|
|
100
100
|
|
|
101
|
+
// ── Approval card (server-side tool gate) ───────────────────────────────────
|
|
102
|
+
/** `aria-label` of the inline server-side-tool approval card. */
|
|
103
|
+
approveAction: string;
|
|
104
|
+
/** Fallback approval prompt when the interrupt carries no message. */
|
|
105
|
+
approvalPrompt: string;
|
|
106
|
+
/** Approve button (runs the gated server-side tool). */
|
|
107
|
+
approve: string;
|
|
108
|
+
/** Deny button (declines the gated server-side tool). */
|
|
109
|
+
deny: string;
|
|
110
|
+
|
|
111
|
+
// ── Question card (the `ask_user` frontend tool) ────────────────────────────
|
|
112
|
+
/** `aria-label` of the inline question card. */
|
|
113
|
+
askUserAction: string;
|
|
114
|
+
/** Radio label for the free-text "other" choice (when custom answers are allowed). */
|
|
115
|
+
otherOption: string;
|
|
116
|
+
/** Placeholder for the free-text answer field. */
|
|
117
|
+
answerPlaceholder: string;
|
|
118
|
+
/** Submit button for the question card. */
|
|
119
|
+
submit: string;
|
|
120
|
+
|
|
101
121
|
// ── Chat-history drawer ─────────────────────────────────────────────────────
|
|
102
122
|
/** Drawer heading. */
|
|
103
123
|
chats: string;
|
|
@@ -186,6 +206,16 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
|
186
206
|
confirm: "Confirm",
|
|
187
207
|
cancel: "Cancel",
|
|
188
208
|
|
|
209
|
+
approveAction: "Approve action",
|
|
210
|
+
approvalPrompt: "Approve this action?",
|
|
211
|
+
approve: "Approve",
|
|
212
|
+
deny: "Deny",
|
|
213
|
+
|
|
214
|
+
askUserAction: "Question",
|
|
215
|
+
otherOption: "Other…",
|
|
216
|
+
answerPlaceholder: "Type your answer…",
|
|
217
|
+
submit: "Submit",
|
|
218
|
+
|
|
189
219
|
chats: "Chats",
|
|
190
220
|
noConversations: "No conversations yet.",
|
|
191
221
|
rename: "Rename",
|
package/src/ui/voice_input.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface VoiceInputOptions {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* The composer's voice-input control
|
|
18
|
+
* The composer's voice-input control: a mic button that records via
|
|
19
19
|
* `MediaRecorder`, then POSTs the clip through a {@link TranscribeHandler} and
|
|
20
20
|
* hands the transcript back via `onText`.
|
|
21
21
|
*
|
|
@@ -38,6 +38,7 @@ export class VoiceInput {
|
|
|
38
38
|
#recorder: MediaRecorder | null = null;
|
|
39
39
|
#stream: MediaStream | null = null;
|
|
40
40
|
#chunks: Blob[] = [];
|
|
41
|
+
#disposed = false;
|
|
41
42
|
|
|
42
43
|
constructor(options: VoiceInputOptions) {
|
|
43
44
|
this.#transcribe = options.transcribe;
|
|
@@ -94,7 +95,26 @@ export class VoiceInput {
|
|
|
94
95
|
this.#recorder?.stop();
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Tear the control down — the teardown path when the host element is removed
|
|
100
|
+
* mid-recording. Stops any live `MediaRecorder`, releases the mic tracks (so
|
|
101
|
+
* the browser's recording indicator clears), and suppresses the pending
|
|
102
|
+
* transcription: a disconnected control must not fire `onText` back into a
|
|
103
|
+
* detached element.
|
|
104
|
+
*/
|
|
105
|
+
dispose(): void {
|
|
106
|
+
this.#disposed = true;
|
|
107
|
+
if (this.#recorder !== null && this.#recorder.state !== "inactive") {
|
|
108
|
+
this.#recorder.stop();
|
|
109
|
+
}
|
|
110
|
+
this.#recorder = null;
|
|
111
|
+
this.#releaseStream();
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
async #finish(mimeType: string): Promise<void> {
|
|
115
|
+
if (this.#disposed) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
98
118
|
this.#releaseStream();
|
|
99
119
|
this.#setState("transcribing");
|
|
100
120
|
const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.11.0";
|