@artooi/ag-ui-web-component 0.34.0 → 0.35.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 +501 -1
- package/README.md +232 -13
- package/dist/ag-ui-web-component.bundle.js +614 -96
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +76 -3
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +37 -2
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +14 -0
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/highlight_overlay.d.ts +47 -0
- package/dist/dom/highlight_overlay.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1886 -320
- package/dist/index.js.map +4 -4
- package/dist/tools/chat_surface_tools.d.ts +96 -0
- package/dist/tools/chat_surface_tools.d.ts.map +1 -0
- package/dist/tools/page_action_tools.d.ts +2 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/ui/clamp_launcher.d.ts +10 -5
- package/dist/ui/clamp_launcher.d.ts.map +1 -1
- package/dist/ui/clamp_panel.d.ts +2 -2
- package/dist/ui/clamp_panel.d.ts.map +1 -1
- package/dist/ui/launcher_drag.d.ts +2 -2
- package/dist/ui/launcher_drag.d.ts.map +1 -1
- package/dist/ui/launcher_placement.d.ts +14 -1
- package/dist/ui/launcher_placement.d.ts.map +1 -1
- package/dist/ui/panel_drag.d.ts.map +1 -1
- package/dist/ui/place_widget.d.ts +9 -1
- package/dist/ui/place_widget.d.ts.map +1 -1
- package/dist/ui/run_notice.d.ts +14 -3
- package/dist/ui/run_notice.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/thread_drawer.d.ts +21 -0
- package/dist/ui/thread_drawer.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/package.json +1 -1
- package/src/constants.ts +82 -3
- package/src/core/ag_ui_chat.ts +965 -55
- package/src/dom/animations.ts +30 -0
- package/src/dom/highlight_overlay.ts +256 -0
- package/src/index.ts +12 -0
- package/src/tools/chat_surface_tools.ts +207 -0
- package/src/tools/page_action_tools.ts +2 -0
- package/src/ui/clamp_launcher.ts +25 -7
- package/src/ui/clamp_panel.ts +10 -4
- package/src/ui/launcher_drag.ts +11 -2
- package/src/ui/launcher_placement.ts +34 -8
- package/src/ui/panel_drag.ts +4 -0
- package/src/ui/place_widget.ts +11 -3
- package/src/ui/run_notice.ts +32 -3
- package/src/ui/styles.ts +563 -45
- package/src/ui/thread_drawer.ts +138 -8
- package/src/ui/ui_strings.ts +24 -0
- package/src/version.ts +1 -1
package/src/ui/thread_drawer.ts
CHANGED
|
@@ -2,6 +2,14 @@ import type { ThreadMeta } from "../core/conversation_store.js";
|
|
|
2
2
|
import { type RelativeTimeFormatter, relativeTime } from "./relative_time.js";
|
|
3
3
|
import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* How many conversations there have to be before the filter appears.
|
|
7
|
+
*
|
|
8
|
+
* A search box above a list you can already read in one glance is a control
|
|
9
|
+
* asking to be used on nothing.
|
|
10
|
+
*/
|
|
11
|
+
export const FILTER_FROM = 8;
|
|
12
|
+
|
|
5
13
|
/** Actions the host ({@link AgUiChat}) wires to the drawer's rows. */
|
|
6
14
|
export interface ThreadDrawerCallbacks {
|
|
7
15
|
/** A row was picked — load that thread and make it active. */
|
|
@@ -12,6 +20,15 @@ export interface ThreadDrawerCallbacks {
|
|
|
12
20
|
readonly onRename: (threadId: string, title: string) => void;
|
|
13
21
|
/** A row was deleted (after the inline confirm). */
|
|
14
22
|
readonly onDelete: (threadId: string) => void;
|
|
23
|
+
/**
|
|
24
|
+
* The list opened or closed.
|
|
25
|
+
*
|
|
26
|
+
* There are five ways out of it -- the close control, the backdrop, Escape,
|
|
27
|
+
* picking a row and starting a new chat -- and only two of them go through
|
|
28
|
+
* the host. A docked list moves the transcript over, so the host has to know
|
|
29
|
+
* about all five or the transcript stays shifted around nothing.
|
|
30
|
+
*/
|
|
31
|
+
readonly onVisibility?: (open: boolean) => void;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
/**
|
|
@@ -39,6 +56,24 @@ export class ThreadDrawer {
|
|
|
39
56
|
#strings: UiStrings;
|
|
40
57
|
#threads: readonly ThreadMeta[] = [];
|
|
41
58
|
#activeId = "";
|
|
59
|
+
/** Dismisses the list without starting a new conversation. */
|
|
60
|
+
#closeButton: HTMLButtonElement;
|
|
61
|
+
/** Narrows the list. Only rendered once there is enough of a list to narrow. */
|
|
62
|
+
#filter: HTMLInputElement;
|
|
63
|
+
/** The current filter text, lowercased once rather than per row. */
|
|
64
|
+
#query = "";
|
|
65
|
+
/**
|
|
66
|
+
* Whether the list covers the conversation, or sits beside it.
|
|
67
|
+
*
|
|
68
|
+
* Everything a dialog implies follows from this and nothing else does: the
|
|
69
|
+
* modal role, the focus trap, and taking focus on open. A list docked beside
|
|
70
|
+
* a full-page conversation is a region of that page -- telling assistive
|
|
71
|
+
* technology to ignore the rest of the document while it is showing would be
|
|
72
|
+
* a lie, and stealing focus into it would interrupt someone mid-sentence to
|
|
73
|
+
* announce a list they can already see.
|
|
74
|
+
*/
|
|
75
|
+
#modal = true;
|
|
76
|
+
|
|
42
77
|
/** The element focused before the drawer opened, restored on close. */
|
|
43
78
|
#lastFocused: HTMLElement | null = null;
|
|
44
79
|
|
|
@@ -81,13 +116,41 @@ export class ThreadDrawer {
|
|
|
81
116
|
this.close();
|
|
82
117
|
this.#callbacks.onNew();
|
|
83
118
|
});
|
|
84
|
-
|
|
119
|
+
// An explicit way back. The backdrop closes on click, which is enough
|
|
120
|
+
// wherever a strip of it is showing -- but the embedded placement widens
|
|
121
|
+
// the panel to the full width of the host's box, so there is no backdrop
|
|
122
|
+
// left to hit and the only exits were Escape, picking a row, or starting a
|
|
123
|
+
// new conversation. The first is invisible and the last is destructive of
|
|
124
|
+
// the thing you were looking at.
|
|
125
|
+
this.#closeButton = document.createElement("button");
|
|
126
|
+
this.#closeButton.type = "button";
|
|
127
|
+
this.#closeButton.className = "drawer-close";
|
|
128
|
+
this.#closeButton.setAttribute("part", "drawer-close");
|
|
129
|
+
this.#closeButton.title = strings.closeHistory;
|
|
130
|
+
this.#closeButton.setAttribute("aria-label", strings.closeHistory);
|
|
131
|
+
this.#closeButton.append(document.createTextNode("\u00d7"));
|
|
132
|
+
this.#closeButton.addEventListener("click", () => this.close());
|
|
133
|
+
header.append(this.#heading, this.#newButton, this.#closeButton);
|
|
134
|
+
|
|
135
|
+
// Hidden until the list is long enough to be worth narrowing. A search box
|
|
136
|
+
// above four conversations is a control asking to be used on something
|
|
137
|
+
// already entirely visible.
|
|
138
|
+
this.#filter = document.createElement("input");
|
|
139
|
+
this.#filter.type = "search";
|
|
140
|
+
this.#filter.className = "drawer-filter";
|
|
141
|
+
this.#filter.setAttribute("part", "drawer-filter");
|
|
142
|
+
this.#filter.placeholder = strings.searchConversations;
|
|
143
|
+
this.#filter.setAttribute("aria-label", strings.searchConversations);
|
|
144
|
+
this.#filter.addEventListener("input", () => {
|
|
145
|
+
this.#query = this.#filter.value.trim().toLowerCase();
|
|
146
|
+
this.#renderList();
|
|
147
|
+
});
|
|
85
148
|
|
|
86
149
|
this.#list = document.createElement("div");
|
|
87
150
|
this.#list.className = "drawer-list";
|
|
88
151
|
this.#list.setAttribute("part", "drawer-list");
|
|
89
152
|
|
|
90
|
-
this.#panel.append(header, this.#list);
|
|
153
|
+
this.#panel.append(header, this.#filter, this.#list);
|
|
91
154
|
this.element.append(backdrop, this.#panel);
|
|
92
155
|
}
|
|
93
156
|
|
|
@@ -116,9 +179,28 @@ export class ThreadDrawer {
|
|
|
116
179
|
this.#panel.setAttribute("aria-label", strings.chatHistory);
|
|
117
180
|
this.#heading.textContent = strings.chats;
|
|
118
181
|
this.#newButton.textContent = strings.newChat;
|
|
182
|
+
this.#closeButton.title = strings.closeHistory;
|
|
183
|
+
this.#closeButton.setAttribute("aria-label", strings.closeHistory);
|
|
184
|
+
this.#filter.placeholder = strings.searchConversations;
|
|
185
|
+
this.#filter.setAttribute("aria-label", strings.searchConversations);
|
|
119
186
|
this.#renderList();
|
|
120
187
|
}
|
|
121
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Whether the list is a dialog over the conversation or a rail beside it.
|
|
191
|
+
* The host decides, because only the placement knows whether there is room.
|
|
192
|
+
*/
|
|
193
|
+
setModal(modal: boolean): void {
|
|
194
|
+
this.#modal = modal;
|
|
195
|
+
if (modal) {
|
|
196
|
+
this.#panel.setAttribute("role", "dialog");
|
|
197
|
+
this.#panel.setAttribute("aria-modal", "true");
|
|
198
|
+
} else {
|
|
199
|
+
this.#panel.setAttribute("role", "region");
|
|
200
|
+
this.#panel.removeAttribute("aria-modal");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
122
204
|
isOpen(): boolean {
|
|
123
205
|
return !this.element.hidden;
|
|
124
206
|
}
|
|
@@ -131,7 +213,10 @@ export class ThreadDrawer {
|
|
|
131
213
|
// the panel (its first control) so keyboard users land inside the dialog.
|
|
132
214
|
this.#lastFocused = this.#activeElement() as HTMLElement | null;
|
|
133
215
|
this.element.hidden = false;
|
|
134
|
-
this.#
|
|
216
|
+
if (this.#modal) {
|
|
217
|
+
this.#newButton.focus();
|
|
218
|
+
}
|
|
219
|
+
this.#callbacks.onVisibility?.(true);
|
|
135
220
|
}
|
|
136
221
|
|
|
137
222
|
close(): void {
|
|
@@ -139,8 +224,13 @@ export class ThreadDrawer {
|
|
|
139
224
|
return;
|
|
140
225
|
}
|
|
141
226
|
this.element.hidden = true;
|
|
142
|
-
|
|
227
|
+
// Only if it was taken. Restoring focus that was never moved would pull it
|
|
228
|
+
// back from wherever the user has since put it.
|
|
229
|
+
if (this.#modal) {
|
|
230
|
+
this.#lastFocused?.focus();
|
|
231
|
+
}
|
|
143
232
|
this.#lastFocused = null;
|
|
233
|
+
this.#callbacks.onVisibility?.(false);
|
|
144
234
|
}
|
|
145
235
|
|
|
146
236
|
toggle(): void {
|
|
@@ -163,7 +253,11 @@ export class ThreadDrawer {
|
|
|
163
253
|
this.close();
|
|
164
254
|
return;
|
|
165
255
|
}
|
|
166
|
-
|
|
256
|
+
// Escape still closes a docked rail -- it is a way out, not a modal
|
|
257
|
+
// convention -- but nothing beyond this point applies to one. Tabbing out
|
|
258
|
+
// of a list that sits beside the conversation should reach the
|
|
259
|
+
// conversation, which is the whole difference.
|
|
260
|
+
if (event.key !== "Tab" || !this.#modal) {
|
|
167
261
|
return;
|
|
168
262
|
}
|
|
169
263
|
const focusables = Array.from(
|
|
@@ -190,19 +284,55 @@ export class ThreadDrawer {
|
|
|
190
284
|
|
|
191
285
|
#renderList(): void {
|
|
192
286
|
this.#list.replaceChildren();
|
|
193
|
-
|
|
287
|
+
const hideFilter = this.#threads.length < FILTER_FROM;
|
|
288
|
+
this.#filter.hidden = hideFilter;
|
|
289
|
+
// Cleared as it goes, or the query outlives the control that set it: a
|
|
290
|
+
// list that drops below the threshold while a query matches nothing shows
|
|
291
|
+
// "no conversations match that" over conversations that are right there,
|
|
292
|
+
// with nothing left on screen to clear. Reopening does not help either --
|
|
293
|
+
// only a new drawer would.
|
|
294
|
+
if (hideFilter && this.#query !== "") {
|
|
295
|
+
this.#filter.value = "";
|
|
296
|
+
this.#query = "";
|
|
297
|
+
}
|
|
298
|
+
const shown = this.#matching();
|
|
299
|
+
if (shown.length === 0) {
|
|
194
300
|
const empty = document.createElement("div");
|
|
195
301
|
empty.className = "drawer-empty";
|
|
196
302
|
empty.setAttribute("part", "drawer-empty");
|
|
197
|
-
|
|
303
|
+
// Two different situations wearing one sentence would be a small lie:
|
|
304
|
+
// "no conversations yet" reads as data loss when what happened is that a
|
|
305
|
+
// filter matched nothing.
|
|
306
|
+
empty.textContent =
|
|
307
|
+
this.#threads.length === 0 ? this.#strings.noConversations : this.#strings.noMatches;
|
|
198
308
|
this.#list.appendChild(empty);
|
|
199
309
|
return;
|
|
200
310
|
}
|
|
201
|
-
for (const meta of
|
|
311
|
+
for (const meta of shown) {
|
|
202
312
|
this.#list.appendChild(this.#renderRow(meta));
|
|
203
313
|
}
|
|
204
314
|
}
|
|
205
315
|
|
|
316
|
+
/**
|
|
317
|
+
* The rows the filter leaves.
|
|
318
|
+
*
|
|
319
|
+
* Title and preview both, because the title is often the model's summary of
|
|
320
|
+
* a conversation and the thing being looked for is as likely to be a phrase
|
|
321
|
+
* from inside it. Client-side over what the drawer already holds: the server
|
|
322
|
+
* index is fetched whole, so a query would be a round trip to filter a list
|
|
323
|
+
* already in memory.
|
|
324
|
+
*/
|
|
325
|
+
#matching(): readonly ThreadMeta[] {
|
|
326
|
+
if (this.#query === "") {
|
|
327
|
+
return this.#threads;
|
|
328
|
+
}
|
|
329
|
+
return this.#threads.filter(
|
|
330
|
+
(meta) =>
|
|
331
|
+
meta.title.toLowerCase().includes(this.#query) ||
|
|
332
|
+
meta.preview.toLowerCase().includes(this.#query),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
206
336
|
#renderRow(meta: ThreadMeta): HTMLDivElement {
|
|
207
337
|
const row = document.createElement("div");
|
|
208
338
|
row.className = "drawer-row";
|
package/src/ui/ui_strings.ts
CHANGED
|
@@ -15,6 +15,22 @@ export interface UiStrings {
|
|
|
15
15
|
title: string;
|
|
16
16
|
/** History button + drawer dialog label. */
|
|
17
17
|
chatHistory: string;
|
|
18
|
+
/** Dismiss the chat-history list and return to the conversation. */
|
|
19
|
+
closeHistory: string;
|
|
20
|
+
/** Placeholder in the conversation-list filter. */
|
|
21
|
+
searchConversations: string;
|
|
22
|
+
/** Names the row of messages waiting for the run to finish. */
|
|
23
|
+
queued: string;
|
|
24
|
+
/** Drops one waiting message. `{text}` is the message. */
|
|
25
|
+
removeQueued: string;
|
|
26
|
+
/** Shown when a filter matches nothing, unlike having no conversations. */
|
|
27
|
+
noMatches: string;
|
|
28
|
+
/** Notice text when the agent moved its own panel out of the way. */
|
|
29
|
+
chatMoved: string;
|
|
30
|
+
/** Notice text when the agent collapsed its own panel. */
|
|
31
|
+
chatMinimised: string;
|
|
32
|
+
/** The one control a run notice may carry. */
|
|
33
|
+
undo: string;
|
|
18
34
|
/** New-chat button (header + drawer). */
|
|
19
35
|
newChat: string;
|
|
20
36
|
/** Collapse button. */
|
|
@@ -285,6 +301,14 @@ export interface UiStrings {
|
|
|
285
301
|
export const DEFAULT_UI_STRINGS: UiStrings = {
|
|
286
302
|
title: "Assistant",
|
|
287
303
|
chatHistory: "Chat history",
|
|
304
|
+
closeHistory: "Close history",
|
|
305
|
+
searchConversations: "Search conversations",
|
|
306
|
+
queued: "Waiting to send",
|
|
307
|
+
removeQueued: 'Do not send "{text}"',
|
|
308
|
+
noMatches: "No conversations match that.",
|
|
309
|
+
chatMoved: "Moved this panel out of the way",
|
|
310
|
+
chatMinimised: "Minimised this panel",
|
|
311
|
+
undo: "Undo",
|
|
288
312
|
newChat: "New chat",
|
|
289
313
|
collapse: "Collapse",
|
|
290
314
|
expand: "Expand",
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.35.0";
|