@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.
- package/README.md +159 -69
- package/dist/{_colors-CZ6OlPbL.js → _colors-CpZO-88A.js} +24 -1
- package/dist/_recover-run.d.ts +2 -2
- package/dist/_submission-state.d.ts +92 -0
- package/dist/_upload-files.d.ts +2 -2
- package/dist/_upload-report.d.ts +26 -0
- package/dist/{_utils-CyzjK0gW.js → _utils-DnQDM9Uy.js} +3 -3
- package/dist/_utils.d.ts +3 -3
- package/dist/_web-storage.d.ts +43 -0
- package/dist/_workflow-files.d.ts +1 -1
- package/dist/agent-state-labels.d.ts +60 -0
- package/dist/audio.js +20 -14
- package/dist/{chat-view-Bv5VFJIE.js → chat-view-C_3T7Ln8.js} +96 -29
- package/dist/{client-config-DD820zHn.js → client-config-DJQHnYjm.js} +4 -4
- package/dist/client-config.d.ts +4 -4
- package/dist/client-dir.d.ts +1 -1
- package/dist/client-dir.js +1 -1
- package/dist/components/_colors.d.ts +23 -0
- package/dist/components/_form-readiness.d.ts +1 -1
- package/dist/components/bullet-list.d.ts +74 -0
- package/dist/components/button.js +3 -3
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +16 -20
- package/dist/components/controls.js +3 -3
- package/dist/components/facts.d.ts +81 -0
- package/dist/components/form-fields.d.ts +6 -6
- package/dist/components/form-types.d.ts +1 -1
- package/dist/components/form.d.ts +1 -1
- package/dist/components/message-list.js +1 -1
- package/dist/components/session-error-banner.d.ts +69 -0
- package/dist/components/start-screen.js +1 -1
- package/dist/components/tool-call-block.js +1 -1
- package/dist/components/tool-config-context.d.ts +1 -1
- package/dist/components/workflow-progress.d.ts +11 -4
- package/dist/context.d.ts +142 -19
- package/dist/context.js +155 -17
- package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
- package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
- package/dist/default-client/assets/index-BowRD-xq.js +284 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.d.ts +19 -19
- package/dist/define-client.js +19 -19
- package/dist/hooks.d.ts +44 -8
- package/dist/hooks.js +19 -13
- package/dist/index.d.ts +14 -7
- package/dist/index.js +551 -231
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +5 -5
- package/dist/{message-list-C0pL7x41.js → message-list-CdOnSh5m.js} +19 -12
- package/dist/page.d.ts +11 -11
- package/dist/session-core-audio-setup.d.ts +1 -1
- package/dist/session-core-dial.d.ts +0 -2
- package/dist/{session-core-C9elBIdu.js → session-core-gwePM95B.js} +125 -52
- package/dist/session-core-messages.d.ts +2 -2
- package/dist/session-core-types.d.ts +58 -1
- package/dist/session-core.d.ts +6 -6
- package/dist/session-core.js +2 -2
- package/dist/session-resume-store.d.ts +3 -3
- package/dist/{tool-call-block-Bunc6rCw.js → tool-call-block-C2t_5fpp.js} +27 -9
- package/dist/{tool-config-context-Bh8p3DtG.js → tool-config-context-Es4YUzV2.js} +1 -1
- package/dist/types.d.ts +19 -4
- package/dist/types.js +2 -2
- package/dist/url-chips-D62RTz7v.js +239 -0
- package/dist/use-conversation.d.ts +1 -1
- package/dist/use-copy.d.ts +74 -0
- package/dist/use-download-url.d.ts +2 -2
- package/dist/use-flash.d.ts +61 -0
- package/dist/{use-user-transcript-DFTSEuZN.js → use-user-transcript-uyHhzy4d.js} +3 -2
- package/dist/use-workflow-form.d.ts +35 -3
- package/dist/use-workflow-progress.d.ts +1 -1
- package/dist/{use-workflow-run-CXGEcM0l.js → use-workflow-run-CNahvlgL.js} +12 -15
- package/dist/use-workflow-run.d.ts +2 -2
- package/dist/use-workflow-runs.d.ts +1 -1
- package/dist/use-workflow-stream.d.ts +2 -2
- package/dist/use-workflows.d.ts +2 -2
- package/dist/workflow-client.d.ts +3 -3
- package/dist/workflow-field-kind.d.ts +95 -0
- package/package.json +2 -2
- package/styles.css +78 -0
- package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
- package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
- package/dist/url-chips-C2u7QPv8.js +0 -100
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useTheme } from "./context.js";
|
|
2
|
-
import {
|
|
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-
|
|
5
|
-
import { n as useToolConfig } from "./tool-config-context-
|
|
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
|
-
}),
|
|
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 `
|
|
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
|
-
* `
|
|
27
|
-
* chrome that bypasses `
|
|
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 `
|
|
142
|
-
* defaults `platformUrl` from `location.href`, while `
|
|
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
|
-
* `
|
|
26
|
-
* chrome that bypasses `
|
|
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 `
|
|
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
|
|
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,
|
|
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,
|
|
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?:
|
|
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,
|
|
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,
|
|
132
|
+
export declare function useWorkflowProgress<T = string>(runId: string | undefined, options?: {
|
|
133
133
|
api?: WorkflowApi;
|
|
134
134
|
namespace?: string;
|
|
135
135
|
startIndex?: number;
|