@alexkroman1/aai-ui 13.3.0 → 15.0.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 (82) hide show
  1. package/README.md +159 -69
  2. package/dist/{_colors-CZ6OlPbL.js → _colors-CpZO-88A.js} +24 -1
  3. package/dist/_recover-run.d.ts +2 -2
  4. package/dist/_submission-state.d.ts +92 -0
  5. package/dist/_upload-files.d.ts +2 -2
  6. package/dist/_upload-report.d.ts +26 -0
  7. package/dist/{_utils-CyzjK0gW.js → _utils-DnQDM9Uy.js} +3 -3
  8. package/dist/_utils.d.ts +3 -3
  9. package/dist/_web-storage.d.ts +43 -0
  10. package/dist/_workflow-files.d.ts +1 -1
  11. package/dist/agent-state-labels.d.ts +60 -0
  12. package/dist/audio.js +20 -14
  13. package/dist/{chat-view-Bv5VFJIE.js → chat-view-C_3T7Ln8.js} +96 -29
  14. package/dist/{client-config-DD820zHn.js → client-config-DJQHnYjm.js} +4 -4
  15. package/dist/client-config.d.ts +4 -4
  16. package/dist/client-dir.d.ts +1 -1
  17. package/dist/client-dir.js +1 -1
  18. package/dist/components/_colors.d.ts +23 -0
  19. package/dist/components/_form-readiness.d.ts +1 -1
  20. package/dist/components/bullet-list.d.ts +74 -0
  21. package/dist/components/button.js +3 -3
  22. package/dist/components/chat-view.js +1 -1
  23. package/dist/components/console-shell.d.ts +16 -20
  24. package/dist/components/controls.js +3 -3
  25. package/dist/components/facts.d.ts +81 -0
  26. package/dist/components/form-fields.d.ts +6 -6
  27. package/dist/components/form-types.d.ts +1 -1
  28. package/dist/components/form.d.ts +1 -1
  29. package/dist/components/message-list.js +1 -1
  30. package/dist/components/session-error-banner.d.ts +69 -0
  31. package/dist/components/start-screen.js +1 -1
  32. package/dist/components/tool-call-block.js +1 -1
  33. package/dist/components/tool-config-context.d.ts +1 -1
  34. package/dist/components/workflow-progress.d.ts +11 -4
  35. package/dist/context.d.ts +142 -19
  36. package/dist/context.js +155 -17
  37. package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
  38. package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
  39. package/dist/default-client/assets/index-BowRD-xq.js +284 -0
  40. package/dist/default-client/index.html +2 -2
  41. package/dist/define-client.d.ts +19 -19
  42. package/dist/define-client.js +19 -19
  43. package/dist/hooks.d.ts +44 -8
  44. package/dist/hooks.js +19 -13
  45. package/dist/index.d.ts +14 -7
  46. package/dist/index.js +551 -231
  47. package/dist/internal.d.ts +2 -2
  48. package/dist/internal.js +5 -5
  49. package/dist/{message-list-C0pL7x41.js → message-list-CdOnSh5m.js} +19 -12
  50. package/dist/page.d.ts +11 -11
  51. package/dist/session-core-audio-setup.d.ts +1 -1
  52. package/dist/session-core-dial.d.ts +0 -2
  53. package/dist/{session-core-C9elBIdu.js → session-core-gwePM95B.js} +125 -52
  54. package/dist/session-core-messages.d.ts +2 -2
  55. package/dist/session-core-types.d.ts +58 -1
  56. package/dist/session-core.d.ts +6 -6
  57. package/dist/session-core.js +2 -2
  58. package/dist/session-resume-store.d.ts +3 -3
  59. package/dist/{tool-call-block-Bunc6rCw.js → tool-call-block-C2t_5fpp.js} +27 -9
  60. package/dist/{tool-config-context-Bh8p3DtG.js → tool-config-context-Es4YUzV2.js} +1 -1
  61. package/dist/types.d.ts +19 -4
  62. package/dist/types.js +2 -2
  63. package/dist/url-chips-D62RTz7v.js +239 -0
  64. package/dist/use-conversation.d.ts +1 -1
  65. package/dist/use-copy.d.ts +74 -0
  66. package/dist/use-download-url.d.ts +2 -2
  67. package/dist/use-flash.d.ts +61 -0
  68. package/dist/{use-user-transcript-DFTSEuZN.js → use-user-transcript-uyHhzy4d.js} +3 -2
  69. package/dist/use-workflow-form.d.ts +35 -3
  70. package/dist/use-workflow-progress.d.ts +1 -1
  71. package/dist/{use-workflow-run-CXGEcM0l.js → use-workflow-run-CNahvlgL.js} +12 -15
  72. package/dist/use-workflow-run.d.ts +2 -2
  73. package/dist/use-workflow-runs.d.ts +1 -1
  74. package/dist/use-workflow-stream.d.ts +2 -2
  75. package/dist/use-workflows.d.ts +2 -2
  76. package/dist/workflow-client.d.ts +3 -3
  77. package/dist/workflow-field-kind.d.ts +95 -0
  78. package/package.json +2 -2
  79. package/styles.css +78 -0
  80. package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
  81. package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
  82. package/dist/url-chips-C2u7QPv8.js +0 -100
