@artooi/ag-ui-web-component 0.20.0 → 0.21.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +135 -1
  2. package/README.md +286 -20
  3. package/dist/ag-ui-web-component.bundle.js +407 -382
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/core/ag_ui_chat.d.ts +82 -1
  6. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  7. package/dist/core/create_http_agent.d.ts +9 -0
  8. package/dist/core/create_http_agent.d.ts.map +1 -1
  9. package/dist/core/remote_conversation_store.d.ts +8 -1
  10. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  11. package/dist/core/run_index.d.ts +8 -1
  12. package/dist/core/run_index.d.ts.map +1 -1
  13. package/dist/core/transcribe_audio.d.ts +7 -0
  14. package/dist/core/transcribe_audio.d.ts.map +1 -1
  15. package/dist/core/upload_attachment.d.ts +9 -0
  16. package/dist/core/upload_attachment.d.ts.map +1 -1
  17. package/dist/core/utils.d.ts +12 -0
  18. package/dist/core/utils.d.ts.map +1 -0
  19. package/dist/dom/animations.d.ts +51 -11
  20. package/dist/dom/animations.d.ts.map +1 -1
  21. package/dist/dom/dom_driver.d.ts +12 -7
  22. package/dist/dom/dom_driver.d.ts.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +766 -430
  26. package/dist/index.js.map +3 -3
  27. package/dist/ui/styles.d.ts +1 -1
  28. package/dist/ui/styles.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/core/ag_ui_chat.ts +292 -31
  31. package/src/core/create_http_agent.ts +14 -3
  32. package/src/core/remote_conversation_store.ts +25 -6
  33. package/src/core/run_index.ts +27 -5
  34. package/src/core/transcribe_audio.ts +20 -5
  35. package/src/core/upload_attachment.ts +13 -0
  36. package/src/core/utils.ts +18 -0
  37. package/src/dom/animations.ts +175 -31
  38. package/src/dom/dom_driver.ts +18 -12
  39. package/src/index.ts +2 -0
  40. package/src/ui/styles.ts +377 -352
  41. package/src/version.ts +1 -1
@@ -1,3 +1,5 @@
1
+ import { withCredentials } from "./utils.js";
2
+
1
3
  /**
2
4
  * The composer's voice-transcription contract: take a recorded audio `Blob` and
3
5
  * resolve to the transcript text. The built-in handler is {@link transcribeAudio}
@@ -13,6 +15,13 @@ export interface TranscribeOptions {
13
15
  readonly url: string;
14
16
  /** Extra HTTP headers (CSRF / auth), read fresh per request. */
15
17
  readonly headers?: Record<string, string>;
18
+ /**
19
+ * Cookie policy, as `fetch`'s own `credentials` mode. Unset leaves the
20
+ * browser default (`same-origin`), which sends no cookies to an endpoint on
21
+ * another origin; a cookie-authenticated cross-origin deployment needs
22
+ * `"include"`.
23
+ */
24
+ readonly credentials?: RequestCredentials;
16
25
  }
17
26
 
18
27
  /**
@@ -29,11 +38,17 @@ export async function transcribeAudio(audio: Blob, options: TranscribeOptions):
29
38
  // reads the blob's content type).
30
39
  form.append("audio", audio, "recording.webm");
31
40
 
32
- const response = await fetch(options.url, {
33
- method: "POST",
34
- headers: { ...(options.headers ?? {}) },
35
- body: form,
36
- });
41
+ const response = await fetch(
42
+ options.url,
43
+ withCredentials(
44
+ {
45
+ method: "POST",
46
+ headers: { ...(options.headers ?? {}) },
47
+ body: form,
48
+ },
49
+ options.credentials,
50
+ ),
51
+ );
37
52
  if (!response.ok) {
38
53
  throw new Error(await errorMessage(response));
39
54
  }
@@ -26,6 +26,15 @@ export interface UploadOptions {
26
26
  readonly url: string;
27
27
  /** Extra HTTP headers (CSRF / auth), read fresh per upload. */
28
28
  readonly headers?: Record<string, string>;
29
+ /**
30
+ * Cookie policy, spelled as `fetch`'s `credentials` mode for consistency with
31
+ * the component's other endpoints — but carried by `XMLHttpRequest`, which
32
+ * only has the two-state `withCredentials`. `"include"` sets it; every other
33
+ * value leaves it off, which matches `"same-origin"`. `"omit"` therefore
34
+ * **cannot** be honoured for a same-origin upload (XHR always sends cookies
35
+ * there); use a custom {@link UploadHandler} if that matters.
36
+ */
37
+ readonly credentials?: RequestCredentials;
29
38
  /** Progress callback, `0..1`, fired as the body uploads. */
