@alexkroman1/aai-ui 13.2.0 → 14.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-j8XMToi9.js → _colors-CpZO-88A.js} +25 -2
- package/dist/{_module-url-C_4gRVL0.js → _module-url-C13kAJ87.js} +1 -1
- package/dist/_recover-run.d.ts +13 -7
- 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-B6498_bm.js → _utils-DnQDM9Uy.js} +4 -4
- 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/{aai-logo-CXZGPSIY.js → aai-logo-CFlomZlS.js} +1 -1
- package/dist/agent-state-labels.d.ts +60 -0
- package/dist/audio.js +21 -15
- package/dist/{chat-view-DDTtrh7N.js → chat-view-C_3T7Ln8.js} +100 -33
- package/dist/{client-config-BT_kWID5.js → client-config-DJQHnYjm.js} +5 -5
- package/dist/client-config.d.ts +4 -4
- package/dist/client-dir.d.ts +1 -1
- package/dist/client-dir.js +2 -2
- 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 +4 -4
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +16 -20
- package/dist/components/controls.js +4 -4
- 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/sidebar-layout.js +1 -1
- package/dist/components/start-screen.js +4 -4
- 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 +156 -18
- 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-RAZ-29Sz.js +284 -0
- package/dist/default-client/index.html +2 -2
- package/dist/default-client.d.ts +1 -1
- package/dist/define-client.d.ts +19 -19
- package/dist/define-client.js +20 -20
- package/dist/{eyebrow-C6ZFuiz6.js → eyebrow-UfmSz9yy.js} +1 -1
- package/dist/hooks.d.ts +44 -8
- package/dist/hooks.js +20 -14
- package/dist/index.d.ts +13 -8
- package/dist/index.js +1447 -1160
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +5 -5
- package/dist/{message-list-BJYyuIcR.js → message-list-CdOnSh5m.js} +23 -16
- 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-DxBYsfHA.js → session-core-gwePM95B.js} +135 -62
- 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-tcPQAkcP.js → tool-call-block-C2t_5fpp.js} +30 -12
- package/dist/{tool-config-context-DzAofqi_.js → tool-config-context-Es4YUzV2.js} +2 -2
- package/dist/types.d.ts +19 -4
- package/dist/types.js +3 -3
- package/dist/{url-chips-YqhCjWfQ.js → url-chips-BxhzZgk2.js} +5 -5
- package/dist/use-conversation.d.ts +1 -1
- package/dist/use-run-key.d.ts +44 -10
- package/dist/{use-user-transcript-C14qWFu2.js → use-user-transcript-uyHhzy4d.js} +4 -3
- package/dist/use-workflow-form.d.ts +64 -91
- package/dist/{use-workflow-progress-Cu0SxMyg.js → use-workflow-run-CP2ekKPV.js} +254 -257
- package/dist/use-workflow-stream.d.ts +5 -2
- package/dist/use-workflows.d.ts +77 -0
- package/dist/workflow-client.d.ts +1 -1
- package/dist/worklets/capture-processor.js +2 -2
- package/dist/worklets/playback-processor.js +2 -2
- package/package.json +6 -6
- 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/tsdown.config.d.ts +0 -2
|
@@ -149,7 +149,7 @@ export type SessionSnapshot = {
|
|
|
149
149
|
*
|
|
150
150
|
* @public
|
|
151
151
|
*/
|
|
152
|
-
export type
|
|
152
|
+
export type BrowserSession = {
|
|
153
153
|
/** Return the current immutable state snapshot. */
|
|
154
154
|
getSnapshot(): SessionSnapshot;
|
|
155
155
|
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
@@ -195,6 +195,30 @@ export type SessionCore = {
|
|
|
195
195
|
* per-session tool state, greeting included.
|
|
196
196
|
*/
|
|
197
197
|
end(): void;
|
|
198
|
+
/**
|
|
199
|
+
* End the current call and immediately begin a new one — `end()` then
|
|
200
|
+
* `start()`, which is what "New Conversation" means for an agent that keeps
|
|
201
|
+
* SESSION-SCOPED STATE.
|
|
202
|
+
*
|
|
203
|
+
* `reset()` is the one whose name suggests this and it is not the same
|
|
204
|
+
* thing: it clears the transcript and reconnects, but the reconnect carries
|
|
205
|
+
* the same `?sessionId=`, so every `sessionSlot` on the server survives —
|
|
206
|
+
* the game world, the incident board, the cart. A caller who asked to start
|
|
207
|
+
* over gets a blank transcript in front of the old state. This drops the
|
|
208
|
+
* session id, so the next connect mints a fresh one and the greeting plays
|
|
209
|
+
* again.
|
|
210
|
+
*
|
|
211
|
+
* Three templates had each written `session.end(); session.start();` with
|
|
212
|
+
* the same paragraph explaining why `reset()` was wrong; the six on the
|
|
213
|
+
* stock shell could not, because {@link Controls} called `reset()` for them.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* declare const session: import("@alexkroman1/aai-ui").Session;
|
|
218
|
+
* session.restart();
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
restart(): void;
|
|
198
222
|
/** Alias for `disconnect` for use with `using`. */
|
|
199
223
|
[Symbol.dispose](): void;
|
|
200
224
|
};
|
|
@@ -247,3 +271,36 @@ export type ConnState = {
|
|
|
247
271
|
* buffered during mic-permission never finishes playing. */
|
|
248
272
|
preInitDone: boolean;
|
|
249
273
|
};
|
|
274
|
+
/**
|
|
275
|
+
* The two liveness fields at rest.
|
|
276
|
+
*
|
|
277
|
+
* `running` and `recording` ride with almost every state transition and were
|
|
278
|
+
* spread as a pair of literals at seven sites across three modules — the same
|
|
279
|
+
* shape `session-core-state.ts` folded `state` and `error` out of, one field
|
|
280
|
+
* short. Naming it relates them: a transition that ends the call says so once,
|
|
281
|
+
* and a reader looking for "what stops the mic" finds one symbol rather than a
|
|
282
|
+
* grep.
|
|
283
|
+
*
|
|
284
|
+
* It is deliberately NOT the whole snapshot patch — a transition still supplies
|
|
285
|
+
* its own `agentState.apply(...)` projection beside this.
|
|
286
|
+
*/
|
|
287
|
+
export declare const STOPPED: {
|
|
288
|
+
readonly running: false;
|
|
289
|
+
readonly recording: false;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* A turn boundary: end the current turn and settle whatever it was playing.
|
|
293
|
+
*
|
|
294
|
+
* The two calls are one fact and were written out at four sites — `cancel()`
|
|
295
|
+
* and `reset()` here, `reply.cancelled` and `session.reset` on the server side
|
|
296
|
+
* — where the pair is load-bearing in both halves. The bump stops a stale drain
|
|
297
|
+
* continuation from stamping `"listening"` over a state the session has since
|
|
298
|
+
* moved to; the flush settles the interrupted turn's `done()` so it cannot
|
|
299
|
+
* strand.
|
|
300
|
+
*
|
|
301
|
+
* Two further sites bump WITHOUT flushing (`cleanupAudio`, a committed user
|
|
302
|
+
* transcript) and stay spelled out, which is the point of naming this one: a
|
|
303
|
+
* bump on its own now reads as a deliberate choice rather than a forgotten
|
|
304
|
+
* flush.
|
|
305
|
+
*/
|
|
306
|
+
export declare function bargeIn(conn: ConnState): void;
|
package/dist/session-core.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BrowserSession } from "./session-core-types.ts";
|
|
2
2
|
import { type VoiceSessionOptions } from "./types.ts";
|
|
3
3
|
/**
|
|
4
4
|
* Create a framework-agnostic voice session core that connects to an AAI
|
|
@@ -7,24 +7,24 @@ import { type VoiceSessionOptions } from "./types.ts";
|
|
|
7
7
|
* Uses a subscribe/getSnapshot pattern for state management, compatible with
|
|
8
8
|
* React's `useSyncExternalStore` and other external store integrations.
|
|
9
9
|
*
|
|
10
|
-
* Most clients never call this: `
|
|
10
|
+
* Most clients never call this: `mountClient()` creates a core and installs it in
|
|
11
11
|
* React context for the hooks. Reach for it directly when building a
|
|
12
12
|
* non-React UI (or wiring the session into another framework's store).
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```ts
|
|
16
|
-
* import {
|
|
16
|
+
* import { createBrowserSession, type SessionSnapshot } from "@alexkroman1/aai-ui";
|
|
17
17
|
*
|
|
18
18
|
* declare function render(snapshot: SessionSnapshot): void;
|
|
19
19
|
*
|
|
20
|
-
* const session =
|
|
20
|
+
* const session = createBrowserSession({ platformUrl: "https://host/my-agent/" });
|
|
21
21
|
* session.subscribe(() => render(session.getSnapshot()));
|
|
22
22
|
* session.start();
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
25
|
* @param options - Session configuration including the platform server URL.
|
|
26
|
-
* @returns A {@link
|
|
26
|
+
* @returns A {@link BrowserSession} handle for controlling the session.
|
|
27
27
|
*
|
|
28
28
|
* @public
|
|
29
29
|
*/
|
|
30
|
-
export declare function
|
|
30
|
+
export declare function createBrowserSession(options: VoiceSessionOptions): BrowserSession;
|
package/dist/session-core.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
export {
|
|
1
|
+
import { t as createBrowserSession } from "./session-core-gwePM95B.js";
|
|
2
|
+
export { createBrowserSession };
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
* Keyed by the agent's own URL, so two agents served from one origin — which is
|
|
24
24
|
* every deployed agent, at `/:slug/` — cannot inherit each other's session.
|
|
25
25
|
*
|
|
26
|
-
* Every access is guarded
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Every access is guarded, and the guard lives in `_web-storage.ts`: a session
|
|
27
|
+
* that cannot be remembered must degrade to today's behaviour rather than
|
|
28
|
+
* failing to start.
|
|
29
29
|
*/
|
|
30
30
|
/** The stored session id for this agent, or undefined. @internal */
|
|
31
31
|
export declare function readStoredSessionId(platformUrl: string): string | undefined;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useTheme } from "./context.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as Eyebrow } from "./eyebrow-
|
|
4
|
-
import { i as tryParseJSON, r as truncate } from "./_utils-
|
|
5
|
-
import { n as useToolConfig } from "./tool-config-context-
|
|
2
|
+
import { a as inkTint } from "./_colors-CpZO-88A.js";
|
|
3
|
+
import { t as Eyebrow } from "./eyebrow-UfmSz9yy.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";
|
|
9
|
-
//#region components/tool-call-row.tsx
|
|
9
|
+
//#region src/components/tool-call-row.tsx
|
|
10
10
|
/** @jsxImportSource react */
|
|
11
11
|
const VARIANT_CLASSES = {
|
|
12
12
|
default: {
|
|
@@ -116,13 +116,36 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
//#endregion
|
|
119
|
-
//#region components/tool-call-block.tsx
|
|
119
|
+
//#region src/components/tool-call-block.tsx
|
|
120
120
|
/** @jsxImportSource react */
|
|
121
121
|
function formatResult(result) {
|
|
122
122
|
const parsed = tryParseJSON(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
|
-
//#region components/tool-config-context.ts
|
|
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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PACER_BURST_MS, PIPELINE_PLAYBACK_GRACE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "@alexkroman1/aai/internal";
|
|
2
|
-
//#region types.ts
|
|
2
|
+
//#region src/types.ts
|
|
3
3
|
/**
|
|
4
4
|
* `getUserMedia` audio constraints for every capture path in this package.
|
|
5
5
|
*
|
|
@@ -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 = {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useSessionSelector, useTheme } from "./context.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as pageBaseUrl } from "./_utils-
|
|
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
4
|
import clsx from "clsx";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
|
-
//#region components/url-chips.tsx
|
|
7
|
+
//#region src/components/url-chips.tsx
|
|
8
8
|
/** @jsxImportSource react */
|
|
9
9
|
/** How long the "Copied" confirmation replaces the label after a click. */
|
|
10
10
|
const COPIED_FEEDBACK_MS = 1500;
|
|
@@ -32,12 +32,12 @@ function UrlChip({ label, url, hint, testId, className }) {
|
|
|
32
32
|
type: "button",
|
|
33
33
|
onClick: copy,
|
|
34
34
|
title: `${hint} (click to copy)\n${url}`,
|
|
35
|
-
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",
|
|
35
|
+
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),
|
|
36
36
|
style: {
|
|
37
37
|
background: inkTint(theme.text, theme.surface, 3),
|
|
38
38
|
borderColor: theme.border,
|
|
39
39
|
color: inkTint(theme.text, theme.surface, 65),
|
|
40
|
-
|
|
40
|
+
...focusRingStyle(theme.primary)
|
|
41
41
|
},
|
|
42
42
|
"data-testid": testId,
|
|
43
43
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -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
|
package/dist/use-run-key.d.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The handle a page keeps on the runs it started, across a reload.
|
|
3
3
|
*
|
|
4
|
-
* `useWorkflowSubmit
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* `useWorkflowSubmit` is what makes a run survivable — the run id is that
|
|
5
|
+
* hook's own state, so a refresh loses it while the run carries on — and the
|
|
6
|
+
* `key` is what it looks the run up BY, because it is a lookup CAPABILITY:
|
|
7
|
+
* there is no per-user filtering behind `find`, so the key IS the scoping
|
|
8
|
+
* mechanism. Choosing one is easy to get wrong in three separate ways, and six
|
|
9
|
+
* shipped templates had each written the same twenty lines to get it right.
|
|
10
|
+
* This is those lines.
|
|
11
|
+
*
|
|
12
|
+
* **`useWorkflowSubmit` now mints one for itself** ({@link useDefaultRunKey}),
|
|
13
|
+
* so a page resumes its own run across a reload with nothing written at the
|
|
14
|
+
* call site — six of six page templates passed `useRunKey()` and
|
|
15
|
+
* `recover: true`, which is a default in the wrong place. The hook stays
|
|
16
|
+
* PUBLIC for the page that wants to choose: an app with accounts passes the
|
|
17
|
+
* ACCOUNT's own id instead, and a run then follows the person to a new device,
|
|
18
|
+
* which is a promise only a login can keep; a page whose run outlives the tab
|
|
19
|
+
* passes `useRunKey({ storage: "local" })`.
|
|
11
20
|
*
|
|
12
21
|
* ## Three properties, and the rejected alternatives are why each one matters
|
|
13
22
|
*
|
|
@@ -42,7 +51,9 @@
|
|
|
42
51
|
* coming back on Friday to press Stop is the ordinary case rather than an edge
|
|
43
52
|
* one, and a tab-scoped key would answer that with an empty form beside a run
|
|
44
53
|
* still posting somewhere. It is as far as a key can go without a login, and no
|
|
45
|
-
* further. `podcast-digest` is that template
|
|
54
|
+
* further. `podcast-digest` is that template, and the reason this hook is still
|
|
55
|
+
* called by name anywhere; the other five take the tab-scoped default the
|
|
56
|
+
* submit hook mints for them.
|
|
46
57
|
*
|
|
47
58
|
* ## Anything ELSE a page stores back must be VALIDATED on read
|
|
48
59
|
*
|
|
@@ -77,8 +88,31 @@
|
|
|
77
88
|
* failing to render.
|
|
78
89
|
*/
|
|
79
90
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
91
|
+
* The key `useWorkflowSubmit` uses when the page named none.
|
|
92
|
+
*
|
|
93
|
+
* Two things it does that a plain `useRunKey()` at the call site cannot, and
|
|
94
|
+
* both are about a page that DID name one:
|
|
95
|
+
*
|
|
96
|
+
* - **It mints nothing when the caller has a key.** Minting writes to storage,
|
|
97
|
+
* so an unconditional `useRunKey()` inside the hook would leave a slot behind
|
|
98
|
+
* on every page that passes an account id and never reads it back.
|
|
99
|
+
* - **It stays reactive to the caller's key.** A key that arrives late — an
|
|
100
|
+
* account id resolved after a login — must reach the lookup, which re-asks on
|
|
101
|
+
* a changed key by design; freezing it into `useState` would pin the page to
|
|
102
|
+
* whatever it held on its first render.
|
|
103
|
+
*
|
|
104
|
+
* The minted half is still frozen for the component's life, which is what
|
|
105
|
+
* `useRunKey` freezes it for: a fresh key per render would record every run
|
|
106
|
+
* under a name the next load cannot produce.
|
|
107
|
+
*
|
|
108
|
+
* @param explicit - The caller's own key, or undefined for a page with none.
|
|
109
|
+
* @returns The key to record runs under and look them up by.
|
|
110
|
+
*
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
export declare function useDefaultRunKey(explicit: string | undefined): string;
|
|
114
|
+
/**
|
|
115
|
+
* A lookup key for `useWorkflowSubmit({ key })`, stable across reloads.
|
|
82
116
|
*
|
|
83
117
|
* @param options - See the module doc for the whole argument. The storage kind
|
|
84
118
|
* is read once, when the key is minted: a value that changed afterwards would
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useSessionSelector } from "./context.js";
|
|
2
|
-
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
//#region src/use-user-transcript.ts
|
|
3
4
|
/**
|
|
4
5
|
* `useUserTranscript` — what the caller is saying RIGHT NOW, read correctly.
|
|
5
6
|
*
|
|
@@ -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) {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The hook a FORM needs, as against the one a status view does.
|
|
3
3
|
*
|
|
4
4
|
* `useWorkflowRun` (`workflow-client.ts`) watches a run you already have.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `useWorkflowRun`.
|
|
5
|
+
* This is what comes before it: `useWorkflowSubmit` starts a run and hands the
|
|
6
|
+
* id straight to `useWorkflowRun`. Its sibling `useWorkflows` — the listing
|
|
7
|
+
* `<WorkflowFields>` renders a form from — is `use-workflows.ts`.
|
|
9
8
|
*
|
|
10
9
|
* ## `useWorkflowSubmit` — a form's two halves in one hook
|
|
11
10
|
*
|
|
@@ -32,76 +31,10 @@
|
|
|
32
31
|
* `wait` here when the page really does want one request, and the run is
|
|
33
32
|
* followed from the same id either way.
|
|
34
33
|
*/
|
|
35
|
-
import type { AnyWorkflowDef,
|
|
34
|
+
import type { AnyWorkflowDef, UploadParallelOption, UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
36
35
|
import type { FormValues } from "./components/form-types.ts";
|
|
37
36
|
import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
|
|
38
37
|
import type { SubmitInputOf } from "./workflow-def-types.ts";
|
|
39
|
-
/** Options for {@link useWorkflows}. */
|
|
40
|
-
export type UseWorkflowsOptions = {
|
|
41
|
-
/** The client to read the listing with. Defaults to one for the page's own agent. */
|
|
42
|
-
api?: WorkflowApi;
|
|
43
|
-
/**
|
|
44
|
-
* Skip the lookup entirely, reporting an empty listing that is not loading.
|
|
45
|
-
*
|
|
46
|
-
* For a caller that may or may not need the listing and cannot decide with a
|
|
47
|
-
* conditional hook — `<WorkflowFields>` handed a summary rather than a name is
|
|
48
|
-
* the one in this package. It reports `loading: false`, because a skipped
|
|
49
|
-
* lookup is finished rather than pending.
|
|
50
|
-
*/
|
|
51
|
-
skip?: boolean;
|
|
52
|
-
};
|
|
53
|
-
/** What {@link useWorkflows} reports. */
|
|
54
|
-
export type UseWorkflowsResult = {
|
|
55
|
-
/** The agent's declared workflows, each with the JSON Schema of its input. */
|
|
56
|
-
workflows: WorkflowSummary[];
|
|
57
|
-
/** True until the listing lands, so a form can hold its fields back. */
|
|
58
|
-
loading: boolean;
|
|
59
|
-
/** The lookup's failure. Set alongside an EMPTY list, which is why it exists. */
|
|
60
|
-
error: string | undefined;
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Read the agent's declared workflows.
|
|
64
|
-
*
|
|
65
|
-
* What `<WorkflowFields>` renders a form FROM: each summary carries the JSON
|
|
66
|
-
* Schema of that workflow's input, converted server-side precisely so a browser
|
|
67
|
-
* can read it.
|
|
68
|
-
*
|
|
69
|
-
* The failure is reported rather than swallowed, because the alternative is an
|
|
70
|
-
* empty list — which renders as a form with no fields and reads as "this agent
|
|
71
|
-
* declares no workflows" about an agent that was merely unreachable.
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```tsx
|
|
75
|
-
* import { useWorkflows } from "@alexkroman1/aai-ui";
|
|
76
|
-
*
|
|
77
|
-
* // A page rendering its own chrome from the listing — a picker, say. A form
|
|
78
|
-
* // for ONE workflow wants `<WorkflowFields workflow="name" />` instead,
|
|
79
|
-
* // which does this lookup itself.
|
|
80
|
-
* function WorkflowPicker({ onPick }: { onPick: (name: string) => void }) {
|
|
81
|
-
* const { workflows, loading, error } = useWorkflows();
|
|
82
|
-
* if (loading) return <p>Loading…</p>;
|
|
83
|
-
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
84
|
-
* return (
|
|
85
|
-
* <ul>
|
|
86
|
-
* {workflows.map((summary) => (
|
|
87
|
-
* <li key={summary.name}>
|
|
88
|
-
* <button type="button" onClick={() => onPick(summary.name)}>
|
|
89
|
-
* {summary.description ?? summary.name}
|
|
90
|
-
* </button>
|
|
91
|
-
* </li>
|
|
92
|
-
* ))}
|
|
93
|
-
* </ul>
|
|
94
|
-
* );
|
|
95
|
-
* }
|
|
96
|
-
* ```
|
|
97
|
-
*
|
|
98
|
-
* @param opts - See {@link UseWorkflowsOptions}.
|
|
99
|
-
* @returns The listing, its loading flag and its failure — see
|
|
100
|
-
* {@link UseWorkflowsResult}.
|
|
101
|
-
*
|
|
102
|
-
* @public
|
|
103
|
-
*/
|
|
104
|
-
export declare function useWorkflows(opts?: UseWorkflowsOptions): UseWorkflowsResult;
|
|
105
38
|
/**
|
|
106
39
|
* What {@link WorkflowSubmission.upload} reports while the bytes are going.
|
|
107
40
|
*
|
|
@@ -186,6 +119,38 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
|
|
|
186
119
|
cancel: () => Promise<boolean>;
|
|
187
120
|
/** The run, once started, followed to completion. */
|
|
188
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;
|
|
189
154
|
/**
|
|
190
155
|
* True from `submit()` until the run reaches a terminal status.
|
|
191
156
|
*
|
|
@@ -229,29 +194,37 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
|
|
|
229
194
|
export type UseWorkflowSubmitOptions = {
|
|
230
195
|
/** The client to start runs with. Defaults to one for the page's own agent. */
|
|
231
196
|
api?: WorkflowApi;
|
|
232
|
-
/**
|
|
197
|
+
/**
|
|
198
|
+
* Correlation key recorded with the run, for finding it again without the id.
|
|
199
|
+
*
|
|
200
|
+
* **Defaulted**, to an opaque per-page key in `sessionStorage` that the next
|
|
201
|
+
* load produces again — `useRunKey()`'s, minted by the hook. Pass one to
|
|
202
|
+
* scope runs to something the page knows better: an ACCOUNT's own id, which
|
|
203
|
+
* is what makes a run follow the person to a new device, or
|
|
204
|
+
* `useRunKey({ storage: "local" })` for a run that outlives the tab by
|
|
205
|
+
* design. The key is a lookup CAPABILITY (there is no per-user filtering
|
|
206
|
+
* behind `find`), it must fit the route's 256-character bound, and anything
|
|
207
|
+
* derived from a person's own input both collides and carries what they
|
|
208
|
+
* typed — `use-run-key.ts` argues every alternative.
|
|
209
|
+
*/
|
|
233
210
|
key?: string;
|
|
234
211
|
/**
|
|
235
212
|
* On mount, adopt the newest run this `key` already has.
|
|
236
213
|
*
|
|
237
|
-
* **This is what makes a reload survivable.** The run id is
|
|
238
|
-
* state, so a refresh loses it while the run carries on — and
|
|
239
|
-
* cannot name a run cannot show it, cancel it or wake it.
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* Inert without a `key`, because the key IS the lookup. Opt-in because a
|
|
245
|
-
* `key` on its own means only "record this with the run", which is what a
|
|
246
|
-
* page passing an account id may well want; adopting a run is a decision
|
|
247
|
-
* about the page.
|
|
214
|
+
* **This is what makes a reload survivable, and it is ON.** The run id is
|
|
215
|
+
* this hook's own state, so a refresh loses it while the run carries on — and
|
|
216
|
+
* a page that cannot name a run cannot show it, cancel it or wake it. The
|
|
217
|
+
* hook asks `find(workflow, key)` once as it mounts and follows whatever
|
|
218
|
+
* comes back, so the answer, the progress and the controls are all there
|
|
219
|
+
* again.
|
|
248
220
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
254
|
-
*
|
|
221
|
+
* It used to be opt-in, on the argument that a `key` alone means only "record
|
|
222
|
+
* this with the run" — true of `ctx.workflows.start({ key })`, where there is
|
|
223
|
+
* no page to put a run back on, and not of a form: six of six page templates
|
|
224
|
+
* passed `useRunKey()` and `recover: true` together, which is a default in
|
|
225
|
+
* the wrong place. `false` is the opt-out, and what it buys is a form that
|
|
226
|
+
* always opens empty — no lookup on mount, and a live run reachable only by
|
|
227
|
+
* an id the page has already lost.
|
|
255
228
|
*/
|
|
256
229
|
recover?: boolean;
|
|
257
230
|
/**
|
|
@@ -273,7 +246,7 @@ export type UseWorkflowSubmitOptions = {
|
|
|
273
246
|
* the default costs nothing where it would not have paid. See
|
|
274
247
|
* `UploadOptions.parallel`.
|
|
275
248
|
*/
|
|
276
|
-
parallel?:
|
|
249
|
+
parallel?: UploadParallelOption;
|
|
277
250
|
};
|
|
278
251
|
/**
|
|
279
252
|
* Start a workflow from a form, and follow the run it creates.
|