@alexkroman1/aai-ui 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/_submission-timing.d.ts +40 -0
- package/dist/_submission.d.ts +56 -0
- package/dist/_theme.d.ts +17 -0
- package/dist/_theme.test-d.d.ts +14 -0
- package/dist/{_utils-Mi-SCFA1.js → _utils-BlZBJPdj.js} +4 -5
- package/dist/_utils.d.ts +4 -5
- package/dist/agent-state.d.ts +4 -2
- package/dist/append-dictation.d.ts +23 -0
- package/dist/audio/dictation.d.ts +36 -0
- package/dist/audio/index.d.ts +4 -2
- package/dist/audio/index.js +44 -1
- package/dist/{chat-view-W6KcdjJY.js → chat-view-BwKpF0MN.js} +27 -17
- package/dist/{client-config-CzvWmw4X.js → client-config-DcMCWmkO.js} +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/chat-view.js +1 -2
- package/dist/components/controls.js +2 -2
- package/dist/components/form-fields.d.ts +24 -5
- package/dist/components/form-types.d.ts +32 -0
- package/dist/components/form.d.ts +1 -1
- package/dist/components/message-list.js +1 -2
- package/dist/components/session-state-dot.d.ts +30 -18
- package/dist/components/sidebar-layout.js +1 -1
- package/dist/components/start-screen.js +2 -2
- package/dist/components/tool-call-block.js +1 -2
- package/dist/components/tool-config-context.d.ts +21 -4
- package/dist/components/workflow-fields.d.ts +36 -12
- package/dist/components/workflow-pending-note.d.ts +30 -12
- package/dist/context-BF0KsI9X.js +473 -0
- package/dist/context.js +1 -381
- package/dist/default-client/assets/audio-CtBvSS3L.js +1 -0
- package/dist/default-client/assets/{capture-processor-Cx1AdMHL.js → capture-processor-BZXisGjZ.js} +1 -1
- package/dist/default-client/assets/index-BkjCv4MX.css +2 -0
- package/dist/default-client/assets/index-DUQoaxdq.js +127 -0
- package/dist/default-client/assets/{playback-processor-CCwTknSu.js → playback-processor-BHk-iXEU.js} +1 -1
- package/dist/default-client/index.html +2 -2
- package/dist/default-client.d.ts +1 -3
- package/dist/define-client.d.ts +30 -28
- package/dist/define-client.js +84 -43
- package/dist/dictation-transcriber.d.ts +49 -0
- package/dist/{eyebrow-UfmSz9yy.js → eyebrow-Bp4y1rZ9.js} +1 -1
- package/dist/{hooks-DN4gHYsj.js → hooks-1CoQY_Fc.js} +2 -2
- package/dist/hooks.js +1 -2
- package/dist/index.d.ts +11 -8
- package/dist/index.js +849 -263
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +6 -6
- package/dist/{message-list-BsaBkBgo.js → message-list-BBHLK05X.js} +3 -3
- package/dist/route-fetch.d.ts +7 -1
- package/dist/session/index.js +1 -1
- package/dist/{session-3lgPYNNO.js → session-Rud8mmU-.js} +2 -2
- package/dist/{tool-call-block-uXtMOUWN.js → tool-call-block-C8s6AhjS.js} +4 -4
- package/dist/tool-config-context-Bd-7xrfP.js +28 -0
- package/dist/types.d.ts +47 -2
- package/dist/{url-chips-oyy_Eiqh.js → url-chips-C_SHU6fq.js} +2 -2
- package/dist/use-dictation.d.ts +145 -0
- package/dist/use-route.d.ts +49 -7
- package/dist/use-run-key.d.ts +6 -1
- package/dist/{use-user-transcript-uyHhzy4d.js → use-user-transcript-lOu_uMnf.js} +1 -1
- package/dist/use-workflow-form.d.ts +74 -172
- package/dist/{use-workflow-run-DdhWmbx-.js → use-workflow-run-BTEemod0.js} +2 -2
- package/dist/use-workflow-runs.d.ts +8 -2
- package/dist/use-workflow-stream.d.ts +9 -5
- package/dist/workflow-def-types.d.ts +18 -2
- package/dist/workflow-field-kind.d.ts +59 -7
- package/dist/workflow-submission.d.ts +265 -0
- package/package.json +2 -2
- package/dist/default-client/assets/audio-Ct7JpSNT.js +0 -1
- package/dist/default-client/assets/index-BlUYCTB5.js +0 -126
- package/dist/default-client/assets/index-_-x5JLLa.css +0 -2
- package/dist/tool-config-context-Es4YUzV2.js +0 -19
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a workflow SUBMISSION is, as a type: the object `useWorkflowSubmit` and
|
|
3
|
+
* `useWorkflowStream` both hand back. Moved out of `use-workflow-form.ts` at
|
|
4
|
+
* the source-length cap, on the seam `form-types.ts` is cut on — the two hooks
|
|
5
|
+
* and the form statechart all name these, and a type living in one of them
|
|
6
|
+
* would make the others import a hook to say what they return.
|
|
7
|
+
*/
|
|
8
|
+
import type { UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
9
|
+
import type { FormValues } from "./components/form-types.ts";
|
|
10
|
+
import type { WorkflowRun } from "./workflow-client.ts";
|
|
11
|
+
import type { SubmitInputOf } from "./workflow-def-types.ts";
|
|
12
|
+
/**
|
|
13
|
+
* What {@link WorkflowSubmission.upload} reports while the bytes are going.
|
|
14
|
+
*
|
|
15
|
+
* The SDK's per-request {@link UploadProgress} plus WHICH file it describes,
|
|
16
|
+
* because a form is allowed more than one and a bar over "the upload" would
|
|
17
|
+
* restart at zero partway through with nothing to say why.
|
|
18
|
+
*
|
|
19
|
+
* @sealed
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export type UploadStatus = UploadProgress & {
|
|
23
|
+
/** The file being sent, by the name the picker gave it. */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Which file of the submission this is, counting from 1. */
|
|
26
|
+
index: number;
|
|
27
|
+
/** How many files this submission sends in total. */
|
|
28
|
+
count: number;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the person has parked this upload.
|
|
31
|
+
*
|
|
32
|
+
* A paused upload is not a stopped one: the windows already stored stay stored,
|
|
33
|
+
* `loaded` holds where it got to, and resuming sends what is missing rather than
|
|
34
|
+
* the file. So a bar rendering this reads "Paused at 62%", never "62% and
|
|
35
|
+
* frozen" — which is what a page could otherwise only guess from a number that
|
|
36
|
+
* stopped moving, the same ambiguity `complete` exists to remove on the run side.
|
|
37
|
+
*/
|
|
38
|
+
paused: boolean;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The two marks a submission's wall clock is read from — press to settle,
|
|
42
|
+
* across the upload, the run, and the gap between them, which no server-side
|
|
43
|
+
* number can give: `output.elapsedMs` is the RUN's own clock and begins after
|
|
44
|
+
* the upload. Only the browser holds both ends.
|
|
45
|
+
*
|
|
46
|
+
* `performance.now()` marks rather than elapsed durations, and that is the
|
|
47
|
+
* load-bearing part: an elapsed number is stale the instant it is computed, so
|
|
48
|
+
* a page holding one has to re-render to keep it true. A mark lets the one
|
|
49
|
+
* section that prints a running clock re-render on its own interval and read
|
|
50
|
+
* `performance.now() - submittedAt` at render. Monotonic, so a clock
|
|
51
|
+
* correction cannot make a run look instant, or negative.
|
|
52
|
+
*
|
|
53
|
+
* @sealed
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export type WorkflowSubmissionTiming = {
|
|
57
|
+
/** When `submit()` was called on this mount, or `undefined` before the first. */
|
|
58
|
+
submittedAt?: number | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* When the submission settled — the run reached a terminal status, or the
|
|
61
|
+
* submit itself failed — measured at that moment rather than at the next
|
|
62
|
+
* render. `undefined` while it is in flight, and for a run this page did
|
|
63
|
+
* not start (there was no press to time from).
|
|
64
|
+
*/
|
|
65
|
+
settledAt?: number | undefined;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Who can find the run again — the lifetime of the key it was recorded under.
|
|
69
|
+
*
|
|
70
|
+
* - `"tab"` — `useWorkflowSubmit`'s own `sessionStorage` key: a reload or a
|
|
71
|
+
* same-tab navigation finds the run; a new tab does not.
|
|
72
|
+
* - `"browser"` — a `localStorage` key (`key: { storage: "local" }`, or
|
|
73
|
+
* `useRunKey({ storage: "local" })`), or a key the page supplied itself (an
|
|
74
|
+
* account's id): any tab on this browser finds it, so closing the tab loses
|
|
75
|
+
* nothing.
|
|
76
|
+
* - `"none"` — nothing will find it: `recover: false`, or `useWorkflowStream`,
|
|
77
|
+
* whose run is reading the bytes THIS page is sending and is ended, not
|
|
78
|
+
* orphaned, by a reload.
|
|
79
|
+
*
|
|
80
|
+
* `<WorkflowPendingNote>` reads it to say the right sentence; the raw fact is
|
|
81
|
+
* published so a page with its own note can too.
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
export type WorkflowSubmissionScope = "tab" | "browser" | "none";
|
|
86
|
+
/**
|
|
87
|
+
* What {@link useWorkflowSubmit} returns.
|
|
88
|
+
*
|
|
89
|
+
* @see {@link WorkflowStreamSubmission} — an ALIAS of this type, returned by
|
|
90
|
+
* `useWorkflowStream`, which is a drop-in sibling. Exactly two fields MEAN
|
|
91
|
+
* something different there, and both differences follow from WHEN the run is
|
|
92
|
+
* created: there, `submit()` resolves when the UPLOAD finishes rather than when
|
|
93
|
+
* the run is accepted, and `run` is non-`undefined` from before the bytes are
|
|
94
|
+
* in, so a page can render `<WorkflowProgress>` beside the upload bar instead
|
|
95
|
+
* of after it. Here the run does not exist until the last byte lands.
|
|
96
|
+
*
|
|
97
|
+
* @sealed
|
|
98
|
+
*/
|
|
99
|
+
export type WorkflowSubmission<R = unknown, I = unknown> = {
|
|
100
|
+
/**
|
|
101
|
+
* Start a run with this input. Resolves once the run EXISTS — progress
|
|
102
|
+
* arrives through `run` — so a `<Form>`'s handler can await it to know the
|
|
103
|
+
* submission was accepted.
|
|
104
|
+
*/
|
|
105
|
+
submit: (input: I) => Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Start a run from a `<Form>`'s values, which are UNVALIDATED.
|
|
108
|
+
*
|
|
109
|
+
* The same function as {@link WorkflowSubmission.submit}, with the type the
|
|
110
|
+
* form path can honestly offer. `FormValues` is `Record<string, unknown>`
|
|
111
|
+
* scraped off the DOM at submit time — a `<TextField name="limit">`
|
|
112
|
+
* contributes a string whatever the schema says — so the shape is not known
|
|
113
|
+
* here and the SERVER is what checks it against the workflow's schema.
|
|
114
|
+
*
|
|
115
|
+
* Two doors rather than one loose one: `submit` takes the workflow's own
|
|
116
|
+
* input type, so a hand-built object is checked at compile time and
|
|
117
|
+
* `submit({ ur1: 42 })` is an error; widening it to accept `FormValues` would
|
|
118
|
+
* have made every object satisfy it and given the typing back. Reaching for
|
|
119
|
+
* this one is the author saying "these came from a form", which is a fact
|
|
120
|
+
* about the values and not a cast.
|
|
121
|
+
*/
|
|
122
|
+
submitForm: (values: FormValues) => Promise<void>;
|
|
123
|
+
/** Clear the run and any error, putting the form back to its initial state. */
|
|
124
|
+
reset: () => void;
|
|
125
|
+
/**
|
|
126
|
+
* End the current run's `sleep()` early — "file it now" — resolving how many
|
|
127
|
+
* pending sleeps were interrupted.
|
|
128
|
+
*
|
|
129
|
+
* Bound to the run this submission is following, which is the point: it is
|
|
130
|
+
* the only reason a page holding one of these hooks needed an `api` of its
|
|
131
|
+
* own. `0` is an answer rather than a failure (the run had already moved past
|
|
132
|
+
* its wait, or there is no run yet), so nothing here has to be guarded.
|
|
133
|
+
*/
|
|
134
|
+
wake: () => Promise<number>;
|
|
135
|
+
/**
|
|
136
|
+
* Stop the current run, resolving whether this call is what ended it.
|
|
137
|
+
*
|
|
138
|
+
* `false` for a run that had already finished, and for no run at all — the
|
|
139
|
+
* SDK's contract, because two tabs pressing Stop is ordinary. Distinct from
|
|
140
|
+
* `reset()`, which puts the FORM back and leaves the run running.
|
|
141
|
+
*/
|
|
142
|
+
cancel: () => Promise<boolean>;
|
|
143
|
+
/** The run, once started, followed to completion. */
|
|
144
|
+
run: WorkflowRun<R> | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* True from `submit()` on this mount until `reset()` — did THIS page start
|
|
147
|
+
* the run it is showing?
|
|
148
|
+
*
|
|
149
|
+
* A page needs it to say the right sentence and cannot derive it: a run
|
|
150
|
+
* ADOPTED by the mount-time lookup after a reload looks exactly like one this
|
|
151
|
+
* page started. Six templates kept a `useState(false)` next to this hook, set
|
|
152
|
+
* it in their `onSubmit` and mirrored it in their `onClear` — shadow state
|
|
153
|
+
* for a fact only this hook can know, since it is the thing that decides
|
|
154
|
+
* between `submit()` and the recovery lookup. One of the six grew a fourth
|
|
155
|
+
* branch and had to move the whole note into its own module with its own
|
|
156
|
+
* spec, which is what a seam missing one layer down looks like.
|
|
157
|
+
*
|
|
158
|
+
* The RAW fact rather than a derived "recovered", deliberately: with `run`
|
|
159
|
+
* these are three states, not two, and the third is the one a page most needs
|
|
160
|
+
* to explain. `startedHere` is "you pressed the button"; `!startedHere &&
|
|
161
|
+
* !run` is the mount-time lookup still going; `!startedHere && run` is a run
|
|
162
|
+
* this browser started earlier, now in front of somebody who did not press
|
|
163
|
+
* anything. A boolean meaning only the last of those cannot express the
|
|
164
|
+
* middle one.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```ts
|
|
168
|
+
* declare const submission: import("@alexkroman1/aai-ui").WorkflowSubmission;
|
|
169
|
+
* const note = submission.startedHere
|
|
170
|
+
* ? "You can close this tab or reload it — this page will find the run again."
|
|
171
|
+
* : submission.run === undefined
|
|
172
|
+
* ? "Looking for a run this tab started earlier…"
|
|
173
|
+
* : "Still working on the run this tab started earlier. Reloading is safe.";
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
startedHere: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* True from `submit()` until the run reaches a terminal status.
|
|
179
|
+
*
|
|
180
|
+
* The WORK, not the request: a run outlives its `POST`, and a submit button
|
|
181
|
+
* that re-enabled on the response would invite a second submission of work
|
|
182
|
+
* already in flight.
|
|
183
|
+
*/
|
|
184
|
+
pending: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* How far the submission's files have got, while any are still going.
|
|
187
|
+
*
|
|
188
|
+
* Undefined before the first byte and again from the moment the last one
|
|
189
|
+
* lands, so a page can render `{upload && <UploadProgressBar upload={upload} />}`
|
|
190
|
+
* and the bar exists exactly for as long as there is an upload to describe. A
|
|
191
|
+
* form with no files never sets it at all.
|
|
192
|
+
*
|
|
193
|
+
* The wait it covers is the one `run` cannot: a run does not EXIST until its
|
|
194
|
+
* input is stored, so `pending` is true and there is nothing to poll — which
|
|
195
|
+
* for a 200 MB recording is minutes of a page that looks stuck.
|
|
196
|
+
*/
|
|
197
|
+
upload: UploadStatus | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* Park the upload where it is, stopping the bytes in flight.
|
|
200
|
+
*
|
|
201
|
+
* The windows already stored stay stored, so `resumeUpload()` sends what is
|
|
202
|
+
* missing rather than the file — which is the difference between a pause a
|
|
203
|
+
* person will actually use on a 200 MB recording and a cancel dressed up as one.
|
|
204
|
+
*
|
|
205
|
+
* `submit()`'s promise stays unresolved across a pause, because the submission
|
|
206
|
+
* genuinely has not finished: the run does not exist until the last byte lands,
|
|
207
|
+
* so resolving here would tell a `<Form>` the work was accepted when nothing has
|
|
208
|
+
* been started. A no-op when there is no upload in flight.
|
|
209
|
+
*/
|
|
210
|
+
pauseUpload: () => void;
|
|
211
|
+
/** Continue a paused upload, sending only the windows the store does not have. */
|
|
212
|
+
resumeUpload: () => void;
|
|
213
|
+
/** The submit's own failure (a rejected input), or the watch's. */
|
|
214
|
+
error: string | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Press-to-settle marks for the submission THIS mount made — see
|
|
217
|
+
* {@link WorkflowSubmissionTiming}. Both `undefined` before the first
|
|
218
|
+
* `submit()` and again after `reset()`; a run adopted by the mount-time
|
|
219
|
+
* lookup has no press to time from and sets neither.
|
|
220
|
+
*/
|
|
221
|
+
timing: WorkflowSubmissionTiming;
|
|
222
|
+
/**
|
|
223
|
+
* Who can find the run again after this page is gone — see
|
|
224
|
+
* {@link WorkflowSubmissionScope}. Derived from the hook's options (`key`,
|
|
225
|
+
* `recover`), so a page never restates it; `<WorkflowPendingNote>` reads it.
|
|
226
|
+
*/
|
|
227
|
+
scope: WorkflowSubmissionScope;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* The submission `useWorkflowSubmit<D>` / `useWorkflowStream<D>` hands back for
|
|
231
|
+
* the workflow def `D`, named so a component can take one as a prop.
|
|
232
|
+
*
|
|
233
|
+
* The hook's own return is inferred, so a page that renders everything in one
|
|
234
|
+
* function never needs this. A page that splits does, and there was no way to
|
|
235
|
+
* write it from the outside without three names — `WorkflowSubmission`,
|
|
236
|
+
* `WorkflowOutputOf` and `SubmitInputOf` — composed by hand, which is what a
|
|
237
|
+
* `ReturnType<typeof useWorkflowSubmit>` cannot say because the hook is
|
|
238
|
+
* generic. `podcast-digest-workflow`'s `client.tsx` composed exactly that.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```tsx
|
|
242
|
+
* // file: agent.ts
|
|
243
|
+
* import { agent, workflow } from "@alexkroman1/aai";
|
|
244
|
+
* import { z } from "zod";
|
|
245
|
+
*
|
|
246
|
+
* export const dailyDigest = workflow({
|
|
247
|
+
* input: z.object({ hour: z.number() }),
|
|
248
|
+
* output: z.object({ posted: z.string() }),
|
|
249
|
+
* run: async ({ hour }) => ({ posted: `daily at ${hour}:00` }),
|
|
250
|
+
* });
|
|
251
|
+
*
|
|
252
|
+
* export default agent({ name: "Digest", workflows: { dailyDigest } });
|
|
253
|
+
*
|
|
254
|
+
* // file: client.tsx
|
|
255
|
+
* import type { WorkflowSubmissionOf } from "@alexkroman1/aai-ui";
|
|
256
|
+
* import type { dailyDigest } from "./agent.ts";
|
|
257
|
+
*
|
|
258
|
+
* function ScheduleControls({ submission }: { submission: WorkflowSubmissionOf<typeof dailyDigest> }) {
|
|
259
|
+
* return submission.run?.status === "completed" ? <p>{submission.run.output.posted}</p> : null;
|
|
260
|
+
* }
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* @public
|
|
264
|
+
*/
|
|
265
|
+
export type WorkflowSubmissionOf<D> = WorkflowSubmission<WorkflowOutputOf<D>, SubmitInputOf<D>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"use-stick-to-bottom": "^1.1.6",
|
|
47
47
|
"use-sync-external-store": "^1.7.0",
|
|
48
48
|
"xstate": "^5.33.2",
|
|
49
|
-
"@alexkroman1/aai": "0.
|
|
49
|
+
"@alexkroman1/aai": "0.18.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e,i as t,t as n}from"./index-BlUYCTB5.js";var r={echoCancellation:!0,noiseSuppression:!1,autoGainControl:!1,voiceIsolation:!1};function i(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function a(e){let t=Error(`Audio ${e} worklet crashed`);return console.error(`[aai-ui]`,t.message),t}function o(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function s(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){let{promise:e,resolve:t}=Promise.withResolvers(),n=setTimeout(t,250);return i=()=>{clearTimeout(n),t()},r.port.postMessage({event:`stop`}),e}}}async function c(e){let{sampleRate:t,captureWorkletSrc:n,maxSeconds:a}=e,c=new AudioContext({sampleRate:t,latencyHint:`interactive`}),l=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...r}}),u;try{[u]=await Promise.all([l,c.resume(),c.audioWorklet.addModule(n)]),i(c.sampleRate,t,`capture`)}catch(e){throw o(l),await c.close().catch(()=>{}),e}let d=Math.round(t*a)*2,f=[],p=0,m=!1,h=null,g=c.createMediaStreamSource(u),_=s(c,e=>{if(h){h.onChunk(e);return}for(f.push(e),p+=e.byteLength;p>d&&f.length>1;)p-=f.shift()?.byteLength??0},()=>{h?h.onSilent?.():m=!0});g.connect(_.node),_.start();let v=()=>{let e=f;return f=[],p=0,e},y=!1,b=async()=>{y||(y=!0,g.disconnect(),_.node.disconnect(),await c.close().catch(e=>{console.warn(`AudioContext close failed:`,e)}))};return{sampleRate:t,stream:u,ctx:c,handOff(e){h=e;let t=v();return t.length>0&&e.onBuffered(t),m&&e.onSilent?.(),{mic:g,capture:_}},async detach(){return await _.stop(),await b(),{chunks:v(),sampleRate:t}},async close(){y||await _.stop();for(let e of u.getTracks())e.stop();await b()}}}function l(e){let t=new Int16Array(e.length);for(let n=0;n<e.length;n++){let r=Math.max(-1,Math.min(1,e[n]??0));t[n]=r<0?r*32768:r*32767}return t.buffer}async function u(e,t,r){let i=e.reduce((e,t)=>e+t.byteLength/2,0);if(i===0)return[];if(t===r)return e;try{let a=Math.round(i*r/t),o=new OfflineAudioContext(1,a,r),s=o.createBuffer(1,i,t),c=s.getChannelData(0),u=0;for(let t of e)for(let e of new Int16Array(t))c[u++]=e/32768;let d=o.createBufferSource();d.buffer=s,d.connect(o.destination),d.start();let f=(await o.startRendering()).getChannelData(0),p=Math.max(1,Math.round(r*n)),m=[];for(let e=0;e<f.length;e+=p)m.push(l(f.subarray(e,e+p)));return m}catch(e){return console.warn(`[aai-ui] pre-connect audio dropped: resample failed`,e),[]}}async function d(e,t,n){let r=e.createMediaStreamSource(t),i=s(e,n.onMicData,n.onMicSilent);if(r.connect(i.node),n.buffered){let{chunks:t,sampleRate:r}=n.buffered,i=await u(t,r,e.sampleRate);i.length>0&&n.onBuffered(i)}return i.start(),{mic:r,capture:i}}async function f(n){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:f,onError:p,onPlaybackStats:m,onPlaybackProgress:h,onMicSilent:g,preConnect:_}=n,v=n.onPreConnectAudio??(e=>{for(let t of e)f(t)}),y=_?.sampleRate===s?_:null,b=_&&!y?await _.detach():null,x=new AudioContext({sampleRate:c,latencyHint:`playback`}),S=!y&&s===c,C=y?.ctx??(S?x:new AudioContext({sampleRate:s,latencyHint:`interactive`}));async function w(){let e=S?[x]:[x,C];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let T=_?Promise.resolve(_.stream):navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...r}}),E;try{[E]=await Promise.all([T,x.resume(),C.resume(),y?void 0:C.audioWorklet.addModule(l),x.audioWorklet.addModule(u)]),i(C.sampleRate,s,`capture`),i(x.sampleRate,c,`playback`)}catch(e){throw o(T),await w(),e}let{mic:D,capture:O}=y?y.handOff({onBuffered:v,onChunk:f,onSilent:g}):await d(C,E,{onMicData:f,onMicSilent:g,buffered:b,onBuffered:v});O.node.onprocessorerror=()=>p?.(a(`capture`));let k=null,A=0,j=null,M=new AbortController;function N(){let e=j;j=null,e?.settle()}function P(e){e.stats&&e.stats.concealedSamples>0&&m?.(e.stats),e.reason!==`interrupt`&&(j===null||e.turn===j.turn)&&N()}function F(){if(k)return k;let e=new AudioWorkletNode(x,`playback-processor`);return e.connect(x.destination),e.port.onmessage=e=>{e.data.event===`stop`?P(e.data):e.data.event===`progress`&&h?.(e.data.bufferedMs)},e.onprocessorerror=()=>{let e=a(`playback`);N(),p?.(e)},k=e,e}let I={enqueue(e){M.signal.aborted||e.byteLength!==0&&F().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){if(!k)return Promise.resolve();let n=++A;if(k.port.postMessage({event:`done`,turn:n}),x.state!==`running`)return N(),Promise.resolve();let{promise:r,resolve:i}=Promise.withResolvers();N();let a=()=>{clearInterval(o),clearTimeout(s),j?.settle===a&&(j=null),i()},o=setInterval(()=>{x.state!==`running`&&a()},e),s=setTimeout(a,t);return j={turn:n,settle:a},r},flush(){k&&(N(),k.port.postMessage({event:`interrupt`}))},async close(){if(!M.signal.aborted){M.abort(),await O.stop(),D.disconnect(),O.node.disconnect(),k&&k.disconnect();for(let e of E.getTracks())e.stop();await w()}},async[Symbol.asyncDispose](){await I.close()}};return I}export{f as createVoiceIO,c as openPreConnectCapture};
|