30
39
  readonly onProgress?: (fraction: number) => void;
31
40
  /** Abort signal to cancel the in-flight upload. */
@@ -48,6 +57,10 @@ export function uploadAttachment(file: File, options: UploadOptions): Promise<At
48
57
 
49
58
  const xhr = new XMLHttpRequest();
50
59
  xhr.open("POST", options.url);
60
+ // Cross-origin cookies ride only when asked for, exactly as with the fetch
61
+ // sites; without this an upload to another subdomain is anonymous and 401s
62
+ // while the run itself succeeds.
63
+ xhr.withCredentials = options.credentials === "include";
51
64
  for (const [key, value] of Object.entries(options.headers ?? {})) {
52
65
  xhr.setRequestHeader(key, value);
53
66
  }
@@ -0,0 +1,18 @@
1
+ // Non-exported-from-index helpers shared by the core transport modules.
2
+
3
+ /**
4
+ * Overlay a `credentials` mode onto a fetch `init`, or hand the `init` back
5
+ * untouched when the caller has none configured.
6
+ *
7
+ * Absent and `undefined` are not the same thing here. `exactOptionalPropertyTypes`
8
+ * rejects an explicit `credentials: undefined`, and writing one anyway would
9
+ * state a mode where the point is to leave the browser's own default in place.
10
+ * Every fetch this component makes has to make that choice, so it is made in one
11
+ * place rather than repeated (and eventually forgotten) at each call site.
12
+ */
13
+ export function withCredentials(
14
+ init: RequestInit | undefined,
15
+ credentials: RequestCredentials | undefined,
16
+ ): RequestInit | undefined {
17
+ return credentials === undefined ? init : { ...init, credentials };
18
+ }
@@ -8,14 +8,61 @@
8
8
 
9
9
  import { setNativeChecked, setNativeValue } from "./native_setter.js";
10
10
 
11
+ /** Ring colour when the host page has not themed `--ag-ui-accent`. */
11
12
  const ACCENT = "#4f46e5";
12
13
 
14
+ /**
15
+ * Fallback for the box-shadow rings, which read as a soft halo rather than a
16
+ * line and so carry their own alpha.
17
+ */
18
+ const ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
19
+
20
+ const ACCENT_PROPERTY = "--ag-ui-accent";
21
+
13
22
  function delay(ms: number): Promise<void> {
14
23
  return new Promise<void>((resolve) => {
15
24
  setTimeout(resolve, ms);
16
25
  });
17
26
  }
18
27
 
28
+ /**
29
+ * Whether the user has asked the OS/browser to minimise motion.
30
+ *
31
+ * Honoured by every primitive that *moves* something: the action animations
32
+ * skip their hold delays, `scrollIntoCenterView` jumps instead of gliding, and
33
+ * the focus flash drops its fade while still holding the ring long enough to be
34
+ * seen — reduced motion asks for no animation, not for no feedback. The
35
+ * character-typing and highlight primitives predate this and keep their
36
+ * explicit-duration contract.
37
+ */
38
+ export function prefersReducedMotion(): boolean {
39
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
40
+ }
41
+
42
+ /** Like {@link delay}, but resolves immediately under reduced motion. */
43
+ function motionDelay(ms: number): Promise<void> {
44
+ if (ms <= 0 || prefersReducedMotion()) {
45
+ return Promise.resolve();
46
+ }
47
+ return delay(ms);
48
+ }
49
+
50
+ /**
51
+ * Resolve the accent colour from the **target's** computed style.
52
+ *
53
+ * `--ag-ui-accent` inherits, so reading it off the element the agent is about
54
+ * to touch picks up whatever the host's cascade produced there — a themed page
55
+ * gets flashed in its own colour instead of this package's indigo.
56
+ */
57
+ function accentColor(el: HTMLElement, fallback: string): string {
58
+ const themed = window.getComputedStyle(el).getPropertyValue(ACCENT_PROPERTY).trim();
59
+ return themed === "" ? fallback : themed;
60
+ }
61
+
62
+ function ringShadow(el: HTMLElement): string {
63
+ return `0 0 0 3px ${accentColor(el, ACCENT_HALO)}`;
64
+ }
65
+
19
66
  /** An element whose `value` can be typed into. */