@@ -1,8 +1,8 @@
1
1
  import { useTheme } from "./context.js";
2
- import { r as inkTint } from "./_colors-CZ6OlPbL.js";
2
+ import { a as inkTint } from "./_colors-CpZO-88A.js";
3
3
  import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
4
- import { i as tryParseJSON, r as truncate } from "./_utils-CyzjK0gW.js";
5
- import { n as useToolConfig } from "./tool-config-context-Bh8p3DtG.js";
4
+ import { i as tryParseJSON, r as truncate } from "./_utils-DnQDM9Uy.js";
5
+ import { n as useToolConfig } from "./tool-config-context-Es4YUzV2.js";
6
6
  import clsx from "clsx";
7
7
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
8
  import { memo, useMemo, useState } from "react";
@@ -123,6 +123,29 @@ function formatResult(result) {
123
123
  return parsed === result ? result : JSON.stringify(parsed, null, 2);
124
124
  }
125
125
  /**
126
+ * The expanded result pane.
127
+ *
128
+ * Its own component so the parse-and-pretty-print happens only when the row is
129
+ * actually OPEN. `ToolCallRow` renders `children` under `isOpen && canExpand`,
130
+ * and a row starts closed — so computing this in the parent meant every row
131
+ * paid `JSON.parse` + `JSON.stringify(_, null, 2)` over its whole result, and
132
+ * then RETAINED the pretty string, for a panel nobody clicked. The burst case
133
+ * is `history.restored`, which mounts up to `DEFAULT_MAX_HISTORY` rows in one
134
+ * commit.
135
+ *
136
+ * Creating this element is free; React only runs the body once it is mounted.
137
+ */
138
+ function ToolCallResult({ result }) {
139
+ const theme = useTheme();
140
+ const formatted = useMemo(() => formatResult(result), [result]);
141
+ if (!formatted) return null;
142
+ return /* @__PURE__ */ jsx("pre", {
143
+ className: "font-aai-mono text-xs px-3.5 py-3 m-0 whitespace-pre-wrap wrap-break-word",
144
+ style: { color: inkTint(theme.text, theme.surface, 75) },
145
+ children: formatted
146
+ });
147
+ }
148
+ /**
126
149
  * Renders a tool invocation as the design system's console row (see
127
150
  * `ToolCallRow`): a small outlined "TOOL" chip (or the tool's configured
128
151
  * icon), the tool name in mono, a truncated args preview, and a rotating
@@ -151,7 +174,6 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
151
174
  const title = config?.label || toolCall.name;
152
175
  const icon = config?.icon;
153
176
  const canExpand = !isPending && Boolean(toolCall.result);
154
- const formatted = useMemo(() => toolCall.result ? formatResult(toolCall.result) : "", [toolCall.result]);
155
177
  const subtitle = useMemo(() => {
156
178
  const args = toolCall.args;
157
179
  if (toolCall.name === "run_code" && args.code) return truncate(String(args.code).split("\n")[0] ?? "");
@@ -175,11 +197,7 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
175
197
  borderColor: theme.border
176
198
  },
177
199
  children: String(toolCall.args.code)
178
- }), formatted && /* @__PURE__ */ jsx("pre", {
179
- className: "font-aai-mono text-xs px-3.5 py-3 m-0 whitespace-pre-wrap wrap-break-word",
180
- style: { color: inkTint(theme.text, theme.surface, 75) },
181
- children: formatted
182
- })] }) : void 0
200
+ }), toolCall.result && /* @__PURE__ */ jsx(ToolCallResult, { result: toolCall.result })] }) : void 0
183
201
  });
