@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/dist/index.js
CHANGED
|
@@ -1,25 +1,189 @@
|
|
|
1
|
-
import { n as fetchClientConfig } from "./client-config-
|
|
2
|
-
import { i as AutoScroll, n as Markdown, r as useConversation, t as MessageList } from "./message-list-
|
|
3
|
-
import { ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
|
|
4
|
-
import {
|
|
1
|
+
import { n as fetchClientConfig } from "./client-config-DJQHnYjm.js";
|
|
2
|
+
import { i as AutoScroll, n as Markdown, r as useConversation, t as MessageList } from "./message-list-CdOnSh5m.js";
|
|
3
|
+
import { ThemeProvider, useSession, useSessionActions, useSessionError, useSessionSelector, useSessionStatus, useTheme } from "./context.js";
|
|
4
|
+
import { a as inkTint, i as focusRingStyle, n as FOCUS_RING } from "./_colors-CpZO-88A.js";
|
|
5
5
|
import { Button } from "./components/button.js";
|
|
6
|
-
import { n as ConsoleShell, t as ChatView } from "./chat-view-
|
|
7
|
-
import { n as setPageTitle } from "./_utils-
|
|
6
|
+
import { n as ConsoleShell, r as SessionErrorBanner, t as ChatView } from "./chat-view-C_3T7Ln8.js";
|
|
7
|
+
import { n as setPageTitle } from "./_utils-DnQDM9Uy.js";
|
|
8
8
|
import { Controls } from "./components/controls.js";
|
|
9
|
-
import { n as useUserTranscript } from "./use-user-transcript-
|
|
10
|
-
import { n as ToolCallRow } from "./tool-call-block-
|
|
9
|
+
import { n as useUserTranscript } from "./use-user-transcript-uyHhzy4d.js";
|
|
10
|
+
import { n as ToolCallRow } from "./tool-call-block-C2t_5fpp.js";
|
|
11
11
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
12
12
|
import { StartScreen } from "./components/start-screen.js";
|
|
13
|
-
import { a as useWorkflowProgress, c as isTerminal, o as useWorkflowApiRef, r as useWorkflowRun, s as createWorkflowApi } from "./use-workflow-run-
|
|
14
|
-
import { t as
|
|
15
|
-
import {
|
|
13
|
+
import { a as useWorkflowProgress, c as isTerminal, o as useWorkflowApiRef, r as useWorkflowRun, s as createWorkflowApi } from "./use-workflow-run-CP2ekKPV.js";
|
|
14
|
+
import { i as urlSlot, n as storageGet, r as storageSet, t as createBrowserSession } from "./session-core-gwePM95B.js";
|
|
15
|
+
import { mountClient, mountRoot, resolveContainer } from "./define-client.js";
|
|
16
16
|
import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
17
17
|
import clsx from "clsx";
|
|
18
18
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
19
|
-
import { createContext, createElement, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
19
|
+
import { createContext, createElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
20
20
|
import { errorMessage } from "@alexkroman1/aai";
|
|
21
21
|
import { formatBytes, isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
22
22
|
import { createEpoch } from "@alexkroman1/aai/internal";
|
|
23
|
+
//#region src/agent-state-labels.ts
|
|
24
|
+
/**
|
|
25
|
+
* The default label per {@link AgentState}.
|
|
26
|
+
*
|
|
27
|
+
* Override the ones your page has a better word for and keep the rest:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* import type { AgentState } from "@alexkroman1/aai-ui";
|
|
31
|
+
* import { AGENT_STATE_LABELS } from "@alexkroman1/aai-ui";
|
|
32
|
+
*
|
|
33
|
+
* // A dispatch board that shouts, and renames one state.
|
|
34
|
+
* const STATE_LABEL = { ...AGENT_STATE_LABELS, thinking: "Processing" };
|
|
35
|
+
* const shout = (s: AgentState) => STATE_LABEL[s].toUpperCase();
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* **Sentence case, deliberately.** A template that wants caps applies its own
|
|
39
|
+
* `.toUpperCase()`, and a template that wants Title Case is already there;
|
|
40
|
+
* shipping the shouted form instead would leave the two chromes that do not
|
|
41
|
+
* shout with a string they have to un-shout, which no case transform does
|
|
42
|
+
* correctly.
|
|
43
|
+
*
|
|
44
|
+
* Two wordings are decisions rather than transliterations of the member name:
|
|
45
|
+
*
|
|
46
|
+
* - `disconnected` is **"Idle"**. It is the state a session is in BEFORE it has
|
|
47
|
+
* ever started as well as after it ends, so it is the first word most callers
|
|
48
|
+
* see; "Disconnected" reads as a fault on a page where nothing has gone
|
|
49
|
+
* wrong yet. Both chromes that mapped this state by hand chose "Idle" too.
|
|
50
|
+
* - `connecting` and `thinking` carry an ellipsis, `listening` and `speaking`
|
|
51
|
+
* do not. The first two are waits with nothing for the caller to do; the
|
|
52
|
+
* other two describe someone actually talking. Same distinction
|
|
53
|
+
* `WORKFLOW_STATUS_LABELS` draws with its one "Working…".
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
const AGENT_STATE_LABELS = {
|
|
58
|
+
disconnected: "Idle",
|
|
59
|
+
connecting: "Connecting…",
|
|
60
|
+
ready: "Ready",
|
|
61
|
+
listening: "Listening",
|
|
62
|
+
thinking: "Thinking…",
|
|
63
|
+
speaking: "Speaking",
|
|
64
|
+
error: "Error"
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/components/bullet-list.tsx
|
|
68
|
+
/** @jsxImportSource react */
|
|
69
|
+
/**
|
|
70
|
+
* A disc-bulleted list of short strings — a run's key points, findings, risks.
|
|
71
|
+
*
|
|
72
|
+
* Five pages had written this, byte-identical apart from a `text-sm` suffix on
|
|
73
|
+
* two of them, and all five had the same two defects. Both are the reason this
|
|
74
|
+
* is a component rather than four lines a page repeats:
|
|
75
|
+
*
|
|
76
|
+
* - **All five keyed by the string itself, and these lists are MODEL OUTPUT.**
|
|
77
|
+
* Two identical bullets are entirely plausible — a summariser that repeats
|
|
78
|
+
* itself is a bad summary, not a bad program — and a repeated string is then
|
|
79
|
+
* a duplicate `key`: React warns, and the two `<li>`s contend for one slot in
|
|
80
|
+
* the reconciliation. What is keyed here instead is the content PLUS how many
|
|
81
|
+
* times that content has already appeared in this list, which is unique by
|
|
82
|
+
* construction and unchanged by a re-render that did not change the text.
|
|
83
|
+
* (Position alone would also be sound — these lists are replaced wholesale by
|
|
84
|
+
* each new output and never reordered — but it is what `noArrayIndexKey`
|
|
85
|
+
* exists to talk you out of, and a unique key is one `Map` away, so there is
|
|
86
|
+
* no reason to spend a lint suppression on it.)
|
|
87
|
+
* - **Three of the five rendered an empty `<ul>` under a heading.** Two had
|
|
88
|
+
* hand-rolled `if (items.length === 0) return null` and three had not, so the
|
|
89
|
+
* same absent field was "nothing" on two pages and a stray heading with a
|
|
90
|
+
* void under it on three. Emptiness renders NOTHING here, `title` included:
|
|
91
|
+
* a heading over no bullets is a claim the run did not make.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* import { BulletList } from "@alexkroman1/aai-ui";
|
|
96
|
+
*
|
|
97
|
+
* function Findings({ risks }: { risks: string[] }) {
|
|
98
|
+
* return <BulletList title="Risks" items={risks} size="sm" />;
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @param props - Bullet-list props.
|
|
103
|
+
*
|
|
104
|
+
* @public
|
|
105
|
+
*/
|
|
106
|
+
function BulletList({ items, title, size = "base", className }) {
|
|
107
|
+
if (items.length === 0) return null;
|
|
108
|
+
const seen = /* @__PURE__ */ new Map();
|
|
109
|
+
const list = /* @__PURE__ */ jsx("ul", {
|
|
110
|
+
className: clsx("flex list-disc flex-col gap-1 pl-5", size === "sm" && "text-sm", className),
|
|
111
|
+
children: items.map((item) => {
|
|
112
|
+
const nth = seen.get(item) ?? 0;
|
|
113
|
+
seen.set(item, nth + 1);
|
|
114
|
+
return /* @__PURE__ */ jsx("li", { children: item }, `${nth}:${item}`);
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
const heading = title === void 0 || title === null || title === false ? void 0 : title;
|
|
118
|
+
if (heading === void 0) return list;
|
|
119
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
120
|
+
className: "flex flex-col gap-1",
|
|
121
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
122
|
+
className: "text-sm font-medium opacity-70",
|
|
123
|
+
children: heading
|
|
124
|
+
}), list]
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/components/facts.tsx
|
|
129
|
+
/** @jsxImportSource react */
|
|
130
|
+
/** What goes between two facts. A separator, not a bullet — U+00B7, not U+2022. */
|
|
131
|
+
const FACT_SEPARATOR = " · ";
|
|
132
|
+
/**
|
|
133
|
+
* A muted line of run facts, joined by `·` — "6 segments · 12:04 of audio ·
|
|
134
|
+
* 1,840 words".
|
|
135
|
+
*
|
|
136
|
+
* Nine pages had written this by hand under four different typographies for
|
|
137
|
+
* one role, two of them (`call-audit` and `spoken-summary`) byte-identical down
|
|
138
|
+
* to the payload. Three things it takes off the caller:
|
|
139
|
+
*
|
|
140
|
+
* - **The separator cannot be forgotten, and neither can the space around it.**
|
|
141
|
+
* Four of the nine carried a literal `{" "}` at the end of a line, because
|
|
142
|
+
* Prettier's wrap ate the space that made `· ` read as a separator rather
|
|
143
|
+
* than as punctuation glued to the next word. A line that is correct only
|
|
144
|
+
* because somebody remembered an invisible JSX expression is exactly the
|
|
145
|
+
* thing a component should own.
|
|
146
|
+
* - **A fact worth omitting is omitted, and by the caller's own condition.**
|
|
147
|
+
* The hand-written shape for a conditional fact was to splice the separator
|
|
148
|
+
* into the string — `{x ? \` · budget exhausted\` : ""}` — which puts the
|
|
149
|
+
* punctuation in two places and gets the leading separator wrong the moment
|
|
150
|
+
* the fact before it also disappears. Passing the condition and letting this
|
|
151
|
+
* drop it keeps the separator in one place.
|
|
152
|
+
* - **A line with nothing left to say renders NOTHING.** With every fact
|
|
153
|
+
* conditional, the alternative is a muted empty row, or a bare `·`.
|
|
154
|
+
*
|
|
155
|
+
* The facts are JOINED into one string rather than interleaved as elements, and
|
|
156
|
+
* that is why the prop is text: joined, there is no per-fact `key` to invent —
|
|
157
|
+
* the same reasoning `WorkflowProgress` gives for its log lines. A line that
|
|
158
|
+
* genuinely needs an element in it (a link) wants its own markup.
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```tsx
|
|
162
|
+
* import { Facts } from "@alexkroman1/aai-ui";
|
|
163
|
+
*
|
|
164
|
+
* function RunFacts({ words, cut }: { words: number; cut: number }) {
|
|
165
|
+
* return <Facts size="xs" items={[`${words} words`, cut > 0 && `${cut} blind cuts`]} />;
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* @param props - Facts-line props.
|
|
170
|
+
*
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
function Facts({ items, size = "sm", as = "p", className }) {
|
|
174
|
+
const shown = items.filter((item) => item !== false && item !== null && item !== void 0 && item !== "");
|
|
175
|
+
if (shown.length === 0) return null;
|
|
176
|
+
const text = shown.join(FACT_SEPARATOR);
|
|
177
|
+
const classes = clsx(size === "xs" ? "text-xs opacity-60" : "text-sm opacity-70", className);
|
|
178
|
+
return as === "span" ? /* @__PURE__ */ jsx("span", {
|
|
179
|
+
className: classes,
|
|
180
|
+
children: text
|
|
181
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
182
|
+
className: classes,
|
|
183
|
+
children: text
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
23
187
|
//#region src/components/_form-readiness.ts
|
|
24
188
|
/**
|
|
25
189
|
* Whether a form's own fields exist yet.
|
|
@@ -47,7 +211,7 @@ import { createEpoch } from "@alexkroman1/aai/internal";
|
|
|
47
211
|
* one describes an element that EXISTS. So this is a context rather than an
|
|
48
212
|
* attribute, and it carries the one fact a DOM read cannot recover.
|
|
49
213
|
*
|
|
50
|
-
* A form with no such children is ready by definition — `
|
|
214
|
+
* A form with no such children is ready by definition — `useFormReadiness`
|
|
51
215
|
* outside a provider reports nothing pending, so every hand-written form is
|
|
52
216
|
* unaffected and `Form` keeps working outside this package.
|
|
53
217
|
*/
|
|
@@ -173,7 +337,7 @@ function readFiles(files, read) {
|
|
|
173
337
|
if (read === "upload") return Promise.resolve([...files]);
|
|
174
338
|
return Promise.all(files.map((file) => describeFile(file, read)));
|
|
175
339
|
}
|
|
176
|
-
/** The `data-aai-read` attribute as a {@link
|
|
340
|
+
/** The `data-aai-read` attribute as a {@link FileReadMode}, defaulting to `"none"`. */
|
|
177
341
|
function readMode(raw) {
|
|
178
342
|
return raw === "text" || raw === "dataUrl" || raw === "upload" ? raw : "none";
|
|
179
343
|
}
|
|
@@ -274,12 +438,12 @@ function Field({ label, hint, htmlFor, className, children }) {
|
|
|
274
438
|
function useControlProps() {
|
|
275
439
|
const theme = useTheme();
|
|
276
440
|
return {
|
|
277
|
-
className: clsx("w-full rounded-aai border px-3 py-2 text-sm font-aai",
|
|
441
|
+
className: clsx("w-full rounded-aai border px-3 py-2 text-sm font-aai", FOCUS_RING, "disabled:cursor-not-allowed disabled:opacity-50"),
|
|
278
442
|
style: {
|
|
279
443
|
background: theme.surface,
|
|
280
444
|
color: theme.text,
|
|
281
445
|
borderColor: theme.border,
|
|
282
|
-
|
|
446
|
+
...focusRingStyle(theme.primary)
|
|
283
447
|
}
|
|
284
448
|
};
|
|
285
449
|
}
|
|
@@ -311,16 +475,14 @@ function useFileControlProps() {
|
|
|
311
475
|
};
|
|
312
476
|
}
|
|
313
477
|
/**
|
|
314
|
-
*
|
|
478
|
+
* `TextField` and `NumberField`, which differ only in the `type` they set.
|
|
315
479
|
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* @public
|
|
480
|
+
* Internal, because the two stay separate PUBLIC exports: each is named in the
|
|
481
|
+
* `components` capability contract and in the API report, and a caller writes
|
|
482
|
+
* the one whose value shape it wants — `NumberField` is the one that
|
|
483
|
+
* contributes a number to {@link FormValues}.
|
|
322
484
|
*/
|
|
323
|
-
function
|
|
485
|
+
function InputField({ defaultType, name, label, hint, className, ...rest }) {
|
|
324
486
|
const id = useId();
|
|
325
487
|
const control = useControlProps();
|
|
326
488
|
return /* @__PURE__ */ jsx(Field, {
|
|
@@ -331,13 +493,29 @@ function TextField({ name, label, hint, className, ...rest }) {
|
|
|
331
493
|
children: /* @__PURE__ */ jsx("input", {
|
|
332
494
|
id,
|
|
333
495
|
name,
|
|
334
|
-
type:
|
|
496
|
+
type: defaultType,
|
|
335
497
|
...control,
|
|
336
498
|
...rest
|
|
337
499
|
})
|
|
338
500
|
});
|
|
339
501
|
}
|
|
340
502
|
/**
|
|
503
|
+
* A single-line text input.
|
|
504
|
+
*
|
|
505
|
+
* Accepts every `<input>` attribute except `name` and `className`, which this
|
|
506
|
+
* component owns, plus the shared {@link FieldShell} props.
|
|
507
|
+
*
|
|
508
|
+
* @param props - {@link FieldShell} props plus `<input>` attributes.
|
|
509
|
+
*
|
|
510
|
+
* @public
|
|
511
|
+
*/
|
|
512
|
+
function TextField(props) {
|
|
513
|
+
return /* @__PURE__ */ jsx(InputField, {
|
|
514
|
+
defaultType: "text",
|
|
515
|
+
...props
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
341
519
|
* A number input. Contributes a NUMBER to {@link FormValues}, or nothing when
|
|
342
520
|
* left empty.
|
|
343
521
|
*
|
|
@@ -349,21 +527,10 @@ function TextField({ name, label, hint, className, ...rest }) {
|
|
|
349
527
|
*
|
|
350
528
|
* @public
|
|
351
529
|
*/
|
|
352
|
-
function NumberField(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
label,
|
|
357
|
-
hint,
|
|
358
|
-
htmlFor: id,
|
|
359
|
-
className,
|
|
360
|
-
children: /* @__PURE__ */ jsx("input", {
|
|
361
|
-
id,
|
|
362
|
-
name,
|
|
363
|
-
type: "number",
|
|
364
|
-
...control,
|
|
365
|
-
...rest
|
|
366
|
-
})
|
|
530
|
+
function NumberField(props) {
|
|
531
|
+
return /* @__PURE__ */ jsx(InputField, {
|
|
532
|
+
defaultType: "number",
|
|
533
|
+
...props
|
|
367
534
|
});
|
|
368
535
|
}
|
|
369
536
|
/**
|
|
@@ -474,14 +641,14 @@ function CheckboxField({ name, label, hint, className, ...rest }) {
|
|
|
474
641
|
* travel in it. With `upload` the field contributes the `File` itself,
|
|
475
642
|
* `useWorkflowSubmit` stores it through `POST /workflows/uploads` before
|
|
476
643
|
* starting the run, and the input carries the upload id — which a step reads
|
|
477
|
-
* windows of with `
|
|
644
|
+
* windows of with `stepReadUpload`. Declaring the property in the workflow's
|
|
478
645
|
* `uploads` list makes `<WorkflowFields>` render exactly this, so a declared
|
|
479
646
|
* form needs no file markup at all.
|
|
480
647
|
*
|
|
481
648
|
* **Without it the field describes the file and does not read it.** `read`
|
|
482
649
|
* exists for the cases where the bytes really are small and really are the
|
|
483
650
|
* input — a CSV of ids, a config — and the size is the author's to check. See
|
|
484
|
-
* {@link
|
|
651
|
+
* {@link FileReadMode} for the four values; `upload` is shorthand for
|
|
485
652
|
* `read="upload"`.
|
|
486
653
|
*
|
|
487
654
|
* Otherwise accepts every `<input>` attribute except `name`, `className` and
|
|
@@ -997,13 +1164,13 @@ function humanize(name) {
|
|
|
997
1164
|
*
|
|
998
1165
|
* The complement of a status line, and the reason both exist: a run is
|
|
999
1166
|
* `running` for its whole life, so a one-round job and a ten-round one look
|
|
1000
|
-
* identical while they happen. These lines come from the run itself (`
|
|
1167
|
+
* identical while they happen. These lines come from the run itself (`stepReport()`
|
|
1001
1168
|
* in a `"use step"` body), which is the only channel a workflow has before it
|
|
1002
1169
|
* produces an output.
|
|
1003
1170
|
*
|
|
1004
|
-
*
|
|
1171
|
+
* Four rules are baked in, and they are why this is a component rather than
|
|
1005
1172
|
* three lines each page writes for itself — the two templates that had written
|
|
1006
|
-
* it had written
|
|
1173
|
+
* it had written three of them, comments included:
|
|
1007
1174
|
*
|
|
1008
1175
|
* - **It renders nothing until there is something to render.** `supported` is
|
|
1009
1176
|
* what keeps this from being an empty box forever on an agent deployed before
|
|
@@ -1017,6 +1184,13 @@ function humanize(name) {
|
|
|
1017
1184
|
* or opening a finished run tomorrow — shows how it got there rather than an
|
|
1018
1185
|
* empty box. That is `useWorkflowProgress`'s doing; this is what makes it
|
|
1019
1186
|
* visible.
|
|
1187
|
+
* - **They are ANNOUNCED**, for the reason the first paragraph gives: this is
|
|
1188
|
+
* the only channel a run has before it produces an output, and a `<pre>` that
|
|
1189
|
+
* grows is a silent one. A screen-reader user pressing "Digest" got nothing
|
|
1190
|
+
* between the click and a terminal state minutes later — no "fetching", no
|
|
1191
|
+
* "summarising", no evidence the button did anything. See `role="log"` below.
|
|
1192
|
+
* The six pages that render this pass only `className`, so no template could
|
|
1193
|
+
* have fixed it locally; that is what makes it this component's job.
|
|
1020
1194
|
*
|
|
1021
1195
|
* @example
|
|
1022
1196
|
* ```tsx
|
|
@@ -1033,27 +1207,33 @@ function humanize(name) {
|
|
|
1033
1207
|
*/
|
|
1034
1208
|
function WorkflowProgress({ runId, api, className, placeholder, lines }) {
|
|
1035
1209
|
const { progress, streaming, supported } = useWorkflowProgress(runId, omitUndefined({ api }));
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1210
|
+
const text = useMemo(() => {
|
|
1211
|
+
const shown = lines === void 0 ? progress : progress.slice(Math.max(progress.length - lines, 0));
|
|
1212
|
+
return shown.length === 0 ? null : shown.join("\n");
|
|
1213
|
+
}, [progress, lines]);
|
|
1214
|
+
if (!supported || text === null) return placeholder ?? null;
|
|
1038
1215
|
return /* @__PURE__ */ jsxs("pre", {
|
|
1216
|
+
role: "log",
|
|
1217
|
+
"aria-live": "polite",
|
|
1218
|
+
"aria-atomic": "false",
|
|
1039
1219
|
className: clsx(className ?? "whitespace-pre-wrap border-l pl-4 text-xs opacity-70"),
|
|
1040
|
-
children: [
|
|
1220
|
+
children: [text, streaming && "\n…"]
|
|
1041
1221
|
});
|
|
1042
1222
|
}
|
|
1043
1223
|
//#endregion
|
|
1044
1224
|
//#region src/page.tsx
|
|
1045
1225
|
/** @jsxImportSource react */
|
|
1046
1226
|
/**
|
|
1047
|
-
* `
|
|
1227
|
+
* `mountPage()` — mount a WORKFLOW APP's UI: React, theme, no session.
|
|
1048
1228
|
*
|
|
1049
|
-
* The twin of `
|
|
1229
|
+
* The twin of `mountClient()` for an agent whose front door is a form rather than a
|
|
1050
1230
|
* microphone (`workflowApp()`). It is a separate entry rather than
|
|
1051
|
-
* an option on `
|
|
1052
|
-
* constructs a `
|
|
1231
|
+
* an option on `mountClient()` because of what `mountClient()` unavoidably does: it
|
|
1232
|
+
* constructs a `BrowserSession`, which owns a WebSocket URL provider, an audio
|
|
1053
1233
|
* graph, and a microphone request. A flag would have to make all of that
|
|
1054
1234
|
* conditional, and every session hook would then have to answer "what does this
|
|
1055
1235
|
* mean with no session?" — so the honest split is two mounts. A page that wants
|
|
1056
|
-
* voice uses `
|
|
1236
|
+
* voice uses `mountClient()`; a page that wants neither audio nor a socket uses this.
|
|
1057
1237
|
*
|
|
1058
1238
|
* Authoring is otherwise identical — the file is still `client.tsx`, still
|
|
1059
1239
|
* React, still Tailwind, still the same theme tokens — so a workflow app reads
|
|
@@ -1069,7 +1249,7 @@ function WorkflowProgress({ runId, api, className, placeholder, lines }) {
|
|
|
1069
1249
|
*
|
|
1070
1250
|
* @example
|
|
1071
1251
|
* ```tsx
|
|
1072
|
-
* import { createWorkflowApi,
|
|
1252
|
+
* import { createWorkflowApi, mountPage, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
1073
1253
|
* import { useState } from "react";
|
|
1074
1254
|
*
|
|
1075
1255
|
* // Hoisted: a client built in render is a new object every render.
|
|
@@ -1088,14 +1268,14 @@ function WorkflowProgress({ runId, api, className, placeholder, lines }) {
|
|
|
1088
1268
|
* );
|
|
1089
1269
|
* }
|
|
1090
1270
|
*
|
|
1091
|
-
*
|
|
1271
|
+
* mountPage({ name: "Digest", component: App });
|
|
1092
1272
|
* ```
|
|
1093
1273
|
*
|
|
1094
1274
|
* @throws If the target element is not found in the DOM.
|
|
1095
1275
|
*
|
|
1096
1276
|
* @public
|
|
1097
1277
|
*/
|
|
1098
|
-
function
|
|
1278
|
+
function mountPage(config) {
|
|
1099
1279
|
const container = resolveContainer(config.target);
|
|
1100
1280
|
setPageTitle(config.name);
|
|
1101
1281
|
return mountRoot(container, createElement(ThemeProvider, { value: config.theme }, createElement(config.component)));
|
|
@@ -1286,13 +1466,7 @@ function useDownloadUrl(uploadId, opts = {}) {
|
|
|
1286
1466
|
const PREFIX$1 = "aai:run-key:";
|
|
1287
1467
|
/** This page's own slot — see "The slot is keyed by the page's own URL". */
|
|
1288
1468
|
function slotFor() {
|
|
1289
|
-
|
|
1290
|
-
if (href === void 0) return PREFIX$1;
|
|
1291
|
-
try {
|
|
1292
|
-
return `${PREFIX$1}${new URL("./", href).href}`;
|
|
1293
|
-
} catch {
|
|
1294
|
-
return `${PREFIX$1}${href}`;
|
|
1295
|
-
}
|
|
1469
|
+
return urlSlot(PREFIX$1, "./");
|
|
1296
1470
|
}
|
|
1297
1471
|
/**
|
|
1298
1472
|
* Read the key this page already has, or mint and remember one.
|
|
@@ -1302,17 +1476,12 @@ function slotFor() {
|
|
|
1302
1476
|
* hand `recover` one nothing was ever started under.
|
|
1303
1477
|
*/
|
|
1304
1478
|
function mintRunKey(storage) {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
store?.setItem(slot, minted);
|
|
1312
|
-
return minted;
|
|
1313
|
-
} catch {
|
|
1314
|
-
return crypto.randomUUID();
|
|
1315
|
-
}
|
|
1479
|
+
const slot = slotFor();
|
|
1480
|
+
const stored = storageGet(storage, slot);
|
|
1481
|
+
if (stored !== void 0) return stored;
|
|
1482
|
+
const minted = crypto.randomUUID();
|
|
1483
|
+
storageSet(storage, slot, minted);
|
|
1484
|
+
return minted;
|
|
1316
1485
|
}
|
|
1317
1486
|
/**
|
|
1318
1487
|
* The key `useWorkflowSubmit` uses when the page named none.
|
|
@@ -1338,7 +1507,7 @@ function mintRunKey(storage) {
|
|
|
1338
1507
|
* @internal
|
|
1339
1508
|
*/
|
|
1340
1509
|
function useDefaultRunKey(explicit) {
|
|
1341
|
-
const [minted] = useState(() => explicit === void 0 ? mintRunKey("session") :
|
|
1510
|
+
const [minted] = useState(() => explicit === void 0 ? mintRunKey("session") : "");
|
|
1342
1511
|
return explicit ?? minted;
|
|
1343
1512
|
}
|
|
1344
1513
|
/**
|
|
@@ -1422,11 +1591,11 @@ function useRunKey(options = {}) {
|
|
|
1422
1591
|
*/
|
|
1423
1592
|
function useRecoveredRun(opts) {
|
|
1424
1593
|
const { workflow, key, enabled, getClient } = opts;
|
|
1425
|
-
const [recovering, setRecovering] = useState(enabled
|
|
1594
|
+
const [recovering, setRecovering] = useState(enabled);
|
|
1426
1595
|
const handlers = useRef(opts);
|
|
1427
1596
|
handlers.current = opts;
|
|
1428
1597
|
useEffect(() => {
|
|
1429
|
-
if (!enabled
|
|
1598
|
+
if (!enabled) return;
|
|
1430
1599
|
let cancelled = false;
|
|
1431
1600
|
setRecovering(true);
|
|
1432
1601
|
getClient().find(workflow, key, { limit: 1 }).then((found) => {
|
|
@@ -1489,6 +1658,99 @@ function useRunControls(runId, getClient) {
|
|
|
1489
1658
|
};
|
|
1490
1659
|
}
|
|
1491
1660
|
//#endregion
|
|
1661
|
+
//#region src/_upload-pause.ts
|
|
1662
|
+
/**
|
|
1663
|
+
* The two buttons a page puts over a live upload, bound to whichever gate the
|
|
1664
|
+
* submission is currently holding.
|
|
1665
|
+
*
|
|
1666
|
+
* `useWorkflowSubmit` and `useWorkflowStream` both own an {@link UploadGate} and
|
|
1667
|
+
* both reported the pause the same way: park the gate, then fold `paused` into
|
|
1668
|
+
* the status the bar is already drawing. Folded rather than replaced, because
|
|
1669
|
+
* everything else about that status — which file, how far, of how many — is
|
|
1670
|
+
* still true. Two copies of that rule are two copies that can stop agreeing
|
|
1671
|
+
* about what a paused bar says.
|
|
1672
|
+
*/
|
|
1673
|
+
/**
|
|
1674
|
+
* Bind pause/resume to the live submission's gate.
|
|
1675
|
+
*
|
|
1676
|
+
* @param getGate - Reads the gate out of the caller's ref, so the callbacks stay
|
|
1677
|
+
* stable across submissions rather than being re-created per gate.
|
|
1678
|
+
* @param setUpload - The status setter the progress bar renders from.
|
|
1679
|
+
*
|
|
1680
|
+
* @internal
|
|
1681
|
+
*/
|
|
1682
|
+
function useUploadPause(getGate, setUpload) {
|
|
1683
|
+
const setPaused = useCallback((paused) => {
|
|
1684
|
+
setUpload((current) => current ? {
|
|
1685
|
+
...current,
|
|
1686
|
+
paused
|
|
1687
|
+
} : current);
|
|
1688
|
+
}, [setUpload]);
|
|
1689
|
+
return {
|
|
1690
|
+
pauseUpload: useCallback(() => {
|
|
1691
|
+
getGate()?.pause();
|
|
1692
|
+
setPaused(true);
|
|
1693
|
+
}, [getGate, setPaused]),
|
|
1694
|
+
resumeUpload: useCallback(() => {
|
|
1695
|
+
getGate()?.resume();
|
|
1696
|
+
setPaused(false);
|
|
1697
|
+
}, [getGate, setPaused])
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1701
|
+
//#region src/_submission-state.ts
|
|
1702
|
+
/** The shared submission scaffold. See the module doc. */
|
|
1703
|
+
function useSubmissionState() {
|
|
1704
|
+
const [runId, setRunId] = useState(void 0);
|
|
1705
|
+
const [starting, setStarting] = useState(false);
|
|
1706
|
+
const [startError, setStartError] = useState(void 0);
|
|
1707
|
+
const [upload, setUpload] = useState(void 0);
|
|
1708
|
+
const current = useRef(void 0);
|
|
1709
|
+
const begin = useCallback((token) => {
|
|
1710
|
+
setStarting(true);
|
|
1711
|
+
setStartError(void 0);
|
|
1712
|
+
setRunId(void 0);
|
|
1713
|
+
current.current?.gate.cancel();
|
|
1714
|
+
current.current = token;
|
|
1715
|
+
}, []);
|
|
1716
|
+
const end = useCallback((token) => {
|
|
1717
|
+
if (current.current !== token) return;
|
|
1718
|
+
current.current = void 0;
|
|
1719
|
+
setStarting(false);
|
|
1720
|
+
setUpload(void 0);
|
|
1721
|
+
}, []);
|
|
1722
|
+
const reset = useCallback(() => {
|
|
1723
|
+
current.current?.gate.cancel();
|
|
1724
|
+
current.current = void 0;
|
|
1725
|
+
setRunId(void 0);
|
|
1726
|
+
setStartError(void 0);
|
|
1727
|
+
setUpload(void 0);
|
|
1728
|
+
}, []);
|
|
1729
|
+
const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => current.current?.gate, []), setUpload);
|
|
1730
|
+
return {
|
|
1731
|
+
runId,
|
|
1732
|
+
starting,
|
|
1733
|
+
startError,
|
|
1734
|
+
upload,
|
|
1735
|
+
actions: useMemo(() => ({
|
|
1736
|
+
setRunId,
|
|
1737
|
+
setStartError,
|
|
1738
|
+
setUpload,
|
|
1739
|
+
begin,
|
|
1740
|
+
end,
|
|
1741
|
+
reset,
|
|
1742
|
+
pauseUpload,
|
|
1743
|
+
resumeUpload
|
|
1744
|
+
}), [
|
|
1745
|
+
begin,
|
|
1746
|
+
end,
|
|
1747
|
+
reset,
|
|
1748
|
+
pauseUpload,
|
|
1749
|
+
resumeUpload
|
|
1750
|
+
])
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
//#endregion
|
|
1492
1754
|
//#region src/_upload-recall.ts
|
|
1493
1755
|
/**
|
|
1494
1756
|
* Where an upload's ID survives a page RELOAD.
|
|
@@ -1554,9 +1816,9 @@ function fingerprint(file) {
|
|
|
1554
1816
|
}
|
|
1555
1817
|
/** This scope's remembered ids, or nothing at all — a parse failure is nothing. */
|
|
1556
1818
|
function read(scope) {
|
|
1819
|
+
const raw = storageGet("session", keyFor(scope));
|
|
1820
|
+
if (raw === void 0) return {};
|
|
1557
1821
|
try {
|
|
1558
|
-
const raw = globalThis.sessionStorage?.getItem(keyFor(scope));
|
|
1559
|
-
if (raw === null || raw === void 0) return {};
|
|
1560
1822
|
const parsed = JSON.parse(raw);
|
|
1561
1823
|
return isRecord(parsed) ? parsed : {};
|
|
1562
1824
|
} catch {
|
|
@@ -1564,9 +1826,7 @@ function read(scope) {
|
|
|
1564
1826
|
}
|
|
1565
1827
|
}
|
|
1566
1828
|
function write(scope, entries) {
|
|
1567
|
-
|
|
1568
|
-
globalThis.sessionStorage?.setItem(keyFor(scope), JSON.stringify(entries));
|
|
1569
|
-
} catch {}
|
|
1829
|
+
storageSet("session", keyFor(scope), JSON.stringify(entries));
|
|
1570
1830
|
}
|
|
1571
1831
|
/**
|
|
1572
1832
|
* The id this file was last being stored under in this tab, if any.
|
|
@@ -1710,7 +1970,7 @@ function createUploadGate() {
|
|
|
1710
1970
|
*
|
|
1711
1971
|
* Its own module because both submit hooks need the identical answer and then do
|
|
1712
1972
|
* two different things with it — `useWorkflowSubmit` stores each file and passes
|
|
1713
|
-
* its id, `useWorkflowStream`
|
|
1973
|
+
* its id, `useWorkflowStream` mints the id first so the run can start on it.
|
|
1714
1974
|
* A second copy of this predicate would be a form field that one hook treats as a
|
|
1715
1975
|
* file and the other does not, which is invisible until the run reads the wrong
|
|
1716
1976
|
* kind of string.
|
|
@@ -1914,43 +2174,39 @@ async function uploadFiles(api, input, report, parallel, session) {
|
|
|
1914
2174
|
return out;
|
|
1915
2175
|
}
|
|
1916
2176
|
//#endregion
|
|
1917
|
-
//#region src/_upload-
|
|
2177
|
+
//#region src/_upload-report.ts
|
|
1918
2178
|
/**
|
|
1919
|
-
*
|
|
1920
|
-
* submission is currently holding.
|
|
2179
|
+
* What the bar draws, as a comparable string.
|
|
1921
2180
|
*
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
* still true. Two copies of that rule are two copies that can stop agreeing
|
|
1927
|
-
* about what a paused bar says.
|
|
2181
|
+
* The fraction is quantized to a tenth of a percent — finer than anything the
|
|
2182
|
+
* bar or the byte readout resolves. Everything else is compared exactly:
|
|
2183
|
+
* `paused` is a state change a person is waiting to see, and the name/index
|
|
2184
|
+
* pair moving means a different FILE, which must never be coalesced away.
|
|
1928
2185
|
*/
|
|
2186
|
+
function renderKey(status) {
|
|
2187
|
+
const { name, index, count, loaded, total, fraction, paused } = status;
|
|
2188
|
+
const shown = fraction === void 0 ? loaded : Math.round(fraction * 1e3);
|
|
2189
|
+
return `${paused} ${index} ${count} ${total ?? -1} ${shown} ${name}`;
|
|
2190
|
+
}
|
|
1929
2191
|
/**
|
|
1930
|
-
*
|
|
2192
|
+
* Wrap an `UploadStatus` setter so redundant reports never reach React.
|
|
1931
2193
|
*
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
1936
|
-
* @internal
|
|
2194
|
+
* Clearing the status (`undefined`) always passes and resets the comparison —
|
|
2195
|
+
* it ends one file's bar, and the next file must not be coalesced against the
|
|
2196
|
+
* previous one's last frame.
|
|
1937
2197
|
*/
|
|
1938
|
-
function
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
resumeUpload: useCallback(() => {
|
|
1951
|
-
getGate()?.resume();
|
|
1952
|
-
setPaused(false);
|
|
1953
|
-
}, [getGate, setPaused])
|
|
2198
|
+
function coalesceUploadReports(set) {
|
|
2199
|
+
let last;
|
|
2200
|
+
return (status) => {
|
|
2201
|
+
if (status === void 0) {
|
|
2202
|
+
last = void 0;
|
|
2203
|
+
set(void 0);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
const key = renderKey(status);
|
|
2207
|
+
if (key === last) return;
|
|
2208
|
+
last = key;
|
|
2209
|
+
set(status);
|
|
1954
2210
|
};
|
|
1955
2211
|
}
|
|
1956
2212
|
//#endregion
|
|
@@ -2025,78 +2281,66 @@ function useUploadPause(getGate, setUpload) {
|
|
|
2025
2281
|
function useWorkflowSubmit(workflow, opts = {}) {
|
|
2026
2282
|
const { api, recover = true, wait, intervalMs, parallel } = opts;
|
|
2027
2283
|
const key = useDefaultRunKey(opts.key);
|
|
2028
|
-
const
|
|
2029
|
-
const
|
|
2030
|
-
const [startError, setStartError] = useState(void 0);
|
|
2031
|
-
const [upload, setUpload] = useState(void 0);
|
|
2032
|
-
const session = useRef(void 0);
|
|
2284
|
+
const state = useSubmissionState();
|
|
2285
|
+
const { runId, actions } = state;
|
|
2033
2286
|
const getClient = useWorkflowApiRef(api);
|
|
2034
2287
|
const tracked = useWorkflowRun(runId, omitUndefined({
|
|
2035
2288
|
api,
|
|
2036
2289
|
intervalMs
|
|
2037
2290
|
}));
|
|
2038
2291
|
const { wake, cancel } = useRunControls(runId, getClient);
|
|
2292
|
+
const [startedHere, setStartedHere] = useState(false);
|
|
2039
2293
|
const recovering = useRecoveredRun({
|
|
2040
2294
|
workflow,
|
|
2041
2295
|
key,
|
|
2042
2296
|
enabled: recover,
|
|
2043
2297
|
getClient,
|
|
2044
2298
|
onFound: (found) => {
|
|
2045
|
-
setRunId((current) => current ?? found);
|
|
2299
|
+
actions.setRunId((current) => current ?? found);
|
|
2046
2300
|
},
|
|
2047
|
-
onError: setStartError
|
|
2301
|
+
onError: actions.setStartError
|
|
2048
2302
|
});
|
|
2049
2303
|
const submit = useCallback(async (input) => {
|
|
2050
2304
|
const client = getClient();
|
|
2051
|
-
setStarting(true);
|
|
2052
|
-
setStartError(void 0);
|
|
2053
|
-
setRunId(void 0);
|
|
2054
|
-
session.current?.gate.cancel();
|
|
2055
2305
|
const current = createUploadSession(workflow);
|
|
2056
|
-
|
|
2306
|
+
setStartedHere(true);
|
|
2307
|
+
actions.begin(current);
|
|
2057
2308
|
try {
|
|
2058
2309
|
const options = omitUndefined({ key });
|
|
2059
|
-
const started = await uploadFiles(client, input, setUpload, parallel, current);
|
|
2060
|
-
setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
|
|
2310
|
+
const started = await uploadFiles(client, input, coalesceUploadReports(actions.setUpload), parallel, current);
|
|
2311
|
+
actions.setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
|
|
2061
2312
|
...options,
|
|
2062
2313
|
wait
|
|
2063
2314
|
})).runId);
|
|
2064
2315
|
} catch (err) {
|
|
2065
|
-
if (!current.gate.cancelled) setStartError(errorMessage(err));
|
|
2316
|
+
if (!current.gate.cancelled) actions.setStartError(errorMessage(err));
|
|
2066
2317
|
} finally {
|
|
2067
|
-
|
|
2068
|
-
session.current = void 0;
|
|
2069
|
-
setStarting(false);
|
|
2070
|
-
setUpload(void 0);
|
|
2071
|
-
}
|
|
2318
|
+
actions.end(current);
|
|
2072
2319
|
}
|
|
2073
2320
|
}, [
|
|
2074
2321
|
workflow,
|
|
2075
2322
|
key,
|
|
2076
2323
|
wait,
|
|
2077
2324
|
parallel,
|
|
2078
|
-
getClient
|
|
2325
|
+
getClient,
|
|
2326
|
+
actions
|
|
2079
2327
|
]);
|
|
2080
|
-
const reset = useCallback(() => {
|
|
2081
|
-
session.current?.gate.cancel();
|
|
2082
|
-
session.current = void 0;
|
|
2083
|
-
setRunId(void 0);
|
|
2084
|
-
setStartError(void 0);
|
|
2085
|
-
setUpload(void 0);
|
|
2086
|
-
}, []);
|
|
2087
|
-
const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => session.current?.gate, []), setUpload);
|
|
2088
2328
|
return {
|
|
2089
2329
|
submit,
|
|
2090
2330
|
submitForm: submit,
|
|
2091
|
-
reset
|
|
2331
|
+
reset: useCallback(() => {
|
|
2332
|
+
setStartedHere(false);
|
|
2333
|
+
actions.reset();
|
|
2334
|
+
}, [actions]),
|
|
2092
2335
|
wake,
|
|
2093
2336
|
cancel,
|
|
2094
|
-
pauseUpload,
|
|
2095
|
-
resumeUpload,
|
|
2337
|
+
pauseUpload: actions.pauseUpload,
|
|
2338
|
+
resumeUpload: actions.resumeUpload,
|
|
2096
2339
|
run: tracked.run,
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2340
|
+
startedHere,
|
|
2341
|
+
pending: recovering || state.starting || tracked.polling,
|
|
2342
|
+
upload: state.upload,
|
|
2343
|
+
error: state.startError ?? tracked.error
|
|
2100
2344
|
};
|
|
2101
2345
|
}
|
|
2102
2346
|
//#endregion
|
|
@@ -2226,7 +2470,7 @@ function useWorkflowRuns(workflow, opts = {}) {
|
|
|
2226
2470
|
* earlier version of this hook took a `cut` callback and uploaded N parts into a
|
|
2227
2471
|
* "group" that a separate call had to seal; it worked, and every piece of it was
|
|
2228
2472
|
* something the caller had to get right. The whole of that is replaced by the store
|
|
2229
|
-
* publishing `size` as bytes land — which `
|
|
2473
|
+
* publishing `size` as bytes land — which `stepReadUpload` already clamped to — so the
|
|
2230
2474
|
* run does exactly what it does over a finished file and simply waits for windows to
|
|
2231
2475
|
* become present.
|
|
2232
2476
|
*
|
|
@@ -2304,25 +2548,21 @@ function useWorkflowRuns(workflow, opts = {}) {
|
|
|
2304
2548
|
*/
|
|
2305
2549
|
function useWorkflowStream(workflow, opts = {}) {
|
|
2306
2550
|
const { api, key, intervalMs, parallel } = opts;
|
|
2307
|
-
const
|
|
2308
|
-
const
|
|
2309
|
-
const [startError, setStartError] = useState(void 0);
|
|
2310
|
-
const [upload, setUpload] = useState(void 0);
|
|
2311
|
-
const gateRef = useRef(void 0);
|
|
2551
|
+
const state = useSubmissionState();
|
|
2552
|
+
const { runId, actions } = state;
|
|
2312
2553
|
const getClient = useWorkflowApiRef(api);
|
|
2313
2554
|
const tracked = useWorkflowRun(runId, omitUndefined({
|
|
2314
2555
|
api,
|
|
2315
2556
|
intervalMs
|
|
2316
2557
|
}));
|
|
2317
2558
|
const { wake, cancel } = useRunControls(runId, getClient);
|
|
2559
|
+
const [startedHere, setStartedHere] = useState(false);
|
|
2318
2560
|
const submit = useCallback(async (input) => {
|
|
2319
2561
|
const client = getClient();
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const gate = createUploadGate();
|
|
2325
|
-
gateRef.current = gate;
|
|
2562
|
+
const current = { gate: createUploadGate() };
|
|
2563
|
+
const { gate } = current;
|
|
2564
|
+
setStartedHere(true);
|
|
2565
|
+
actions.begin(current);
|
|
2326
2566
|
let started;
|
|
2327
2567
|
try {
|
|
2328
2568
|
const id = randomUploadId();
|
|
@@ -2335,7 +2575,7 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2335
2575
|
});
|
|
2336
2576
|
started = begun.runId;
|
|
2337
2577
|
const chosen = begun.file;
|
|
2338
|
-
setRunId(started);
|
|
2578
|
+
actions.setRunId(started);
|
|
2339
2579
|
if (!chosen) return;
|
|
2340
2580
|
await streamFile({
|
|
2341
2581
|
client,
|
|
@@ -2343,45 +2583,38 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2343
2583
|
id,
|
|
2344
2584
|
file: chosen,
|
|
2345
2585
|
parallel,
|
|
2346
|
-
report: setUpload
|
|
2586
|
+
report: coalesceUploadReports(actions.setUpload)
|
|
2347
2587
|
});
|
|
2348
2588
|
await client.wake(started).catch(() => void 0);
|
|
2349
2589
|
} catch (err) {
|
|
2350
|
-
if (!gate.cancelled) setStartError(errorMessage(err));
|
|
2590
|
+
if (!gate.cancelled) actions.setStartError(errorMessage(err));
|
|
2351
2591
|
if (started) await client.cancel(started).catch(() => void 0);
|
|
2352
2592
|
} finally {
|
|
2353
|
-
|
|
2354
|
-
gateRef.current = void 0;
|
|
2355
|
-
setStarting(false);
|
|
2356
|
-
setUpload(void 0);
|
|
2357
|
-
}
|
|
2593
|
+
actions.end(current);
|
|
2358
2594
|
}
|
|
2359
2595
|
}, [
|
|
2360
2596
|
workflow,
|
|
2361
2597
|
key,
|
|
2362
2598
|
parallel,
|
|
2363
|
-
getClient
|
|
2599
|
+
getClient,
|
|
2600
|
+
actions
|
|
2364
2601
|
]);
|
|
2365
|
-
const reset = useCallback(() => {
|
|
2366
|
-
gateRef.current?.cancel();
|
|
2367
|
-
gateRef.current = void 0;
|
|
2368
|
-
setRunId(void 0);
|
|
2369
|
-
setStartError(void 0);
|
|
2370
|
-
setUpload(void 0);
|
|
2371
|
-
}, []);
|
|
2372
|
-
const { pauseUpload, resumeUpload } = useUploadPause(useCallback(() => gateRef.current, []), setUpload);
|
|
2373
2602
|
return {
|
|
2374
2603
|
submit,
|
|
2375
2604
|
submitForm: submit,
|
|
2376
|
-
reset
|
|
2605
|
+
reset: useCallback(() => {
|
|
2606
|
+
setStartedHere(false);
|
|
2607
|
+
actions.reset();
|
|
2608
|
+
}, [actions]),
|
|
2377
2609
|
wake,
|
|
2378
2610
|
cancel,
|
|
2379
|
-
pauseUpload,
|
|
2380
|
-
resumeUpload,
|
|
2611
|
+
pauseUpload: actions.pauseUpload,
|
|
2612
|
+
resumeUpload: actions.resumeUpload,
|
|
2381
2613
|
run: tracked.run,
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2614
|
+
startedHere,
|
|
2615
|
+
pending: state.starting || tracked.polling,
|
|
2616
|
+
upload: state.upload,
|
|
2617
|
+
error: state.startError ?? tracked.error
|
|
2385
2618
|
};
|
|
2386
2619
|
}
|
|
2387
2620
|
/**
|
|
@@ -2503,4 +2736,4 @@ const WORKFLOW_STATUS_LABELS = {
|
|
|
2503
2736
|
cancelled: "Cancelled"
|
|
2504
2737
|
};
|
|
2505
2738
|
//#endregion
|
|
2506
|
-
export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls, Field, FileField, Form, Markdown, MessageList, NumberField, SelectField, SidebarLayout, StartScreen, SubmitButton, TextAreaField, TextField, ToolCallRow, UploadProgressBar, WORKFLOW_STATUS_LABELS, WorkflowFields, WorkflowProgress,
|
|
2739
|
+
export { AGENT_STATE_LABELS, AutoScroll, BulletList, Button, ChatView, CheckboxField, ConsoleShell, Controls, Facts, Field, FileField, Form, Markdown, MessageList, NumberField, SelectField, SessionErrorBanner, SidebarLayout, StartScreen, SubmitButton, TextAreaField, TextField, ToolCallRow, UploadProgressBar, WORKFLOW_STATUS_LABELS, WorkflowFields, WorkflowProgress, createBrowserSession, createWorkflowApi, fetchClientConfig, isTerminal, mountClient, mountPage, useAgentState, useConversation, useDownloadUrl, useEvent, useRunKey, useSession, useSessionActions, useSessionError, useSessionSelector, useSessionStatus, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };
|