@alexkroman1/aai-ui 13.3.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-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-RAZ-29Sz.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 +11 -7
- package/dist/index.js +418 -185
- 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-C2u7QPv8.js → url-chips-BxhzZgk2.js} +4 -4
- package/dist/use-conversation.d.ts +1 -1
- package/dist/{use-user-transcript-DFTSEuZN.js → use-user-transcript-uyHhzy4d.js} +3 -2
- package/dist/use-workflow-form.d.ts +34 -2
- package/dist/{use-workflow-run-CXGEcM0l.js → use-workflow-run-CP2ekKPV.js} +3 -6
- package/dist/use-workflow-stream.d.ts +1 -1
- package/dist/workflow-client.d.ts +1 -1
- 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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @alexkroman1/aai-ui
|
|
2
2
|
|
|
3
|
-
The browser client for aai agents: React 19
|
|
3
|
+
The browser client for aai agents: React 19 hooks and components over a
|
|
4
4
|
framework-agnostic session core (WebSocket + microphone + playback).
|
|
5
5
|
|
|
6
6
|
```sh
|
|
@@ -11,64 +11,156 @@ Every agent gets this UI for free — `aai dev` and deployed agents serve a
|
|
|
11
11
|
default client built from this package. Install it directly when the agent
|
|
12
12
|
has its own `client.tsx`.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## UI integration
|
|
15
|
+
|
|
16
|
+
The hooks here are what a page reads an agent with: the live call, the state
|
|
17
|
+
the agent projects, the tool calls as they run, and the durable runs it
|
|
18
|
+
started. **There is no route to write.** The agent server already serves the
|
|
19
|
+
session socket and the workflow HTTP API, so a component talks to a live
|
|
20
|
+
agent with no glue file in between — a client is one `client.tsx` calling one
|
|
21
|
+
mount.
|
|
22
|
+
|
|
23
|
+
**Two front doors, two mounts.** A voice agent's page calls `mountClient()` and
|
|
15
24
|
talks to a live session. A workflow app's page (`workflowApp()`, or
|
|
16
|
-
`agent({ page: "static" })`) calls `
|
|
25
|
+
`agent({ page: "static" })`) calls `mountPage()` and talks to the workflow HTTP
|
|
17
26
|
API — no session, no socket, no microphone. Both are still `client.tsx`,
|
|
18
27
|
still React, still the same theme tokens.
|
|
19
28
|
|
|
20
|
-
|
|
29
|
+
### Agent `agent.ts`
|
|
30
|
+
|
|
31
|
+
The session's state lives in a slot, and `syncState` is what the browser sees
|
|
32
|
+
— pushed after every tool call, so the client never rebuilds it from events:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { agent, sessionSlot } from "@alexkroman1/aai";
|
|
36
|
+
|
|
37
|
+
const desk = sessionSlot("desk", () => ({ symbols: [] as string[] }));
|
|
38
|
+
|
|
39
|
+
export default agent({
|
|
40
|
+
name: "Market Desk",
|
|
41
|
+
systemPrompt: "You look up quotes and keep the caller's watchlist.",
|
|
42
|
+
syncState: desk.projection((state) => ({ symbols: state.symbols })),
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Tool `tools/get_quote.ts`
|
|
47
|
+
|
|
48
|
+
A tool is a **file**, named by its own filename. Nothing registers it, and
|
|
49
|
+
`agent()` takes no `tools` field:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { tool } from "@alexkroman1/aai";
|
|
53
|
+
import { z } from "zod";
|
|
54
|
+
|
|
55
|
+
export default tool({
|
|
56
|
+
description: "Look up the latest price for one ticker symbol",
|
|
57
|
+
inputSchema: z.object({ symbol: z.string() }),
|
|
58
|
+
execute: async ({ symbol }, ctx) => {
|
|
59
|
+
const res = await fetch(`https://api.example.com/quote/${symbol}`, {
|
|
60
|
+
headers: { authorization: `Bearer ${ctx.env.QUOTES_API_KEY}` },
|
|
61
|
+
signal: ctx.signal,
|
|
62
|
+
});
|
|
63
|
+
return (await res.json()) as { symbol: string; price: number };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
21
67
|
|
|
22
|
-
|
|
23
|
-
|
|
68
|
+
### UI component for a tool `quote-card.tsx`
|
|
69
|
+
|
|
70
|
+
`useToolResult` fires once per settled call of one named tool, with the
|
|
71
|
+
result already parsed. A component that mounts late still receives the
|
|
72
|
+
results of calls that finished before it — a result is a value the UI is
|
|
73
|
+
driven from, not a moment:
|
|
24
74
|
|
|
25
75
|
```tsx
|
|
26
|
-
import "
|
|
27
|
-
import {
|
|
76
|
+
import { useState } from "react";
|
|
77
|
+
import { useToolResult } from "@alexkroman1/aai-ui";
|
|
28
78
|
|
|
29
|
-
type
|
|
79
|
+
type Quote = { symbol: string; price: number };
|
|
30
80
|
|
|
31
|
-
function
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
export default function QuoteCard() {
|
|
82
|
+
const [quote, setQuote] = useState<Quote>();
|
|
83
|
+
useToolResult<Quote>("get_quote", (result) => setQuote(result));
|
|
84
|
+
|
|
85
|
+
if (!quote) return null;
|
|
36
86
|
return (
|
|
37
|
-
<
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
<p>
|
|
88
|
+
{quote.symbol} — {quote.price.toFixed(2)}
|
|
89
|
+
</p>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`useToolCallStart` is the same hook for the other end of the call — the
|
|
95
|
+
pending invocation and its arguments, for a spinner rather than a value.
|
|
96
|
+
Either one takes the tool's own shape as a type argument, derived from the
|
|
97
|
+
tool module by a **type-only** import that is erased and so pulls no server
|
|
98
|
+
code into the bundle: `useToolResult<InferToolOutput<typeof getQuote>>(…)`
|
|
99
|
+
for the result, `useToolCallStart<InferToolInput<typeof getQuote>>(…)` for
|
|
100
|
+
the arguments.
|
|
101
|
+
|
|
102
|
+
### Client `client.tsx`
|
|
103
|
+
|
|
104
|
+
`mountClient()` mounts the default chat shell — start screen, transcript,
|
|
105
|
+
controls — with your components in it. `sidebar` takes the COMPONENT, not an
|
|
106
|
+
element:
|
|
107
|
+
|
|
108
|
+
```tsx no-check
|
|
109
|
+
import "@alexkroman1/aai-ui/styles.css";
|
|
110
|
+
import { mountClient, useAgentState } from "@alexkroman1/aai-ui";
|
|
111
|
+
import QuoteCard from "./quote-card.tsx";
|
|
112
|
+
|
|
113
|
+
function Watchlist() {
|
|
114
|
+
// Whatever `syncState` projected, live. Null until the first push, which is
|
|
115
|
+
// a moment the UI has to render.
|
|
116
|
+
const desk = useAgentState<{ symbols: string[] }>();
|
|
117
|
+
return (
|
|
118
|
+
<div>
|
|
119
|
+
<QuoteCard />
|
|
120
|
+
{desk?.symbols.map((symbol) => <div key={symbol}>{symbol}</div>)}
|
|
42
121
|
</div>
|
|
43
122
|
);
|
|
44
123
|
}
|
|
45
124
|
|
|
46
|
-
|
|
47
|
-
|
|
125
|
+
mountClient({
|
|
126
|
+
name: "Market Desk",
|
|
127
|
+
sidebar: Watchlist,
|
|
128
|
+
// Icon and label per tool, for the transcript's tool rows.
|
|
129
|
+
tools: { get_quote: { icon: "📈", label: "Fetching quote" } },
|
|
130
|
+
theme: { primary: "#2f9e44" },
|
|
131
|
+
});
|
|
48
132
|
```
|
|
49
133
|
|
|
134
|
+
The type argument above restates a shape the agent already knows. Move the
|
|
135
|
+
slot and its projection into a `shared.ts` both ends import, pass the
|
|
136
|
+
projection itself — `useAgentState(deskProjection)` — and the state is typed
|
|
137
|
+
from the projection and the pre-first-push frame derived from it, so there is
|
|
138
|
+
nothing to restate and no branch for the first render.
|
|
139
|
+
|
|
140
|
+
Pass `component` instead of `sidebar` to replace the whole shell. It renders
|
|
141
|
+
inside the same providers, so every hook here works in it unchanged.
|
|
142
|
+
|
|
50
143
|
## A workflow app
|
|
51
144
|
|
|
52
|
-
`
|
|
53
|
-
one control per scalar property of the workflow's
|
|
54
|
-
a field to the schema adds it to the page:
|
|
145
|
+
`mountPage()` mounts a form over the agent's workflows and installs no session.
|
|
146
|
+
`<WorkflowFields>` renders one control per scalar property of the workflow's
|
|
147
|
+
own input schema, so adding a field to the schema adds it to the page:
|
|
55
148
|
|
|
56
|
-
```tsx
|
|
149
|
+
```tsx
|
|
57
150
|
import "@alexkroman1/aai-ui/styles.css";
|
|
58
151
|
import {
|
|
59
152
|
Form,
|
|
60
|
-
|
|
153
|
+
mountPage,
|
|
61
154
|
SubmitButton,
|
|
62
155
|
UploadProgressBar,
|
|
63
156
|
useWorkflowSubmit,
|
|
64
157
|
WorkflowFields,
|
|
65
158
|
WorkflowProgress,
|
|
66
159
|
} from "@alexkroman1/aai-ui";
|
|
67
|
-
// Type-only, so it is ERASED — the agent module is not in this bundle.
|
|
68
|
-
import type { digest } from "./agent.ts";
|
|
69
160
|
|
|
70
161
|
function App() {
|
|
71
|
-
const { submitForm, run, pending, upload, error } =
|
|
162
|
+
const { submitForm, run, pending, upload, error } =
|
|
163
|
+
useWorkflowSubmit("digest");
|
|
72
164
|
return (
|
|
73
165
|
<main className="mx-auto flex max-w-2xl flex-col gap-6 p-8">
|
|
74
166
|
<Form onSubmit={submitForm} error={error}>
|
|
@@ -78,45 +170,56 @@ function App() {
|
|
|
78
170
|
{/* The upload is its own wait: the run does not exist until the bytes
|
|
79
171
|
are in, so nothing else on the page can describe it. */}
|
|
80
172
|
<UploadProgressBar upload={upload} />
|
|
81
|
-
{/* What the run has SAID, from `
|
|
173
|
+
{/* What the run has SAID, from `stepReport()` in its steps. */}
|
|
82
174
|
<WorkflowProgress runId={run?.runId} />
|
|
83
175
|
{run?.status === "completed" && <pre>{JSON.stringify(run.output)}</pre>}
|
|
84
176
|
</main>
|
|
85
177
|
);
|
|
86
178
|
}
|
|
87
179
|
|
|
88
|
-
|
|
180
|
+
mountPage({ name: "Digest", component: App });
|
|
89
181
|
```
|
|
90
182
|
|
|
91
|
-
`
|
|
92
|
-
|
|
93
|
-
|
|
183
|
+
Naming the workflow's def — `useWorkflowSubmit<typeof digest>("digest")`, off
|
|
184
|
+
a **type-only** import of `agent.ts` — is what makes `submit()` take the
|
|
185
|
+
declared input and `run.status === "completed"` narrow to a typed
|
|
186
|
+
`run.output`. The import is erased, so it pulls no server code into the
|
|
187
|
+
bundle.
|
|
94
188
|
|
|
95
189
|
## What is in the package
|
|
96
190
|
|
|
97
|
-
The reference
|
|
98
|
-
grouped by what it is for.
|
|
99
|
-
|
|
100
|
-
**Mounts** — `
|
|
101
|
-
returns (`ClientConfig`, `ClientHandle`, `PageConfig`,
|
|
102
|
-
`fetchClientConfig()` reads the agent's declared
|
|
103
|
-
which `
|
|
104
|
-
|
|
105
|
-
**The live call** — `useSession()` for the whole snapshot
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
191
|
+
The [API reference](https://alexkroman.github.io/agent/) groups by TypeScript
|
|
192
|
+
kind. This is the same surface grouped by what it is for.
|
|
193
|
+
|
|
194
|
+
**Mounts** — `mountClient()`, `mountPage()`, the config each takes and the
|
|
195
|
+
handle each returns (`ClientConfig`, `ClientHandle`, `PageConfig`,
|
|
196
|
+
`PageHandle`). `fetchClientConfig()` reads the agent's declared
|
|
197
|
+
`name`/`greeting` on a page, which `mountClient()` does for itself.
|
|
198
|
+
|
|
199
|
+
**The live call** — `useSession()` for the whole snapshot plus the controls
|
|
200
|
+
(`start`, `toggle`, `cancel`, `reset`, `resetState`, `restart`, `disconnect`,
|
|
201
|
+
`end`); `useSessionSelector()` for one slice, with `useSessionStatus()`,
|
|
202
|
+
`useSessionError()` and `useSessionActions()` as the narrow reads a custom
|
|
203
|
+
chrome repeats; `useUserTranscript()` for the caller's in-progress turn, which
|
|
204
|
+
keeps `null` (silent) and `""` (speech, no words yet) apart; `useConversation()`
|
|
205
|
+
for the interleaved transcript with nothing rendered. `createBrowserSession()` is
|
|
206
|
+
the same session as a plain store with an immutable snapshot per change, for a
|
|
207
|
+
non-React client. `SessionSnapshot`, `AgentState`, `ChatMessage`,
|
|
109
208
|
`ToolCallInfo`, `SessionError` and `SessionErrorCode` come with it.
|
|
110
209
|
|
|
111
210
|
**What the agent pushes** — `useAgentState()` (the `syncState` projection),
|
|
112
211
|
`useToolResult()` / `useToolCallStart()` (tool calls as they run), and
|
|
113
|
-
`useEvent()` (custom events from `ctx.send
|
|
212
|
+
`useEvent()` (custom events from `ctx.send`, and the one to prefer in new
|
|
213
|
+
code over reading tool results).
|
|
114
214
|
|
|
115
|
-
**Chat chrome** — `ChatView` is the whole surface; `
|
|
116
|
-
`
|
|
215
|
+
**Chat chrome** — `ChatView` is the whole surface; `ConsoleShell`,
|
|
216
|
+
`MessageList`, `Controls`, `Markdown`, `ToolCallRow`, `SessionErrorBanner`,
|
|
217
|
+
`StartScreen`, `SidebarLayout`, `AutoScroll`, `BulletList`, `Facts` and
|
|
117
218
|
`Button` are its parts, exported so a custom chrome reuses them rather than
|
|
118
219
|
approximating them. `AutoScroll` is the one to reach for directly: it is the
|
|
119
220
|
only scroll-pinning implementation here, and it needs a **bounded height**.
|
|
221
|
+
`AGENT_STATE_LABELS` and `WORKFLOW_STATUS_LABELS` are the default state words,
|
|
222
|
+
so a chrome overrides the one term it has a better word for.
|
|
120
223
|
|
|
121
224
|
**Forms** — `Form` collects typed values off the DOM on submit. `Field`,
|
|
122
225
|
`TextField`, `NumberField`, `TextAreaField`, `SelectField`, `CheckboxField`,
|
|
@@ -127,9 +230,11 @@ them from a schema. A plain `<input name="x">` works identically.
|
|
|
127
230
|
`useWorkflowSubmit()` / `useWorkflowStream()` (start one and watch it),
|
|
128
231
|
`useWorkflowRun()` (watch one by id), `useWorkflowRuns()` (the history),
|
|
129
232
|
`useWorkflowProgress()` (what a run has written), `useWorkflows()` (the
|
|
130
|
-
listing)
|
|
131
|
-
|
|
132
|
-
|
|
233
|
+
listing), `useDownloadUrl()` (an upload a run produced, as a URL a DOM element
|
|
234
|
+
accepts), `useRunKey()` (the storage-backed key a submission is recovered by).
|
|
235
|
+
`<WorkflowProgress>` and `<UploadProgressBar>` are the
|
|
236
|
+
rendered halves. `WorkflowRun`, `WorkflowSummary`, `WorkflowInputOf`,
|
|
237
|
+
`WorkflowOutputOf` and `isTerminal()` are the vocabulary.
|
|
133
238
|
|
|
134
239
|
**Theme** — `useTheme()` and `ClientTheme`.
|
|
135
240
|
|
|
@@ -140,23 +245,8 @@ of them something a `client.tsx` reaches for:
|
|
|
140
245
|
|
|
141
246
|
| Subpath | Reach for it when |
|
|
142
247
|
| --- | --- |
|
|
143
|
-
| `/client-dir` | serving the prebuilt default client from Node — `defaultClientDir()`, the filesystem path `
|
|
144
|
-
| `/internal` | never, from application code: the plumbing `
|
|
145
|
-
|
|
146
|
-
## Hooks
|
|
147
|
-
|
|
148
|
-
Inside components rendered by `client()`:
|
|
149
|
-
|
|
150
|
-
- `useSession()` — connection state, transcript, and the call controls:
|
|
151
|
-
`start`, `toggle`, `cancel`, `reset`, `resetState`, `disconnect`, `end`.
|
|
152
|
-
- `useAgentState<T>()` — the agent's `syncState` projection, live.
|
|
153
|
-
- `useToolResult(name, cb)` / `useToolCallStart(name, cb)` — observe tool
|
|
154
|
-
calls as they run (e.g. to render a card per result).
|
|
155
|
-
- `useEvent(name, cb)` — custom events the agent pushes with `ctx.send`.
|
|
156
|
-
- `useTheme()` — the resolved theme colors for custom components.
|
|
157
|
-
|
|
158
|
-
For a non-React integration, `createSessionCore()` exposes the same session
|
|
159
|
-
as a plain store with an immutable snapshot per change.
|
|
248
|
+
| `/client-dir` | serving the prebuilt default client from Node — `defaultClientDir()`, the filesystem path `createRuntimeServer({ clientDir })` wants |
|
|
249
|
+
| `/internal` | never, from application code: the plumbing `mountClient()` installs for itself (the session and theme providers, the default shell's URL chips, the tool-config context, the pre-connection lookup). Not a public API and not covered by semver |
|
|
160
250
|
|
|
161
251
|
## Documentation
|
|
162
252
|
|
|
@@ -51,5 +51,28 @@ function primaryTint(primary, surface, pct) {
|
|
|
51
51
|
function mixInto(color, ground, pct) {
|
|
52
52
|
return `color-mix(in srgb, ${color} ${pct}%, ${ground})`;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The focus ring, as the class half of a pair.
|
|
56
|
+
*
|
|
57
|
+
* The ring is the only thing a keyboard user gets, and it has regressed to
|
|
58
|
+
* invisible twice — `outline-none` shipped here with no replacement, on the
|
|
59
|
+
* button and then on the URL chips, which is WCAG 2.4.7 on every control in the
|
|
60
|
+
* default client. Written as a SHORTHAND so it cannot be undone by Tailwind's
|
|
61
|
+
* shared `--tw-outline-style` variable.
|
|
62
|
+
*
|
|
63
|
+
* It is inert on its own: an outline with no colour draws nothing, so a control
|
|
64
|
+
* using this must also spread {@link focusRingStyle}. Naming the two together
|
|
65
|
+
* is what stops the next control shipping one without the other.
|
|
66
|
+
*/
|
|
67
|
+
const FOCUS_RING = "outline-none focus-visible:[outline:2px_solid] focus-visible:[outline-offset:2px]";
|
|
68
|
+
/**
|
|
69
|
+
* The style half of {@link FOCUS_RING} — the colour the outline draws in.
|
|
70
|
+
*
|
|
71
|
+
* A real CSS property rather than a variant class, so the `focus-visible` rule
|
|
72
|
+
* only has to turn the outline on.
|
|
73
|
+
*/
|
|
74
|
+
function focusRingStyle(primary) {
|
|
75
|
+
return { outlineColor: primary };
|
|
76
|
+
}
|
|
54
77
|
//#endregion
|
|
55
|
-
export {
|
|
78
|
+
export { inkTint as a, focusRingStyle as i, FOCUS_RING as n, primaryTint as o, THINKING_COLOR as r, ERROR_COLOR as t };
|
package/dist/_recover-run.d.ts
CHANGED
|
@@ -55,8 +55,8 @@ import type { WorkflowApi } from "./workflow-client.ts";
|
|
|
55
55
|
export type RecoverRunOptions = {
|
|
56
56
|
/** The workflow whose runs are indexed under `key`. */
|
|
57
57
|
workflow: string;
|
|
58
|
-
/** The caller's handle on its own run
|
|
59
|
-
key: string
|
|
58
|
+
/** The caller's handle on its own run. `useDefaultRunKey` always supplies one. */
|
|
59
|
+
key: string;
|
|
60
60
|
/** Whether the caller asked for this at all — `recover` at the call site. */
|
|
61
61
|
enabled: boolean;
|
|
62
62
|
/** The stable getter from `useWorkflowApiRef`. */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state a form submission carries, for both hooks that make one.
|
|
3
|
+
*
|
|
4
|
+
* `useWorkflowSubmit` and `useWorkflowStream` stay separate hooks — they differ
|
|
5
|
+
* in WHEN the run is created relative to the bytes, which is the whole reason
|
|
6
|
+
* the streaming one exists — but everything around that ordering was written
|
|
7
|
+
* twice: the same four `useState`s, the same ref holding the live submission,
|
|
8
|
+
* the same prologue, the same superseded-submission guard, the same `reset`,
|
|
9
|
+
* and the same pause pair. Roughly forty-five lines and thirty of comment.
|
|
10
|
+
*
|
|
11
|
+
* The copy had already diverged in ways nobody chose, which is the argument for
|
|
12
|
+
* this module rather than for tidiness. What is here is only the scaffold; each
|
|
13
|
+
* hook still owns its own `submit` body.
|
|
14
|
+
*
|
|
15
|
+
* ## The superseded-submission rule
|
|
16
|
+
*
|
|
17
|
+
* A submission that has been replaced owns none of this state any more. Its
|
|
18
|
+
* walk unwinds AFTER the next one has already set `starting`, so clearing
|
|
19
|
+
* unconditionally in a `finally` would report the live submission as finished
|
|
20
|
+
* and drop the bar it is drawing. {@link SubmissionState.end} is that check,
|
|
21
|
+
* made once here instead of at each hook's own `finally`.
|
|
22
|
+
*/
|
|
23
|
+
import type { Dispatch, SetStateAction } from "react";
|
|
24
|
+
import type { UploadGate } from "./_upload-session.ts";
|
|
25
|
+
import type { UploadStatus } from "./use-workflow-form.ts";
|
|
26
|
+
/**
|
|
27
|
+
* What a hook installs as its live submission.
|
|
28
|
+
*
|
|
29
|
+
* Generic over the token because the two hooks hold different things —
|
|
30
|
+
* `useWorkflowSubmit` an `UploadSession` (the walk needs its id maps),
|
|
31
|
+
* `useWorkflowStream` a bare gate in a wrapper — and all this module needs from
|
|
32
|
+
* either is the gate, which is what a supersede and a `reset` cancel.
|
|
33
|
+
*/
|
|
34
|
+
export type SubmissionToken = {
|
|
35
|
+
gate: UploadGate;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The stable half: everything a `submit` body calls.
|
|
39
|
+
*
|
|
40
|
+
* Split from the values deliberately. A `submit` is a `useCallback` and this is
|
|
41
|
+
* one of its dependencies, so a bag that changed identity whenever `upload`
|
|
42
|
+
* changed would rebuild `submit` — and with it the `onSubmit` handed to
|
|
43
|
+
* `<Form>` — on every progress report. Every member here is a `useState`
|
|
44
|
+
* setter or a `useCallback` with no changing dependency, so the bag is stable
|
|
45
|
+
* for the component's life.
|
|
46
|
+
*/
|
|
47
|
+
export type SubmissionActions<S extends SubmissionToken> = {
|
|
48
|
+
/**
|
|
49
|
+
* The full dispatch, not a plain setter: `useWorkflowSubmit`'s recovery
|
|
50
|
+
* lookup writes `(current) => current ?? found`, so that a run the person has
|
|
51
|
+
* already started this session is never displaced by one read back from the
|
|
52
|
+
* key index.
|
|
53
|
+
*/
|
|
54
|
+
setRunId: Dispatch<SetStateAction<string | undefined>>;
|
|
55
|
+
setStartError: Dispatch<SetStateAction<string | undefined>>;
|
|
56
|
+
setUpload: Dispatch<SetStateAction<UploadStatus | undefined>>;
|
|
57
|
+
/**
|
|
58
|
+
* Open a submission: supersede whatever was in flight and install `token`.
|
|
59
|
+
*
|
|
60
|
+
* Cancels the previous gate first, so a stale submission cannot park the new
|
|
61
|
+
* one, and clears the previous run id BEFORE the request rather than when it
|
|
62
|
+
* returns — a finished result sitting under a form that is already submitting
|
|
63
|
+
* again is the one wrong answer this can give, and it looks like a right one.
|
|
64
|
+
*/
|
|
65
|
+
begin: (token: S) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Close `token`, if it is still the live one. See "The superseded-submission
|
|
68
|
+
* rule" above.
|
|
69
|
+
*/
|
|
70
|
+
end: (token: S) => void;
|
|
71
|
+
/** Put the form back: abandon the bytes and drop the result. */
|
|
72
|
+
reset: () => void;
|
|
73
|
+
/** Park the bytes and say so on the bar. */
|
|
74
|
+
pauseUpload: () => void;
|
|
75
|
+
/** Send the rest. */
|
|
76
|
+
resumeUpload: () => void;
|
|
77
|
+
};
|
|
78
|
+
/** What {@link useSubmissionState} hands back. */
|
|
79
|
+
export type SubmissionState<S extends SubmissionToken> = {
|
|
80
|
+
/** The run this submission started, once it has one. */
|
|
81
|
+
runId: string | undefined;
|
|
82
|
+
/** Whether a submission is in flight — the POST and the bytes, not the run. */
|
|
83
|
+
starting: boolean;
|
|
84
|
+
/** The submission's own failure, as against the run's. */
|
|
85
|
+
startError: string | undefined;
|
|
86
|
+
/** The bar's state, or nothing when there is no upload to describe. */
|
|
87
|
+
upload: UploadStatus | undefined;
|
|
88
|
+
/** See {@link SubmissionActions} for why these are their own bag. */
|
|
89
|
+
actions: SubmissionActions<S>;
|
|
90
|
+
};
|
|
91
|
+
/** The shared submission scaffold. See the module doc. */
|
|
92
|
+
export declare function useSubmissionState<S extends SubmissionToken>(): SubmissionState<S>;
|
package/dist/_upload-files.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* one file rather than walking an input and shares only the gate underneath both
|
|
11
11
|
* (`_upload-session.ts`).
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
13
|
+
import type { UploadParallelOption } from "@alexkroman1/aai/workflow-api";
|
|
14
14
|
import { type UploadGate } from "./_upload-session.ts";
|
|
15
15
|
import type { UploadStatus } from "./use-workflow-form.ts";
|
|
16
16
|
import type { WorkflowApi } from "./workflow-client.ts";
|
|
@@ -80,4 +80,4 @@ export declare function createUploadSession(workflow: string): UploadSession;
|
|
|
80
80
|
* last byte lands, so the incomplete record a streamed upload leaves along the
|
|
81
81
|
* way is one nobody reads.
|
|
82
82
|
*/
|
|
83
|
-
export declare function uploadFiles(api: WorkflowApi, input: unknown, report: (status: UploadStatus) => void, parallel:
|
|
83
|
+
export declare function uploadFiles(api: WorkflowApi, input: unknown, report: (status: UploadStatus) => void, parallel: UploadParallelOption | undefined, session: UploadSession): Promise<unknown>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate-limiting the upload progress report to what a bar can actually show.
|
|
3
|
+
*
|
|
4
|
+
* `onProgress` fires on every `XMLHttpRequest` progress event, and the parts
|
|
5
|
+
* uploader runs `UPLOAD_PART_CONCURRENCY` of those at once — so the raw rate is
|
|
6
|
+
* on the order of a hundred reports a second, sustained for the whole upload,
|
|
7
|
+
* which for the multi-hundred-megabyte files this feature exists for is
|
|
8
|
+
* minutes. Each one is a `setState`, and each re-renders the enclosing `<Form>`
|
|
9
|
+
* subtree: every field `<WorkflowFields>` rendered, the bar, and
|
|
10
|
+
* `<WorkflowProgress>` beside it. Progress events arrive as separate
|
|
11
|
+
* macrotasks, so React's automatic batching does not merge them.
|
|
12
|
+
*
|
|
13
|
+
* The bar has at most ~100 visually distinct states. Dropping a report that
|
|
14
|
+
* would render identically is therefore free, and it is the whole mechanism —
|
|
15
|
+
* no timer, so the LAST report of a burst is never left unrendered waiting for
|
|
16
|
+
* a window to close.
|
|
17
|
+
*/
|
|
18
|
+
import type { UploadStatus } from "./use-workflow-form.ts";
|
|
19
|
+
/**
|
|
20
|
+
* Wrap an `UploadStatus` setter so redundant reports never reach React.
|
|
21
|
+
*
|
|
22
|
+
* Clearing the status (`undefined`) always passes and resets the comparison —
|
|
23
|
+
* it ends one file's bar, and the next file must not be coalesced against the
|
|
24
|
+
* previous one's last frame.
|
|
25
|
+
*/
|
|
26
|
+
export declare function coalesceUploadReports(set: (status: UploadStatus | undefined) => void): (status: UploadStatus | undefined) => void;
|
|
@@ -11,7 +11,7 @@ function truncate(s, max = 80) {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* The page this UI is served from — the agent's base URL when none is
|
|
14
|
-
* configured explicitly. One definition so `
|
|
14
|
+
* configured explicitly. One definition so `mountClient()`'s default and the
|
|
15
15
|
* shareable-URL chip can never disagree about which agent the page is.
|
|
16
16
|
*/
|
|
17
17
|
function pageBaseUrl() {
|
|
@@ -22,8 +22,8 @@ function pageBaseUrl() {
|
|
|
22
22
|
* Set the document title, when there is one to set and a document to set it on.
|
|
23
23
|
*
|
|
24
24
|
* Both mounts do this and neither may clobber a title the page's own HTML
|
|
25
|
-
* declared — `
|
|
26
|
-
* to show the name in, `
|
|
25
|
+
* declared — `mountClient()`'s custom-component tier because there is no shell header
|
|
26
|
+
* to show the name in, `mountPage()` because there is no shell at all. One copy, for
|
|
27
27
|
* the reason `resolveContainer` and `mountRoot` are shared: the rule is the
|
|
28
28
|
* same, so the two mounts must not be able to disagree about it.
|
|
29
29
|
*/
|
package/dist/_utils.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function tryParseJSON(str: string | undefined): unknown;
|
|
|
4
4
|
export declare function truncate(s: string, max?: number): string;
|
|
5
5
|
/**
|
|
6
6
|
* The page this UI is served from — the agent's base URL when none is
|
|
7
|
-
* configured explicitly. One definition so `
|
|
7
|
+
* configured explicitly. One definition so `mountClient()`'s default and the
|
|
8
8
|
* shareable-URL chip can never disagree about which agent the page is.
|
|
9
9
|
*/
|
|
10
10
|
export declare function pageBaseUrl(): string;
|
|
@@ -12,8 +12,8 @@ export declare function pageBaseUrl(): string;
|
|
|
12
12
|
* Set the document title, when there is one to set and a document to set it on.
|
|
13
13
|
*
|
|
14
14
|
* Both mounts do this and neither may clobber a title the page's own HTML
|
|
15
|
-
* declared — `
|
|
16
|
-
* to show the name in, `
|
|
15
|
+
* declared — `mountClient()`'s custom-component tier because there is no shell header
|
|
16
|
+
* to show the name in, `mountPage()` because there is no shell at all. One copy, for
|
|
17
17
|
* the reason `resolveContainer` and `mountRoot` are shared: the rule is the
|
|
18
18
|
* same, so the two mounts must not be able to disagree about it.
|
|
19
19
|
*/
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guarded access to the browser's key-value stores.
|
|
3
|
+
*
|
|
4
|
+
* Three modules in this package remember something across a load — the session
|
|
5
|
+
* id (`session-resume-store.ts`), the upload ids a form has minted
|
|
6
|
+
* (`_upload-recall.ts`) and a page's run correlation key (`use-run-key.ts`) —
|
|
7
|
+
* and each had written out the same two rules by hand, eight `try`/`catch`
|
|
8
|
+
* blocks and two URL-derived key builders between them. The tell was that each
|
|
9
|
+
* module's doc cited the others as precedent, which is a convention held by
|
|
10
|
+
* authors reading each other's comments rather than by code.
|
|
11
|
+
*
|
|
12
|
+
* Both rules are here once:
|
|
13
|
+
*
|
|
14
|
+
* - **Every access is guarded.** Storage THROWS outright in some contexts
|
|
15
|
+
* (Safari private mode, an iframe blocked by policy) and is ABSENT in others
|
|
16
|
+
* (any server-side render) — and reaching for the PROPERTY is itself what
|
|
17
|
+
* throws, before a method is called, which is why the guard has to wrap the
|
|
18
|
+
* lookup and not just the call. A caller that cannot remember must degrade to
|
|
19
|
+
* the behaviour it would have had anyway, never fail to render.
|
|
20
|
+
* - **A slot is namespaced by the page's own URL**, so two agents served from
|
|
21
|
+
* one origin — every deployed agent, at `/:slug/` — cannot inherit each
|
|
22
|
+
* other's state.
|
|
23
|
+
*/
|
|
24
|
+
/** Which of the two stores; they differ only in how long an entry outlives the tab. */
|
|
25
|
+
export type StorageKind = "session" | "local";
|
|
26
|
+
/** The stored value, or undefined — for a missing entry and an absent store alike. */
|
|
27
|
+
export declare function storageGet(kind: StorageKind, key: string): string | undefined;
|
|
28
|
+
/** Remember a value. A store that refuses is a no-op, never a throw. */
|
|
29
|
+
export declare function storageSet(kind: StorageKind, key: string, value: string): void;
|
|
30
|
+
/** Forget a value. Nothing stored and no store are the same outcome. */
|
|
31
|
+
export declare function storageRemove(kind: StorageKind, key: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* A storage key namespaced by a URL.
|
|
34
|
+
*
|
|
35
|
+
* `target` is resolved against the document, so a relative path ("./", the
|
|
36
|
+
* default-client case) and the absolute form of the same agent agree on one
|
|
37
|
+
* key.
|
|
38
|
+
*
|
|
39
|
+
* @param prefix - The owning module's namespace, e.g. `"aai:session:"`.
|
|
40
|
+
* @param target - What to resolve — an agent's `platformUrl`, or `"./"` for the
|
|
41
|
+
* page's own directory.
|
|
42
|
+
*/
|
|
43
|
+
export declare function urlSlot(prefix: string, target: string): string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Its own module because both submit hooks need the identical answer and then do
|
|
5
5
|
* two different things with it — `useWorkflowSubmit` stores each file and passes
|
|
6
|
-
* its id, `useWorkflowStream`
|
|
6
|
+
* its id, `useWorkflowStream` mints the id first so the run can start on it.
|
|
7
7
|
* A second copy of this predicate would be a form field that one hook treats as a
|
|
8
8
|
* file and the other does not, which is invisible until the run reads the wrong
|
|
9
9
|
* kind of string.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One word for where the CALL has got to, for each {@link AgentState} there is.
|
|
3
|
+
*
|
|
4
|
+
* The sibling of `WORKFLOW_STATUS_LABELS` next door, for the same reason and
|
|
5
|
+
* with the same shape. Three custom chromes carried three renderings of this
|
|
6
|
+
* one seven-member union, all written as a chain of ternaries over the raw
|
|
7
|
+
* member: one mapped `speaking` to "Narrating" and fell through to "Idle" for
|
|
8
|
+
* everything it had not listed, one shouted (`thinking` → "PROCESSING"), and
|
|
9
|
+
* the third rendered the enum member itself — so a caller of that agent read a
|
|
10
|
+
* lowercase `disconnected` in the header of a page nobody had decided to say
|
|
11
|
+
* that on.
|
|
12
|
+
*
|
|
13
|
+
* The exhaustiveness argument is the one that makes this worth exporting rather
|
|
14
|
+
* than documenting. A `Record` over {@link AgentState} means a state ADDED to
|
|
15
|
+
* the union is a compile error HERE, in one place every page inherits, instead
|
|
16
|
+
* of falling through each page's own `: "Idle"` tail into whichever word that
|
|
17
|
+
* page happened to end on — which is a silent wrong label, not a missing one. A
|
|
18
|
+
* page overriding one member keeps that: spreading a complete record cannot
|
|
19
|
+
* drop a key.
|
|
20
|
+
*
|
|
21
|
+
* There is deliberately no `StatusDot` component beside it. The dot markup and
|
|
22
|
+
* its palette are what each chrome exists to look like — a CRT's glow, a
|
|
23
|
+
* dispatch board's alert colours — and a component would take the one part
|
|
24
|
+
* that is genuinely shared (the words) hostage to the part that is not.
|
|
25
|
+
*/
|
|
26
|
+
import type { AgentState } from "./types.ts";
|
|
27
|
+
/**
|
|
28
|
+
* The default label per {@link AgentState}.
|
|
29
|
+
*
|
|
30
|
+
* Override the ones your page has a better word for and keep the rest:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* import type { AgentState } from "@alexkroman1/aai-ui";
|
|
34
|
+
* import { AGENT_STATE_LABELS } from "@alexkroman1/aai-ui";
|
|
35
|
+
*
|
|
36
|
+
* // A dispatch board that shouts, and renames one state.
|
|
37
|
+
* const STATE_LABEL = { ...AGENT_STATE_LABELS, thinking: "Processing" };
|
|
38
|
+
* const shout = (s: AgentState) => STATE_LABEL[s].toUpperCase();
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* **Sentence case, deliberately.** A template that wants caps applies its own
|
|
42
|
+
* `.toUpperCase()`, and a template that wants Title Case is already there;
|
|
43
|
+
* shipping the shouted form instead would leave the two chromes that do not
|
|
44
|
+
* shout with a string they have to un-shout, which no case transform does
|
|
45
|
+
* correctly.
|
|
46
|
+
*
|
|
47
|
+
* Two wordings are decisions rather than transliterations of the member name:
|
|
48
|
+
*
|
|
49
|
+
* - `disconnected` is **"Idle"**. It is the state a session is in BEFORE it has
|
|
50
|
+
* ever started as well as after it ends, so it is the first word most callers
|
|
51
|
+
* see; "Disconnected" reads as a fault on a page where nothing has gone
|
|
52
|
+
* wrong yet. Both chromes that mapped this state by hand chose "Idle" too.
|
|
53
|
+
* - `connecting` and `thinking` carry an ellipsis, `listening` and `speaking`
|
|
54
|
+
* do not. The first two are waits with nothing for the caller to do; the
|
|
55
|
+
* other two describe someone actually talking. Same distinction
|
|
56
|
+
* `WORKFLOW_STATUS_LABELS` draws with its one "Working…".
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export declare const AGENT_STATE_LABELS: Readonly<Record<AgentState, string>>;
|