184
202
  });
185
203
  //#endregion
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext } from "react";
2
2
  //#region src/components/tool-config-context.ts
3
3
  /**
4
- * Context for tool display configuration. Installed by `client()` from
4
+ * Context for tool display configuration. Installed by `mountClient()` from
5
5
  * `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
6
6
  *
7
7
  * @internal
package/dist/types.d.ts CHANGED
@@ -23,8 +23,8 @@ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
23
23
  *
24
24
  * On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
25
25
  * reason as the audio budgets above: it is a framework decision with no
26
- * `client()` field to set, and the root is the authoring surface. A custom
27
- * chrome that bypasses `client()` and opens its own microphone reaches it there
26
+ * `mountClient()` field to set, and the root is the authoring surface. A custom
27
+ * chrome that bypasses `mountClient()` and opens its own microphone reaches it there
28
28
  * alongside the providers it also needs.
29
29
  */
30
30
  export declare const VOICE_CAPTURE_CONSTRAINTS: MediaTrackConstraints;
@@ -135,11 +135,26 @@ export type SessionError = {
135
135
  readonly code: SessionErrorCode;
136
136
  /** A human-readable description of the error. */
137
137
  readonly message: string;
138
+ /**
139
+ * Whether the session is OVER.
140
+ *
141
+ * `false` means surface the message and keep the session interactive — a
142
+ * turn-level failure over a server that kept running. `true` means the call
143
+ * is dead and the microphone has been released.
144
+ *
145
+ * Required rather than optional, because the wire always carries it
146
+ * (`error.reported` declares `fatal: z.boolean()`) and a client that cannot
147
+ * tell the two apart has to guess which banner to render. It was dropped one
148
+ * line before reaching here for long enough that this type's own doc, and
149
+ * the reference page generated from it, described a field that did not
150
+ * exist.
151
+ */
152
+ readonly fatal: boolean;
138
153
  };
139
154
  /**
140
155
  * Options for creating a voice session — the shared field set accepted by
141
- * both `client()` and `createSessionCore`. The one difference: `client()`
142
- * defaults `platformUrl` from `location.href`, while `createSessionCore`
156
+ * both `mountClient()` and `createBrowserSession`. The one difference: `mountClient()`
157
+ * defaults `platformUrl` from `location.href`, while `createBrowserSession`
143
158
  * requires it.
144
159
  *
145
160
  * @public
package/dist/types.js CHANGED
@@ -22,8 +22,8 @@ import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
22
22
  *
23
23
  * On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
24
24
  * reason as the audio budgets above: it is a framework decision with no
25
- * `client()` field to set, and the root is the authoring surface. A custom
26
- * chrome that bypasses `client()` and opens its own microphone reaches it there
25
+ * `mountClient()` field to set, and the root is the authoring surface. A custom
26
+ * chrome that bypasses `mountClient()` and opens its own microphone reaches it there
27
27
  * alongside the providers it also needs.
28
28
  */
