@alexkroman1/aai-ui 6.3.0 → 6.4.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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which of a submitted form's values are FILES.
3
+ *
4
+ * Its own module because both submit hooks need the identical answer and then do
5
+ * two different things with it — `useWorkflowSubmit` stores each file and passes
6
+ * its id, `useWorkflowStream` cuts it into parts and passes the group they share.
7
+ * A second copy of this predicate would be a form field that one hook treats as a
8
+ * file and the other does not, which is invisible until the run reads the wrong
9
+ * kind of string.
10
+ */
11
+ /**
12
+ * The files a submitted field carries, if that is what it carries.
13
+ *
14
+ * An array counts only when it is files ALL the way through — a mixed array is
15
+ * some other field's value that happens to contain one, and turning half of it
16
+ * into ids would corrupt it silently.
17
+ */
18
+ export declare function filesOf(value: unknown): File[];
package/dist/index.d.ts CHANGED
@@ -30,4 +30,5 @@ export { type UploadStatus, type UseWorkflowSubmitOptions, type UseWorkflowsOpti
30
30
  export { DEFAULT_PROGRESS_POLL_MS, type UseWorkflowProgressResult, useWorkflowProgress, } from "./use-workflow-progress.ts";
31
31
  export { DEFAULT_WORKFLOW_POLL_MS, MAX_MISSING_READS, type UseWorkflowRunResult, useWorkflowRun, } from "./use-workflow-run.ts";
32
32
  export { type UseWorkflowRunsOptions, type UseWorkflowRunsResult, useWorkflowRuns, } from "./use-workflow-runs.ts";
33
+ export { type UseWorkflowStreamOptions, useWorkflowStream, type WorkflowStreamSubmission, } from "./use-workflow-stream.ts";
33
34
  export { createWorkflowApi, isTerminal, type WorkflowApi, type WorkflowApiOptions, type WorkflowOutputOf, type WorkflowRun, type WorkflowSummary, } from "./workflow-client.ts";
package/dist/index.js CHANGED
@@ -625,6 +625,31 @@ function useWorkflowApiRef(api) {
625
625
  }, []);
626
626
  }
627
627
  //#endregion
628
+ //#region _workflow-files.ts
629
+ /**
630
+ * Which of a submitted form's values are FILES.
631
+ *
632
+ * Its own module because both submit hooks need the identical answer and then do
633
+ * two different things with it — `useWorkflowSubmit` stores each file and passes
634
+ * its id, `useWorkflowStream` cuts it into parts and passes the group they share.
635
+ * A second copy of this predicate would be a form field that one hook treats as a
636
+ * file and the other does not, which is invisible until the run reads the wrong
637
+ * kind of string.
638
+ */
639
+ /**
640
+ * The files a submitted field carries, if that is what it carries.
641
+ *
642
+ * An array counts only when it is files ALL the way through — a mixed array is
643
+ * some other field's value that happens to contain one, and turning half of it
644
+ * into ids would corrupt it silently.
645
+ */
646
+ function filesOf(value) {
647
+ if (value instanceof File) return [value];
648
+ if (!Array.isArray(value)) return [];
649
+ const files = value.filter((one) => one instanceof File);
650
+ return files.length > 0 && files.length === value.length ? files : [];
651
+ }
652
+ //#endregion
628
653
  //#region _repeat-until.ts
