@alexkroman1/aai-ui 6.3.1 → 6.5.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/dist/_workflow-files.d.ts +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +206 -21
- package/dist/use-workflow-form.d.ts +13 -1
- package/dist/use-workflow-stream.d.ts +136 -0
- package/package.json +2 -2
|
@@ -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
|
*
|
|
@@ -1056,7 +1068,7 @@ function filesOf(value) {
|
|
|
1056
1068
|
* so this is invisible to every form that has none — including one whose values
|
|
1057
1069
|
* are not an object at all, which `submit` accepts.
|
|
1058
1070
|
*/
|
|
1059
|
-
async function uploadFiles(api, input, report) {
|
|
1071
|
+
async function uploadFiles(api, input, report, parallel) {
|
|
1060
1072
|
if (!isRecord(input)) return input;
|
|
1061
1073
|
const entries = Object.entries(input);
|
|
1062
1074
|
const count = entries.reduce((total, [, value]) => total + filesOf(value).length, 0);
|
|
@@ -1068,10 +1080,13 @@ async function uploadFiles(api, input, report) {
|
|
|
1068
1080
|
index,
|
|
1069
1081
|
count
|
|
1070
1082
|
};
|
|
1071
|
-
return (await api.upload(file, {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1083
|
+
return (await api.upload(file, {
|
|
1084
|
+
onProgress: (progress) => report({
|
|
1085
|
+
...position,
|
|
1086
|
+
...progress
|
|
1087
|
+
}),
|
|
1088
|
+
...omitUndefined({ parallel })
|
|
1089
|
+
})).id;
|
|
1075
1090
|
};
|
|
1076
1091
|
const out = {};
|
|
1077
1092
|
for (const [name, value] of entries) {
|
|
@@ -1116,7 +1131,7 @@ async function uploadFiles(api, input, report) {
|
|
|
1116
1131
|
* @public
|
|
1117
1132
|
*/
|
|
1118
1133
|
function useWorkflowSubmit(workflow, opts = {}) {
|
|
1119
|
-
const { api, key, wait, intervalMs } = opts;
|
|
1134
|
+
const { api, key, wait, intervalMs, parallel } = opts;
|
|
1120
1135
|
const [runId, setRunId] = useState(void 0);
|
|
1121
1136
|
const [starting, setStarting] = useState(false);
|
|
1122
1137
|
const [startError, setStartError] = useState(void 0);
|
|
@@ -1134,7 +1149,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1134
1149
|
setRunId(void 0);
|
|
1135
1150
|
try {
|
|
1136
1151
|
const options = omitUndefined({ key });
|
|
1137
|
-
const started = await uploadFiles(client, input, setUpload);
|
|
1152
|
+
const started = await uploadFiles(client, input, setUpload, parallel);
|
|
1138
1153
|
setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
|
|
1139
1154
|
...options,
|
|
1140
1155
|
wait
|
|
@@ -1149,6 +1164,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1149
1164
|
workflow,
|
|
1150
1165
|
key,
|
|
1151
1166
|
wait,
|
|
1167
|
+
parallel,
|
|
1152
1168
|
getClient
|
|
1153
1169
|
]),
|
|
1154
1170
|
reset: useCallback(() => {
|
|
@@ -1761,4 +1777,173 @@ function useWorkflowRuns(workflow, opts = {}) {
|
|
|
1761
1777
|
};
|
|
1762
1778
|
}
|
|
1763
1779
|
//#endregion
|
|
1764
|
-
|
|
1780
|
+
//#region use-workflow-stream.ts
|
|
1781
|
+
/**
|
|
1782
|
+
* Starting a run BEFORE its file has finished uploading.
|
|
1783
|
+
*
|
|
1784
|
+
* `useWorkflowSubmit` (`use-workflow-form.ts`) stores every file and then starts the
|
|
1785
|
+
* run, because a `POST /workflows/uploads` cannot answer with an id until the last
|
|
1786
|
+
* byte is in — the store writes an ordinary upload's record last, so "incomplete"
|
|
1787
|
+
* and "no such upload" are deliberately the same answer. For a long recording that
|
|
1788
|
+
* order is most of the wall clock.
|
|
1789
|
+
*
|
|
1790
|
+
* This hook inverts it, and it takes ONE extra idea to do so: the id is the
|
|
1791
|
+
* CLIENT's. It mints one, starts the run on it, and `PUT`s the whole file in a
|
|
1792
|
+
* single streaming request; the upload record exists from the first byte with
|
|
1793
|
+
* `complete: false` and its `size` grows, so the run reads whatever has arrived.
|
|
1794
|
+
*
|
|
1795
|
+
* ```text
|
|
1796
|
+
* start run ─┬──────────────────────────────────────────────► (still running)
|
|
1797
|
+
* │
|
|
1798
|
+
* PUT /uploads/<id> ═══════════════════════════════════► wake
|
|
1799
|
+
* (one request; the run polls `size` and `complete`)
|
|
1800
|
+
* ```
|
|
1801
|
+
*
|
|
1802
|
+
* ## What it does NOT do, which is the point
|
|
1803
|
+
*
|
|
1804
|
+
* There is no cutting, no part numbering, no terminator and no per-part request. An
|
|
1805
|
+
* earlier version of this hook took a `cut` callback and uploaded N parts into a
|
|
1806
|
+
* "group" that a separate call had to seal; it worked, and every piece of it was
|
|
1807
|
+
* something the caller had to get right. The whole of that is replaced by the store
|
|
1808
|
+
* publishing `size` as bytes land — which `readUpload` already clamped to — so the
|
|
1809
|
+
* run does exactly what it does over a finished file and simply waits for windows to
|
|
1810
|
+
* become present.
|
|
1811
|
+
*
|
|
1812
|
+
* It also means the FORMAT knowledge stays where it already was. Deciding where a
|
|
1813
|
+
* recording may be divided is the run's business (`planSegments` in the
|
|
1814
|
+
* transcription template), and nothing here needs to know it is audio at all.
|
|
1815
|
+
*
|
|
1816
|
+
* ## Three things it owns
|
|
1817
|
+
*
|
|
1818
|
+
* - **The id.** Minted here, put in the run input where the workflow's `uploads`
|
|
1819
|
+
* list says, and never seen by the page. It is a capability — anyone holding it
|
|
1820
|
+
* can read the bytes back — so it is a `crypto.randomUUID()` rather than anything
|
|
1821
|
+
* derived from the file, and the store refuses a second `PUT` to it.
|
|
1822
|
+
* - **The wake after the upload.** `POST /workflows/runs/:id/wake` ends a pending
|
|
1823
|
+
* `sleep`, and a run waiting on an upload is asleep between polls — so without
|
|
1824
|
+
* this it learns the file is complete up to a poll interval late, every time. On
|
|
1825
|
+
* the transcription template's 5-second interval that is most of the tail it has
|
|
1826
|
+
* left to pay. Best-effort, because a wake that finds nothing sleeping answers 0
|
|
1827
|
+
* and a missed one costs latency rather than correctness.
|
|
1828
|
+
* - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
|
|
1829
|
+
* so `<UploadProgressBar>` renders either without knowing which hook it came from.
|
|
1830
|
+
*
|
|
1831
|
+
* ## A failed upload CANCELS the run
|
|
1832
|
+
*
|
|
1833
|
+
* An upload that dies stays in the store, incomplete, and `complete` never becomes
|
|
1834
|
+
* true — so a run left behind polls until its own abandonment bound and then fails,
|
|
1835
|
+
* minutes after the page has already reported the error. Cancelling is the honest
|
|
1836
|
+
* end to a submission that did not happen. The cost is that work already done is
|
|
1837
|
+
* thrown away with the run; a caller who wants to resume instead drives
|
|
1838
|
+
* `api.uploadStream` and `api.uploadInfo` directly.
|
|
1839
|
+
*/
|
|
1840
|
+
/**
|
|
1841
|
+
* Start a workflow run and stream a file into it while it works.
|
|
1842
|
+
*
|
|
1843
|
+
* The workflow declares which input property carries the upload
|
|
1844
|
+
* (`workflow({ uploads: ["recording"] })`) — the same declaration
|
|
1845
|
+
* `useWorkflowSubmit` reads, because what the property carries is an upload id
|
|
1846
|
+
* either way. What differs is only WHEN the id becomes valid.
|
|
1847
|
+
*
|
|
1848
|
+
* @typeParam R - The workflow's output type, which is what makes
|
|
1849
|
+
* `run.status === "completed"` narrow to a typed `run.output`. Derive it with
|
|
1850
|
+
* `WorkflowOutputOf<typeof myWorkflow>`.
|
|
1851
|
+
*
|
|
1852
|
+
* @example
|
|
1853
|
+
* ```tsx no-check
|
|
1854
|
+
* const { submit, run, upload, pending, error } = useWorkflowStream("transcribe");
|
|
1855
|
+
*
|
|
1856
|
+
* <Form onSubmit={(values) => submit(values)} error={error}>
|
|
1857
|
+
* <WorkflowFields workflow="transcribe" />
|
|
1858
|
+
* <UploadProgressBar upload={upload} />
|
|
1859
|
+
* <SubmitButton pending={pending}>Transcribe</SubmitButton>
|
|
1860
|
+
* </Form>
|
|
1861
|
+
* ```
|
|
1862
|
+
*
|
|
1863
|
+
* @public
|
|
1864
|
+
*/
|
|
1865
|
+
function useWorkflowStream(workflow, opts = {}) {
|
|
1866
|
+
const { api, key, intervalMs, parallel } = opts;
|
|
1867
|
+
const [runId, setRunId] = useState(void 0);
|
|
1868
|
+
const [starting, setStarting] = useState(false);
|
|
1869
|
+
const [startError, setStartError] = useState(void 0);
|
|
1870
|
+
const [upload, setUpload] = useState(void 0);
|
|
1871
|
+
const getClient = useWorkflowApiRef(api);
|
|
1872
|
+
const tracked = useWorkflowRun(runId, {
|
|
1873
|
+
...api && { api },
|
|
1874
|
+
...omitUndefined({ intervalMs })
|
|
1875
|
+
});
|
|
1876
|
+
return {
|
|
1877
|
+
submit: useCallback(async (input) => {
|
|
1878
|
+
const client = getClient();
|
|
1879
|
+
setStarting(true);
|
|
1880
|
+
setStartError(void 0);
|
|
1881
|
+
setRunId(void 0);
|
|
1882
|
+
let started;
|
|
1883
|
+
try {
|
|
1884
|
+
const field = await uploadField(client, workflow);
|
|
1885
|
+
const chosen = field === void 0 ? void 0 : fileAt(input, field);
|
|
1886
|
+
const id = randomUploadId();
|
|
1887
|
+
const payload = chosen && field ? {
|
|
1888
|
+
...input,
|
|
1889
|
+
[field]: id
|
|
1890
|
+
} : input;
|
|
1891
|
+
started = await client.start(workflow, payload, omitUndefined({ key }));
|
|
1892
|
+
setRunId(started);
|
|
1893
|
+
if (!chosen) return;
|
|
1894
|
+
await client.uploadStream(id, chosen, {
|
|
1895
|
+
name: chosen.name,
|
|
1896
|
+
onProgress: (progress) => setUpload({
|
|
1897
|
+
...progress,
|
|
1898
|
+
name: chosen.name,
|
|
1899
|
+
index: 1,
|
|
1900
|
+
count: 1
|
|
1901
|
+
}),
|
|
1902
|
+
...omitUndefined({ parallel })
|
|
1903
|
+
});
|
|
1904
|
+
await client.wake(started).catch(() => void 0);
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
setStartError(errorMessage(err));
|
|
1907
|
+
if (started) await client.cancel(started).catch(() => void 0);
|
|
1908
|
+
} finally {
|
|
1909
|
+
setStarting(false);
|
|
1910
|
+
setUpload(void 0);
|
|
1911
|
+
}
|
|
1912
|
+
}, [
|
|
1913
|
+
workflow,
|
|
1914
|
+
key,
|
|
1915
|
+
parallel,
|
|
1916
|
+
getClient
|
|
1917
|
+
]),
|
|
1918
|
+
reset: useCallback(() => {
|
|
1919
|
+
setRunId(void 0);
|
|
1920
|
+
setStartError(void 0);
|
|
1921
|
+
setUpload(void 0);
|
|
1922
|
+
}, []),
|
|
1923
|
+
run: tracked.run,
|
|
1924
|
+
pending: starting || tracked.polling,
|
|
1925
|
+
upload,
|
|
1926
|
+
error: startError ?? tracked.error
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
/** A fresh upload id: a capability, so it is random rather than derived. */
|
|
1930
|
+
function randomUploadId() {
|
|
1931
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Which input property this workflow says carries an upload id.
|
|
1935
|
+
*
|
|
1936
|
+
* `undefined` when the workflow declares none, which is not an error: the caller
|
|
1937
|
+
* may be using this hook against a workflow that takes no file at all, and the run
|
|
1938
|
+
* then starts with the input untouched.
|
|
1939
|
+
*/
|
|
1940
|
+
async function uploadField(client, workflow) {
|
|
1941
|
+
return (await client.list()).find((one) => one.name === workflow)?.uploads?.[0];
|
|
1942
|
+
}
|
|
1943
|
+
/** The `File` at `field`, if that is what the form put there. */
|
|
1944
|
+
function fileAt(input, field) {
|
|
1945
|
+
if (!isRecord(input)) return void 0;
|
|
1946
|
+
return filesOf(input[field])[0];
|
|
1947
|
+
}
|
|
1948
|
+
//#endregion
|
|
1949
|
+
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 };
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* followed from the same id either way.
|
|
34
34
|
*/
|
|
35
35
|
import { type WorkflowSummary } from "@alexkroman1/aai";
|
|
36
|
-
import type { UploadProgress } from "@alexkroman1/aai/workflow-api";
|
|
36
|
+
import type { UploadParallel, UploadProgress } from "@alexkroman1/aai/workflow-api";
|
|
37
37
|
import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
|
|
38
38
|
/** Options for {@link useWorkflows}. */
|
|
39
39
|
export type UseWorkflowsOptions = {
|
|
@@ -138,6 +138,18 @@ export type UseWorkflowSubmitOptions = {
|
|
|
138
138
|
wait?: number;
|
|
139
139
|
/** How often the fallback poll re-reads a live run. */
|
|
140
140
|
intervalMs?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Send each chosen file as concurrent parts instead of in one request.
|
|
143
|
+
*
|
|
144
|
+
* `true` for the defaults, or `{ partBytes, concurrency }` to tune them. This is
|
|
145
|
+
* the wait a form with a recording in it actually spends: the run does not exist
|
|
146
|
+
* until its input is stored, so until the last byte lands there is no run to
|
|
147
|
+
* watch and nothing for `<WorkflowProgress>` to say. Splitting the file across
|
|
148
|
+
* connections is what makes that stretch shorter, and it degrades to the single
|
|
149
|
+
* request wherever it would not help — a small file, an older agent — so turning
|
|
150
|
+
* it on is safe for every form. See `UploadOptions.parallel`.
|
|
151
|
+
*/
|
|
152
|
+
parallel?: UploadParallel;
|
|
141
153
|
};
|
|
142
154
|
/**
|
|
143
155
|
* Start a workflow from a form, and follow the run it creates.
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { UploadParallel } from "@alexkroman1/aai/workflow-api";
|
|
61
|
+
import type { UploadStatus } from "./use-workflow-form.ts";
|
|
62
|
+
import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
|
|
63
|
+
/** Options for {@link useWorkflowStream}. */
|
|
64
|
+
export type UseWorkflowStreamOptions = {
|
|
65
|
+
/** The client to start runs with. Defaults to one for the page's own agent. */
|
|
66
|
+
api?: WorkflowApi;
|
|
67
|
+
/** Correlation key recorded with the run, for finding it again without the id. */
|
|
68
|
+
key?: string;
|
|
69
|
+
/** How often the fallback poll re-reads a live run. */
|
|
70
|
+
intervalMs?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Send the file as concurrent parts instead of in one streaming request.
|
|
73
|
+
*
|
|
74
|
+
* It COMPOSES with what this hook is for rather than competing with it: the run
|
|
75
|
+
* still starts before the bytes, and the store still publishes how far the file
|
|
76
|
+
* is readable — that number is the CONTIGUOUS prefix, so a run reading ahead of
|
|
77
|
+
* the uplink sees the same growing file whether one connection or four are
|
|
78
|
+
* filling it. What changes is only how fast it grows.
|
|
79
|
+
*
|
|
80
|
+
* `true` for the defaults, or `{ partBytes, concurrency }` to tune them. See
|
|
81
|
+
* `UploadOptions.parallel`.
|
|
82
|
+
*/
|
|
83
|
+
parallel?: UploadParallel;
|
|
84
|
+
};
|
|
85
|
+
/** What {@link useWorkflowStream} returns. */
|
|
86
|
+
export type WorkflowStreamSubmission<R = unknown> = {
|
|
87
|
+
/**
|
|
88
|
+
* Start a run and stream this input's file into it.
|
|
89
|
+
*
|
|
90
|
+
* Resolves when the upload finishes, NOT when the run does — the run's own
|
|
91
|
+
* progress arrives through `run`. It resolves rather than rejecting on a failed
|
|
92
|
+
* upload; the failure is reported through `error`, the way a form expects.
|
|
93
|
+
*/
|
|
94
|
+
submit: (input: unknown) => Promise<void>;
|
|
95
|
+
/** Clear the run and any error, putting the form back to its initial state. */
|
|
96
|
+
reset: () => void;
|
|
97
|
+
/**
|
|
98
|
+
* The run, from the moment it EXISTS — which here is before its bytes are in.
|
|
99
|
+
*
|
|
100
|
+
* That is the whole difference from `useWorkflowSubmit`, and what lets a page
|
|
101
|
+
* render `<WorkflowProgress>` beside the upload bar rather than after it.
|
|
102
|
+
*/
|
|
103
|
+
run: WorkflowRun<R> | undefined;
|
|
104
|
+
/** True from `submit()` until the run reaches a terminal status. */
|
|
105
|
+
pending: boolean;
|
|
106
|
+
/** How far the upload has got, while it is still going. */
|
|
107
|
+
upload: UploadStatus | undefined;
|
|
108
|
+
/** The submit's own failure (a rejected input, or an upload that would not store). */
|
|
109
|
+
error: string | undefined;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Start a workflow run and stream a file into it while it works.
|
|
113
|
+
*
|
|
114
|
+
* The workflow declares which input property carries the upload
|
|
115
|
+
* (`workflow({ uploads: ["recording"] })`) — the same declaration
|
|
116
|
+
* `useWorkflowSubmit` reads, because what the property carries is an upload id
|
|
117
|
+
* either way. What differs is only WHEN the id becomes valid.
|
|
118
|
+
*
|
|
119
|
+
* @typeParam R - The workflow's output type, which is what makes
|
|
120
|
+
* `run.status === "completed"` narrow to a typed `run.output`. Derive it with
|
|
121
|
+
* `WorkflowOutputOf<typeof myWorkflow>`.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```tsx no-check
|
|
125
|
+
* const { submit, run, upload, pending, error } = useWorkflowStream("transcribe");
|
|
126
|
+
*
|
|
127
|
+
* <Form onSubmit={(values) => submit(values)} error={error}>
|
|
128
|
+
* <WorkflowFields workflow="transcribe" />
|
|
129
|
+
* <UploadProgressBar upload={upload} />
|
|
130
|
+
* <SubmitButton pending={pending}>Transcribe</SubmitButton>
|
|
131
|
+
* </Form>
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @public
|
|
135
|
+
*/
|
|
136
|
+
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
|
+
"version": "6.5.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.
|
|
32
|
+
"@alexkroman1/aai": "6.5.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^19.0.0",
|