29
29
  const VOICE_CAPTURE_CONSTRAINTS = {
@@ -0,0 +1,239 @@
1
+ import { useSessionSelector, useTheme } from "./context.js";
2
+ import { a as inkTint, i as focusRingStyle, n as FOCUS_RING } from "./_colors-CpZO-88A.js";
3
+ import { t as pageBaseUrl } from "./_utils-DnQDM9Uy.js";
4
+ import clsx from "clsx";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useEffect, useRef, useState } from "react";
7
+ //#region src/use-flash.ts
8
+ /**
9
+ * A value that shows itself for a moment and then goes away.
10
+ *
11
+ * The "Copied" on a copy button, the "Saved" under an editor, the "Sent" on a
12
+ * submit. There were three hand-rolled copies of this — two in the studio
13
+ * front-end and one inside this package's own URL chips — each with the same
14
+ * 1500ms and the same comment explaining the same two things the naive version
15
+ * gets wrong. It is one primitive now, and it is PUBLIC because a custom chrome
16
+ * is exactly the caller that needs it: the moment a client renders its own copy
17
+ * button or its own save note, it is writing the timer again.
18
+ */
19
+ /** How long a flashed value stays up. The one duration every caller used. */
20
+ const FLASH_MS = 1500;
21
+ /**
22
+ * A transient value: set it, and it clears itself after `ms`.
23
+ *
24
+ * @remarks
25
+ * The two things this holds that a `useState` plus a `setTimeout` at the call
26
+ * site reliably gets wrong:
27
+ *
28
+ * - **At most ONE live timer.** A second flash while the first is still up
29
+ * re-arms rather than stacking, so the new value gets its full window
30
+ * instead of being cleared early by the previous click's timeout.
31
+ * - **Nothing fires after unmount.** The timer is cleared on teardown, so a
32
+ * chip clicked and then scrolled out of the tree does not `setState` on a
33
+ * component React has already thrown away.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * import { useFlash } from "@alexkroman1/aai-ui";
38
+ *
39
+ * function SaveNote({ onSave }: { onSave: () => Promise<void> }) {
40
+ * const { value: note, flash } = useFlash<string>();
41
+ * return (
42
+ * <button type="button" onClick={() => void onSave().then(() => flash("Saved"))}>
43
+ * {note ?? "Save"}
44
+ * </button>
45
+ * );
46
+ * }
47
+ * ```
48
+ *
49
+ * @typeParam T - What is being flashed.
50
+ * @param ms - How long the value stays up. Defaults to 1500ms, which is what
51
+ * every caller wanted: long enough to read a word, short enough that the
52
+ * control is back to its real label before the reader looks again.
53
+ *
54
+ * @public
55
+ */
56
+ function useFlash(ms = FLASH_MS) {
57
+ const [value, setValue] = useState(null);
58
+ const timer = useRef(void 0);
59
+ useEffect(() => () => clearTimeout(timer.current), []);
60
+ const flash = (next) => {
61
+ setValue(next);
62
+ clearTimeout(timer.current);
63
+ timer.current = setTimeout(() => setValue(null), ms);
64
+ };
65
+ return {
66
+ value,
67
+ flash
68
+ };
69
+ }
70
+ //#endregion
71
+ //#region src/use-copy.ts
72
+ /**
73
+ * Copy-to-clipboard with a per-item flash on the button that triggered it.
74
+ *
75
+ * Anywhere a UI hands the reader a string to paste somewhere else — a session
76
+ * URL, a webhook, a `curl` line — the button wants the same three things, and
77
+ * two of them are easy to get wrong:
78
+ *
79
+ * - The flash is keyed by the copied TEXT, so one row's "Copied" does not light
80
+ * up every button beside it.
81
+ * - A FAILED write is reported rather than swallowed. There is no clipboard in
82
+ * an insecure context, and a browser may refuse the permission; a `catch`
83
+ * that does nothing leaves a button that visibly does nothing when clicked,
84
+ * which reads as a broken page rather than as a blocked capability.
85
+ * - The single-timer / clear-on-unmount half is {@link useFlash}'s.
86
+ */
87
+ /**
88
+ * One copier for a group of copy buttons.
89
+ *
90
+ * @remarks
91
+ * Call it ONCE per group and pass the {@link UseCopyResult} down, rather than once per
92
+ * button: the flash is shared, so clicking a second row clears the first row's
93
+ * "Copied" — which is what makes a list of URLs readable, since two rows
94
+ * claiming to be on the clipboard is a lie about one of them.
95
+ *
96
+ * A chip whose idle text is its own name rather than the word "Copy" passes
97
+ * that name to {@link UseCopyResult.label}; the two outcome words are fixed, for the
98
+ * reason that member's own doc gives.
99
+ *
100
+ * @example
101
+ * ```tsx
102
+ * import { useCopy } from "@alexkroman1/aai-ui";
103
+ *
104
+ * function UrlList({ urls }: { urls: readonly string[] }) {
105
+ * const copier = useCopy();
106
+ * return (
107
+ * <ul>
108
+ * {urls.map((url) => (
109
+ * <li key={url}>
110
+ * <code>{url}</code>
111
+ * <button type="button" onClick={() => copier.copy(url)}>
112
+ * {copier.label(url)}
113
+ * </button>
114
+ * </li>
115
+ * ))}
116
+ * </ul>
117
+ * );
118
+ * }
119
+ * ```
120
+ *
121
+ * @public
122
+ */
123
+ function useCopy() {
124
+ const { value: copied, flash } = useFlash();
125
+ const copy = (text) => {
126
+ const write = navigator.clipboard?.writeText(text);
127
+ if (!write) {
128
+ flash({
129
+ text,
130
+ ok: false
131
+ });
132
+ return;
133
+ }
134
+ write.then(() => flash({
135
+ text,
136
+ ok: true
137
+ }), () => flash({
138
+ text,
139
+ ok: false
140
+ }));
141
+ };
142
+ const label = (text, idle = "Copy") => {
143
+ if (copied?.text !== text) return idle;
144
+ return copied.ok ? "Copied" : "Failed";
145
+ };
146
+ return {
147
+ copy,
148
+ label,
149
+ didCopy: (text) => copied?.text === text && copied.ok
150
+ };
151
+ }
152
+ //#endregion
153
+ //#region src/components/url-chips.tsx
154
+ /** @jsxImportSource react */
155
+ /**
156
+ * A compact labeled chip showing a URL. Click to copy.
157
+ *
158
+ * The label is what makes a pair of these readable — on its own a bare URL
159
+ * leaves you guessing whether it's the page or the socket. It also carries the
160
+ * copy OUTCOME, which is the whole reason the flash is `useCopy`'s rather than
161
+ * this file's: the hand-rolled version swallowed a rejected write, so on an
162
+ * insecure context or a denied permission the chip changed nothing at all and
163
+ * read as a dead button.
164
+ *
165
+ * @internal
166
+ */
167
+ function UrlChip({ label, url, hint, testId, className }) {
168
+ const theme = useTheme();
169
+ const copier = useCopy();
170
+ return /* @__PURE__ */ jsxs("button", {
171
+ type: "button",
172
+ onClick: () => copier.copy(url),
173
+ title: `${hint} (click to copy)\n${url}`,
174
+ className: clsx("flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer text-[11px] leading-none font-aai-mono", FOCUS_RING, className),
175
+ style: {
176
+ background: inkTint(theme.text, theme.surface, 3),
177
+ borderColor: theme.border,
178
+ color: inkTint(theme.text, theme.surface, 65),
179
+ ...focusRingStyle(theme.primary)
180
+ },
181
+ "data-testid": testId,
182
+ children: [/* @__PURE__ */ jsx("span", {
183
+ className: "uppercase tracking-wide shrink-0",
184
+ style: { color: inkTint(theme.text, theme.surface, 75) },
185
+ children: copier.label(url, label)
186
+ }), /* @__PURE__ */ jsx("span", {
187
+ className: "truncate",
188
+ "data-testid": `${testId}-url`,
189
+ children: url
190
+ })]
191
+ });
192
+ }
193
+ /**
194
+ * The session's shareable UI URL — the page this UI is served from, what
195
+ * you'd send someone to talk to the agent.
196
+ *
197
+ * @internal
198
+ */
199
+ function UiUrlChip({ className }) {
200
+ return /* @__PURE__ */ jsx(UrlChip, {
201
+ label: "UI",
202
+ url: pageBaseUrl(),
203
+ hint: "Shareable UI",
204
+ testId: "ui-url-chip",
205
+ className
206
+ });
207
+ }
208
+ /**
209
+ * The session's programmatic WebSocket endpoint — the URL a script or backend
210
+ * can connect to directly instead of using this UI.
211
+ *
212
+ * @internal
213
+ */
214
+ function ApiUrlChip({ className }) {
215
+ const apiUrl = useSessionSelector((s) => s.apiUrl);
216
+ return /* @__PURE__ */ jsx(UrlChip, {
217
+ label: "API",
218
+ url: apiUrl,
219
+ hint: "API endpoint",
220
+ testId: "api-url-chip",
221
+ className
222
+ });
223
+ }
224
+ /**
225
+ * The UI and API URLs side by side. They answer the same question — "how do I
226
+ * reach this agent?" — so they belong together and each needs its label to be
227
+ * told apart. Rendered by the default shell in every session mode (S2S,
228
+ * pipeline).
229
+ *
230
+ * @internal
231
+ */
232
+ function SessionUrlChips({ className }) {
233
+ return /* @__PURE__ */ jsxs("div", {
234
+ className: clsx("flex items-center gap-1.5 min-w-0", className),
235
+ children: [/* @__PURE__ */ jsx(UiUrlChip, { className: "min-w-0 flex-1" }), /* @__PURE__ */ jsx(ApiUrlChip, { className: "min-w-0 flex-1" })]
236
+ });
237
+ }
238
+ //#endregion
239
+ export { useFlash as a, useCopy as i, SessionUrlChips as n, UiUrlChip as r, ApiUrlChip as t };
@@ -88,7 +88,7 @@ export type UseConversationResult = {
88
88
  * Subscribe to the conversation: the interleaved exchange, the streaming
89
89
  * utterance, the live transcript and the thinking rule — with no markup.
90
90
  *
91
- * Must be used inside the provider `client()` installs.
91
+ * Must be used inside the provider `mountClient()` installs.
92
92
  *
93
93
  * @example A custom bubble, keeping every rule `<MessageList>` knows
94
94
  * ```tsx
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Copy-to-clipboard with a per-item flash on the button that triggered it.
3
+ *
4
+ * Anywhere a UI hands the reader a string to paste somewhere else — a session
5
+ * URL, a webhook, a `curl` line — the button wants the same three things, and
6
+ * two of them are easy to get wrong:
7
+ *
8
+ * - The flash is keyed by the copied TEXT, so one row's "Copied" does not light
9
+ * up every button beside it.
10
+ * - A FAILED write is reported rather than swallowed. There is no clipboard in
11
+ * an insecure context, and a browser may refuse the permission; a `catch`
12
+ * that does nothing leaves a button that visibly does nothing when clicked,
13
+ * which reads as a broken page rather than as a blocked capability.
14
+ * - The single-timer / clear-on-unmount half is {@link useFlash}'s.
15
+ */
16
+ /**
17
+ * What {@link useCopy} hands back — the click handler and the two readings a
18
+ * button needs off one shared flash.
19
+ *
20
+ * @public
21
+ */
22
+ export type UseCopyResult = {
23
+ /** Copy `text`, flashing the button that owns it. */
24
+ copy: (text: string) => void;
25
+ /**
26
+ * The button label for `text` — `idle` until it is clicked, then `"Copied"`
27
+ * or `"Failed"` for the length of the flash.
28
+ *
29
+ * Only the idle word is the caller's: it is the button's NAME (`"Copy"`,
30
+ * `"UI"`, `"Webhook URL"`), where the other two are the OUTCOME and are the
31
+ * one bit of state a reader can see. A caller wanting other words for those
32
+ * reads {@link UseCopyResult.didCopy} and writes its own.
33
+ */
34
+ label: (text: string, idle?: string) => string;
35
+ /** True when `text` was the last thing copied, successfully. */
36
+ didCopy: (text: string) => boolean;
37
+ };
38
+ /**
39
+ * One copier for a group of copy buttons.
40
+ *
41
+ * @remarks
42
+ * Call it ONCE per group and pass the {@link UseCopyResult} down, rather than once per
43
+ * button: the flash is shared, so clicking a second row clears the first row's
44
+ * "Copied" — which is what makes a list of URLs readable, since two rows
45
+ * claiming to be on the clipboard is a lie about one of them.
46
+ *
47
+ * A chip whose idle text is its own name rather than the word "Copy" passes
48
+ * that name to {@link UseCopyResult.label}; the two outcome words are fixed, for the
49
+ * reason that member's own doc gives.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * import { useCopy } from "@alexkroman1/aai-ui";
54
+ *
55
+ * function UrlList({ urls }: { urls: readonly string[] }) {
56
+ * const copier = useCopy();
57
+ * return (
58
+ * <ul>
59
+ * {urls.map((url) => (
60
+ * <li key={url}>
61
+ * <code>{url}</code>
62
+ * <button type="button" onClick={() => copier.copy(url)}>
63
+ * {copier.label(url)}
64
+ * </button>
65
+ * </li>
66
+ * ))}
67
+ * </ul>
68
+ * );
69
+ * }
70
+ * ```
71
+ *
72
+ * @public
73
+ */
74
+ export declare function useCopy(): UseCopyResult;
@@ -57,7 +57,7 @@ export type UseDownloadUrlResult = {
57
57
  * @param uploadId - The id a completed run reported, or `undefined` before one
58
58
  * exists — which is what a page passes straight through while it waits, and
59
59
  * reports as idle rather than pending.
60
- * @param opts - See {@link UseDownloadUrlOptions}.
60
+ * @param options - See {@link UseDownloadUrlOptions}.
61
61
  * @returns See {@link UseDownloadUrlResult}.
62
62
  *
63
63
  * @example
@@ -81,4 +81,4 @@ export type UseDownloadUrlResult = {
81
81
  *
82
82
  * @public
83
83
  */
84
- export declare function useDownloadUrl(uploadId: string | undefined, opts?: UseDownloadUrlOptions): UseDownloadUrlResult;
84
+ export declare function useDownloadUrl(uploadId: string | undefined, options?: UseDownloadUrlOptions): UseDownloadUrlResult;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A value that shows itself for a moment and then goes away.
3
+ *
4
+ * The "Copied" on a copy button, the "Saved" under an editor, the "Sent" on a
5
+ * submit. There were three hand-rolled copies of this — two in the studio
6
+ * front-end and one inside this package's own URL chips — each with the same
7
+ * 1500ms and the same comment explaining the same two things the naive version
8
+ * gets wrong. It is one primitive now, and it is PUBLIC because a custom chrome
9
+ * is exactly the caller that needs it: the moment a client renders its own copy
10
+ * button or its own save note, it is writing the timer again.
11
+ */
12
+ /**
13
+ * What {@link useFlash} hands back.
14
+ *
15
+ * @typeParam T - What is being flashed. A `string` for a label; a record for a
16
+ * flash that has to say WHICH thing it belongs to, as `useCopy` does.
17
+ *
18
+ * @public
19
+ */
20
+ export type UseFlashResult<T> = {
21
+ /** What is being shown right now, or `null` between flashes. */
22
+ readonly value: T | null;
23
+ /** Show `value` for the hook's duration, replacing any flash already up. */
24
+ readonly flash: (value: T) => void;
25
+ };
26
+ /**
27
+ * A transient value: set it, and it clears itself after `ms`.
28
+ *
29
+ * @remarks
30
+ * The two things this holds that a `useState` plus a `setTimeout` at the call
31
+ * site reliably gets wrong:
32
+ *
33
+ * - **At most ONE live timer.** A second flash while the first is still up
34
+ * re-arms rather than stacking, so the new value gets its full window
35
+ * instead of being cleared early by the previous click's timeout.
36
+ * - **Nothing fires after unmount.** The timer is cleared on teardown, so a
37
+ * chip clicked and then scrolled out of the tree does not `setState` on a
38
+ * component React has already thrown away.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import { useFlash } from "@alexkroman1/aai-ui";
43
+ *
44
+ * function SaveNote({ onSave }: { onSave: () => Promise<void> }) {
45
+ * const { value: note, flash } = useFlash<string>();
46
+ * return (
47
+ * <button type="button" onClick={() => void onSave().then(() => flash("Saved"))}>
48
+ * {note ?? "Save"}
49
+ * </button>
50
+ * );
51
+ * }
52
+ * ```
53
+ *
54
+ * @typeParam T - What is being flashed.
55
+ * @param ms - How long the value stays up. Defaults to 1500ms, which is what
56
+ * every caller wanted: long enough to read a word, short enough that the
57
+ * control is back to its real label before the reader looks again.
58
+ *
59
+ * @public
60
+ */
61
+ export declare function useFlash<T>(ms?: number): UseFlashResult<T>;
@@ -1,4 +1,5 @@
1
1
  import { useSessionSelector } from "./context.js";
2
+ import { useMemo } from "react";
2
3
  //#region src/use-user-transcript.ts
3
4
  /**
4
5
  * `useUserTranscript` — what the caller is saying RIGHT NOW, read correctly.
@@ -57,11 +58,11 @@ const TRANSCRIBING_PLACEHOLDER = "…";
57
58
  */
58
59
  function useUserTranscript() {
59
60
  const partial = useSessionSelector((snapshot) => snapshot.userTranscript);
60
- return {
61
+ return useMemo(() => ({
61
62
  speaking: partial !== null,
62
63
  text: displayText(partial),
63
64
  partial
64
- };
65
+ }), [partial]);
65
66
  }
66
67
  /** The three cases, spelled out: silent, detected-but-wordless, and words. */
67
68
  function displayText(partial) {
@@ -31,7 +31,7 @@
31
31
  * `wait` here when the page really does want one request, and the run is
32
32
  * followed from the same id either way.
33
33
  */
34
- import type { AnyWorkflowDef, UploadParallel, UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
34
+ import type { AnyWorkflowDef, UploadParallelOption, UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
35
35
  import type { FormValues } from "./components/form-types.ts";
36
36
  import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
37
37
  import type { SubmitInputOf } from "./workflow-def-types.ts";
@@ -119,6 +119,38 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
119
119
  cancel: () => Promise<boolean>;
120
120
  /** The run, once started, followed to completion. */
121
121
  run: WorkflowRun<R> | undefined;
122
+ /**
123
+ * True from `submit()` on this mount until `reset()` — did THIS page start
124
+ * the run it is showing?
125
+ *
126
+ * A page needs it to say the right sentence and cannot derive it: a run
127
+ * ADOPTED by the mount-time lookup after a reload looks exactly like one this
128
+ * page started. Six templates kept a `useState(false)` next to this hook, set
129
+ * it in their `onSubmit` and mirrored it in their `onClear` — shadow state
130
+ * for a fact only this hook can know, since it is the thing that decides
131
+ * between `submit()` and the recovery lookup. One of the six grew a fourth
132
+ * branch and had to move the whole note into its own module with its own
133
+ * spec, which is what a seam missing one layer down looks like.
134
+ *
135
+ * The RAW fact rather than a derived "recovered", deliberately: with `run`
136
+ * these are three states, not two, and the third is the one a page most needs
137
+ * to explain. `startedHere` is "you pressed the button"; `!startedHere &&
138
+ * !run` is the mount-time lookup still going; `!startedHere && run` is a run
139
+ * this browser started earlier, now in front of somebody who did not press
140
+ * anything. A boolean meaning only the last of those cannot express the
141
+ * middle one.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * declare const submission: import("@alexkroman1/aai-ui").WorkflowSubmission;
146
+ * const note = submission.startedHere
147
+ * ? "You can close this tab or reload it — this page will find the run again."
148
+ * : submission.run === undefined
149
+ * ? "Looking for a run this tab started earlier…"
150
+ * : "Still working on the run this tab started earlier. Reloading is safe.";
151
+ * ```
152
+ */
153
+ startedHere: boolean;
122
154
  /**
123
155
  * True from `submit()` until the run reaches a terminal status.
124
156
  *
@@ -214,7 +246,7 @@ export type UseWorkflowSubmitOptions = {
214
246
  * the default costs nothing where it would not have paid. See
215
247
  * `UploadOptions.parallel`.
216
248
  */
217
- parallel?: UploadParallel;
249
+ parallel?: UploadParallelOption;
218
250
  };
219
251
  /**
220
252
  * Start a workflow from a form, and follow the run it creates.
@@ -250,4 +282,4 @@ export type UseWorkflowSubmitOptions = {
250
282
  *
251
283
  * @public
252
284
  */
253
- export declare function useWorkflowSubmit<D extends AnyWorkflowDef>(workflow: string, opts?: UseWorkflowSubmitOptions): WorkflowSubmission<WorkflowOutputOf<D>, SubmitInputOf<D>>;
285
+ export declare function useWorkflowSubmit<D extends AnyWorkflowDef>(workflow: string, options?: UseWorkflowSubmitOptions): WorkflowSubmission<WorkflowOutputOf<D>, SubmitInputOf<D>>;
@@ -129,7 +129,7 @@ export declare const DEFAULT_PROGRESS_POLL_MS = 5000;
129
129
  *
130
130
  * @public
131
131
  */
132
- export declare function useWorkflowProgress<T = string>(runId: string | undefined, opts?: {
132
+ export declare function useWorkflowProgress<T = string>(runId: string | undefined, options?: {
133
133
  api?: WorkflowApi;
134
134
  namespace?: string;
135
135
  startIndex?: number;