@alexkroman1/aai-ui 6.11.0 → 7.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 +104 -3
- package/dist/_run-controls.d.ts +33 -0
- package/dist/{chat-view-ByQFf94G.js → chat-view-BKsFfFZJ.js} +57 -11
- package/dist/client-config-B4nznRvH.js +134 -0
- package/dist/client-config.d.ts +45 -2
- package/dist/client-dir.d.ts +3 -1
- package/dist/client-dir.js +3 -1
- package/dist/components/auto-scroll.d.ts +24 -13
- package/dist/components/button.d.ts +8 -4
- package/dist/components/button.js +4 -4
- package/dist/components/chat-view.d.ts +4 -3
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +71 -10
- package/dist/components/controls.d.ts +15 -4
- package/dist/components/controls.js +48 -2
- package/dist/components/form-fields.d.ts +142 -0
- package/dist/components/form-types.d.ts +6 -0
- package/dist/components/form.d.ts +38 -80
- package/dist/components/markdown.d.ts +31 -5
- package/dist/components/message-list.d.ts +21 -4
- package/dist/components/message-list.js +1 -1
- package/dist/components/sidebar-layout.d.ts +11 -0
- package/dist/components/sidebar-layout.js +2 -0
- package/dist/components/start-screen.d.ts +8 -0
- package/dist/components/start-screen.js +2 -0
- package/dist/components/tool-call-block.js +1 -1
- package/dist/components/tool-call-row.d.ts +24 -0
- package/dist/components/upload-progress.d.ts +17 -7
- package/dist/components/workflow-fields.d.ts +10 -22
- package/dist/components/workflow-progress.d.ts +29 -10
- package/dist/context.d.ts +36 -0
- package/dist/context.js +105 -14
- package/dist/default-client/assets/index-S5fkKi6B.css +2 -0
- package/dist/default-client/assets/index-fEkrcZgo.js +293 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.d.ts +67 -62
- package/dist/define-client.js +56 -22
- package/dist/hooks.d.ts +88 -3
- package/dist/index.d.ts +12 -11
- package/dist/index.js +501 -167
- package/dist/internal.d.ts +40 -0
- package/dist/internal.js +6 -0
- package/dist/{message-list-CpPV7dGx.js → message-list-DHddO4QC.js} +217 -72
- package/dist/{session-core-CAfYmUbg.js → session-core-C2JtLArh.js} +267 -165
- package/dist/session-core-audio-setup.d.ts +3 -0
- package/dist/session-core-messages.d.ts +3 -0
- package/dist/session-core-state.d.ts +146 -0
- package/dist/session-core-types.d.ts +77 -32
- package/dist/session-core.d.ts +3 -2
- package/dist/session-core.js +1 -1
- package/dist/{tool-call-block-D6pTEPrT.js → tool-call-block-DoF-cSIZ.js} +27 -19
- package/dist/tool-config-context-DzAofqi_.js +19 -0
- package/dist/types.d.ts +31 -5
- package/dist/types.js +5 -4
- package/dist/{controls-CjG91QJ4.js → url-chips-DpM7Oocj.js} +3 -46
- package/dist/use-conversation.d.ts +122 -0
- package/dist/use-download-url.d.ts +83 -0
- package/dist/use-workflow-form.d.ts +59 -3
- package/dist/use-workflow-run.d.ts +35 -0
- package/dist/use-workflow-stream.d.ts +36 -62
- package/dist/workflow-client.d.ts +36 -11
- package/dist/workflow-status-labels.d.ts +35 -0
- package/package.json +10 -5
- package/styles.css +14 -0
- package/dist/default-client/assets/index-DTLrhtTF.css +0 -2
- package/dist/default-client/assets/index-DXODx_9r.js +0 -293
package/dist/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as Markdown,
|
|
3
|
-
import {
|
|
1
|
+
import { n as fetchClientConfig } from "./client-config-B4nznRvH.js";
|
|
2
|
+
import { a as useUserTranscript, i as TRANSCRIBING_PLACEHOLDER, n as Markdown, o as AutoScroll, r as useConversation, t as MessageList } from "./message-list-DHddO4QC.js";
|
|
3
|
+
import { ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
|
|
4
4
|
import { r as inkTint } from "./_colors-CcAi2FOU.js";
|
|
5
5
|
import { Button } from "./components/button.js";
|
|
6
|
-
import { t as ChatView } from "./chat-view-
|
|
6
|
+
import { n as ConsoleShell, t as ChatView } from "./chat-view-BKsFfFZJ.js";
|
|
7
7
|
import { n as setPageTitle, t as pageBaseUrl } from "./_utils-B6498_bm.js";
|
|
8
|
-
import {
|
|
9
|
-
import { n as
|
|
8
|
+
import { Controls } from "./components/controls.js";
|
|
9
|
+
import { n as ToolCallRow } from "./tool-call-block-DoF-cSIZ.js";
|
|
10
10
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
11
11
|
import { StartScreen } from "./components/start-screen.js";
|
|
12
|
-
import {
|
|
12
|
+
import { t as createSessionCore } from "./session-core-C2JtLArh.js";
|
|
13
13
|
import { client, mountRoot, resolveContainer } from "./define-client.js";
|
|
14
14
|
import { useAgentState, useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
15
15
|
import clsx from "clsx";
|
|
16
16
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
17
17
|
import { createContext, createElement, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
18
|
-
import { errorMessage
|
|
18
|
+
import { errorMessage } from "@alexkroman1/aai";
|
|
19
19
|
import { isRecord, omitUndefined } from "@alexkroman1/aai/utils";
|
|
20
|
-
import { createWorkflowApiClient, readEventStream } from "@alexkroman1/aai/workflow-api";
|
|
20
|
+
import { createWorkflowApiClient, isTerminal, isTerminal as isTerminal$1, readEventStream } from "@alexkroman1/aai/workflow-api";
|
|
21
21
|
import { createEpoch } from "@alexkroman1/aai/internal";
|
|
22
22
|
//#region components/_form-readiness.ts
|
|
23
23
|
/**
|
|
@@ -200,104 +200,52 @@ function dataUrl(file) {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
//#endregion
|
|
203
|
-
//#region components/form.tsx
|
|
203
|
+
//#region components/form-fields.tsx
|
|
204
204
|
/** @jsxImportSource react */
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* A workflow app's front door is a form: name a recording, upload a file, press
|
|
209
|
-
* submit. Nothing here knew how to render one — the components in this package
|
|
210
|
-
* are all about a live session (a transcript, a mic button, a tool-call row) —
|
|
211
|
-
* so every such page started by hand-rolling labels, inputs, a submit button and
|
|
212
|
-
* the value collection between them, and did it differently each time.
|
|
206
|
+
* The controls a form is made of — the shell, the six fields, and the styling
|
|
207
|
+
* they share.
|
|
213
208
|
*
|
|
214
|
-
*
|
|
209
|
+
* Split out of `form.tsx` when that file crossed the repo's source-file line
|
|
210
|
+
* cap; `form.tsx` re-exports every name here, so it stays the one import path
|
|
211
|
+
* for everything a form needs and nothing a caller writes changed.
|
|
215
212
|
*
|
|
216
|
-
*
|
|
217
|
-
* object from the named controls. That is what makes a field here nothing more
|
|
218
|
-
* than a styled `<input>`: no registration, no controlled-component ceremony,
|
|
219
|
-
* and a plain `<input name="x">` a caller writes themselves works exactly like
|
|
220
|
-
* the ones below.
|
|
213
|
+
* ## Every field takes its element's own attributes
|
|
221
214
|
*
|
|
222
|
-
*
|
|
223
|
-
* `
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
215
|
+
* Each control is `FieldShell & Omit<…HTMLAttributes, "name" | "className">`,
|
|
216
|
+
* so `required`, `placeholder`, `accept`, `min`/`max`, `aria-*` and the rest
|
|
217
|
+
* pass straight through to the element. `name` and `className` are owned here:
|
|
218
|
+
* `name` is the key the field contributes to {@link FormValues} and is put on
|
|
219
|
+
* the control itself, and `className` styles the field's WRAPPER, the control
|
|
220
|
+
* taking the shared styling below.
|
|
228
221
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* them somewhere a step can read them and contributes the handle instead.
|
|
222
|
+
* Note this means the per-property doc comments on the intersections do not
|
|
223
|
+
* reach the published reference — see "A memoized component must NAME its
|
|
224
|
+
* props type" in `packages/aai-ui/CLAUDE.md` — which is why each component's
|
|
225
|
+
* own description carries what a caller needs.
|
|
234
226
|
*/
|
|
235
227
|
/**
|
|
236
|
-
*
|
|
228
|
+
* Label + control + hint, in the layout every field here uses.
|
|
237
229
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
230
|
+
* Exported so a caller's own control gets the same shell rather than an
|
|
231
|
+
* approximation of it.
|
|
240
232
|
*
|
|
241
233
|
* @example
|
|
242
234
|
* ```tsx
|
|
243
|
-
* import {
|
|
235
|
+
* import { Field, Form } from "@alexkroman1/aai-ui";
|
|
244
236
|
*
|
|
245
|
-
* function
|
|
237
|
+
* function ColorForm() {
|
|
246
238
|
* return (
|
|
247
|
-
* <Form onSubmit={(
|
|
248
|
-
* <
|
|
249
|
-
*
|
|
239
|
+
* <Form onSubmit={() => undefined}>
|
|
240
|
+
* <Field label="Accent" hint="Any CSS color." htmlFor="accent">
|
|
241
|
+
* <input id="accent" name="accent" type="color" />
|
|
242
|
+
* </Field>
|
|
250
243
|
* </Form>
|
|
251
244
|
* );
|
|
252
245
|
* }
|
|
253
246
|
* ```
|
|
254
247
|
*
|
|
255
|
-
* @
|
|
256
|
-
*/
|
|
257
|
-
function Form({ onSubmit, error, children, className, ...rest }) {
|
|
258
|
-
const theme = useTheme();
|
|
259
|
-
const [busy, setBusy] = useState(false);
|
|
260
|
-
const { pending: fieldsPending, declare } = useFormReadiness();
|
|
261
|
-
return /* @__PURE__ */ jsxs("form", {
|
|
262
|
-
onSubmit: useCallback((event) => {
|
|
263
|
-
event.preventDefault();
|
|
264
|
-
if (busy || fieldsPending) return;
|
|
265
|
-
const form = event.currentTarget;
|
|
266
|
-
setBusy(true);
|
|
267
|
-
(async () => {
|
|
268
|
-
try {
|
|
269
|
-
await onSubmit(await collectValues(form));
|
|
270
|
-
} finally {
|
|
271
|
-
setBusy(false);
|
|
272
|
-
}
|
|
273
|
-
})();
|
|
274
|
-
}, [
|
|
275
|
-
busy,
|
|
276
|
-
fieldsPending,
|
|
277
|
-
onSubmit
|
|
278
|
-
]),
|
|
279
|
-
className: clsx("flex flex-col gap-5 font-aai", className),
|
|
280
|
-
...rest,
|
|
281
|
-
children: [/* @__PURE__ */ jsx("fieldset", {
|
|
282
|
-
disabled: busy || fieldsPending,
|
|
283
|
-
className: "contents",
|
|
284
|
-
children: /* @__PURE__ */ jsx(FormReadinessProvider, {
|
|
285
|
-
value: declare,
|
|
286
|
-
children
|
|
287
|
-
})
|
|
288
|
-
}), error !== void 0 && error !== "" && /* @__PURE__ */ jsx("p", {
|
|
289
|
-
role: "alert",
|
|
290
|
-
className: "text-sm",
|
|
291
|
-
style: { color: theme.primary },
|
|
292
|
-
children: error
|
|
293
|
-
})]
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Label + control + hint, in the layout every field here uses.
|
|
298
|
-
*
|
|
299
|
-
* Exported so a caller's own control gets the same shell rather than an
|
|
300
|
-
* approximation of it.
|
|
248
|
+
* @param props - Field-shell props.
|
|
301
249
|
*
|
|
302
250
|
* @public
|
|
303
251
|
*/
|
|
@@ -364,6 +312,11 @@ function useFileControlProps() {
|
|
|
364
312
|
/**
|
|
365
313
|
* A single-line text input.
|
|
366
314
|
*
|
|
315
|
+
* Accepts every `<input>` attribute except `name` and `className`, which this
|
|
316
|
+
* component owns, plus the shared {@link FieldShell} props.
|
|
317
|
+
*
|
|
318
|
+
* @param props - {@link FieldShell} props plus `<input>` attributes.
|
|
319
|
+
*
|
|
367
320
|
* @public
|
|
368
321
|
*/
|
|
369
322
|
function TextField({ name, label, hint, className, ...rest }) {
|
|
@@ -386,6 +339,12 @@ function TextField({ name, label, hint, className, ...rest }) {
|
|
|
386
339
|
* A number input. Contributes a NUMBER to {@link FormValues}, or nothing when
|
|
387
340
|
* left empty.
|
|
388
341
|
*
|
|
342
|
+
* Accepts every `<input>` attribute except `name`, `className` and `type`,
|
|
343
|
+
* plus the shared {@link FieldShell} props — so `min`, `max` and `step` are
|
|
344
|
+
* passed straight through.
|
|
345
|
+
*
|
|
346
|
+
* @param props - {@link FieldShell} props plus `<input>` attributes.
|
|
347
|
+
*
|
|
389
348
|
* @public
|
|
390
349
|
*/
|
|
391
350
|
function NumberField({ name, label, hint, className, ...rest }) {
|
|
@@ -407,6 +366,11 @@ function NumberField({ name, label, hint, className, ...rest }) {
|
|
|
407
366
|
/**
|
|
408
367
|
* A multi-line text input.
|
|
409
368
|
*
|
|
369
|
+
* Accepts every `<textarea>` attribute except `name` and `className`, plus the
|
|
370
|
+
* shared {@link FieldShell} props. `rows` defaults to 4.
|
|
371
|
+
*
|
|
372
|
+
* @param props - {@link FieldShell} props plus `<textarea>` attributes.
|
|
373
|
+
*
|
|
410
374
|
* @public
|
|
411
375
|
*/
|
|
412
376
|
function TextAreaField({ name, label, hint, className, rows = 4, ...rest }) {
|
|
@@ -426,8 +390,19 @@ function TextAreaField({ name, label, hint, className, rows = 4, ...rest }) {
|
|
|
426
390
|
});
|
|
427
391
|
}
|
|
428
392
|
/**
|
|
429
|
-
* A dropdown.
|
|
430
|
-
*
|
|
393
|
+
* A dropdown.
|
|
394
|
+
*
|
|
395
|
+
* `options` is the short form — a list of strings, or of
|
|
396
|
+
* `{ value, label }` pairs when the two differ. Pass `children` instead for
|
|
397
|
+
* full control over the `<option>` elements; `children` wins when both are
|
|
398
|
+
* given.
|
|
399
|
+
*
|
|
400
|
+
* Otherwise accepts every `<select>` attribute except `name` and `className`,
|
|
401
|
+
* plus the shared {@link FieldShell} props. Note `multiple` works and
|
|
402
|
+
* contributes an ARRAY (`[]` when nothing is chosen).
|
|
403
|
+
*
|
|
404
|
+
* @param props - {@link FieldShell} props, `options`, and `<select>`
|
|
405
|
+
* attributes.
|
|
431
406
|
*
|
|
432
407
|
* @public
|
|
433
408
|
*/
|
|
@@ -456,6 +431,12 @@ function SelectField({ name, label, hint, className, options, children, ...rest
|
|
|
456
431
|
/**
|
|
457
432
|
* A checkbox. Contributes a BOOLEAN to {@link FormValues}.
|
|
458
433
|
*
|
|
434
|
+
* Accepts every `<input>` attribute except `name`, `className` and `type`,
|
|
435
|
+
* plus the shared {@link FieldShell} props. The label renders beside the box
|
|
436
|
+
* rather than above it, so `hint` is the place for guidance.
|
|
437
|
+
*
|
|
438
|
+
* @param props - {@link FieldShell} props plus `<input>` attributes.
|
|
439
|
+
*
|
|
459
440
|
* @public
|
|
460
441
|
*/
|
|
461
442
|
function CheckboxField({ name, label, hint, className, ...rest }) {
|
|
@@ -493,7 +474,16 @@ function CheckboxField({ name, label, hint, className, ...rest }) {
|
|
|
493
474
|
*
|
|
494
475
|
* **Without it the field describes the file and does not read it.** `read`
|
|
495
476
|
* exists for the cases where the bytes really are small and really are the
|
|
496
|
-
* input — a CSV of ids, a config — and the size is the author's to check.
|
|
477
|
+
* input — a CSV of ids, a config — and the size is the author's to check. See
|
|
478
|
+
* {@link FileRead} for the four values; `upload` is shorthand for
|
|
479
|
+
* `read="upload"`.
|
|
480
|
+
*
|
|
481
|
+
* Otherwise accepts every `<input>` attribute except `name`, `className` and
|
|
482
|
+
* `type`, plus the shared {@link FieldShell} props — so `accept` and
|
|
483
|
+
* `multiple` are passed straight through.
|
|
484
|
+
*
|
|
485
|
+
* @param props - {@link FieldShell} props, `read`/`upload`, and `<input>`
|
|
486
|
+
* attributes.
|
|
497
487
|
*
|
|
498
488
|
* @public
|
|
499
489
|
*/
|
|
@@ -515,20 +505,144 @@ function FileField({ name, label, hint, className, read = "none", upload = false
|
|
|
515
505
|
})
|
|
516
506
|
});
|
|
517
507
|
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region components/form.tsx
|
|
510
|
+
/** @jsxImportSource react */
|
|
511
|
+
/**
|
|
512
|
+
* Simple forms, for the pages that are not conversations.
|
|
513
|
+
*
|
|
514
|
+
* A workflow app's front door is a form: name a recording, upload a file, press
|
|
515
|
+
* submit. Nothing here knew how to render one — the components in this package
|
|
516
|
+
* are all about a live session (a transcript, a mic button, a tool-call row) —
|
|
517
|
+
* so every such page started by hand-rolling labels, inputs, a submit button and
|
|
518
|
+
* the value collection between them, and did it differently each time.
|
|
519
|
+
*
|
|
520
|
+
* ## Values come off the DOM, not out of React state
|
|
521
|
+
*
|
|
522
|
+
* {@link Form} reads its own `<form>` element on submit and builds one plain
|
|
523
|
+
* object from the named controls. That is what makes a field here nothing more
|
|
524
|
+
* than a styled `<input>`: no registration, no controlled-component ceremony,
|
|
525
|
+
* and a plain `<input name="x">` a caller writes themselves works exactly like
|
|
526
|
+
* the ones below.
|
|
527
|
+
*
|
|
528
|
+
* It also means the values are TYPED rather than all-strings, which a
|
|
529
|
+
* `new FormData(form)` cannot give: a number field yields a number, a checkbox a
|
|
530
|
+
* boolean, an empty optional field nothing at all. That matters because these
|
|
531
|
+
* values go straight into a workflow's input, where a zod schema is waiting —
|
|
532
|
+
* `"3"` against `z.number()` is a rejected run, and the browser is the only
|
|
533
|
+
* place that still knows the field was `type="number"`.
|
|
534
|
+
*
|
|
535
|
+
* ## A file field either describes a file or uploads it
|
|
536
|
+
*
|
|
537
|
+
* See {@link FileField}. A workflow input is journaled and replayed on every
|
|
538
|
+
* resume, so file BYTES do not belong in it — `upload` is the field that sends
|
|
539
|
+
* them somewhere a step can read them and contributes the handle instead.
|
|
540
|
+
*/
|
|
541
|
+
/**
|
|
542
|
+
* A form that hands its values to `onSubmit` as one object.
|
|
543
|
+
*
|
|
544
|
+
* Native validation still applies — a `required` field blocks the submit and the
|
|
545
|
+
* browser says so, which is better than anything this could render.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```tsx
|
|
549
|
+
* import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
|
|
550
|
+
*
|
|
551
|
+
* function NameForm() {
|
|
552
|
+
* return (
|
|
553
|
+
* <Form onSubmit={(values) => console.log(values.topic)}>
|
|
554
|
+
* <TextField name="topic" label="Topic" required />
|
|
555
|
+
* <SubmitButton>Start</SubmitButton>
|
|
556
|
+
* </Form>
|
|
557
|
+
* );
|
|
558
|
+
* }
|
|
559
|
+
* ```
|
|
560
|
+
*
|
|
561
|
+
* @param props - See {@link FormProps}. Every `<form>` attribute except
|
|
562
|
+
* `onSubmit` and `className` is passed through.
|
|
563
|
+
*
|
|
564
|
+
* @public
|
|
565
|
+
*/
|
|
566
|
+
function Form({ onSubmit, error, children, className, ...rest }) {
|
|
567
|
+
const theme = useTheme();
|
|
568
|
+
const [busy, setBusy] = useState(false);
|
|
569
|
+
const { pending: fieldsPending, declare } = useFormReadiness();
|
|
570
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
571
|
+
onSubmit: useCallback((event) => {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
if (busy || fieldsPending) return;
|
|
574
|
+
const form = event.currentTarget;
|
|
575
|
+
setBusy(true);
|
|
576
|
+
(async () => {
|
|
577
|
+
try {
|
|
578
|
+
await onSubmit(await collectValues(form));
|
|
579
|
+
} finally {
|
|
580
|
+
setBusy(false);
|
|
581
|
+
}
|
|
582
|
+
})();
|
|
583
|
+
}, [
|
|
584
|
+
busy,
|
|
585
|
+
fieldsPending,
|
|
586
|
+
onSubmit
|
|
587
|
+
]),
|
|
588
|
+
className: clsx("flex flex-col gap-5 font-aai", className),
|
|
589
|
+
...rest,
|
|
590
|
+
children: [/* @__PURE__ */ jsx("fieldset", {
|
|
591
|
+
disabled: busy || fieldsPending,
|
|
592
|
+
className: "contents",
|
|
593
|
+
children: /* @__PURE__ */ jsx(FormReadinessProvider, {
|
|
594
|
+
value: declare,
|
|
595
|
+
children
|
|
596
|
+
})
|
|
597
|
+
}), error !== void 0 && error !== "" && /* @__PURE__ */ jsx("p", {
|
|
598
|
+
role: "alert",
|
|
599
|
+
className: "text-sm",
|
|
600
|
+
style: { color: theme.primary },
|
|
601
|
+
children: error
|
|
602
|
+
})]
|
|
603
|
+
});
|
|
604
|
+
}
|
|
518
605
|
/**
|
|
519
606
|
* The form's submit button, disabled and relabelled while a submit is in
|
|
520
607
|
* flight.
|
|
521
608
|
*
|
|
609
|
+
* Accepts all standard `<button>` HTML attributes except `type` and `disabled`,
|
|
610
|
+
* in addition to the props below — so `aria-label` on an icon-only submit,
|
|
611
|
+
* `form`, `id`, `title` and `onClick` all work here exactly as they do on
|
|
612
|
+
* {@link Button}. `type` and `disabled` stay owned: this component sets both
|
|
613
|
+
* from `pending`, and letting a caller set either is how a form gets a submit
|
|
614
|
+
* button that does not submit.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* ```tsx
|
|
618
|
+
* import { Form, SubmitButton, TextField } from "@alexkroman1/aai-ui";
|
|
619
|
+
*
|
|
620
|
+
* function Digest({ pending }: { pending: boolean }) {
|
|
621
|
+
* return (
|
|
622
|
+
* <Form onSubmit={() => undefined}>
|
|
623
|
+
* <TextField name="url" label="Link" required />
|
|
624
|
+
* <SubmitButton pending={pending} variant="secondary" size="lg">
|
|
625
|
+
* Summarize
|
|
626
|
+
* </SubmitButton>
|
|
627
|
+
* </Form>
|
|
628
|
+
* );
|
|
629
|
+
* }
|
|
630
|
+
* ```
|
|
631
|
+
*
|
|
632
|
+
* @param props - Button props.
|
|
633
|
+
*
|
|
522
634
|
* @public
|
|
523
635
|
*/
|
|
524
|
-
function SubmitButton({ children, pending = false, pendingLabel = "Working…", size, className }) {
|
|
636
|
+
function SubmitButton({ children, pending = false, pendingLabel = "Working…", size, variant, className, ...rest }) {
|
|
525
637
|
return /* @__PURE__ */ jsx(Button, {
|
|
526
638
|
type: "submit",
|
|
527
639
|
disabled: pending,
|
|
528
640
|
...omitUndefined({
|
|
529
641
|
size,
|
|
642
|
+
variant,
|
|
530
643
|
className
|
|
531
644
|
}),
|
|
645
|
+
...rest,
|
|
532
646
|
children: pending ? pendingLabel : children
|
|
533
647
|
});
|
|
534
648
|
}
|
|
@@ -614,13 +728,7 @@ function formatBytes(bytes) {
|
|
|
614
728
|
* }
|
|
615
729
|
* ```
|
|
616
730
|
*
|
|
617
|
-
* @param
|
|
618
|
-
* so a page may pass its state straight through.
|
|
619
|
-
* @param onPause - The hook's `pauseUpload`. Pass it together with `onResume` to
|
|
620
|
-
* get the control; pass neither for a bar that only reports.
|
|
621
|
-
* @param onResume - The hook's `resumeUpload`.
|
|
622
|
-
* @param className - Replaces the default classes rather than extending them,
|
|
623
|
-
* so a custom chrome is not fighting a default it did not ask for.
|
|
731
|
+
* @param props - Progress-bar props.
|
|
624
732
|
*
|
|
625
733
|
* @public
|
|
626
734
|
*/
|
|
@@ -674,6 +782,44 @@ function UploadProgressBar({ upload, onPause, onResume, className }) {
|
|
|
674
782
|
});
|
|
675
783
|
}
|
|
676
784
|
//#endregion
|
|
785
|
+
//#region _run-controls.ts
|
|
786
|
+
/**
|
|
787
|
+
* The two things a page does TO a run it started, bound to the run it has.
|
|
788
|
+
*
|
|
789
|
+
* `useWorkflowSubmit` and `useWorkflowStream` both hold a run id and neither
|
|
790
|
+
* handed it back, so a page that wanted "send it now" or "stop" had to hold an
|
|
791
|
+
* `api` of its own purely to write `api.wake(runId)` — which is the whole reason
|
|
792
|
+
* the two raw-primitive template pages keep a client at module scope. That is a
|
|
793
|
+
* page carrying the transport to make up for a hook withholding its own state.
|
|
794
|
+
*
|
|
795
|
+
* Both calls answer rather than fail when there is nothing to act on — `0`
|
|
796
|
+
* sleeps ended, `false` this call did not end it — which is the SDK's own
|
|
797
|
+
* contract for them (two tabs pressing Stop is ordinary), and it is what lets
|
|
798
|
+
* the no-run case be the same answer rather than a special one a caller has to
|
|
799
|
+
* branch on.
|
|
800
|
+
*/
|
|
801
|
+
/**
|
|
802
|
+
* Bind `wake` and `cancel` to whatever run the hook is currently following.
|
|
803
|
+
*
|
|
804
|
+
* @param runId - The live run, or `undefined` before one exists.
|
|
805
|
+
* @param getClient - The stable getter from `useWorkflowApiRef`.
|
|
806
|
+
* @returns Two callbacks, stable while `runId` is.
|
|
807
|
+
*
|
|
808
|
+
* @internal
|
|
809
|
+
*/
|
|
810
|
+
function useRunControls(runId, getClient) {
|
|
811
|
+
return {
|
|
812
|
+
wake: useCallback(async () => {
|
|
813
|
+
if (runId === void 0) return 0;
|
|
814
|
+
return await getClient().wake(runId);
|
|
815
|
+
}, [runId, getClient]),
|
|
816
|
+
cancel: useCallback(async () => {
|
|
817
|
+
if (runId === void 0) return false;
|
|
818
|
+
return await getClient().cancel(runId);
|
|
819
|
+
}, [runId, getClient])
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
677
823
|
//#region _upload-recall.ts
|
|
678
824
|
/**
|
|
679
825
|
* Where an upload's ID survives a page RELOAD.
|
|
@@ -1101,6 +1247,32 @@ async function uploadFiles(api, input, report, parallel, session) {
|
|
|
1101
1247
|
//#endregion
|
|
1102
1248
|
//#region workflow-client.ts
|
|
1103
1249
|
/**
|
|
1250
|
+
* Browser client for the workflow HTTP API (`aai/host/workflow-api.ts`).
|
|
1251
|
+
*
|
|
1252
|
+
* This is the whole client half of a WORKFLOW APP: an agent whose front door is
|
|
1253
|
+
* a form rather than a microphone (`workflowApp()`) starts runs
|
|
1254
|
+
* here and watches them for the answer. It deliberately does NOT go through
|
|
1255
|
+
* `SessionCore` — there is no socket, no audio graph, and no session to resume.
|
|
1256
|
+
*
|
|
1257
|
+
* **The requests themselves are the SDK's now**
|
|
1258
|
+
* (`createWorkflowApiClient`, `@alexkroman1/aai/workflow-api`), and what is left
|
|
1259
|
+
* here is the one thing that is genuinely a BROWSER's: the default base URL.
|
|
1260
|
+
* Every route, every query, the 404-is-an-answer rule and the `wait` clamp were
|
|
1261
|
+
* written three times over — here, in the studio's Workflows card, and in
|
|
1262
|
+
* `aai workflow` — and the parts the copies disagreed on were exactly the ones a
|
|
1263
|
+
* reader cannot check by eye. The SDK module's doc carries that argument; this
|
|
1264
|
+
* file must not grow a second implementation of any of it.
|
|
1265
|
+
*
|
|
1266
|
+
* The one thing worth knowing before using it: **a run outlives the page.**
|
|
1267
|
+
* Starting one resolves as soon as the run is created, so `runId` is the only
|
|
1268
|
+
* handle that matters and it stays valid across a reload, a different device, or
|
|
1269
|
+
* `curl` — which is what makes `useWorkflowRun` a watch rather than a
|
|
1270
|
+
* subscription to something the page owns.
|
|
1271
|
+
*
|
|
1272
|
+
* The loop that keeps asking lives in `use-workflow-run.ts`, and the streaming
|
|
1273
|
+
* fast path under it in `workflow-events.ts`.
|
|
1274
|
+
*/
|
|
1275
|
+
/**
|
|
1104
1276
|
* Create a workflow API client aimed at the agent serving this page.
|
|
1105
1277
|
*
|
|
1106
1278
|
* Hoist it out of the component that uses it. `useWorkflowRun` holds the client
|
|
@@ -1108,6 +1280,32 @@ async function uploadFiles(api, input, report, parallel, session) {
|
|
|
1108
1280
|
* but a client built in render is still a new `fetch` closure every time and
|
|
1109
1281
|
* reads as though it were free.
|
|
1110
1282
|
*
|
|
1283
|
+
* @example
|
|
1284
|
+
* ```tsx
|
|
1285
|
+
* import { createWorkflowApi, useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
1286
|
+
* import { useState } from "react";
|
|
1287
|
+
*
|
|
1288
|
+
* // Module scope, not render scope — see above.
|
|
1289
|
+
* const api = createWorkflowApi();
|
|
1290
|
+
*
|
|
1291
|
+
* function StartDigest() {
|
|
1292
|
+
* const [runId, setRunId] = useState<string>();
|
|
1293
|
+
* const { run } = useWorkflowRun(runId, { api });
|
|
1294
|
+
* return (
|
|
1295
|
+
* <button
|
|
1296
|
+
* type="button"
|
|
1297
|
+
* onClick={() => void api.start("digest", { url: "…" }).then(setRunId)}
|
|
1298
|
+
* >
|
|
1299
|
+
* {run ? run.status : "Start"}
|
|
1300
|
+
* </button>
|
|
1301
|
+
* );
|
|
1302
|
+
* }
|
|
1303
|
+
* ```
|
|
1304
|
+
*
|
|
1305
|
+
* @param opts - See {@link WorkflowApiOptions}. Both fields are optional; the
|
|
1306
|
+
* default base URL is the page's own origin and path.
|
|
1307
|
+
* @returns The call set — see {@link WorkflowApi}.
|
|
1308
|
+
*
|
|
1111
1309
|
* @public
|
|
1112
1310
|
*/
|
|
1113
1311
|
function createWorkflowApi(opts = {}) {
|
|
@@ -1365,10 +1563,45 @@ function pollUntilTerminal(getClient, runId, intervalMs, onRun, onError, onStopp
|
|
|
1365
1563
|
* STOPS on a terminal status, so a finished run costs nothing; passing
|
|
1366
1564
|
* `undefined` (nothing started yet) also costs nothing.
|
|
1367
1565
|
*
|
|
1566
|
+
* @example
|
|
1567
|
+
* ```tsx
|
|
1568
|
+
* import type { ToolInputSchema, WorkflowDef } from "@alexkroman1/aai";
|
|
1569
|
+
* import type { WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
1570
|
+
* import { useWorkflowRun } from "@alexkroman1/aai-ui";
|
|
1571
|
+
*
|
|
1572
|
+
* // A real page writes `import type { digest } from "./agent.ts"`. Stood in
|
|
1573
|
+
* // for here so the example compiles on its own.
|
|
1574
|
+
* declare const digest: WorkflowDef<ToolInputSchema, Promise<{ points: string[] }>>;
|
|
1575
|
+
*
|
|
1576
|
+
* type Digest = WorkflowOutputOf<typeof digest>;
|
|
1577
|
+
*
|
|
1578
|
+
* function RunPanel({ runId }: { runId: string | undefined }) {
|
|
1579
|
+
* // The type argument is what makes `run.output` a `Digest` below.
|
|
1580
|
+
* const { run, error, polling } = useWorkflowRun<Digest>(runId);
|
|
1581
|
+
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
1582
|
+
* if (run?.status === "completed") {
|
|
1583
|
+
* return (
|
|
1584
|
+
* <ul>
|
|
1585
|
+
* {run.output.points.map((point) => (
|
|
1586
|
+
* <li key={point}>{point}</li>
|
|
1587
|
+
* ))}
|
|
1588
|
+
* </ul>
|
|
1589
|
+
* );
|
|
1590
|
+
* }
|
|
1591
|
+
* return <p>{polling ? "Working…" : "Nothing running."}</p>;
|
|
1592
|
+
* }
|
|
1593
|
+
* ```
|
|
1594
|
+
*
|
|
1368
1595
|
* @typeParam R - The workflow's output type. Supplying it is what makes
|
|
1369
1596
|
* `run.status === "completed"` narrow to a typed `run.output` instead of
|
|
1370
1597
|
* `unknown`. Derive it with `WorkflowOutputOf<typeof myWorkflow>` — a
|
|
1371
1598
|
* type-only import of `agent.ts` is erased, so it costs the bundle nothing.
|
|
1599
|
+
* @param runId - The run to watch. `undefined` costs nothing, so a page may
|
|
1600
|
+
* pass its state straight through before a run exists.
|
|
1601
|
+
* @param opts - `api` when the page holds its own client; `intervalMs` to
|
|
1602
|
+
* change the poll interval the stream falls back to.
|
|
1603
|
+
* @returns The latest snapshot, the last read's error, and whether the watch
|
|
1604
|
+
* is still going — see {@link UseWorkflowRunResult}.
|
|
1372
1605
|
*
|
|
1373
1606
|
* @public
|
|
1374
1607
|
*/
|
|
@@ -1460,6 +1693,35 @@ function useWorkflowRun(runId, opts = {}) {
|
|
|
1460
1693
|
* empty list — which renders as a form with no fields and reads as "this agent
|
|
1461
1694
|
* declares no workflows" about an agent that was merely unreachable.
|
|
1462
1695
|
*
|
|
1696
|
+
* @example
|
|
1697
|
+
* ```tsx
|
|
1698
|
+
* import { useWorkflows } from "@alexkroman1/aai-ui";
|
|
1699
|
+
*
|
|
1700
|
+
* // A page rendering its own chrome from the listing — a picker, say. A form
|
|
1701
|
+
* // for ONE workflow wants `<WorkflowFields workflow="name" />` instead,
|
|
1702
|
+
* // which does this lookup itself.
|
|
1703
|
+
* function WorkflowPicker({ onPick }: { onPick: (name: string) => void }) {
|
|
1704
|
+
* const { workflows, loading, error } = useWorkflows();
|
|
1705
|
+
* if (loading) return <p>Loading…</p>;
|
|
1706
|
+
* if (error !== undefined) return <p role="alert">{error}</p>;
|
|
1707
|
+
* return (
|
|
1708
|
+
* <ul>
|
|
1709
|
+
* {workflows.map((summary) => (
|
|
1710
|
+
* <li key={summary.name}>
|
|
1711
|
+
* <button type="button" onClick={() => onPick(summary.name)}>
|
|
1712
|
+
* {summary.description ?? summary.name}
|
|
1713
|
+
* </button>
|
|
1714
|
+
* </li>
|
|
1715
|
+
* ))}
|
|
1716
|
+
* </ul>
|
|
1717
|
+
* );
|
|
1718
|
+
* }
|
|
1719
|
+
* ```
|
|
1720
|
+
*
|
|
1721
|
+
* @param opts - See {@link UseWorkflowsOptions}.
|
|
1722
|
+
* @returns The listing, its loading flag and its failure — see
|
|
1723
|
+
* {@link UseWorkflowsResult}.
|
|
1724
|
+
*
|
|
1463
1725
|
* @public
|
|
1464
1726
|
*/
|
|
1465
1727
|
function useWorkflows(opts = {}) {
|
|
@@ -1530,6 +1792,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1530
1792
|
api,
|
|
1531
1793
|
intervalMs
|
|
1532
1794
|
}));
|
|
1795
|
+
const { wake, cancel } = useRunControls(runId, getClient);
|
|
1533
1796
|
return {
|
|
1534
1797
|
submit: useCallback(async (input) => {
|
|
1535
1798
|
const client = getClient();
|
|
@@ -1569,6 +1832,8 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1569
1832
|
setStartError(void 0);
|
|
1570
1833
|
setUpload(void 0);
|
|
1571
1834
|
}, []),
|
|
1835
|
+
wake,
|
|
1836
|
+
cancel,
|
|
1572
1837
|
pauseUpload: useCallback(() => {
|
|
1573
1838
|
session.current?.gate.pause();
|
|
1574
1839
|
setUpload((current) => current ? {
|
|
@@ -1591,6 +1856,26 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1591
1856
|
}
|
|
1592
1857
|
//#endregion
|
|
1593
1858
|
//#region components/workflow-fields.tsx
|
|
1859
|
+
/** @jsxImportSource react */
|
|
1860
|
+
/**
|
|
1861
|
+
* A form built from a workflow's declared input schema.
|
|
1862
|
+
*
|
|
1863
|
+
* `GET workflows` reports each workflow's `inputSchema` as JSON Schema — the
|
|
1864
|
+
* zod schema an author wrote in `agent.ts`, converted at listing time precisely
|
|
1865
|
+
* so a browser can read it. This is what reads it: one `<WorkflowFields>` and a
|
|
1866
|
+
* workflow's form matches its schema by construction, so adding a field to the
|
|
1867
|
+
* schema adds it to the page and nothing can drift.
|
|
1868
|
+
*
|
|
1869
|
+
* ## It covers SCALARS, and says so rather than guessing
|
|
1870
|
+
*
|
|
1871
|
+
* A string, number, integer, boolean or enum has one obvious control each. A
|
|
1872
|
+
* nested object or an array does not — every choice (a JSON textarea, a repeater,
|
|
1873
|
+
* a comma-separated string) is a guess about what the author meant, and a guess
|
|
1874
|
+
* that produces a value the schema then rejects is worse than no field at all.
|
|
1875
|
+
* So those are SKIPPED, and the fields for them are written by hand: every field
|
|
1876
|
+
* in this package is a plain named control, so a hand-written one composes with
|
|
1877
|
+
* a generated one inside the same {@link Form}.
|
|
1878
|
+
*/
|
|
1594
1879
|
/**
|
|
1595
1880
|
* Render one field per scalar property of a workflow's input schema.
|
|
1596
1881
|
*
|
|
@@ -1622,6 +1907,8 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1622
1907
|
* }
|
|
1623
1908
|
* ```
|
|
1624
1909
|
*
|
|
1910
|
+
* @param props - Field-set props.
|
|
1911
|
+
*
|
|
1625
1912
|
* @public
|
|
1626
1913
|
*/
|
|
1627
1914
|
function WorkflowFields({ workflow }) {
|
|
@@ -1946,24 +2233,17 @@ function useWorkflowProgress(runId, opts = {}) {
|
|
|
1946
2233
|
* }
|
|
1947
2234
|
* ```
|
|
1948
2235
|
*
|
|
1949
|
-
* @param
|
|
1950
|
-
* pass its state straight through before a run exists.
|
|
1951
|
-
* @param api - The workflow API client, when the page holds its own. Defaults
|
|
1952
|
-
* to the one `page()` installs.
|
|
1953
|
-
* @param className - Replaces the default classes rather than extending them,
|
|
1954
|
-
* so a custom chrome is not fighting a default it did not ask for.
|
|
1955
|
-
* @param placeholder - Rendered instead of nothing while the run has said
|
|
1956
|
-
* nothing yet — for a page that would otherwise reflow when the first line
|
|
1957
|
-
* lands.
|
|
2236
|
+
* @param props - Progress-log props.
|
|
1958
2237
|
*
|
|
1959
2238
|
* @public
|
|
1960
2239
|
*/
|
|
1961
|
-
function WorkflowProgress({ runId, api, className, placeholder }) {
|
|
2240
|
+
function WorkflowProgress({ runId, api, className, placeholder, lines }) {
|
|
1962
2241
|
const { progress, streaming, supported } = useWorkflowProgress(runId, omitUndefined({ api }));
|
|
1963
|
-
|
|
2242
|
+
const shown = lines === void 0 ? progress : progress.slice(Math.max(progress.length - lines, 0));
|
|
2243
|
+
if (!supported || shown.length === 0) return placeholder ?? null;
|
|
1964
2244
|
return /* @__PURE__ */ jsxs("pre", {
|
|
1965
2245
|
className: clsx(className ?? "whitespace-pre-wrap border-l pl-4 text-xs opacity-70"),
|
|
1966
|
-
children: [
|
|
2246
|
+
children: [shown.join("\n"), streaming && "\n…"]
|
|
1967
2247
|
});
|
|
1968
2248
|
}
|
|
1969
2249
|
//#endregion
|
|
@@ -2027,69 +2307,94 @@ function page(config) {
|
|
|
2027
2307
|
return mountRoot(container, createElement(ThemeProvider, { value: config.theme }, createElement(config.component)));
|
|
2028
2308
|
}
|
|
2029
2309
|
//#endregion
|
|
2030
|
-
//#region use-
|
|
2031
|
-
/**
|
|
2032
|
-
* `
|
|
2033
|
-
*
|
|
2034
|
-
*
|
|
2035
|
-
*
|
|
2036
|
-
*
|
|
2037
|
-
*
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2042
|
-
*
|
|
2043
|
-
*
|
|
2044
|
-
*
|
|
2045
|
-
*
|
|
2046
|
-
*
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
*
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
*
|
|
2053
|
-
*
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
*
|
|
2063
|
-
*
|
|
2064
|
-
*
|
|
2065
|
-
*
|
|
2066
|
-
*
|
|
2310
|
+
//#region use-download-url.ts
|
|
2311
|
+
/**
|
|
2312
|
+
* `useDownloadUrl` — an upload id a run produced, as something `<audio>`,
|
|
2313
|
+
* `<img>` or `<a download>` will accept.
|
|
2314
|
+
*
|
|
2315
|
+
* `api.download(id)` resolves a `Blob`, and it has to: the byte route takes the
|
|
2316
|
+
* same bearer every workflow route does, and neither `<audio src>` nor
|
|
2317
|
+
* `<a href>` can send one. So every page that plays back what a run WROTE ends
|
|
2318
|
+
* up at the same four lines — `download` → `createObjectURL` → state — and the
|
|
2319
|
+
* two that are really the point are the two the four lines are wrapped in:
|
|
2320
|
+
*
|
|
2321
|
+
* - **`URL.revokeObjectURL` on cleanup.** An object URL pins its blob for the
|
|
2322
|
+
* life of the DOCUMENT. Miss it and every completed run's audio stays resident
|
|
2323
|
+
* until the tab closes, which on a page people run all day is a leak measured
|
|
2324
|
+
* in the size of the files.
|
|
2325
|
+
* - **A `cancelled` flag.** A second run settling while the first download is
|
|
2326
|
+
* still in flight otherwise sets state from the stale one, and the page plays
|
|
2327
|
+
* the previous run's audio under the current run's transcript — a wrong answer
|
|
2328
|
+
* that looks like a right one.
|
|
2329
|
+
*
|
|
2330
|
+
* Two templates had written this hook, identically, doc paragraph included, and
|
|
2331
|
+
* `aai-ui` exported no download helper at all. Both also faked `pending` by
|
|
2332
|
+
* checking `url === undefined && error === undefined`, which reads "idle" and
|
|
2333
|
+
* "downloading" as the same thing — so this reports it.
|
|
2334
|
+
*/
|
|
2335
|
+
/** No id: nothing pending, nothing to show. A shared object so `setState` no-ops. */
|
|
2336
|
+
const IDLE = { pending: false };
|
|
2337
|
+
/** Bytes in flight. Shared for the same reason as {@link IDLE}. */
|
|
2338
|
+
const PENDING = { pending: true };
|
|
2339
|
+
/**
|
|
2340
|
+
* Read an upload's bytes and hand back a URL a DOM element can use.
|
|
2341
|
+
*
|
|
2342
|
+
* @param uploadId - The id a completed run reported, or `undefined` before one
|
|
2343
|
+
* exists — which is what a page passes straight through while it waits, and
|
|
2344
|
+
* reports as idle rather than pending.
|
|
2345
|
+
* @param opts - See {@link UseDownloadUrlOptions}.
|
|
2346
|
+
* @returns See {@link UseDownloadUrlResult}.
|
|
2067
2347
|
*
|
|
2068
2348
|
* @example
|
|
2069
2349
|
* ```tsx
|
|
2070
|
-
* import {
|
|
2071
|
-
*
|
|
2072
|
-
* function
|
|
2073
|
-
* const {
|
|
2074
|
-
*
|
|
2075
|
-
*
|
|
2350
|
+
* import { useDownloadUrl, useWorkflowSubmit } from "@alexkroman1/aai-ui";
|
|
2351
|
+
*
|
|
2352
|
+
* function Playback() {
|
|
2353
|
+
* const { run } = useWorkflowSubmit<{ audio: string }>("spokenSummary");
|
|
2354
|
+
* const output = run?.status === "completed" ? run.output : undefined;
|
|
2355
|
+
* const audio = useDownloadUrl(output?.audio);
|
|
2356
|
+
* if (audio.pending) return <p>Fetching audio…</p>;
|
|
2357
|
+
* if (audio.error !== undefined) return <p role="alert">{audio.error}</p>;
|
|
2358
|
+
* return audio.url === undefined ? null : (
|
|
2359
|
+
* <a href={audio.url} download="summary.mp3">
|
|
2360
|
+
* Download
|
|
2361
|
+
* </a>
|
|
2362
|
+
* );
|
|
2076
2363
|
* }
|
|
2077
2364
|
* ```
|
|
2078
2365
|
*
|
|
2079
2366
|
* @public
|
|
2080
2367
|
*/
|
|
2081
|
-
function
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2368
|
+
function useDownloadUrl(uploadId, opts = {}) {
|
|
2369
|
+
const [state, setState] = useState(IDLE);
|
|
2370
|
+
const getClient = useWorkflowApiRef(opts.api);
|
|
2371
|
+
useEffect(() => {
|
|
2372
|
+
if (uploadId === void 0) {
|
|
2373
|
+
setState(IDLE);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
let cancelled = false;
|
|
2377
|
+
let objectUrl;
|
|
2378
|
+
setState(PENDING);
|
|
2379
|
+
getClient().download(uploadId).then((blob) => {
|
|
2380
|
+
if (cancelled) return;
|
|
2381
|
+
objectUrl = URL.createObjectURL(blob);
|
|
2382
|
+
setState({
|
|
2383
|
+
url: objectUrl,
|
|
2384
|
+
pending: false
|
|
2385
|
+
});
|
|
2386
|
+
}).catch((err) => {
|
|
2387
|
+
if (!cancelled) setState({
|
|
2388
|
+
error: errorMessage(err),
|
|
2389
|
+
pending: false
|
|
2390
|
+
});
|
|
2391
|
+
});
|
|
2392
|
+
return () => {
|
|
2393
|
+
cancelled = true;
|
|
2394
|
+
if (objectUrl !== void 0) URL.revokeObjectURL(objectUrl);
|
|
2395
|
+
};
|
|
2396
|
+
}, [uploadId, getClient]);
|
|
2397
|
+
return state;
|
|
2093
2398
|
}
|
|
2094
2399
|
//#endregion
|
|
2095
2400
|
//#region use-workflow-runs.ts
|
|
@@ -2306,6 +2611,7 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2306
2611
|
api,
|
|
2307
2612
|
intervalMs
|
|
2308
2613
|
}));
|
|
2614
|
+
const { wake, cancel } = useRunControls(runId, getClient);
|
|
2309
2615
|
return {
|
|
2310
2616
|
submit: useCallback(async (input) => {
|
|
2311
2617
|
const client = getClient();
|
|
@@ -2361,6 +2667,8 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2361
2667
|
setStartError(void 0);
|
|
2362
2668
|
setUpload(void 0);
|
|
2363
2669
|
}, []),
|
|
2670
|
+
wake,
|
|
2671
|
+
cancel,
|
|
2364
2672
|
pauseUpload: useCallback(() => {
|
|
2365
2673
|
gateRef.current?.pause();
|
|
2366
2674
|
setUpload((current) => current ? {
|
|
@@ -2474,4 +2782,30 @@ function fileAt(input, field) {
|
|
|
2474
2782
|
return filesOf(input[field])[0];
|
|
2475
2783
|
}
|
|
2476
2784
|
//#endregion
|
|
2477
|
-
|
|
2785
|
+
//#region workflow-status-labels.ts
|
|
2786
|
+
/**
|
|
2787
|
+
* The default status line per {@link WorkflowRunStatus}.
|
|
2788
|
+
*
|
|
2789
|
+
* Override the ones your page has a better word for and keep the rest:
|
|
2790
|
+
*
|
|
2791
|
+
* ```ts
|
|
2792
|
+
* import { WORKFLOW_STATUS_LABELS } from "@alexkroman1/aai-ui";
|
|
2793
|
+
*
|
|
2794
|
+
* const STATUS_LINE = { ...WORKFLOW_STATUS_LABELS, running: "Writing…" };
|
|
2795
|
+
* ```
|
|
2796
|
+
*
|
|
2797
|
+
* The wording is deliberately about the RUN rather than about the work — a page
|
|
2798
|
+
* knows what its workflow does and this does not, so `running` is the neutral
|
|
2799
|
+
* "Working…" and every page that cares replaces exactly that key.
|
|
2800
|
+
*
|
|
2801
|
+
* @public
|
|
2802
|
+
*/
|
|
2803
|
+
const WORKFLOW_STATUS_LABELS = {
|
|
2804
|
+
pending: "Queued",
|
|
2805
|
+
running: "Working…",
|
|
2806
|
+
completed: "Done",
|
|
2807
|
+
failed: "Failed",
|
|
2808
|
+
cancelled: "Cancelled"
|
|
2809
|
+
};
|
|
2810
|
+
//#endregion
|
|
2811
|
+
export { AutoScroll, Button, ChatView, CheckboxField, ConsoleShell, Controls, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, Field, FileField, Form, MAX_MISSING_READS, Markdown, MessageList, NumberField, SelectField, SidebarLayout, StartScreen, SubmitButton, TRANSCRIBING_PLACEHOLDER, TextAreaField, TextField, ToolCallRow, UploadProgressBar, WORKFLOW_STATUS_LABELS, WorkflowFields, WorkflowProgress, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, page, useAgentState, useConversation, useDownloadUrl, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };
|