629
654
  /**
630
655
  * A bounded read, re-armed from the SETTLED read — the loop both workflow
@@ -1031,19 +1056,6 @@ function useWorkflows(opts = {}) {
1031
1056
  return state;
1032
1057
  }
1033
1058
  /**
1034
- * The files a submitted field carries, if that is what it carries.
1035
- *
1036
- * An array counts only when it is files ALL the way through — a mixed array is
1037
- * some other field's value that happens to contain one, and turning half of it
1038
- * into ids would corrupt it silently.
1039
- */
1040
- function filesOf(value) {
1041
- if (value instanceof File) return [value];
1042
- if (!Array.isArray(value)) return [];
1043
- const files = value.filter((one) => one instanceof File);
1044
- return files.length > 0 && files.length === value.length ? files : [];
1045
- }
1046
- /**
1047
1059
  * Replace every `File` in a submitted form with the id of a stored upload,
1048
1060
  * reporting how far each one has got.
1049
1061
  *
@@ -1761,4 +1773,171 @@ function useWorkflowRuns(workflow, opts = {}) {
1761
1773
  };
1762
1774
  }
1763
1775
  //#endregion
1764
- export { ApiUrlChip, AutoScroll, Button, ChatView, CheckboxField, Controls, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, Field, FileField, Form, MAX_MISSING_READS, Markdown, MessageList, NumberField, SelectField, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, SubmitButton, TRANSCRIBING_PLACEHOLDER, TextAreaField, TextField, ThemeProvider, ToolCallRow, ToolConfigContext, UiUrlChip, UploadProgressBar, VOICE_CAPTURE_CONSTRAINTS, WorkflowFields, WorkflowProgress, buildAgentUrl, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, loadClientConfig, page, useAgentState, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowSubmit, useWorkflows };
1776
+ //#region use-workflow-stream.ts
1777
+ /**
1778
+ * Starting a run BEFORE its file has finished uploading.
1779
+ *
1780
+ * `useWorkflowSubmit` (`use-workflow-form.ts`) stores every file and then starts the
1781
+ * run, because a `POST /workflows/uploads` cannot answer with an id until the last
1782
+ * byte is in — the store writes an ordinary upload's record last, so "incomplete"
1783
+ * and "no such upload" are deliberately the same answer. For a long recording that
1784
+ * order is most of the wall clock.
1785
+ *
1786
+ * This hook inverts it, and it takes ONE extra idea to do so: the id is the
1787
+ * CLIENT's. It mints one, starts the run on it, and `PUT`s the whole file in a
1788
+ * single streaming request; the upload record exists from the first byte with
1789
+ * `complete: false` and its `size` grows, so the run reads whatever has arrived.
1790
+ *
1791
+ * ```text
1792
+ * start run ─┬──────────────────────────────────────────────► (still running)
1793
+ * │
1794
+ * PUT /uploads/<id> ═══════════════════════════════════► wake
1795
+ * (one request; the run polls `size` and `complete`)
1796
+ * ```
1797
+ *
1798
+ * ## What it does NOT do, which is the point
1799
+ *
1800
+ * There is no cutting, no part numbering, no terminator and no per-part request. An
1801
+ * earlier version of this hook took a `cut` callback and uploaded N parts into a
1802
+ * "group" that a separate call had to seal; it worked, and every piece of it was
1803
+ * something the caller had to get right. The whole of that is replaced by the store
1804
+ * publishing `size` as bytes land — which `readUpload` already clamped to — so the
1805
+ * run does exactly what it does over a finished file and simply waits for windows to
1806
+ * become present.
1807
+ *
1808
+ * It also means the FORMAT knowledge stays where it already was. Deciding where a
1809
+ * recording may be divided is the run's business (`planSegments` in the
1810
+ * transcription template), and nothing here needs to know it is audio at all.
1811
+ *
1812
+ * ## Three things it owns
1813
+ *
1814
+ * - **The id.** Minted here, put in the run input where the workflow's `uploads`
1815
+ * list says, and never seen by the page. It is a capability — anyone holding it
1816
+ * can read the bytes back — so it is a `crypto.randomUUID()` rather than anything
1817
+ * derived from the file, and the store refuses a second `PUT` to it.
1818
+ * - **The wake after the upload.** `POST /workflows/runs/:id/wake` ends a pending
1819
+ * `sleep`, and a run waiting on an upload is asleep between polls — so without
1820
+ * this it learns the file is complete up to a poll interval late, every time. On
1821
+ * the transcription template's 5-second interval that is most of the tail it has
1822
+ * left to pay. Best-effort, because a wake that finds nothing sleeping answers 0
1823
+ * and a missed one costs latency rather than correctness.
1824
+ * - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
1825
+ * so `<UploadProgressBar>` renders either without knowing which hook it came from.
1826
+ *
1827
+ * ## A failed upload CANCELS the run
1828
+ *
1829
+ * An upload that dies stays in the store, incomplete, and `complete` never becomes
1830
+ * true — so a run left behind polls until its own abandonment bound and then fails,
1831
+ * minutes after the page has already reported the error. Cancelling is the honest
1832
+ * end to a submission that did not happen. The cost is that work already done is
1833
+ * thrown away with the run; a caller who wants to resume instead drives
1834
+ * `api.uploadStream` and `api.uploadInfo` directly.
1835
+ */
1836
+ /**
1837
+ * Start a workflow run and stream a file into it while it works.
1838
+ *
1839
+ * The workflow declares which input property carries the upload
1840
+ * (`workflow({ uploads: ["recording"] })`) — the same declaration
1841
+ * `useWorkflowSubmit` reads, because what the property carries is an upload id
1842
+ * either way. What differs is only WHEN the id becomes valid.
1843
+ *
1844
+ * @typeParam R - The workflow's output type, which is what makes
1845
+ * `run.status === "completed"` narrow to a typed `run.output`. Derive it with
1846
+ * `WorkflowOutputOf<typeof myWorkflow>`.
1847
+ *
1848
+ * @example
1849
+ * ```tsx no-check
1850
+ * const { submit, run, upload, pending, error } = useWorkflowStream("transcribe");
1851
+ *
1852
+ * <Form onSubmit={(values) => submit(values)} error={error}>
1853
+ * <WorkflowFields workflow="transcribe" />
1854
+ * <UploadProgressBar upload={upload} />
1855
+ * <SubmitButton pending={pending}>Transcribe</SubmitButton>
1856
+ * </Form>
1857
+ * ```
1858
+ *
1859
+ * @public
1860
+ */
1861
+ function useWorkflowStream(workflow, opts = {}) {
1862
+ const { api, key, intervalMs } = opts;
1863
+ const [runId, setRunId] = useState(void 0);
1864
+ const [starting, setStarting] = useState(false);
1865
+ const [startError, setStartError] = useState(void 0);
1866
+ const [upload, setUpload] = useState(void 0);
1867
+ const getClient = useWorkflowApiRef(api);
1868
+ const tracked = useWorkflowRun(runId, {
1869
+ ...api && { api },
1870
+ ...omitUndefined({ intervalMs })
1871
+ });
1872
+ return {
1873
+ submit: useCallback(async (input) => {
1874
+ const client = getClient();
1875
+ setStarting(true);
1876
+ setStartError(void 0);
1877
+ setRunId(void 0);
1878
+ let started;
1879
+ try {
1880
+ const field = await uploadField(client, workflow);
1881
+ const chosen = field === void 0 ? void 0 : fileAt(input, field);
1882
+ const id = randomUploadId();
1883
+ const payload = chosen && field ? {
1884
+ ...input,
1885
+ [field]: id
1886
+ } : input;
1887
+ started = await client.start(workflow, payload, omitUndefined({ key }));
1888
+ setRunId(started);
1889
+ if (!chosen) return;
1890
+ await client.uploadStream(id, chosen, {
1891
+ name: chosen.name,
1892
+ onProgress: (progress) => setUpload({
1893
+ ...progress,
1894
+ name: chosen.name,
1895
+ index: 1,
1896
+ count: 1
1897
+ })
1898
+ });
1899
+ await client.wake(started).catch(() => void 0);
1900
+ } catch (err) {
1901
+ setStartError(errorMessage(err));
1902
+ if (started) await client.cancel(started).catch(() => void 0);
1903
+ } finally {
1904
+ setStarting(false);
1905
+ setUpload(void 0);
1906
+ }
1907
+ }, [
1908
+ workflow,
1909
+ key,
1910
+ getClient
1911
+ ]),
1912
+ reset: useCallback(() => {
1913
+ setRunId(void 0);
1914
+ setStartError(void 0);
1915
+ setUpload(void 0);
1916
+ }, []),
1917
+ run: tracked.run,
1918
+ pending: starting || tracked.polling,
1919
+ upload,
1920
+ error: startError ?? tracked.error
1921
+ };
1922
+ }
1923
+ /** A fresh upload id: a capability, so it is random rather than derived. */
1924
+ function randomUploadId() {
1925
+ return crypto.randomUUID().replaceAll("-", "");
1926
+ }
1927
+ /**
1928
+ * Which input property this workflow says carries an upload id.
1929
+ *
1930
+ * `undefined` when the workflow declares none, which is not an error: the caller
1931
+ * may be using this hook against a workflow that takes no file at all, and the run
1932
+ * then starts with the input untouched.
1933
+ */
1934
+ async function uploadField(client, workflow) {
1935
+ return (await client.list()).find((one) => one.name === workflow)?.uploads?.[0];
1936
+ }
1937
+ /** The `File` at `field`, if that is what the form put there. */
1938
+ function fileAt(input, field) {
1939
+ if (!isRecord(input)) return void 0;
1940
+ return filesOf(input[field])[0];
1941
+ }
1942
+ //#endregion
1943
+ export { ApiUrlChip, AutoScroll, Button, ChatView, CheckboxField, Controls, DEFAULT_PROGRESS_POLL_MS, DEFAULT_WORKFLOW_POLL_MS, Field, FileField, Form, MAX_MISSING_READS, Markdown, MessageList, NumberField, SelectField, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, SubmitButton, TRANSCRIBING_PLACEHOLDER, TextAreaField, TextField, ThemeProvider, ToolCallRow, ToolConfigContext, UiUrlChip, UploadProgressBar, VOICE_CAPTURE_CONSTRAINTS, WorkflowFields, WorkflowProgress, buildAgentUrl, client, createSessionCore, createWorkflowApi, fetchClientConfig, isTerminal, loadClientConfig, page, useAgentState, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult, useUserTranscript, useWorkflowProgress, useWorkflowRun, useWorkflowRuns, useWorkflowStream, useWorkflowSubmit, useWorkflows };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Starting a run BEFORE its file has finished uploading.
3
+ *
4
+ * `useWorkflowSubmit` (`use-workflow-form.ts`) stores every file and then starts the
5
+ * run, because a `POST /workflows/uploads` cannot answer with an id until the last
6
+ * byte is in — the store writes an ordinary upload's record last, so "incomplete"
7
+ * and "no such upload" are deliberately the same answer. For a long recording that
8
+ * order is most of the wall clock.
9
+ *
10
+ * This hook inverts it, and it takes ONE extra idea to do so: the id is the
11
+ * CLIENT's. It mints one, starts the run on it, and `PUT`s the whole file in a
12
+ * single streaming request; the upload record exists from the first byte with
13
+ * `complete: false` and its `size` grows, so the run reads whatever has arrived.
14
+ *
15
+ * ```text
16
+ * start run ─┬──────────────────────────────────────────────► (still running)
17
+ * │
18
+ * PUT /uploads/<id> ═══════════════════════════════════► wake
19
+ * (one request; the run polls `size` and `complete`)
20
+ * ```
21
+ *
22
+ * ## What it does NOT do, which is the point
23
+ *
24
+ * There is no cutting, no part numbering, no terminator and no per-part request. An
25
+ * earlier version of this hook took a `cut` callback and uploaded N parts into a
26
+ * "group" that a separate call had to seal; it worked, and every piece of it was
27
+ * something the caller had to get right. The whole of that is replaced by the store
28
+ * publishing `size` as bytes land — which `readUpload` already clamped to — so the
29
+ * run does exactly what it does over a finished file and simply waits for windows to
30
+ * become present.
31
+ *
32
+ * It also means the FORMAT knowledge stays where it already was. Deciding where a
33
+ * recording may be divided is the run's business (`planSegments` in the
34
+ * transcription template), and nothing here needs to know it is audio at all.
35
+ *
36
+ * ## Three things it owns
37
+ *
38
+ * - **The id.** Minted here, put in the run input where the workflow's `uploads`
39
+ * list says, and never seen by the page. It is a capability — anyone holding it
40
+ * can read the bytes back — so it is a `crypto.randomUUID()` rather than anything
41
+ * derived from the file, and the store refuses a second `PUT` to it.
42
+ * - **The wake after the upload.** `POST /workflows/runs/:id/wake` ends a pending
43
+ * `sleep`, and a run waiting on an upload is asleep between polls — so without
44
+ * this it learns the file is complete up to a poll interval late, every time. On
45
+ * the transcription template's 5-second interval that is most of the tail it has
46
+ * left to pay. Best-effort, because a wake that finds nothing sleeping answers 0
47
+ * and a missed one costs latency rather than correctness.
48
+ * - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
49
+ * so `<UploadProgressBar>` renders either without knowing which hook it came from.
50
+ *
51
+ * ## A failed upload CANCELS the run
52
+ *
53
+ * An upload that dies stays in the store, incomplete, and `complete` never becomes
54
+ * true — so a run left behind polls until its own abandonment bound and then fails,
55
+ * minutes after the page has already reported the error. Cancelling is the honest
56
+ * end to a submission that did not happen. The cost is that work already done is
57
+ * thrown away with the run; a caller who wants to resume instead drives
58
+ * `api.uploadStream` and `api.uploadInfo` directly.
59
+ */
60
+ import type { UploadStatus } from "./use-workflow-form.ts";
61
+ import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
62
+ /** Options for {@link useWorkflowStream}. */
63
+ export type UseWorkflowStreamOptions = {
64
+ /** The client to start runs with. Defaults to one for the page's own agent. */
65
+ api?: WorkflowApi;
66
+ /** Correlation key recorded with the run, for finding it again without the id. */
67
+ key?: string;
68
+ /** How often the fallback poll re-reads a live run. */
69
+ intervalMs?: number;
70
+ };
71
+ /** What {@link useWorkflowStream} returns. */
72
+ export type WorkflowStreamSubmission<R = unknown> = {
73
+ /**
74
+ * Start a run and stream this input's file into it.
75
+ *
76
+ * Resolves when the upload finishes, NOT when the run does — the run's own
77
+ * progress arrives through `run`. It resolves rather than rejecting on a failed
78
+ * upload; the failure is reported through `error`, the way a form expects.
79
+ */
80
+ submit: (input: unknown) => Promise<void>;
81
+ /** Clear the run and any error, putting the form back to its initial state. */
82
+ reset: () => void;
83
+ /**
84
+ * The run, from the moment it EXISTS — which here is before its bytes are in.
85
+ *
86
+ * That is the whole difference from `useWorkflowSubmit`, and what lets a page
87
+ * render `<WorkflowProgress>` beside the upload bar rather than after it.
88
+ */
89
+ run: WorkflowRun<R> | undefined;
90
+ /** True from `submit()` until the run reaches a terminal status. */
91
+ pending: boolean;
92
+ /** How far the upload has got, while it is still going. */
93
+ upload: UploadStatus | undefined;
94
+ /** The submit's own failure (a rejected input, or an upload that would not store). */
95
+ error: string | undefined;
96
+ };
97
+ /**
98
+ * Start a workflow run and stream a file into it while it works.
99
+ *
100
+ * The workflow declares which input property carries the upload
101
+ * (`workflow({ uploads: ["recording"] })`) — the same declaration
102
+ * `useWorkflowSubmit` reads, because what the property carries is an upload id
103
+ * either way. What differs is only WHEN the id becomes valid.
104
+ *
105
+ * @typeParam R - The workflow's output type, which is what makes
106
+ * `run.status === "completed"` narrow to a typed `run.output`. Derive it with
107
+ * `WorkflowOutputOf<typeof myWorkflow>`.
108
+ *
109
+ * @example
110
+ * ```tsx no-check
111
+ * const { submit, run, upload, pending, error } = useWorkflowStream("transcribe");
112
+ *
113
+ * <Form onSubmit={(values) => submit(values)} error={error}>
114
+ * <WorkflowFields workflow="transcribe" />
115
+ * <UploadProgressBar upload={upload} />
116
+ * <SubmitButton pending={pending}>Transcribe</SubmitButton>
117
+ * </Form>
118
+ * ```
119
+ *
120
+ * @public
121
+ */
122
+ export declare function useWorkflowStream<R = unknown>(workflow: string, opts?: UseWorkflowStreamOptions): WorkflowStreamSubmission<R>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -29,7 +29,7 @@
29
29
  "remark-gfm": "^4.0.1",
30
30
  "use-stick-to-bottom": "^1.1.6",
31
31
  "use-sync-external-store": "^1.6.0",
32
- "@alexkroman1/aai": "6.3.0"
32
+ "@alexkroman1/aai": "6.4.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^19.0.0",