@alexkroman1/aai-ui 6.7.2 → 6.8.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/_upload-files.d.ts +67 -0
- package/dist/_upload-session.d.ts +99 -0
- package/dist/components/upload-progress.d.ts +14 -1
- package/dist/default-client/assets/index-DTLrhtTF.css +2 -0
- package/dist/default-client/index.html +2 -2
- package/dist/index.js +407 -143
- package/dist/use-workflow-form.d.ts +25 -0
- package/dist/use-workflow-stream.d.ts +32 -6
- package/package.json +2 -2
- package/dist/default-client/assets/index-BZlePk3y.css +0 -2
- /package/dist/default-client/assets/{index-CepvxbNU.js → index-DXODx_9r.js} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a form's `File`s into stored upload ids, pauses and all.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `use-workflow-form.ts` for the 500-line cap, and the seam is a
|
|
5
|
+
* real one: that module is the two HOOKS and the state between them, where this
|
|
6
|
+
* is the walk over a submitted input — which is the only part of it that knows
|
|
7
|
+
* what a `File` is, holds a loop, and survives being re-entered.
|
|
8
|
+
*
|
|
9
|
+
* `_`-internal. `useWorkflowSubmit` is the only caller; `useWorkflowStream` sends
|
|
10
|
+
* one file rather than walking an input and shares only the gate underneath both
|
|
11
|
+
* (`_upload-session.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import type { UploadParallel } from "@alexkroman1/aai/workflow-api";
|
|
14
|
+
import { type UploadGate } from "./_upload-session.ts";
|
|
15
|
+
import type { UploadStatus } from "./use-workflow-form.ts";
|
|
16
|
+
import type { WorkflowApi } from "./workflow-client.ts";
|
|
17
|
+
/**
|
|
18
|
+
* What one submission's uploads know about themselves, across pauses.
|
|
19
|
+
*
|
|
20
|
+
* Held by the hook rather than by the walk below, because the walk RE-RUNS: a
|
|
21
|
+
* pause unwinds nothing, so resuming re-enters `uploadFiles` with the same input
|
|
22
|
+
* and the same session, and every file whose bytes are already in is skipped by
|
|
23
|
+
* `stored` rather than sent again.
|
|
24
|
+
*
|
|
25
|
+
* Which is also why the ids live here. A resumable upload is one whose id
|
|
26
|
+
* outlives the attempt that began it — that is the whole mechanism — so an id
|
|
27
|
+
* minted per attempt would make each round a fresh upload of the whole file.
|
|
28
|
+
*/
|
|
29
|
+
export type UploadSession = {
|
|
30
|
+
/** The id each file is being stored under, minted once. */
|
|
31
|
+
ids: Map<File, string>;
|
|
32
|
+
/** Files whose every byte has landed, by the id they landed under. */
|
|
33
|
+
stored: Map<File, string>;
|
|
34
|
+
/** Files that have had an attempt, so the next one must claim the id as its own. */
|
|
35
|
+
tried: Set<File>;
|
|
36
|
+
/** The person's pause. */
|
|
37
|
+
gate: UploadGate;
|
|
38
|
+
};
|
|
39
|
+
/** A fresh session for one submission. */
|
|
40
|
+
export declare function createUploadSession(): UploadSession;
|
|
41
|
+
/**
|
|
42
|
+
* Replace every `File` in a submitted form with the id of a stored upload,
|
|
43
|
+
* reporting how far each one has got.
|
|
44
|
+
*
|
|
45
|
+
* Sequential rather than `Promise.all`: these are large bodies, and a form with
|
|
46
|
+
* two 200 MB recordings should send them one after another rather than compete
|
|
47
|
+
* for the same connection. That is also what makes a single bar honest — one
|
|
48
|
+
* file is in flight at a time, and `index`/`count` say which.
|
|
49
|
+
*
|
|
50
|
+
* Anything that is not a `File` (or an array of them) passes through untouched,
|
|
51
|
+
* so this is invisible to every form that has none — including one whose values
|
|
52
|
+
* are not an object at all, which `submit` accepts.
|
|
53
|
+
*
|
|
54
|
+
* ## `uploadStream`, not `upload`, and the id is the reason
|
|
55
|
+
*
|
|
56
|
+
* The difference between the two calls is only who mints the id — and that is
|
|
57
|
+
* exactly what decides whether an interrupted upload can be picked up again. An
|
|
58
|
+
* `upload` mints its own at the END and hands it back, so a caller whose upload
|
|
59
|
+
* died has nothing to name what was stored and no choice but to send the file
|
|
60
|
+
* again. A `uploadStream` is told the id up front, so the windows already in the
|
|
61
|
+
* store are addressable, which is what both a pause and a server restart need.
|
|
62
|
+
*
|
|
63
|
+
* Nothing else about the submission changes: the run is still started after the
|
|
64
|
+
* last byte lands, so the incomplete record a streamed upload leaves along the
|
|
65
|
+
* way is one nobody reads.
|
|
66
|
+
*/
|
|
67
|
+
export declare function uploadFiles(api: WorkflowApi, input: unknown, report: (status: UploadStatus) => void, parallel: UploadParallel | undefined, session: UploadSession): Promise<unknown>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driving ONE resumable upload from a page: its id, and the gate a person can
|
|
3
|
+
* close.
|
|
4
|
+
*
|
|
5
|
+
* The SDK already survives the server going away — a round that fails for a
|
|
6
|
+
* reason that looks like an outage re-enters itself with `resume: true` and sends
|
|
7
|
+
* only the windows that are missing (`aai/sdk/_upload-resume.ts`). This is the
|
|
8
|
+
* same mechanism turned around and handed to the PERSON: pausing is an outage
|
|
9
|
+
* they caused, resuming is the round after it, and the store cannot tell the two
|
|
10
|
+
* apart because there is nothing to tell apart.
|
|
11
|
+
*
|
|
12
|
+
* ## Why a gate rather than a boolean
|
|
13
|
+
*
|
|
14
|
+
* Pausing has to do two things that a `paused` flag cannot: stop the bytes that
|
|
15
|
+
* are already on the wire, and hold the loop that would send the next ones. So it
|
|
16
|
+
* is an `AbortController` and a promise — abort what is in flight, and park the
|
|
17
|
+
* uploader on `settle()` until somebody opens the gate again.
|
|
18
|
+
*
|
|
19
|
+
* The controller is REPLACED on resume rather than reused, because an aborted
|
|
20
|
+
* signal stays aborted; a caller therefore has to read {@link UploadGate.signal}
|
|
21
|
+
* fresh for each attempt rather than capturing it once.
|
|
22
|
+
*
|
|
23
|
+
* ## The loop this is built for
|
|
24
|
+
*
|
|
25
|
+
* ```ts no-check
|
|
26
|
+
* while (!gate.cancelled) {
|
|
27
|
+
* await gate.settle();
|
|
28
|
+
* if (gate.cancelled) break;
|
|
29
|
+
* try {
|
|
30
|
+
* await api.uploadStream(id, file, { signal: gate.signal, resume: tried });
|
|
31
|
+
* break;
|
|
32
|
+
* } catch (err) {
|
|
33
|
+
* // An abort that is not a cancel is a PAUSE: go back and wait.
|
|
34
|
+
* if (!isAbort(err) || gate.cancelled) throw err;
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* **The catch keys off the abort, not off `gate.paused`,** and that is the one
|
|
40
|
+
* subtle part. Pausing and immediately resuming — a double-click, or a person
|
|
41
|
+
* changing their mind inside the round trip — resolves the gate before the
|
|
42
|
+
* rejection this abort caused has even landed, so a `paused` check would read
|
|
43
|
+
* `false` and rethrow an `AbortError` as though the upload had failed. Every
|
|
44
|
+
* abort reaching that catch was caused by this gate, so every one of them is a
|
|
45
|
+
* pause unless the gate was cancelled outright.
|
|
46
|
+
*/
|
|
47
|
+
/** A person's pause, as the uploader sees it. */
|
|
48
|
+
export type UploadGate = {
|
|
49
|
+
/** Whether the gate is currently closed. For rendering, not for control flow. */
|
|
50
|
+
readonly paused: boolean;
|
|
51
|
+
/** Whether the upload was abandoned. A cancelled gate never opens again. */
|
|
52
|
+
readonly cancelled: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* The signal for the NEXT attempt. Re-read it per attempt: resuming installs a
|
|
55
|
+
* fresh controller, and the previous one stays aborted forever.
|
|
56
|
+
*/
|
|
57
|
+
readonly signal: AbortSignal;
|
|
58
|
+
/** Stop the bytes in flight and hold the uploader. A no-op when already closed. */
|
|
59
|
+
pause: () => void;
|
|
60
|
+
/** Open the gate and install a fresh signal. A no-op when not paused. */
|
|
61
|
+
resume: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Abandon the upload for good.
|
|
64
|
+
*
|
|
65
|
+
* Distinct from a pause in exactly one way that matters to the loop above: it
|
|
66
|
+
* releases the gate rather than holding it, so an uploader parked on `settle()`
|
|
67
|
+
* wakes up and leaves instead of waiting for a resume that is not coming. What
|
|
68
|
+
* a caller does with a cancelled upload's stored windows is its own business —
|
|
69
|
+
* they stay in the store, addressable by an id only the caller has.
|
|
70
|
+
*/
|
|
71
|
+
cancel: () => void;
|
|
72
|
+
/** Resolves as soon as the gate is open — immediately, when it already is. */
|
|
73
|
+
settle: () => Promise<void>;
|
|
74
|
+
};
|
|
75
|
+
/** Whether this rejection is an abort, in either of the two shapes runtimes throw. */
|
|
76
|
+
export declare function isAbortError(err: unknown): boolean;
|
|
77
|
+
/** A fresh upload id: a capability, so it is random rather than derived. */
|
|
78
|
+
export declare function randomUploadId(): string;
|
|
79
|
+
/**
|
|
80
|
+
* Send one file, waiting out however many pauses the person takes.
|
|
81
|
+
*
|
|
82
|
+
* The loop from the module doc, written once: both hooks need exactly this and a
|
|
83
|
+
* second copy of it is a second place for the abort/pause distinction to be got
|
|
84
|
+
* wrong. `send` is handed whether this attempt must CLAIM the id as its own —
|
|
85
|
+
* false the first time, since a fresh id has nothing to resume and saying
|
|
86
|
+
* otherwise waives the refusal that makes a caller-chosen id safe.
|
|
87
|
+
*
|
|
88
|
+
* Throws whatever `send` threw, except an abort the gate caused. A cancelled gate
|
|
89
|
+
* throws too: the caller distinguishes it by reading `gate.cancelled`, which is
|
|
90
|
+
* how an abandoned submission unwinds without being reported as a failure.
|
|
91
|
+
*/
|
|
92
|
+
export declare function sendThroughGate(gate: UploadGate, send: (resume: boolean) => Promise<void>): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* A gate, open.
|
|
95
|
+
*
|
|
96
|
+
* One per upload rather than one per hook: the id and the windows already stored
|
|
97
|
+
* belong to a file, so a gate that outlived its file would resume something else.
|
|
98
|
+
*/
|
|
99
|
+
export declare function createUploadGate(): UploadGate;
|
|
@@ -22,6 +22,14 @@ import type { UploadStatus } from "../use-workflow-form.ts";
|
|
|
22
22
|
* - **The file is NAMED, and counted when there is more than one.** Files are
|
|
23
23
|
* sent one after another, so a single bar otherwise appears to restart from
|
|
24
24
|
* zero partway through with nothing to say why.
|
|
25
|
+
* - **A paused upload SAYS SO, rather than being a bar that stopped.** Those look
|
|
26
|
+
* identical, which is the whole reason `UploadStatus.paused` exists, and the
|
|
27
|
+
* fill stops animating so the difference is visible without reading.
|
|
28
|
+
*
|
|
29
|
+
* The pause control appears only when a handler for it is passed. That is not
|
|
30
|
+
* politeness about props: a button whose press does nothing is worse than no
|
|
31
|
+
* button, and a page holding an `upload` it did not produce (a saved status, a
|
|
32
|
+
* parent's state) has nothing to pause.
|
|
25
33
|
*
|
|
26
34
|
* @example
|
|
27
35
|
* ```tsx
|
|
@@ -41,12 +49,17 @@ import type { UploadStatus } from "../use-workflow-form.ts";
|
|
|
41
49
|
*
|
|
42
50
|
* @param upload - What `useWorkflowSubmit` reports. `undefined` renders nothing,
|
|
43
51
|
* so a page may pass its state straight through.
|
|
52
|
+
* @param onPause - The hook's `pauseUpload`. Pass it together with `onResume` to
|
|
53
|
+
* get the control; pass neither for a bar that only reports.
|
|
54
|
+
* @param onResume - The hook's `resumeUpload`.
|
|
44
55
|
* @param className - Replaces the default classes rather than extending them,
|
|
45
56
|
* so a custom chrome is not fighting a default it did not ask for.
|
|
46
57
|
*
|
|
47
58
|
* @public
|
|
48
59
|
*/
|
|
49
|
-
export declare function UploadProgressBar({ upload, className, }: {
|
|
60
|
+
export declare function UploadProgressBar({ upload, onPause, onResume, className, }: {
|
|
50
61
|
upload?: UploadStatus | undefined;
|
|
62
|
+
onPause?: (() => void) | undefined;
|
|
63
|
+
onResume?: (() => void) | undefined;
|
|
51
64
|
className?: string | undefined;
|
|
52
65
|
}): ReactNode;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-aai:"Monument Grotesk", "ABC Monument Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-aai-serif:"Source Serif 4", "Source Serif Pro", Charter, "Iowan Old Style", Georgia, serif;--font-aai-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--radius-aai:4px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{margin:0;padding:0}}@layer components;@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.order-last{order:9999}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-0\.5{margin-block:calc(var(--spacing) * .5)}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-9{height:calc(var(--spacing) * 9)}.h-11{height:calc(var(--spacing) * 11)}.h-\[7px\]{height:7px}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-\[40vh\]{max-height:40vh}.min-h-0{min-height:0}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-4{width:calc(var(--spacing) * 4)}.w-\[7px\]{width:7px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-75{max-width:calc(var(--spacing) * 75)}.max-w-105{max-width:calc(var(--spacing) * 105)}.max-w-190{max-width:calc(var(--spacing) * 190)}.max-w-\[82\%\]{max-width:82%}.max-w-\[min\(78\%\,64ch\)\]{max-width:min(78%,64ch)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.border-collapse{border-collapse:collapse}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.resize{resize:both}.\[scrollbar-width\:none\]{scrollbar-width:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.appearance-none{appearance:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-aai{border-radius:var(--radius-aai)}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-none{--tw-border-style:none;border-style:none}.border-\(--aai-btn-bd\){border-color:var(--aai-btn-bd)}.bg-\(--aai-btn-bg\){background-color:var(--aai-btn-bg)}.bg-transparent{background-color:#0000}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-7{padding:calc(var(--spacing) * 7)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.font-aai{font-family:var(--font-aai)}.font-aai-mono{font-family:var(--font-aai-mono)}.font-aai-serif{font-family:var(--font-aai-serif)}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.625rem\]{font-size:.625rem}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[22px\]{font-size:22px}.text-\[32px\]{font-size:32px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-\[23px\]{--tw-leading:23px;line-height:23px}.leading-\[130\%\]{--tw-leading:130%;line-height:130%}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.2px\]{--tw-tracking:-.2px;letter-spacing:-.2px}.tracking-\[1\.2px\]{--tw-tracking:1.2px;letter-spacing:1.2px}.tracking-\[1\.4px\]{--tw-tracking:1.4px;letter-spacing:1.4px}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.wrap-break-word{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\(--aai-btn-fg\){color:var(--aai-btn-fg)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.file\:mr-3::file-selector-button{margin-right:calc(var(--spacing) * 3)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-aai::file-selector-button{border-radius:var(--radius-aai)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:px-3::file-selector-button{padding-inline:calc(var(--spacing) * 3)}.file\:py-1\.5::file-selector-button{padding-block:calc(var(--spacing) * 1.5)}.file\:font-aai::file-selector-button{font-family:var(--font-aai)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:\[color\:var\(--aai-file-button-fg\)\]::file-selector-button{color:var(--aai-file-button-fg)}.file\:\[background\:var\(--aai-file-button-bg\)\]::file-selector-button{background:var(--aai-file-button-bg)}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.focus-visible\:\[outline\:2px_solid\]:focus-visible{outline:2px solid}.focus-visible\:\[outline-offset\:2px\]:focus-visible{outline-offset:2px}@media (hover:hover){.enabled\:hover\:border-\(--aai-btn-bd-hover\):enabled:hover{border-color:var(--aai-btn-bd-hover)}.enabled\:hover\:bg-\(--aai-btn-bg-hover\):enabled:hover{background-color:var(--aai-btn-bg-hover)}.enabled\:hover\:text-\(--aai-btn-fg-hover\):enabled:hover{color:var(--aai-btn-fg-hover)}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:ml-auto{margin-left:auto}.sm\:max-w-\[60\%\]{max-width:60%}.sm\:basis-auto{flex-basis:auto}.sm\:px-16{padding-inline:calc(var(--spacing) * 16)}.sm\:py-14{padding-block:calc(var(--spacing) * 14)}}@media (width>=48rem){.md\:order-none{order:0}.md\:h-screen{height:100vh}.md\:max-h-none{max-height:none}.md\:w-\(--aai-sidebar-w\){width:var(--aai-sidebar-w)}.md\:flex-row{flex-direction:row}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}}}@keyframes aai-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.82)}}@keyframes aai-bounce{0%,80%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}@keyframes aai-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.tool-shimmer{-webkit-text-fill-color:transparent;background:linear-gradient(90deg,currentColor 25%,#0000 50%,currentColor 75%) 0 0/200% 100%;-webkit-background-clip:text;background-clip:text;animation:2s infinite aai-shimmer}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<title>aai</title>
|
|
7
7
|
<link rel="icon" href="./favicon.ico" />
|
|
8
8
|
<style>html, body { background: #FBF8F2; margin: 0; }</style>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-DXODx_9r.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="./assets/client-audio-constants-Ck0IJO4c.js">
|
|
11
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="./assets/index-DTLrhtTF.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<main id="app"></main>
|
package/dist/index.js
CHANGED
|
@@ -590,6 +590,14 @@ function formatBytes(bytes) {
|
|
|
590
590
|
* - **The file is NAMED, and counted when there is more than one.** Files are
|
|
591
591
|
* sent one after another, so a single bar otherwise appears to restart from
|
|
592
592
|
* zero partway through with nothing to say why.
|
|
593
|
+
* - **A paused upload SAYS SO, rather than being a bar that stopped.** Those look
|
|
594
|
+
* identical, which is the whole reason `UploadStatus.paused` exists, and the
|
|
595
|
+
* fill stops animating so the difference is visible without reading.
|
|
596
|
+
*
|
|
597
|
+
* The pause control appears only when a handler for it is passed. That is not
|
|
598
|
+
* politeness about props: a button whose press does nothing is worse than no
|
|
599
|
+
* button, and a page holding an `upload` it did not produce (a saved status, a
|
|
600
|
+
* parent's state) has nothing to pause.
|
|
593
601
|
*
|
|
594
602
|
* @example
|
|
595
603
|
* ```tsx
|
|
@@ -609,16 +617,20 @@ function formatBytes(bytes) {
|
|
|
609
617
|
*
|
|
610
618
|
* @param upload - What `useWorkflowSubmit` reports. `undefined` renders nothing,
|
|
611
619
|
* so a page may pass its state straight through.
|
|
620
|
+
* @param onPause - The hook's `pauseUpload`. Pass it together with `onResume` to
|
|
621
|
+
* get the control; pass neither for a bar that only reports.
|
|
622
|
+
* @param onResume - The hook's `resumeUpload`.
|
|
612
623
|
* @param className - Replaces the default classes rather than extending them,
|
|
613
624
|
* so a custom chrome is not fighting a default it did not ask for.
|
|
614
625
|
*
|
|
615
626
|
* @public
|
|
616
627
|
*/
|
|
617
|
-
function UploadProgressBar({ upload, className }) {
|
|
628
|
+
function UploadProgressBar({ upload, onPause, onResume, className }) {
|
|
618
629
|
const theme = useTheme();
|
|
619
630
|
const labelId = useId();
|
|
620
631
|
if (!upload) return null;
|
|
621
|
-
const { name, index, count, loaded, total, fraction } = upload;
|
|
632
|
+
const { name, index, count, loaded, total, fraction, paused } = upload;
|
|
633
|
+
const control = onPause && onResume ? paused ? onResume : onPause : void 0;
|
|
622
634
|
const percent = fraction === void 0 ? void 0 : Math.round(fraction * 100);
|
|
623
635
|
const faint = inkTint(theme.text, theme.surface, 65);
|
|
624
636
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -629,11 +641,20 @@ function UploadProgressBar({ upload, className }) {
|
|
|
629
641
|
id: labelId,
|
|
630
642
|
className: "truncate",
|
|
631
643
|
style: { color: faint },
|
|
632
|
-
children:
|
|
633
|
-
}), /* @__PURE__ */
|
|
634
|
-
className: "shrink-0
|
|
635
|
-
|
|
636
|
-
|
|
644
|
+
children: `${paused ? "Paused" : "Uploading"} ${name}${count > 1 ? ` (${index} of ${count})` : ""}`
|
|
645
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
646
|
+
className: "flex shrink-0 items-baseline gap-3",
|
|
647
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
648
|
+
className: "tabular-nums",
|
|
649
|
+
style: { color: faint },
|
|
650
|
+
children: total === void 0 ? formatBytes(loaded) : `${formatBytes(loaded)} of ${formatBytes(total)}`
|
|
651
|
+
}), control && /* @__PURE__ */ jsx(Button, {
|
|
652
|
+
type: "button",
|
|
653
|
+
variant: "ghost",
|
|
654
|
+
className: "h-6 px-2 text-[0.625rem]",
|
|
655
|
+
onClick: control,
|
|
656
|
+
children: paused ? "Resume" : "Pause"
|
|
657
|
+
})]
|
|
637
658
|
})]
|
|
638
659
|
}), /* @__PURE__ */ jsx("div", {
|
|
639
660
|
role: "progressbar",
|
|
@@ -644,7 +665,7 @@ function UploadProgressBar({ upload, className }) {
|
|
|
644
665
|
className: "h-1.5 w-full overflow-hidden rounded-full",
|
|
645
666
|
style: { backgroundColor: inkTint(theme.text, theme.surface, TRACK_TINT_PCT) },
|
|
646
667
|
children: /* @__PURE__ */ jsx("div", {
|
|
647
|
-
className: clsx("h-full rounded-full transition-[width] duration-200 ease-out", percent === void 0 && "animate-pulse"),
|
|
668
|
+
className: clsx("h-full rounded-full transition-[width] duration-200 ease-out", percent === void 0 && !paused && "animate-pulse"),
|
|
648
669
|
style: {
|
|
649
670
|
backgroundColor: theme.primary,
|
|
650
671
|
width: percent === void 0 ? "100%" : `${percent}%`
|
|
@@ -654,6 +675,243 @@ function UploadProgressBar({ upload, className }) {
|
|
|
654
675
|
});
|
|
655
676
|
}
|
|
656
677
|
//#endregion
|
|
678
|
+
//#region _upload-session.ts
|
|
679
|
+
/** Whether this rejection is an abort, in either of the two shapes runtimes throw. */
|
|
680
|
+
function isAbortError(err) {
|
|
681
|
+
return err instanceof Error && err.name === "AbortError";
|
|
682
|
+
}
|
|
683
|
+
/** A fresh upload id: a capability, so it is random rather than derived. */
|
|
684
|
+
function randomUploadId() {
|
|
685
|
+
return crypto.randomUUID().replaceAll("-", "");
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Send one file, waiting out however many pauses the person takes.
|
|
689
|
+
*
|
|
690
|
+
* The loop from the module doc, written once: both hooks need exactly this and a
|
|
691
|
+
* second copy of it is a second place for the abort/pause distinction to be got
|
|
692
|
+
* wrong. `send` is handed whether this attempt must CLAIM the id as its own —
|
|
693
|
+
* false the first time, since a fresh id has nothing to resume and saying
|
|
694
|
+
* otherwise waives the refusal that makes a caller-chosen id safe.
|
|
695
|
+
*
|
|
696
|
+
* Throws whatever `send` threw, except an abort the gate caused. A cancelled gate
|
|
697
|
+
* throws too: the caller distinguishes it by reading `gate.cancelled`, which is
|
|
698
|
+
* how an abandoned submission unwinds without being reported as a failure.
|
|
699
|
+
*/
|
|
700
|
+
async function sendThroughGate(gate, send) {
|
|
701
|
+
let tried = false;
|
|
702
|
+
for (;;) {
|
|
703
|
+
await gate.settle();
|
|
704
|
+
if (gate.cancelled) throw new Error("Upload cancelled.");
|
|
705
|
+
const resume = tried;
|
|
706
|
+
tried = true;
|
|
707
|
+
try {
|
|
708
|
+
await send(resume);
|
|
709
|
+
return;
|
|
710
|
+
} catch (err) {
|
|
711
|
+
if (gate.cancelled || !isAbortError(err)) throw err;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* A gate, open.
|
|
717
|
+
*
|
|
718
|
+
* One per upload rather than one per hook: the id and the windows already stored
|
|
719
|
+
* belong to a file, so a gate that outlived its file would resume something else.
|
|
720
|
+
*/
|
|
721
|
+
function createUploadGate() {
|
|
722
|
+
let controller = new AbortController();
|
|
723
|
+
let paused = false;
|
|
724
|
+
let cancelled = false;
|
|
725
|
+
let open;
|
|
726
|
+
let closed;
|
|
727
|
+
return {
|
|
728
|
+
get paused() {
|
|
729
|
+
return paused;
|
|
730
|
+
},
|
|
731
|
+
get cancelled() {
|
|
732
|
+
return cancelled;
|
|
733
|
+
},
|
|
734
|
+
get signal() {
|
|
735
|
+
return controller.signal;
|
|
736
|
+
},
|
|
737
|
+
pause() {
|
|
738
|
+
if (paused || cancelled) return;
|
|
739
|
+
paused = true;
|
|
740
|
+
const gate = Promise.withResolvers();
|
|
741
|
+
closed = gate.promise;
|
|
742
|
+
open = gate.resolve;
|
|
743
|
+
controller.abort();
|
|
744
|
+
},
|
|
745
|
+
resume() {
|
|
746
|
+
if (!paused || cancelled) return;
|
|
747
|
+
paused = false;
|
|
748
|
+
controller = new AbortController();
|
|
749
|
+
open?.();
|
|
750
|
+
open = void 0;
|
|
751
|
+
closed = void 0;
|
|
752
|
+
},
|
|
753
|
+
cancel() {
|
|
754
|
+
if (cancelled) return;
|
|
755
|
+
cancelled = true;
|
|
756
|
+
paused = false;
|
|
757
|
+
controller.abort();
|
|
758
|
+
open?.();
|
|
759
|
+
open = void 0;
|
|
760
|
+
closed = void 0;
|
|
761
|
+
},
|
|
762
|
+
async settle() {
|
|
763
|
+
if (closed) await closed;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region _workflow-files.ts
|
|
769
|
+
/**
|
|
770
|
+
* Which of a submitted form's values are FILES.
|
|
771
|
+
*
|
|
772
|
+
* Its own module because both submit hooks need the identical answer and then do
|
|
773
|
+
* two different things with it — `useWorkflowSubmit` stores each file and passes
|
|
774
|
+
* its id, `useWorkflowStream` cuts it into parts and passes the group they share.
|
|
775
|
+
* A second copy of this predicate would be a form field that one hook treats as a
|
|
776
|
+
* file and the other does not, which is invisible until the run reads the wrong
|
|
777
|
+
* kind of string.
|
|
778
|
+
*/
|
|
779
|
+
/**
|
|
780
|
+
* The files a submitted field carries, if that is what it carries.
|
|
781
|
+
*
|
|
782
|
+
* An array counts only when it is files ALL the way through — a mixed array is
|
|
783
|
+
* some other field's value that happens to contain one, and turning half of it
|
|
784
|
+
* into ids would corrupt it silently.
|
|
785
|
+
*/
|
|
786
|
+
function filesOf(value) {
|
|
787
|
+
if (value instanceof File) return [value];
|
|
788
|
+
if (!Array.isArray(value)) return [];
|
|
789
|
+
const files = value.filter((one) => one instanceof File);
|
|
790
|
+
return files.length > 0 && files.length === value.length ? files : [];
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* The input properties still carrying a `File` — i.e. the ones that CANNOT survive
|
|
794
|
+
* being sent.
|
|
795
|
+
*
|
|
796
|
+
* A run input is JSON, and `JSON.stringify(new File(…))` is `{}` — no `toJSON`, no
|
|
797
|
+
* own enumerable properties. So a File left in a payload does not fail to send: it
|
|
798
|
+
* arrives as an empty object, and the workflow rejects it against whatever its own
|
|
799
|
+
* schema says the property should be. Measured in production as
|
|
800
|
+
* `Invalid input for workflow "transcribe": recording: Invalid input` — a message
|
|
801
|
+
* about a type, on a form where the user had picked a perfectly good file.
|
|
802
|
+
*
|
|
803
|
+
* Exported beside {@link filesOf} because it is the same question asked at the
|
|
804
|
+
* other end: that one decides which fields to UPLOAD, this one checks that none
|
|
805
|
+
* were missed. Both hooks are the callers.
|
|
806
|
+
*/
|
|
807
|
+
function fileFields(input) {
|
|
808
|
+
if (!isRecord(input)) return [];
|
|
809
|
+
return Object.entries(input).filter(([, value]) => filesOf(value).length > 0).map(([key]) => key);
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region _upload-files.ts
|
|
813
|
+
/**
|
|
814
|
+
* Turning a form's `File`s into stored upload ids, pauses and all.
|
|
815
|
+
*
|
|
816
|
+
* Split out of `use-workflow-form.ts` for the 500-line cap, and the seam is a
|
|
817
|
+
* real one: that module is the two HOOKS and the state between them, where this
|
|
818
|
+
* is the walk over a submitted input — which is the only part of it that knows
|
|
819
|
+
* what a `File` is, holds a loop, and survives being re-entered.
|
|
820
|
+
*
|
|
821
|
+
* `_`-internal. `useWorkflowSubmit` is the only caller; `useWorkflowStream` sends
|
|
822
|
+
* one file rather than walking an input and shares only the gate underneath both
|
|
823
|
+
* (`_upload-session.ts`).
|
|
824
|
+
*/
|
|
825
|
+
/** A fresh session for one submission. */
|
|
826
|
+
function createUploadSession() {
|
|
827
|
+
return {
|
|
828
|
+
ids: /* @__PURE__ */ new Map(),
|
|
829
|
+
stored: /* @__PURE__ */ new Map(),
|
|
830
|
+
tried: /* @__PURE__ */ new Set(),
|
|
831
|
+
gate: createUploadGate()
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Replace every `File` in a submitted form with the id of a stored upload,
|
|
836
|
+
* reporting how far each one has got.
|
|
837
|
+
*
|
|
838
|
+
* Sequential rather than `Promise.all`: these are large bodies, and a form with
|
|
839
|
+
* two 200 MB recordings should send them one after another rather than compete
|
|
840
|
+
* for the same connection. That is also what makes a single bar honest — one
|
|
841
|
+
* file is in flight at a time, and `index`/`count` say which.
|
|
842
|
+
*
|
|
843
|
+
* Anything that is not a `File` (or an array of them) passes through untouched,
|
|
844
|
+
* so this is invisible to every form that has none — including one whose values
|
|
845
|
+
* are not an object at all, which `submit` accepts.
|
|
846
|
+
*
|
|
847
|
+
* ## `uploadStream`, not `upload`, and the id is the reason
|
|
848
|
+
*
|
|
849
|
+
* The difference between the two calls is only who mints the id — and that is
|
|
850
|
+
* exactly what decides whether an interrupted upload can be picked up again. An
|
|
851
|
+
* `upload` mints its own at the END and hands it back, so a caller whose upload
|
|
852
|
+
* died has nothing to name what was stored and no choice but to send the file
|
|
853
|
+
* again. A `uploadStream` is told the id up front, so the windows already in the
|
|
854
|
+
* store are addressable, which is what both a pause and a server restart need.
|
|
855
|
+
*
|
|
856
|
+
* Nothing else about the submission changes: the run is still started after the
|
|
857
|
+
* last byte lands, so the incomplete record a streamed upload leaves along the
|
|
858
|
+
* way is one nobody reads.
|
|
859
|
+
*/
|
|
860
|
+
async function uploadFiles(api, input, report, parallel, session) {
|
|
861
|
+
if (!isRecord(input)) return input;
|
|
862
|
+
const entries = Object.entries(input);
|
|
863
|
+
const count = entries.reduce((total, [, value]) => total + filesOf(value).length, 0);
|
|
864
|
+
let index = 0;
|
|
865
|
+
const store = async (file) => {
|
|
866
|
+
index += 1;
|
|
867
|
+
const done = session.stored.get(file);
|
|
868
|
+
if (done !== void 0) return done;
|
|
869
|
+
let id = session.ids.get(file);
|
|
870
|
+
if (id === void 0) {
|
|
871
|
+
id = randomUploadId();
|
|
872
|
+
session.ids.set(file, id);
|
|
873
|
+
}
|
|
874
|
+
const position = {
|
|
875
|
+
name: file.name,
|
|
876
|
+
index,
|
|
877
|
+
count
|
|
878
|
+
};
|
|
879
|
+
await sendThroughGate(session.gate, async (resume) => {
|
|
880
|
+
await api.uploadStream(id, file, {
|
|
881
|
+
name: file.name,
|
|
882
|
+
signal: session.gate.signal,
|
|
883
|
+
onProgress: (progress) => report({
|
|
884
|
+
...position,
|
|
885
|
+
...progress,
|
|
886
|
+
paused: session.gate.paused
|
|
887
|
+
}),
|
|
888
|
+
...omitUndefined({
|
|
889
|
+
parallel,
|
|
890
|
+
resume: resume ? true : void 0
|
|
891
|
+
})
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
session.stored.set(file, id);
|
|
895
|
+
return id;
|
|
896
|
+
};
|
|
897
|
+
const out = {};
|
|
898
|
+
for (const [name, value] of entries) {
|
|
899
|
+
if (value instanceof File) {
|
|
900
|
+
out[name] = await store(value);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const chosen = filesOf(value);
|
|
904
|
+
if (chosen.length === 0) {
|
|
905
|
+
out[name] = value;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const ids = [];
|
|
909
|
+
for (const file of chosen) ids.push(await store(file));
|
|
910
|
+
out[name] = ids;
|
|
911
|
+
}
|
|
912
|
+
return out;
|
|
913
|
+
}
|
|
914
|
+
//#endregion
|
|
657
915
|
//#region workflow-client.ts
|
|
658
916
|
/**
|
|
659
917
|
* Create a workflow API client aimed at the agent serving this page.
|
|
@@ -720,50 +978,6 @@ function useWorkflowApiRef(api) {
|
|
|
720
978
|
}, []);
|
|
721
979
|
}
|
|
722
980
|
//#endregion
|
|
723
|
-
//#region _workflow-files.ts
|
|
724
|
-
/**
|
|
725
|
-
* Which of a submitted form's values are FILES.
|
|
726
|
-
*
|
|
727
|
-
* Its own module because both submit hooks need the identical answer and then do
|
|
728
|
-
* two different things with it — `useWorkflowSubmit` stores each file and passes
|
|
729
|
-
* its id, `useWorkflowStream` cuts it into parts and passes the group they share.
|
|
730
|
-
* A second copy of this predicate would be a form field that one hook treats as a
|
|
731
|
-
* file and the other does not, which is invisible until the run reads the wrong
|
|
732
|
-
* kind of string.
|
|
733
|
-
*/
|
|
734
|
-
/**
|
|
735
|
-
* The files a submitted field carries, if that is what it carries.
|
|
736
|
-
*
|
|
737
|
-
* An array counts only when it is files ALL the way through — a mixed array is
|
|
738
|
-
* some other field's value that happens to contain one, and turning half of it
|
|
739
|
-
* into ids would corrupt it silently.
|
|
740
|
-
*/
|
|
741
|
-
function filesOf(value) {
|
|
742
|
-
if (value instanceof File) return [value];
|
|
743
|
-
if (!Array.isArray(value)) return [];
|
|
744
|
-
const files = value.filter((one) => one instanceof File);
|
|
745
|
-
return files.length > 0 && files.length === value.length ? files : [];
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* The input properties still carrying a `File` — i.e. the ones that CANNOT survive
|
|
749
|
-
* being sent.
|
|
750
|
-
*
|
|
751
|
-
* A run input is JSON, and `JSON.stringify(new File(…))` is `{}` — no `toJSON`, no
|
|
752
|
-
* own enumerable properties. So a File left in a payload does not fail to send: it
|
|
753
|
-
* arrives as an empty object, and the workflow rejects it against whatever its own
|
|
754
|
-
* schema says the property should be. Measured in production as
|
|
755
|
-
* `Invalid input for workflow "transcribe": recording: Invalid input` — a message
|
|
756
|
-
* about a type, on a form where the user had picked a perfectly good file.
|
|
757
|
-
*
|
|
758
|
-
* Exported beside {@link filesOf} because it is the same question asked at the
|
|
759
|
-
* other end: that one decides which fields to UPLOAD, this one checks that none
|
|
760
|
-
* were missed. Both hooks are the callers.
|
|
761
|
-
*/
|
|
762
|
-
function fileFields(input) {
|
|
763
|
-
if (!isRecord(input)) return [];
|
|
764
|
-
return Object.entries(input).filter(([, value]) => filesOf(value).length > 0).map(([key]) => key);
|
|
765
|
-
}
|
|
766
|
-
//#endregion
|
|
767
981
|
//#region _repeat-until.ts
|
|
768
982
|
/**
|
|
769
983
|
* A bounded read, re-armed from the SETTLED read — the loop both workflow
|
|
@@ -1170,56 +1384,6 @@ function useWorkflows(opts = {}) {
|
|
|
1170
1384
|
return state;
|
|
1171
1385
|
}
|
|
1172
1386
|
/**
|
|
1173
|
-
* Replace every `File` in a submitted form with the id of a stored upload,
|
|
1174
|
-
* reporting how far each one has got.
|
|
1175
|
-
*
|
|
1176
|
-
* Sequential rather than `Promise.all`: these are large bodies, and a form with
|
|
1177
|
-
* two 200 MB recordings should send them one after another rather than compete
|
|
1178
|
-
* for the same connection. That is also what makes a single bar honest — one
|
|
1179
|
-
* file is in flight at a time, and `index`/`count` say which.
|
|
1180
|
-
*
|
|
1181
|
-
* Anything that is not a `File` (or an array of them) passes through untouched,
|
|
1182
|
-
* so this is invisible to every form that has none — including one whose values
|
|
1183
|
-
* are not an object at all, which `submit` accepts.
|
|
1184
|
-
*/
|
|
1185
|
-
async function uploadFiles(api, input, report, parallel) {
|
|
1186
|
-
if (!isRecord(input)) return input;
|
|
1187
|
-
const entries = Object.entries(input);
|
|
1188
|
-
const count = entries.reduce((total, [, value]) => total + filesOf(value).length, 0);
|
|
1189
|
-
let index = 0;
|
|
1190
|
-
const store = async (file) => {
|
|
1191
|
-
index += 1;
|
|
1192
|
-
const position = {
|
|
1193
|
-
name: file.name,
|
|
1194
|
-
index,
|
|
1195
|
-
count
|
|
1196
|
-
};
|
|
1197
|
-
return (await api.upload(file, {
|
|
1198
|
-
onProgress: (progress) => report({
|
|
1199
|
-
...position,
|
|
1200
|
-
...progress
|
|
1201
|
-
}),
|
|
1202
|
-
...omitUndefined({ parallel })
|
|
1203
|
-
})).id;
|
|
1204
|
-
};
|
|
1205
|
-
const out = {};
|
|
1206
|
-
for (const [name, value] of entries) {
|
|
1207
|
-
if (value instanceof File) {
|
|
1208
|
-
out[name] = await store(value);
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
const chosen = filesOf(value);
|
|
1212
|
-
if (chosen.length === 0) {
|
|
1213
|
-
out[name] = value;
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
const ids = [];
|
|
1217
|
-
for (const file of chosen) ids.push(await store(file));
|
|
1218
|
-
out[name] = ids;
|
|
1219
|
-
}
|
|
1220
|
-
return out;
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
1387
|
* Start a workflow from a form, and follow the run it creates.
|
|
1224
1388
|
*
|
|
1225
1389
|
* @typeParam R - The workflow's output type, which is what makes
|
|
@@ -1250,6 +1414,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1250
1414
|
const [starting, setStarting] = useState(false);
|
|
1251
1415
|
const [startError, setStartError] = useState(void 0);
|
|
1252
1416
|
const [upload, setUpload] = useState(void 0);
|
|
1417
|
+
const session = useRef(void 0);
|
|
1253
1418
|
const getClient = useWorkflowApiRef(api);
|
|
1254
1419
|
const tracked = useWorkflowRun(runId, omitUndefined({
|
|
1255
1420
|
api,
|
|
@@ -1261,18 +1426,24 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1261
1426
|
setStarting(true);
|
|
1262
1427
|
setStartError(void 0);
|
|
1263
1428
|
setRunId(void 0);
|
|
1429
|
+
session.current?.gate.cancel();
|
|
1430
|
+
const current = createUploadSession();
|
|
1431
|
+
session.current = current;
|
|
1264
1432
|
try {
|
|
1265
1433
|
const options = omitUndefined({ key });
|
|
1266
|
-
const started = await uploadFiles(client, input, setUpload, parallel);
|
|
1434
|
+
const started = await uploadFiles(client, input, setUpload, parallel, current);
|
|
1267
1435
|
setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
|
|
1268
1436
|
...options,
|
|
1269
1437
|
wait
|
|
1270
1438
|
})).runId);
|
|
1271
1439
|
} catch (err) {
|
|
1272
|
-
setStartError(errorMessage(err));
|
|
1440
|
+
if (!current.gate.cancelled) setStartError(errorMessage(err));
|
|
1273
1441
|
} finally {
|
|
1274
|
-
|
|
1275
|
-
|
|
1442
|
+
if (session.current === current) {
|
|
1443
|
+
session.current = void 0;
|
|
1444
|
+
setStarting(false);
|
|
1445
|
+
setUpload(void 0);
|
|
1446
|
+
}
|
|
1276
1447
|
}
|
|
1277
1448
|
}, [
|
|
1278
1449
|
workflow,
|
|
@@ -1282,10 +1453,26 @@ function useWorkflowSubmit(workflow, opts = {}) {
|
|
|
1282
1453
|
getClient
|
|
1283
1454
|
]),
|
|
1284
1455
|
reset: useCallback(() => {
|
|
1456
|
+
session.current?.gate.cancel();
|
|
1457
|
+
session.current = void 0;
|
|
1285
1458
|
setRunId(void 0);
|
|
1286
1459
|
setStartError(void 0);
|
|
1287
1460
|
setUpload(void 0);
|
|
1288
1461
|
}, []),
|
|
1462
|
+
pauseUpload: useCallback(() => {
|
|
1463
|
+
session.current?.gate.pause();
|
|
1464
|
+
setUpload((current) => current ? {
|
|
1465
|
+
...current,
|
|
1466
|
+
paused: true
|
|
1467
|
+
} : current);
|
|
1468
|
+
}, []),
|
|
1469
|
+
resumeUpload: useCallback(() => {
|
|
1470
|
+
session.current?.gate.resume();
|
|
1471
|
+
setUpload((current) => current ? {
|
|
1472
|
+
...current,
|
|
1473
|
+
paused: false
|
|
1474
|
+
} : current);
|
|
1475
|
+
}, []),
|
|
1289
1476
|
run: tracked.run,
|
|
1290
1477
|
pending: starting || tracked.polling,
|
|
1291
1478
|
upload,
|
|
@@ -1944,18 +2131,33 @@ function useWorkflowRuns(workflow, opts = {}) {
|
|
|
1944
2131
|
* - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
|
|
1945
2132
|
* so `<UploadProgressBar>` renders either without knowing which hook it came from.
|
|
1946
2133
|
*
|
|
1947
|
-
* ## A failed upload is RESUMED
|
|
2134
|
+
* ## A failed upload is RESUMED, and only a spent budget cancels the run
|
|
1948
2135
|
*
|
|
1949
2136
|
* An upload that dies stays in the store, incomplete, and `complete` never becomes
|
|
1950
2137
|
* true — so a run left behind polls until its own abandonment bound and then fails,
|
|
1951
2138
|
* minutes after the page has already reported the error.
|
|
1952
2139
|
*
|
|
1953
2140
|
* That used to be the whole story, and it threw away a run and a file together for
|
|
1954
|
-
* what is usually one dropped connection near the end.
|
|
1955
|
-
*
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
2141
|
+
* what is usually one dropped connection near the end. **The resume lives in the
|
|
2142
|
+
* SDK now** (`aai/sdk/_upload-resume.ts`): a round that fails for a reason that
|
|
2143
|
+
* looks like an outage is re-entered with `resume: true`, sending only the windows
|
|
2144
|
+
* the store does not already have, on a budget sized to outlast a redeploy. The run
|
|
2145
|
+
* is still waiting on the same id, so a resume that succeeds is invisible to it.
|
|
2146
|
+
*
|
|
2147
|
+
* This hook used to hand-roll one such retry and no longer does — one resume with
|
|
2148
|
+
* no wait in front of it covers a dropped connection and cannot cover the case that
|
|
2149
|
+
* actually strands people, which is the agent restarting underneath the upload.
|
|
2150
|
+
* Cancelling the run is what happens when the whole budget is spent, and it is the
|
|
2151
|
+
* honest end to a submission that did not happen.
|
|
2152
|
+
*
|
|
2153
|
+
* ## Pausing is the same mechanism, asked for
|
|
2154
|
+
*
|
|
2155
|
+
* `pauseUpload()` aborts the bytes in flight and holds the uploader;
|
|
2156
|
+
* `resumeUpload()` sends what is missing. The store cannot tell that from an
|
|
2157
|
+
* outage, because there is nothing to tell apart — see `_upload-session.ts`. The
|
|
2158
|
+
* RUN is untouched either way: it goes on polling the id it was started with, and
|
|
2159
|
+
* `stream.ts`'s idle bound (five minutes of no new bytes) is what decides that a
|
|
2160
|
+
* pause has become an abandonment.
|
|
1959
2161
|
*/
|
|
1960
2162
|
/**
|
|
1961
2163
|
* Start a workflow run and stream a file into it while it works.
|
|
@@ -1988,6 +2190,7 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
1988
2190
|
const [starting, setStarting] = useState(false);
|
|
1989
2191
|
const [startError, setStartError] = useState(void 0);
|
|
1990
2192
|
const [upload, setUpload] = useState(void 0);
|
|
2193
|
+
const gateRef = useRef(void 0);
|
|
1991
2194
|
const getClient = useWorkflowApiRef(api);
|
|
1992
2195
|
const tracked = useWorkflowRun(runId, omitUndefined({
|
|
1993
2196
|
api,
|
|
@@ -1999,42 +2202,41 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
1999
2202
|
setStarting(true);
|
|
2000
2203
|
setStartError(void 0);
|
|
2001
2204
|
setRunId(void 0);
|
|
2205
|
+
gateRef.current?.cancel();
|
|
2206
|
+
const gate = createUploadGate();
|
|
2207
|
+
gateRef.current = gate;
|
|
2002
2208
|
let started;
|
|
2003
2209
|
try {
|
|
2004
|
-
const field = await uploadField(client, workflow);
|
|
2005
|
-
const chosen = field === void 0 ? void 0 : fileAt(input, field);
|
|
2006
2210
|
const id = randomUploadId();
|
|
2007
|
-
const
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2211
|
+
const begun = await beginRun({
|
|
2212
|
+
client,
|
|
2213
|
+
workflow,
|
|
2214
|
+
input,
|
|
2215
|
+
id,
|
|
2216
|
+
...omitUndefined({ key })
|
|
2217
|
+
});
|
|
2218
|
+
started = begun.runId;
|
|
2219
|
+
const chosen = begun.file;
|
|
2013
2220
|
setRunId(started);
|
|
2014
2221
|
if (!chosen) return;
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
}),
|
|
2024
|
-
...omitUndefined({
|
|
2025
|
-
parallel,
|
|
2026
|
-
resume: resume ? true : void 0
|
|
2027
|
-
})
|
|
2028
|
-
});
|
|
2029
|
-
};
|
|
2030
|
-
await send(false).catch(async () => await send(true));
|
|
2222
|
+
await streamFile({
|
|
2223
|
+
client,
|
|
2224
|
+
gate,
|
|
2225
|
+
id,
|
|
2226
|
+
file: chosen,
|
|
2227
|
+
parallel,
|
|
2228
|
+
report: setUpload
|
|
2229
|
+
});
|
|
2031
2230
|
await client.wake(started).catch(() => void 0);
|
|
2032
2231
|
} catch (err) {
|
|
2033
|
-
setStartError(errorMessage(err));
|
|
2232
|
+
if (!gate.cancelled) setStartError(errorMessage(err));
|
|
2034
2233
|
if (started) await client.cancel(started).catch(() => void 0);
|
|
2035
2234
|
} finally {
|
|
2036
|
-
|
|
2037
|
-
|
|
2235
|
+
if (gateRef.current === gate) {
|
|
2236
|
+
gateRef.current = void 0;
|
|
2237
|
+
setStarting(false);
|
|
2238
|
+
setUpload(void 0);
|
|
2239
|
+
}
|
|
2038
2240
|
}
|
|
2039
2241
|
}, [
|
|
2040
2242
|
workflow,
|
|
@@ -2043,10 +2245,26 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2043
2245
|
getClient
|
|
2044
2246
|
]),
|
|
2045
2247
|
reset: useCallback(() => {
|
|
2248
|
+
gateRef.current?.cancel();
|
|
2249
|
+
gateRef.current = void 0;
|
|
2046
2250
|
setRunId(void 0);
|
|
2047
2251
|
setStartError(void 0);
|
|
2048
2252
|
setUpload(void 0);
|
|
2049
2253
|
}, []),
|
|
2254
|
+
pauseUpload: useCallback(() => {
|
|
2255
|
+
gateRef.current?.pause();
|
|
2256
|
+
setUpload((current) => current ? {
|
|
2257
|
+
...current,
|
|
2258
|
+
paused: true
|
|
2259
|
+
} : current);
|
|
2260
|
+
}, []),
|
|
2261
|
+
resumeUpload: useCallback(() => {
|
|
2262
|
+
gateRef.current?.resume();
|
|
2263
|
+
setUpload((current) => current ? {
|
|
2264
|
+
...current,
|
|
2265
|
+
paused: false
|
|
2266
|
+
} : current);
|
|
2267
|
+
}, []),
|
|
2050
2268
|
run: tracked.run,
|
|
2051
2269
|
pending: starting || tracked.polling,
|
|
2052
2270
|
upload,
|
|
@@ -2054,6 +2272,56 @@ function useWorkflowStream(workflow, opts = {}) {
|
|
|
2054
2272
|
};
|
|
2055
2273
|
}
|
|
2056
2274
|
/**
|
|
2275
|
+
* Read the declaration, substitute the id, and start the run.
|
|
2276
|
+
*
|
|
2277
|
+
* Everything that has to happen BEFORE a byte moves, which is the inversion this
|
|
2278
|
+
* hook exists for. One small `list()` per submit, deliberately: holding the
|
|
2279
|
+
* listing in state would make a submit before it landed a race, and the failure
|
|
2280
|
+
* mode of that race is a `File` reaching a run input.
|
|
2281
|
+
*/
|
|
2282
|
+
async function beginRun(opts) {
|
|
2283
|
+
const { client, workflow, input, id } = opts;
|
|
2284
|
+
const field = await uploadField(client, workflow);
|
|
2285
|
+
const chosen = field === void 0 ? void 0 : fileAt(input, field);
|
|
2286
|
+
const payload = chosen && field ? {
|
|
2287
|
+
...input,
|
|
2288
|
+
[field]: id
|
|
2289
|
+
} : input;
|
|
2290
|
+
assertSendable(workflow, payload, field);
|
|
2291
|
+
return {
|
|
2292
|
+
runId: await client.start(workflow, payload, omitUndefined({ key: opts.key })),
|
|
2293
|
+
file: chosen
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Send the file, waiting out however many pauses the person takes.
|
|
2298
|
+
*
|
|
2299
|
+
* Its own function rather than a block inside `submit` because `submit` is
|
|
2300
|
+
* already carrying the ORDER this hook exists for — read the declaration, mint
|
|
2301
|
+
* the id, start the run, then the bytes, then the wake — and the sending is the
|
|
2302
|
+
* one step of that list with a loop in it.
|
|
2303
|
+
*/
|
|
2304
|
+
async function streamFile(opts) {
|
|
2305
|
+
const { client, gate, id, file, parallel, report } = opts;
|
|
2306
|
+
await sendThroughGate(gate, async (resume) => {
|
|
2307
|
+
await client.uploadStream(id, file, {
|
|
2308
|
+
name: file.name,
|
|
2309
|
+
signal: gate.signal,
|
|
2310
|
+
onProgress: (progress) => report({
|
|
2311
|
+
...progress,
|
|
2312
|
+
name: file.name,
|
|
2313
|
+
index: 1,
|
|
2314
|
+
count: 1,
|
|
2315
|
+
paused: gate.paused
|
|
2316
|
+
}),
|
|
2317
|
+
...omitUndefined({
|
|
2318
|
+
parallel,
|
|
2319
|
+
resume: resume ? true : void 0
|
|
2320
|
+
})
|
|
2321
|
+
});
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
/**
|
|
2057
2325
|
* Refuse a payload carrying a `File`, before a run is started over it.
|
|
2058
2326
|
*
|
|
2059
2327
|
* A File cannot be SENT: a run input is JSON and `JSON.stringify(new File(…))` is
|
|
@@ -2080,10 +2348,6 @@ function assertSendable(workflow, payload, field) {
|
|
|
2080
2348
|
const declares = field === void 0 ? "" : ` (it declares "${field}")`;
|
|
2081
2349
|
throw new Error(`Cannot start "${workflow}": ${unsendable.join(", ")} ${carries} the workflow does not declare as an upload${declares}. Add the property to \`workflow({ uploads: [...] })\`, or submit an upload id.`);
|
|
2082
2350
|
}
|
|
2083
|
-
/** A fresh upload id: a capability, so it is random rather than derived. */
|
|
2084
|
-
function randomUploadId() {
|
|
2085
|
-
return crypto.randomUUID().replaceAll("-", "");
|
|
2086
|
-
}
|
|
2087
2351
|
/**
|
|
2088
2352
|
* Which input property this workflow says carries an upload id.
|
|
2089
2353
|
*
|
|
@@ -88,6 +88,16 @@ export type UploadStatus = UploadProgress & {
|
|
|
88
88
|
index: number;
|
|
89
89
|
/** How many files this submission sends in total. */
|
|
90
90
|
count: number;
|
|
91
|
+
/**
|
|
92
|
+
* Whether the person has parked this upload.
|
|
93
|
+
*
|
|
94
|
+
* A paused upload is not a stopped one: the windows already stored stay stored,
|
|
95
|
+
* `loaded` holds where it got to, and resuming sends what is missing rather than
|
|
96
|
+
* the file. So a bar rendering this reads "Paused at 62%", never "62% and
|
|
97
|
+
* frozen" — which is what a page could otherwise only guess from a number that
|
|
98
|
+
* stopped moving, the same ambiguity `complete` exists to remove on the run side.
|
|
99
|
+
*/
|
|
100
|
+
paused: boolean;
|
|
91
101
|
};
|
|
92
102
|
/** What {@link useWorkflowSubmit} returns. */
|
|
93
103
|
export type WorkflowSubmission<R = unknown> = {
|
|
@@ -122,6 +132,21 @@ export type WorkflowSubmission<R = unknown> = {
|
|
|
122
132
|
* for a 200 MB recording is minutes of a page that looks stuck.
|
|
123
133
|
*/
|
|
124
134
|
upload: UploadStatus | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Park the upload where it is, stopping the bytes in flight.
|
|
137
|
+
*
|
|
138
|
+
* The windows already stored stay stored, so `resumeUpload()` sends what is
|
|
139
|
+
* missing rather than the file — which is the difference between a pause a
|
|
140
|
+
* person will actually use on a 200 MB recording and a cancel dressed up as one.
|
|
141
|
+
*
|
|
142
|
+
* `submit()`'s promise stays unresolved across a pause, because the submission
|
|
143
|
+
* genuinely has not finished: the run does not exist until the last byte lands,
|
|
144
|
+
* so resolving here would tell a `<Form>` the work was accepted when nothing has
|
|
145
|
+
* been started. A no-op when there is no upload in flight.
|
|
146
|
+
*/
|
|
147
|
+
pauseUpload: () => void;
|
|
148
|
+
/** Continue a paused upload, sending only the windows the store does not have. */
|
|
149
|
+
resumeUpload: () => void;
|
|
125
150
|
/** The submit's own failure (a rejected input), or the watch's. */
|
|
126
151
|
error: string | undefined;
|
|
127
152
|
};
|
|
@@ -48,18 +48,33 @@
|
|
|
48
48
|
* - **Reporting the bytes.** The same `UploadStatus` `useWorkflowSubmit` reports,
|
|
49
49
|
* so `<UploadProgressBar>` renders either without knowing which hook it came from.
|
|
50
50
|
*
|
|
51
|
-
* ## A failed upload is RESUMED
|
|
51
|
+
* ## A failed upload is RESUMED, and only a spent budget cancels the run
|
|
52
52
|
*
|
|
53
53
|
* An upload that dies stays in the store, incomplete, and `complete` never becomes
|
|
54
54
|
* true — so a run left behind polls until its own abandonment bound and then fails,
|
|
55
55
|
* minutes after the page has already reported the error.
|
|
56
56
|
*
|
|
57
57
|
* That used to be the whole story, and it threw away a run and a file together for
|
|
58
|
-
* what is usually one dropped connection near the end.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* what is usually one dropped connection near the end. **The resume lives in the
|
|
59
|
+
* SDK now** (`aai/sdk/_upload-resume.ts`): a round that fails for a reason that
|
|
60
|
+
* looks like an outage is re-entered with `resume: true`, sending only the windows
|
|
61
|
+
* the store does not already have, on a budget sized to outlast a redeploy. The run
|
|
62
|
+
* is still waiting on the same id, so a resume that succeeds is invisible to it.
|
|
63
|
+
*
|
|
64
|
+
* This hook used to hand-roll one such retry and no longer does — one resume with
|
|
65
|
+
* no wait in front of it covers a dropped connection and cannot cover the case that
|
|
66
|
+
* actually strands people, which is the agent restarting underneath the upload.
|
|
67
|
+
* Cancelling the run is what happens when the whole budget is spent, and it is the
|
|
68
|
+
* honest end to a submission that did not happen.
|
|
69
|
+
*
|
|
70
|
+
* ## Pausing is the same mechanism, asked for
|
|
71
|
+
*
|
|
72
|
+
* `pauseUpload()` aborts the bytes in flight and holds the uploader;
|
|
73
|
+
* `resumeUpload()` sends what is missing. The store cannot tell that from an
|
|
74
|
+
* outage, because there is nothing to tell apart — see `_upload-session.ts`. The
|
|
75
|
+
* RUN is untouched either way: it goes on polling the id it was started with, and
|
|
76
|
+
* `stream.ts`'s idle bound (five minutes of no new bytes) is what decides that a
|
|
77
|
+
* pause has become an abandonment.
|
|
63
78
|
*/
|
|
64
79
|
import type { UploadParallel } from "@alexkroman1/aai/workflow-api";
|
|
65
80
|
import type { UploadStatus } from "./use-workflow-form.ts";
|
|
@@ -109,6 +124,17 @@ export type WorkflowStreamSubmission<R = unknown> = {
|
|
|
109
124
|
pending: boolean;
|
|
110
125
|
/** How far the upload has got, while it is still going. */
|
|
111
126
|
upload: UploadStatus | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Park the upload where it is, stopping the bytes in flight.
|
|
129
|
+
*
|
|
130
|
+
* The RUN keeps going — it is watching an upload id, and a paused upload is one
|
|
131
|
+
* whose `size` has stopped growing, which is exactly what a slow uplink looks
|
|
132
|
+
* like. So a pause costs nothing until the workflow's own idle bound decides the
|
|
133
|
+
* uploader is gone (five minutes in `transcription-workflow`).
|
|
134
|
+
*/
|
|
135
|
+
pauseUpload: () => void;
|
|
136
|
+
/** Continue a paused upload, sending only the windows the store does not have. */
|
|
137
|
+
resumeUpload: () => void;
|
|
112
138
|
/** The submit's own failure (a rejected input, or an upload that would not store). */
|
|
113
139
|
error: string | undefined;
|
|
114
140
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.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.8.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^19.0.0",
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--font-aai:"Monument Grotesk", "ABC Monument Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;--font-aai-serif:"Source Serif 4", "Source Serif Pro", Charter, "Iowan Old Style", Georgia, serif;--font-aai-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;--radius-aai:4px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html,body{margin:0;padding:0}}@layer components;@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.order-last{order:9999}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-0\.5{margin-block:calc(var(--spacing) * .5)}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-2\.5{margin-block:calc(var(--spacing) * 2.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-4{height:calc(var(--spacing) * 4)}.h-9{height:calc(var(--spacing) * 9)}.h-11{height:calc(var(--spacing) * 11)}.h-\[7px\]{height:7px}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-\[40vh\]{max-height:40vh}.min-h-0{min-height:0}.min-h-5{min-height:calc(var(--spacing) * 5)}.min-h-screen{min-height:100vh}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-4{width:calc(var(--spacing) * 4)}.w-\[7px\]{width:7px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-75{max-width:calc(var(--spacing) * 75)}.max-w-105{max-width:calc(var(--spacing) * 105)}.max-w-190{max-width:calc(var(--spacing) * 190)}.max-w-\[82\%\]{max-width:82%}.max-w-\[min\(78\%\,64ch\)\]{max-width:min(78%,64ch)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-full{flex-basis:100%}.border-collapse{border-collapse:collapse}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.resize{resize:both}.\[scrollbar-width\:none\]{scrollbar-width:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.appearance-none{appearance:none}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded-aai{border-radius:var(--radius-aai)}.rounded-full{border-radius:2147483647px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-none{--tw-border-style:none;border-style:none}.border-\(--aai-btn-bd\){border-color:var(--aai-btn-bd)}.bg-\(--aai-btn-bg\){background-color:var(--aai-btn-bg)}.bg-transparent{background-color:#0000}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-7{padding:calc(var(--spacing) * 7)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-7{padding-inline:calc(var(--spacing) * 7)}.px-10{padding-inline:calc(var(--spacing) * 10)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.font-aai{font-family:var(--font-aai)}.font-aai-mono{font-family:var(--font-aai-mono)}.font-aai-serif{font-family:var(--font-aai-serif)}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[22px\]{font-size:22px}.text-\[32px\]{font-size:32px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.leading-\[22px\]{--tw-leading:22px;line-height:22px}.leading-\[23px\]{--tw-leading:23px;line-height:23px}.leading-\[130\%\]{--tw-leading:130%;line-height:130%}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.2px\]{--tw-tracking:-.2px;letter-spacing:-.2px}.tracking-\[1\.2px\]{--tw-tracking:1.2px;letter-spacing:1.2px}.tracking-\[1\.4px\]{--tw-tracking:1.4px;letter-spacing:1.4px}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-balance{text-wrap:balance}.wrap-break-word{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\(--aai-btn-fg\){color:var(--aai-btn-fg)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.file\:mr-3::file-selector-button{margin-right:calc(var(--spacing) * 3)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-aai::file-selector-button{border-radius:var(--radius-aai)}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:px-3::file-selector-button{padding-inline:calc(var(--spacing) * 3)}.file\:py-1\.5::file-selector-button{padding-block:calc(var(--spacing) * 1.5)}.file\:font-aai::file-selector-button{font-family:var(--font-aai)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.file\:\[color\:var\(--aai-file-button-fg\)\]::file-selector-button{color:var(--aai-file-button-fg)}.file\:\[background\:var\(--aai-file-button-bg\)\]::file-selector-button{background:var(--aai-file-button-bg)}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.focus-visible\:\[outline\:2px_solid\]:focus-visible{outline:2px solid}.focus-visible\:\[outline-offset\:2px\]:focus-visible{outline-offset:2px}@media (hover:hover){.enabled\:hover\:border-\(--aai-btn-bd-hover\):enabled:hover{border-color:var(--aai-btn-bd-hover)}.enabled\:hover\:bg-\(--aai-btn-bg-hover\):enabled:hover{background-color:var(--aai-btn-bg-hover)}.enabled\:hover\:text-\(--aai-btn-fg-hover\):enabled:hover{color:var(--aai-btn-fg-hover)}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:ml-auto{margin-left:auto}.sm\:max-w-\[60\%\]{max-width:60%}.sm\:basis-auto{flex-basis:auto}.sm\:px-16{padding-inline:calc(var(--spacing) * 16)}.sm\:py-14{padding-block:calc(var(--spacing) * 14)}}@media (width>=48rem){.md\:order-none{order:0}.md\:h-screen{height:100vh}.md\:max-h-none{max-height:none}.md\:w-\(--aai-sidebar-w\){width:var(--aai-sidebar-w)}.md\:flex-row{flex-direction:row}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.md\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}}}@keyframes aai-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.45;transform:scale(.82)}}@keyframes aai-bounce{0%,80%,to{opacity:.3;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}@keyframes aai-shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}.tool-shimmer{-webkit-text-fill-color:transparent;background:linear-gradient(90deg,currentColor 25%,#0000 50%,currentColor 75%) 0 0/200% 100%;-webkit-background-clip:text;background-clip:text;animation:2s infinite aai-shimmer}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}}
|
|
File without changes
|