20
67
  export type TextLikeElement = HTMLInputElement | HTMLTextAreaElement;
21
68
 
@@ -59,7 +106,7 @@ export async function highlightThenClick(
59
106
  const highlightMs = options.highlightMs ?? 280;
60
107
  const previousOutline = el.style.outline;
61
108
  const previousOffset = el.style.outlineOffset;
62
- el.style.outline = `2px solid ${ACCENT}`;
109
+ el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
63
110
  el.style.outlineOffset = "2px";
64
111
  await delay(highlightMs);
65
112
  el.style.outline = previousOutline;
@@ -67,46 +114,143 @@ export async function highlightThenClick(
67
114
  el.click();
68
115
  }
69
116
 
70
- /** Scroll ``el`` to the vertical centre of the viewport. */
71
- export function scrollIntoCenterView(el: HTMLElement): void {
72
- el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
117
+ export interface ScrollOptions {
118
+ /**
119
+ * How long to wait for a moving scroll to settle before giving up.
120
+ * Default 600.
121
+ */
122
+ settleMs?: number;
73
123
  }
74
124
 
125
+ const SCROLL_SETTLE_MS = 600;
126
+
127
+ /** How long to give a smooth scroll to start moving before assuming it will not. */
128
+ const SCROLL_START_MS = 100;
129
+
130
+ /**
131
+ * Scroll ``el`` to the vertical centre of the viewport.
132
+ *
133
+ * The returned promise resolves once the scroll has settled, so a caller can
134
+ * hold its animation until the element has stopped moving — a ring drawn
135
+ * mid-glide lands somewhere the user is not looking yet. Awaiting is optional;
136
+ * the scroll itself is requested synchronously.
137
+ */
138
+ export function scrollIntoCenterView(el: HTMLElement, options: ScrollOptions = {}): Promise<void> {
139
+ const reduced = prefersReducedMotion();
140
+ el.scrollIntoView({
141
+ block: "center",
142
+ inline: "nearest",
143
+ behavior: reduced ? "auto" : "smooth",
144
+ });
145
+ if (reduced) {
146
+ return Promise.resolve();
147
+ }
148
+ return new Promise<void>((resolve) => {
149
+ let timer: ReturnType<typeof setTimeout>;
150
+ const settle = (): void => {
151
+ clearTimeout(timer);
152
+ document.removeEventListener("scroll", started, true);
153
+ document.removeEventListener("scrollend", settle, true);
154
+ resolve();
155
+ };
156
+ const started = (): void => {
157
+ document.removeEventListener("scroll", started, true);
158
+ clearTimeout(timer);
159
+ timer = setTimeout(settle, options.settleMs ?? SCROLL_SETTLE_MS);
160
+ };
161
+ // Neither event bubbles, and both fire on whichever ancestor actually
162
+ // scrolled, so listen on the document in the capture phase.
163
+ //
164
+ // Two timeouts, because two things can go wrong. An element already in
165
+ // view never scrolls and so never fires scrollend — waiting the full
166
+ // budget for it would add dead time to every action, hence the short
167
+ // did-anything-move probe. Once something is moving, the longer budget
168
+ // covers browsers that do not implement scrollend at all (Safari gained it
169
+ // only recently).
170
+ timer = setTimeout(settle, SCROLL_START_MS);
171
+ document.addEventListener("scroll", started, true);
172
+ document.addEventListener("scrollend", settle, true);
173
+ });
174
+ }
175
+
176
+ /** Total on-screen life of the flash ring, hold plus fade. */
177
+ const FLASH_MS = 1200;
178
+
179
+ /** Share of {@link FLASH_MS} spent fading out rather than holding. */
180
+ const FLASH_FADE_RATIO = 1 / 3;
181
+
75
182
  export interface FlashOptions {
76
- /** Milliseconds to hold the focus flash. Default 200. */
183
+ /**
184
+ * Total milliseconds the ring is on screen, hold plus fade. Default 1200 —
185
+ * long enough for someone who does not yet know where to look to find it.
186
+ */
77
187
  flashMs?: number;
188
+ /** Ring colour. Defaults to the target's `--ag-ui-accent`, else the package accent. */
189
+ color?: string;
190
+ /**
191
+ * Move keyboard focus to the element as well. Defaults to false for
192
+ * {@link flash} and true for {@link focusWithFlash}.
193
+ */
194
+ focus?: boolean;
78
195
  }
79
196
 
80
- /** Focus ``el`` and briefly flash a ring around it. */
81
- export async function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Promise<void> {
82
- const flashMs = options.flashMs ?? 200;
83
- el.focus();
84
- const previousShadow = el.style.boxShadow;
85
- el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
86
- await delay(flashMs);
87
- el.style.boxShadow = previousShadow;
197
+ async function flashRing(
198
+ el: HTMLElement,
199
+ options: FlashOptions,
200
+ focusByDefault: boolean,
201
+ ): Promise<void> {
202
+ if (options.focus ?? focusByDefault) {
203
+ // preventScroll: a caller's smooth scroll is typically still in flight, and
204
+ // the instant scroll focus() would otherwise do fights it.
205
+ el.focus({ preventScroll: true });
206
+ }
207
+ const flashMs = options.flashMs ?? FLASH_MS;
208
+ if (flashMs <= 0) {
209
+ return;
210
+ }
211
+ const previousOutline = el.style.outline;
212
+ const previousOffset = el.style.outlineOffset;
213
+ const previousTransition = el.style.transition;
214
+ const color = options.color ?? accentColor(el, ACCENT);
215
+ // outline, not box-shadow: a shadow paints outside the border box, so any
216
+ // overflow:hidden ancestor sharing the target's box (a card, a table cell)
217
+ // clips the whole ring away while the helper still reports success.
218
+ el.style.outline = `3px solid ${color}`;
219
+ el.style.outlineOffset = "2px";
220
+ const fadeMs = prefersReducedMotion() ? 0 : Math.round(flashMs * FLASH_FADE_RATIO);
221
+ await delay(flashMs - fadeMs);
222
+ if (fadeMs > 0) {
223
+ // Setting the transition and the new colour in the same task is fine: the
224
+ // browser starts transitions from the after-change style.
225
+ el.style.transition = `outline-color ${fadeMs}ms ease-out`;
226
+ el.style.outline = "3px solid transparent";
227
+ await delay(fadeMs);
228
+ }
229
+ el.style.outline = previousOutline;
230
+ el.style.outlineOffset = previousOffset;
231
+ el.style.transition = previousTransition;
88
232
  }
89
233
 
90
- const RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
91
-
92
234
  /**
93
- * Whether the user has asked the OS/browser to minimise motion.
235
+ * Flash a ring around ``el`` without touching focus.
94
236
  *
95
- * The richer action animations below check this and skip their hold delays so
96
- * the agent's edits still happen (events fire, values set) but without the
97
- * visible pause. The character-typing / highlight primitives above predate this
98
- * and keep their explicit-duration contract.
237
+ * Prefer this over {@link focusWithFlash} when the point is to *show* the user
238
+ * something: moving focus steals it from the composer, can fire blur validation
239
+ * on whatever they were mid-edit in, and can close an open menu.
99
240
  */
100
- export function prefersReducedMotion(): boolean {
101
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
241
+ export function flash(el: HTMLElement, options: FlashOptions = {}): Promise<void> {
242
+ return flashRing(el, options, false);
102
243
  }
103
244
 
104
- /** Like {@link delay}, but resolves immediately under reduced motion. */
105
- function motionDelay(ms: number): Promise<void> {
106
- if (ms <= 0 || prefersReducedMotion()) {
107
- return Promise.resolve();
108
- }
109
- return delay(ms);
245
+ /**
246
+ * Focus ``el`` and flash a ring around it.
247
+ *
248
+ * Focus moves unless ``options.focus`` says otherwise — that is the documented
249
+ * behaviour of this name. Use {@link flash} for a highlight that leaves focus
250
+ * where the user put it.
251
+ */
252
+ export function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Promise<void> {
253
+ return flashRing(el, options, true);
110
254
  }
111
255
 
112
256
  export interface PressOptions {
@@ -126,7 +270,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
126
270
  const previousShadow = el.style.boxShadow;
127
271
  el.style.transition = "transform 80ms ease";
128
272
  el.style.transform = "scale(0.96)";
129
- el.style.boxShadow = RING;
273
+ el.style.boxShadow = ringShadow(el);
130
274
  await motionDelay(pressMs);
131
275
  el.style.transform = previousTransform;
132
276
  el.style.transition = previousTransition;
@@ -166,7 +310,7 @@ export async function selectOption(
166
310
  const highlightMs = options.highlightMs ?? 220;
167
311
  const previousOutline = el.style.outline;
168
312
  const previousOffset = el.style.outlineOffset;
169
- el.style.outline = `2px solid ${ACCENT}`;
313
+ el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
170
314
  el.style.outlineOffset = "2px";
171
315
  await motionDelay(highlightMs);
172
316
  setNativeValue(el, option.value);
@@ -192,7 +336,7 @@ export async function toggleControl(
192
336
  ): Promise<void> {
193
337
  const flashMs = options.flashMs ?? 200;
194
338
  const previousShadow = el.style.boxShadow;
195
- el.style.boxShadow = RING;
339
+ el.style.boxShadow = ringShadow(el);
196
340
  await motionDelay(flashMs);
197
341
  setNativeChecked(el, checked);
198
342
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type FlashOptions,
2
3
  focusWithFlash,
3
4
  type HighlightClickOptions,
4
5
  highlightThenClick,
@@ -23,20 +24,25 @@ import { setNativeChecked, setNativeValue } from "./native_setter.js";
23
24
  * `fill_field(name, value)` finds `#id_<name>` then calls {@link fillField}.
24
25
  */
25
26
 
26
- export interface FillFieldOptions extends TypeOptions, FlashOptionsLike {}
27
-
28
- interface FlashOptionsLike {
29
- flashMs?: number;
30
- }
27
+ /**
28
+ * A field has to hold focus to be typed into, so ``focus`` is not on offer
29
+ * here — everything else the flash takes is.
30
+ */
31
+ export interface FillFieldOptions extends TypeOptions, Omit<FlashOptions, "focus"> {}
31
32
 
32
- /** Scroll to, focus (with a flash), and type ``value`` into a text field. */
33
+ /**
34
+ * Scroll to, focus (with a flash), and type ``value`` into a text field.
35
+ *
36
+ * The scroll is awaited: typing into an element that is still gliding past
37
+ * shows the user nothing.
38
+ */
33
39
  export async function fillField(
34
40
  el: TextLikeElement,
35
41
  value: string,
36
42
  options: FillFieldOptions = {},
37
43
  ): Promise<void> {
38
- scrollIntoCenterView(el);
39
- await focusWithFlash(el, { flashMs: options.flashMs ?? 0 });
44
+ await scrollIntoCenterView(el);
45
+ await focusWithFlash(el, { ...options, flashMs: options.flashMs ?? 0 });
40
46
  await typeInto(el, value, options);
41
47
  }
42
48
 
@@ -45,13 +51,13 @@ export async function clickElement(
45
51
  el: HTMLElement,
46
52
  options: HighlightClickOptions = {},
47
53
  ): Promise<void> {
48
- scrollIntoCenterView(el);
54
+ await scrollIntoCenterView(el);
49
55
  await highlightThenClick(el, options);
50
56
  }
51
57
 
52
58
  /** Scroll to a button/control and click it with a visible "press" animation. */
53
59
  export async function pressButton(el: HTMLElement, options: PressOptions = {}): Promise<void> {
54
- scrollIntoCenterView(el);
60
+ await scrollIntoCenterView(el);
55
61
  await pressThenClick(el, options);
56
62
  }
57
63
 
@@ -61,7 +67,7 @@ export async function selectControl(
61
67
  value: string,
62
68
  options: SelectOptions = {},
63
69
  ): Promise<void> {
64
- scrollIntoCenterView(el);
70
+ await scrollIntoCenterView(el);
65
71
  await selectOption(el, value, options);
66
72
  }
67
73
 
@@ -71,7 +77,7 @@ export async function toggleCheckbox(
71
77
  checked: boolean,
72
78
  options: ToggleOptions = {},
73
79
  ): Promise<void> {
74
- scrollIntoCenterView(el);
80
+ await scrollIntoCenterView(el);
75
81
  await toggleControl(el, checked, options);
76
82
  }
77
83
 
package/src/index.ts CHANGED
@@ -64,12 +64,14 @@ export {
64
64
  } from "./core/upload_attachment.js";
65
65
  export {
66
66
  type FlashOptions,
67
+ flash,
67
68
  focusWithFlash,
68
69
  type HighlightClickOptions,
69
70
  highlightThenClick,
70
71
  type PressOptions,
71
72
  prefersReducedMotion,
72
73
  pressThenClick,
74
+ type ScrollOptions,
73
75
  type SelectOptions,
74
76
  scrollIntoCenterView,
75
77
  selectOption,