@alexkroman1/aai-cli 6.6.0 → 6.7.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.
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"publish:agent": "aai publish"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@alexkroman1/aai": "^6.
|
|
17
|
-
"@alexkroman1/aai-ui": "^6.
|
|
16
|
+
"@alexkroman1/aai": "^6.7.0",
|
|
17
|
+
"@alexkroman1/aai-ui": "^6.7.0",
|
|
18
18
|
"@workflow/world-postgres": "4.3.3",
|
|
19
19
|
"react": "^19.2.8",
|
|
20
20
|
"react-dom": "^19.2.8",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"zod": "^4.4.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@alexkroman1/aai-cli": "^6.
|
|
26
|
+
"@alexkroman1/aai-cli": "^6.7.0",
|
|
27
27
|
"@tailwindcss/vite": "^4.3.3",
|
|
28
28
|
"@types/node": "^26.2.0",
|
|
29
29
|
"@types/react": "^19.2.18",
|
|
@@ -106,6 +106,22 @@
|
|
|
106
106
|
* whose schema has an object or array property writes those fields itself, in
|
|
107
107
|
* the same `<Form>` — every field in `@alexkroman1/aai-ui` is a plain named
|
|
108
108
|
* control, so declared and hand-written ones mix freely.
|
|
109
|
+
*
|
|
110
|
+
* ## Two waits, ONE number
|
|
111
|
+
*
|
|
112
|
+
* The two bars describe the two stretches separately, and neither answers the
|
|
113
|
+
* question a reader comparing the three modes is actually asking: how long from
|
|
114
|
+
* pressing Transcribe to having a transcript. Nothing on the server can answer it
|
|
115
|
+
* either — `output.elapsedMs` is the RUN's own wall clock, so it starts after the
|
|
116
|
+
* bytes are stored in two of the three modes and misses the whole upload, which is
|
|
117
|
+
* most of the wait on a long file over a slow link. Only the browser holds both
|
|
118
|
+
* ends, so `useTotalLatency` is a stopwatch here: started by the submit, ticking
|
|
119
|
+
* across the upload and the run alike, and frozen the moment the run settles.
|
|
120
|
+
*
|
|
121
|
+
* `<TotalLatency>` also prints the SPLIT once the run reports its own elapsed —
|
|
122
|
+
* before the run and inside it — because the two numbers on screen otherwise
|
|
123
|
+
* disagree with no way to see why, and their difference is exactly what picking a
|
|
124
|
+
* mode or unchecking `parallel` moves.
|
|
109
125
|
*/
|
|
110
126
|
|
|
111
127
|
import "@alexkroman1/aai-ui/styles.css";
|
|
@@ -124,7 +140,7 @@ import {
|
|
|
124
140
|
WorkflowProgress,
|
|
125
141
|
type WorkflowRun,
|
|
126
142
|
} from "@alexkroman1/aai-ui";
|
|
127
|
-
import { useEffect, useMemo, useState } from "react";
|
|
143
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
128
144
|
import type { transcribe } from "./agent.ts";
|
|
129
145
|
import { ApiHelp } from "./api-help.tsx";
|
|
130
146
|
import {
|
|
@@ -187,6 +203,132 @@ const MODES: readonly { mode: Mode; label: string; note: string }[] = [
|
|
|
187
203
|
/** Most past runs the history list shows. */
|
|
188
204
|
const HISTORY_LIMIT = 10;
|
|
189
205
|
|
|
206
|
+
/**
|
|
207
|
+
* How often the running stopwatch re-renders.
|
|
208
|
+
*
|
|
209
|
+
* Under a second, so the displayed seconds turn over promptly rather than up to a
|
|
210
|
+
* second late; nothing reads this value, since the elapsed time is measured from
|
|
211
|
+
* the clock at render (see {@link useTotalLatency}).
|
|
212
|
+
*/
|
|
213
|
+
const STOPWATCH_TICK_MS = 250;
|
|
214
|
+
|
|
215
|
+
/** What {@link useTotalLatency} reports. */
|
|
216
|
+
type TotalLatency = {
|
|
217
|
+
/**
|
|
218
|
+
* Milliseconds since the submit — ticking while the submission is in flight,
|
|
219
|
+
* frozen at the finish, and undefined before the first one.
|
|
220
|
+
*/
|
|
221
|
+
elapsedMs: number | undefined;
|
|
222
|
+
/** Whether the clock is still running, which is what makes the label honest. */
|
|
223
|
+
running: boolean;
|
|
224
|
+
/** Start (or restart) the clock. Called from the form's own submit handler. */
|
|
225
|
+
start: () => void;
|
|
226
|
+
/** Drop it, for a panel that no longer describes the submission it timed. */
|
|
227
|
+
clear: () => void;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Wall clock from the submit to the finish, across both waits.
|
|
232
|
+
*
|
|
233
|
+
* `inFlight` is the submission's own `pending` — true from `submit()` until the run
|
|
234
|
+
* reaches a terminal status — so the clock covers the upload, the run, and the
|
|
235
|
+
* gap between them, which is the whole of what a reader waits for and is the one
|
|
236
|
+
* measurement no server-side number can make.
|
|
237
|
+
*
|
|
238
|
+
* Two details it would be easy to get wrong:
|
|
239
|
+
*
|
|
240
|
+
* - **The interval re-renders; it does not accumulate.** The elapsed time is read
|
|
241
|
+
* from the clock at render, so a tick the tab throttled or dropped cannot make
|
|
242
|
+
* the number lag behind real time.
|
|
243
|
+
* - **`performance.now()`, not `Date.now()`.** It is monotonic, so a clock
|
|
244
|
+
* correction (NTP, a laptop waking up) cannot make a transcription look
|
|
245
|
+
* instant — or negative.
|
|
246
|
+
*/
|
|
247
|
+
function useTotalLatency(inFlight: boolean): TotalLatency {
|
|
248
|
+
const [startedAt, setStartedAt] = useState<number | undefined>(undefined);
|
|
249
|
+
const [frozenMs, setFrozenMs] = useState<number | undefined>(undefined);
|
|
250
|
+
// Re-render trigger only — see the doc above.
|
|
251
|
+
const [, tick] = useState(0);
|
|
252
|
+
// Whether `inFlight` has been seen true since the last `start()`. Without it,
|
|
253
|
+
// a start that lands one render before the submission reports itself in flight
|
|
254
|
+
// would freeze the clock at zero instead of running it.
|
|
255
|
+
const began = useRef(false);
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (startedAt === undefined || frozenMs !== undefined) return;
|
|
259
|
+
if (inFlight) {
|
|
260
|
+
began.current = true;
|
|
261
|
+
const id = setInterval(() => tick((n) => n + 1), STOPWATCH_TICK_MS);
|
|
262
|
+
return () => clearInterval(id);
|
|
263
|
+
}
|
|
264
|
+
// Measured here rather than at render, so the frozen number is the one at the
|
|
265
|
+
// moment the run settled rather than whenever this page next drew.
|
|
266
|
+
if (began.current) setFrozenMs(performance.now() - startedAt);
|
|
267
|
+
}, [startedAt, frozenMs, inFlight]);
|
|
268
|
+
|
|
269
|
+
const start = useCallback(() => {
|
|
270
|
+
began.current = false;
|
|
271
|
+
setFrozenMs(undefined);
|
|
272
|
+
setStartedAt(performance.now());
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
275
|
+
const clear = useCallback(() => {
|
|
276
|
+
began.current = false;
|
|
277
|
+
setStartedAt(undefined);
|
|
278
|
+
setFrozenMs(undefined);
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
elapsedMs: frozenMs ?? (startedAt === undefined ? undefined : performance.now() - startedAt),
|
|
283
|
+
running: startedAt !== undefined && frozenMs === undefined,
|
|
284
|
+
start,
|
|
285
|
+
clear,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The one number the two bars cannot give: click to transcript.
|
|
291
|
+
*
|
|
292
|
+
* Rendered above the run panel rather than inside it, because the stretch it
|
|
293
|
+
* covers starts before there IS a run — in two of the three modes the run does
|
|
294
|
+
* not exist until the upload finishes, so a clock living in the panel would
|
|
295
|
+
* appear only after the wait it is supposed to be timing.
|
|
296
|
+
*
|
|
297
|
+
* `runMs` is the run's own elapsed, once it reports one. The remainder is
|
|
298
|
+
* everything the run could not see: storing the file (or, in streaming mode,
|
|
299
|
+
* minting the upload id), the `POST` that starts the run, and the poll that
|
|
300
|
+
* notices it finished. Clamped at zero, because the two numbers come from two
|
|
301
|
+
* different clocks on two different machines and a few milliseconds the wrong way
|
|
302
|
+
* would otherwise print a negative.
|
|
303
|
+
*/
|
|
304
|
+
function TotalLatency({
|
|
305
|
+
elapsedMs,
|
|
306
|
+
running,
|
|
307
|
+
runMs,
|
|
308
|
+
}: {
|
|
309
|
+
elapsedMs: number | undefined;
|
|
310
|
+
running: boolean;
|
|
311
|
+
runMs: number | undefined;
|
|
312
|
+
}) {
|
|
313
|
+
if (elapsedMs === undefined) return null;
|
|
314
|
+
const outside = runMs === undefined ? undefined : Math.max(0, elapsedMs - runMs);
|
|
315
|
+
return (
|
|
316
|
+
<section className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 rounded-md border px-5 py-3">
|
|
317
|
+
<h2 className="text-sm font-medium uppercase tracking-[1.2px]">
|
|
318
|
+
{running ? "Elapsed" : "Total latency"}
|
|
319
|
+
</h2>
|
|
320
|
+
<span className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
|
321
|
+
<span className="text-sm tabular-nums">{duration(elapsedMs)}</span>
|
|
322
|
+
{runMs !== undefined && outside !== undefined && (
|
|
323
|
+
<span className="text-xs tabular-nums opacity-60">
|
|
324
|
+
{duration(outside)} before the run · {duration(runMs)} inside it
|
|
325
|
+
</span>
|
|
326
|
+
)}
|
|
327
|
+
</span>
|
|
328
|
+
</section>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
190
332
|
function TranscriptionDesk() {
|
|
191
333
|
const [mode, setMode] = useState<Mode>("streaming");
|
|
192
334
|
// Whether the browser cuts the recording up and sends the pieces at once. One
|
|
@@ -211,6 +353,8 @@ function TranscriptionDesk() {
|
|
|
211
353
|
// Which past run the reader is looking at, if any. Its own state rather than
|
|
212
354
|
// a route, because a workflow app is one page and a run id is not a place.
|
|
213
355
|
const [openId, setOpenId] = useState<string | undefined>(undefined);
|
|
356
|
+
// Click to transcript, measured here because only the browser sees both ends.
|
|
357
|
+
const total = useTotalLatency(pending);
|
|
214
358
|
|
|
215
359
|
// The list is read once and re-read on demand (see `useWorkflowRuns`), and
|
|
216
360
|
// this is the "on demand": the moment the run this page started settles, the
|
|
@@ -233,14 +377,34 @@ function TranscriptionDesk() {
|
|
|
233
377
|
</p>
|
|
234
378
|
</header>
|
|
235
379
|
|
|
236
|
-
|
|
380
|
+
{/* The clock goes with the mode: switching swaps `active` for another hook's
|
|
381
|
+
run, and a total measured over a different submission would be a number
|
|
382
|
+
for something the panel below is no longer showing. */}
|
|
383
|
+
<ModePicker
|
|
384
|
+
mode={mode}
|
|
385
|
+
onPick={(next) => {
|
|
386
|
+
setMode(next);
|
|
387
|
+
total.clear();
|
|
388
|
+
}}
|
|
389
|
+
disabled={pending}
|
|
390
|
+
/>
|
|
237
391
|
|
|
238
392
|
<UploadPicker parallel={parallel} onPick={setParallel} disabled={pending} />
|
|
239
393
|
|
|
240
394
|
{/* No mapping: the collected values already match the input schema. All three
|
|
241
395
|
workflows declare `recording` as an upload, so the same picker serves every
|
|
242
396
|
mode — how the bytes travel is not a question to ask a person. */}
|
|
243
|
-
|
|
397
|
+
{/* The clock starts HERE, which is as close to the press as a page can get:
|
|
398
|
+
`<Form>` calls this once the browser's own validation has passed and it has
|
|
399
|
+
read the controls, and an upload field contributes its `File` unread — so
|
|
400
|
+
what separates this line from the click is a microtask, not the file. */}
|
|
401
|
+
<Form
|
|
402
|
+
onSubmit={(values) => {
|
|
403
|
+
total.start();
|
|
404
|
+
return submit(values);
|
|
405
|
+
}}
|
|
406
|
+
error={error}
|
|
407
|
+
>
|
|
244
408
|
{/* The NAME, so the schema is fetched here rather than by this page. */}
|
|
245
409
|
<WorkflowFields workflow={WORKFLOWS[mode]} />
|
|
246
410
|
{/* Unguarded on purpose: it renders nothing until there are bytes in
|
|
@@ -249,7 +413,21 @@ function TranscriptionDesk() {
|
|
|
249
413
|
<SubmitButton pending={pending}>Transcribe</SubmitButton>
|
|
250
414
|
</Form>
|
|
251
415
|
|
|
252
|
-
|
|
416
|
+
<TotalLatency
|
|
417
|
+
elapsedMs={total.elapsedMs}
|
|
418
|
+
running={total.running}
|
|
419
|
+
runMs={run?.status === "completed" ? run.output.elapsedMs : undefined}
|
|
420
|
+
/>
|
|
421
|
+
|
|
422
|
+
{run && (
|
|
423
|
+
<RunPanel
|
|
424
|
+
run={run}
|
|
425
|
+
onClear={() => {
|
|
426
|
+
reset();
|
|
427
|
+
total.clear();
|
|
428
|
+
}}
|
|
429
|
+
/>
|
|
430
|
+
)}
|
|
253
431
|
|
|
254
432
|
<History
|
|
255
433
|
runs={history.runs}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"p-timeout": "^7.0.1",
|
|
45
45
|
"vite": "^8.2.1",
|
|
46
46
|
"zod": "^4.4.3",
|
|
47
|
-
"@alexkroman1/aai": "6.
|
|
48
|
-
"@alexkroman1/aai-ui": "6.
|
|
47
|
+
"@alexkroman1/aai": "6.7.0",
|
|
48
|
+
"@alexkroman1/aai-ui": "6.7.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"playwright": "^1.62.1",
|