@alexkroman1/aai-ui 6.4.0 → 6.5.1

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.
@@ -6,7 +6,7 @@
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-aXDcAQ6M.js"></script>
9
+ <script type="module" crossorigin src="./assets/index-CepvxbNU.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="./assets/client-audio-constants-Ck0IJO4c.js">
11
11
  <link rel="stylesheet" crossorigin href="./assets/index-BZlePk3y.css">
12
12
  </head>
@@ -1,8 +1,8 @@
1
- import { r as fetchClientConfig, t as createSessionCore } from "./session-core-ClKdVgRU.js";
1
+ import { r as fetchClientConfig, t as createSessionCore } from "./session-core-CAfYmUbg.js";
2
2
  import { SessionProvider, ThemeProvider } from "./context.js";
3
- import { t as ChatView } from "./chat-view-CK61bWWx.js";
4
- import { t as pageBaseUrl } from "./_utils-CsqbIVGI.js";
5
- import { n as ToolConfigContext } from "./tool-call-block-CrLN7xlI.js";
3
+ import { t as ChatView } from "./chat-view-ByQFf94G.js";
4
+ import { n as setPageTitle, t as pageBaseUrl } from "./_utils-B6498_bm.js";
5
+ import { n as ToolConfigContext } from "./tool-call-block-D6pTEPrT.js";
6
6
  import { SidebarLayout } from "./components/sidebar-layout.js";
7
7
  import { StartScreen } from "./components/start-screen.js";
8
8
  import { jsx } from "react/jsx-runtime";
@@ -165,7 +165,7 @@ function client(config) {
165
165
  resumeSessionId: config.resumeSessionId,
166
166
  WebSocket: config.WebSocket
167
167
  });
168
- if (config.name && typeof document !== "undefined") document.title = config.name;
168
+ setPageTitle(config.name);
169
169
  const rootNode = config.component ? createElement(config.component) : createElement(DefaultRoot, {
170
170
  platformUrl,
171
171
  name: config.name,
package/dist/hooks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useSessionSelector } from "./context.js";
2
- import { r as tryParseJSON } from "./_utils-CsqbIVGI.js";
2
+ import { i as tryParseJSON } from "./_utils-B6498_bm.js";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region hooks.ts
5
5
  /** Index of the first item whose sequence number is above the watermark (tail scan from the end). */
@@ -44,8 +44,9 @@ function processToolCallTail(toolCalls, cursor, isSettled, onNew) {
44
44
  * and invokes `fire` once per tool call reaching `status`.
45
45
  */
46
46
  function useToolCallEffect(status, args, fire) {
47
- const filterName = typeof args[0] === "string" ? args[0] : null;
48
- const callback = typeof args[0] === "string" ? args[1] : args[0];
47
+ const first = args[0];
48
+ const filterName = typeof first === "string" ? first : null;
49
+ const callback = filterName === null ? first : args[1];
49
50
  const toolCalls = useSessionSelector((s) => s.toolCalls);
50
51
  const cursorRef = useRef({
51
52
  seq: 0,
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { i as loadClientConfig, n as buildAgentUrl, r as fetchClientConfig, t as createSessionCore } from "./session-core-ClKdVgRU.js";
2
- import { n as Markdown, r as AutoScroll, t as MessageList } from "./message-list-BwA3rdPi.js";
1
+ import { i as loadClientConfig, n as buildAgentUrl, r as fetchClientConfig, t as createSessionCore } from "./session-core-CAfYmUbg.js";
2
+ import { n as Markdown, r as AutoScroll, t as MessageList } from "./message-list-CpPV7dGx.js";
3
3
  import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
4
4
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
5
5
  import { Button } from "./components/button.js";
6
- import { t as ChatView } from "./chat-view-CK61bWWx.js";
7
- import { t as pageBaseUrl } from "./_utils-CsqbIVGI.js";
8
- import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-Cy_YVfsa.js";
9
- import { n as ToolConfigContext, r as ToolCallRow } from "./tool-call-block-CrLN7xlI.js";
6
+ import { t as ChatView } from "./chat-view-ByQFf94G.js";
7
+ import { n as setPageTitle, t as pageBaseUrl } from "./_utils-B6498_bm.js";
8
+ import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-CjG91QJ4.js";
9
+ import { n as ToolConfigContext, r as ToolCallRow } from "./tool-call-block-D6pTEPrT.js";
10
10
  import { SidebarLayout } from "./components/sidebar-layout.js";
11
11
  import { StartScreen } from "./components/start-screen.js";
12
12
  import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
@@ -526,6 +526,7 @@ function UploadProgressBar({ upload, className }) {
526
526
  if (!upload) return null;
527
527
  const { name, index, count, loaded, total, fraction } = upload;
528
528
  const percent = fraction === void 0 ? void 0 : Math.round(fraction * 100);
529
+ const faint = inkTint(theme.text, theme.surface, 65);
529
530
  return /* @__PURE__ */ jsxs("div", {
530
531
  className: clsx(className ?? "flex flex-col gap-1.5"),
531
532
  children: [/* @__PURE__ */ jsxs("div", {
@@ -533,11 +534,11 @@ function UploadProgressBar({ upload, className }) {
533
534
  children: [/* @__PURE__ */ jsx("span", {
534
535
  id: labelId,
535
536
  className: "truncate",
536
- style: { color: inkTint(theme.text, theme.surface, 65) },
537
+ style: { color: faint },
537
538
  children: count > 1 ? `Uploading ${name} (${index} of ${count})` : `Uploading ${name}`
538
539
  }), /* @__PURE__ */ jsx("span", {
539
540
  className: "shrink-0 tabular-nums",
540
- style: { color: inkTint(theme.text, theme.surface, 65) },
541
+ style: { color: faint },
541
542
  children: total === void 0 ? formatBytes(loaded) : `${formatBytes(loaded)} of ${formatBytes(total)}`
542
543
  })]
543
544
  }), /* @__PURE__ */ jsx("div", {
@@ -649,6 +650,25 @@ function filesOf(value) {
649
650
  const files = value.filter((one) => one instanceof File);
650
651
  return files.length > 0 && files.length === value.length ? files : [];
651
652
  }
653
+ /**
654
+ * The input properties still carrying a `File` — i.e. the ones that CANNOT survive
655
+ * being sent.
656
+ *
657
+ * A run input is JSON, and `JSON.stringify(new File(…))` is `{}` — no `toJSON`, no
658
+ * own enumerable properties. So a File left in a payload does not fail to send: it
659
+ * arrives as an empty object, and the workflow rejects it against whatever its own
660
+ * schema says the property should be. Measured in production as
661
+ * `Invalid input for workflow "transcribe": recording: Invalid input` — a message
662
+ * about a type, on a form where the user had picked a perfectly good file.
663
+ *
664
+ * Exported beside {@link filesOf} because it is the same question asked at the
665
+ * other end: that one decides which fields to UPLOAD, this one checks that none
666
+ * were missed. Both hooks are the callers.
667
+ */
668
+ function fileFields(input) {
669
+ if (!isRecord(input)) return [];
670
+ return Object.entries(input).filter(([, value]) => filesOf(value).length > 0).map(([key]) => key);
671
+ }
652
672
  //#endregion
653
673
  //#region _repeat-until.ts
654
674
  /**
@@ -1068,7 +1088,7 @@ function useWorkflows(opts = {}) {
1068
1088
  * so this is invisible to every form that has none — including one whose values
1069
1089
  * are not an object at all, which `submit` accepts.
1070
1090
  */
1071
- async function uploadFiles(api, input, report) {
1091
+ async function uploadFiles(api, input, report, parallel) {
1072
1092
  if (!isRecord(input)) return input;
1073
1093
  const entries = Object.entries(input);
1074
1094
  const count = entries.reduce((total, [, value]) => total + filesOf(value).length, 0);
@@ -1080,10 +1100,13 @@ async function uploadFiles(api, input, report) {
1080
1100
  index,
1081
1101
  count
1082
1102
  };
1083
- return (await api.upload(file, { onProgress: (progress) => report({
1084
- ...position,
1085
- ...progress
1086
- }) })).id;
1103
+ return (await api.upload(file, {
1104
+ onProgress: (progress) => report({
1105
+ ...position,
1106
+ ...progress
1107
+ }),
1108
+ ...omitUndefined({ parallel })
1109
+ })).id;
1087
1110
  };
1088
1111
  const out = {};
1089
1112
  for (const [name, value] of entries) {
@@ -1128,16 +1151,16 @@ async function uploadFiles(api, input, report) {
1128
1151
  * @public
1129
1152
  */
1130
1153
  function useWorkflowSubmit(workflow, opts = {}) {
1131
- const { api, key, wait, intervalMs } = opts;
1154
+ const { api, key, wait, intervalMs, parallel } = opts;
1132
1155
  const [runId, setRunId] = useState(void 0);
1133
1156
  const [starting, setStarting] = useState(false);
1134
1157
  const [startError, setStartError] = useState(void 0);
1135
1158
  const [upload, setUpload] = useState(void 0);
1136
1159
  const getClient = useWorkflowApiRef(api);
1137
- const tracked = useWorkflowRun(runId, {
1138
- ...api && { api },
1139
- ...omitUndefined({ intervalMs })
1140
- });
1160
+ const tracked = useWorkflowRun(runId, omitUndefined({
1161
+ api,
1162
+ intervalMs
1163
+ }));
1141
1164
  return {
1142
1165
  submit: useCallback(async (input) => {
1143
1166
  const client = getClient();
@@ -1146,7 +1169,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
1146
1169
  setRunId(void 0);
1147
1170
  try {
1148
1171
  const options = omitUndefined({ key });
1149
- const started = await uploadFiles(client, input, setUpload);
1172
+ const started = await uploadFiles(client, input, setUpload, parallel);
1150
1173
  setRunId(wait === void 0 ? await client.start(workflow, started, options) : (await client.startAndWait(workflow, started, {
1151
1174
  ...options,
1152
1175
  wait
@@ -1161,6 +1184,7 @@ function useWorkflowSubmit(workflow, opts = {}) {
1161
1184
  workflow,
1162
1185
  key,
1163
1186
  wait,
1187
+ parallel,
1164
1188
  getClient
1165
1189
  ]),
1166
1190
  reset: useCallback(() => {
@@ -1243,7 +1267,8 @@ function SchemaField({ name, property, required, upload = false }) {
1243
1267
  ...hint,
1244
1268
  ...defaults
1245
1269
  });
1246
- switch (typeOf(property)) {
1270
+ const type = typeOf(property);
1271
+ switch (type) {
1247
1272
  case "boolean": return /* @__PURE__ */ jsx(CheckboxField, {
1248
1273
  name,
1249
1274
  label,
@@ -1255,7 +1280,7 @@ function SchemaField({ name, property, required, upload = false }) {
1255
1280
  name,
1256
1281
  label,
1257
1282
  required,
1258
- step: typeOf(property) === "integer" ? 1 : "any",
1283
+ step: type === "integer" ? 1 : "any",
1259
1284
  ...hint,
1260
1285
  ...defaults
1261
1286
  });
@@ -1542,7 +1567,7 @@ function useWorkflowProgress(runId, opts = {}) {
1542
1567
  * @public
1543
1568
  */
1544
1569
  function WorkflowProgress({ runId, api, className, placeholder }) {
1545
- const { progress, streaming, supported } = useWorkflowProgress(runId, api ? { api } : {});
1570
+ const { progress, streaming, supported } = useWorkflowProgress(runId, omitUndefined({ api }));
1546
1571
  if (!supported || progress.length === 0) return placeholder ?? null;
1547
1572
  return /* @__PURE__ */ jsxs("pre", {
1548
1573
  className: clsx(className ?? "whitespace-pre-wrap border-l pl-4 text-xs opacity-70"),
@@ -1606,7 +1631,7 @@ function WorkflowProgress({ runId, api, className, placeholder }) {
1606
1631
  */
1607
1632
  function page(config) {
1608
1633
  const container = resolveContainer(config.target);
1609
- if (config.name && typeof document !== "undefined") document.title = config.name;
1634
+ setPageTitle(config.name);
1610
1635
  return mountRoot(container, createElement(ThemeProvider, { value: config.theme }, createElement(config.component)));
1611
1636
  }
1612
1637
  //#endregion
@@ -1859,16 +1884,16 @@ function useWorkflowRuns(workflow, opts = {}) {
1859
1884
  * @public
1860
1885
  */
1861
1886
  function useWorkflowStream(workflow, opts = {}) {
1862
- const { api, key, intervalMs } = opts;
1887
+ const { api, key, intervalMs, parallel } = opts;
1863
1888
  const [runId, setRunId] = useState(void 0);
1864
1889
  const [starting, setStarting] = useState(false);
1865
1890
  const [startError, setStartError] = useState(void 0);
1866
1891
  const [upload, setUpload] = useState(void 0);
1867
1892
  const getClient = useWorkflowApiRef(api);
1868
- const tracked = useWorkflowRun(runId, {
1869
- ...api && { api },
1870
- ...omitUndefined({ intervalMs })
1871
- });
1893
+ const tracked = useWorkflowRun(runId, omitUndefined({
1894
+ api,
1895
+ intervalMs
1896
+ }));
1872
1897
  return {
1873
1898
  submit: useCallback(async (input) => {
1874
1899
  const client = getClient();
@@ -1884,6 +1909,7 @@ function useWorkflowStream(workflow, opts = {}) {
1884
1909
  ...input,
1885
1910
  [field]: id
1886
1911
  } : input;
1912
+ assertSendable(workflow, payload, field);
1887
1913
  started = await client.start(workflow, payload, omitUndefined({ key }));
1888
1914
  setRunId(started);
1889
1915
  if (!chosen) return;
@@ -1894,7 +1920,8 @@ function useWorkflowStream(workflow, opts = {}) {
1894
1920
  name: chosen.name,
1895
1921
  index: 1,
1896
1922
  count: 1
1897
- })
1923
+ }),
1924
+ ...omitUndefined({ parallel })
1898
1925
  });
1899
1926
  await client.wake(started).catch(() => void 0);
1900
1927
  } catch (err) {
@@ -1907,6 +1934,7 @@ function useWorkflowStream(workflow, opts = {}) {
1907
1934
  }, [
1908
1935
  workflow,
1909
1936
  key,
1937
+ parallel,
1910
1938
  getClient
1911
1939
  ]),
1912
1940
  reset: useCallback(() => {
@@ -1920,6 +1948,33 @@ function useWorkflowStream(workflow, opts = {}) {
1920
1948
  error: startError ?? tracked.error
1921
1949
  };
1922
1950
  }
1951
+ /**
1952
+ * Refuse a payload carrying a `File`, before a run is started over it.
1953
+ *
1954
+ * A File cannot be SENT: a run input is JSON and `JSON.stringify(new File(…))` is
1955
+ * `{}` (see `fileFields`). So without this the run starts, rejects its own input,
1956
+ * and reports a type error about a property the user filled in correctly — which is
1957
+ * what production did, five times: `Invalid input for workflow "transcribe":
1958
+ * recording: Invalid input`, from a page whose file picker was working.
1959
+ *
1960
+ * The reachable cause is a workflow whose listing declares no `uploads` for the
1961
+ * property the form put a file in — one built before the declaration existed, or a
1962
+ * page pointing at the wrong workflow — so the message names BOTH halves: the
1963
+ * property that carries a file, and the one (if any) the workflow says is its
1964
+ * upload. A message naming only one of them leaves the reader guessing which end
1965
+ * to change.
1966
+ *
1967
+ * A throw rather than a silent repair: this hook cannot know whether the right fix
1968
+ * is to declare the property or to stop sending the file, and guessing either way
1969
+ * would start a run the caller did not mean.
1970
+ */
1971
+ function assertSendable(workflow, payload, field) {
1972
+ const unsendable = fileFields(payload);
1973
+ if (unsendable.length === 0) return;
1974
+ const carries = unsendable.length === 1 ? "carries a file" : "carry files";
1975
+ const declares = field === void 0 ? "" : ` (it declares "${field}")`;
1976
+ 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.`);
1977
+ }
1923
1978
  /** A fresh upload id: a capability, so it is random rather than derived. */
1924
1979
  function randomUploadId() {
1925
1980
  return crypto.randomUUID().replaceAll("-", "");
@@ -1,6 +1,6 @@
1
1
  import { useSessionSelector, useTheme } from "./context.js";
2
2
  import { i as primaryTint, r as inkTint } from "./_colors-CcAi2FOU.js";
3
- import { t as ToolCallBlock } from "./tool-call-block-CrLN7xlI.js";
3
+ import { t as ToolCallBlock } from "./tool-call-block-D6pTEPrT.js";
4
4
  import clsx from "clsx";
5
5
  import { StickToBottom } from "use-stick-to-bottom";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -232,12 +232,6 @@ function openReconnectingSocket(urlProvider) {
232
232
  return new ReconnectingWebSocket(urlProvider, void 0, RECONNECT_OPTIONS);
233
233
  }
234
234
  /**
235
- * True while `socket` is a reconnecting socket that will retry after the
236
- * close event currently being handled. partysocket schedules the retry
237
- * *before* dispatching `close`, so `retryCount` already names the attempt
238
- * just scheduled — at `maxRetries` it has given up.
239
- */
240
- /**
241
235
  * Force a fresh connection attempt on a reconnecting socket, reporting
242
236
  * whether it was one. Used for a failure partysocket cannot see: a socket it
243
237
  * considers open and healthy, whose peer never completed OUR handshake.
@@ -249,6 +243,12 @@ function forceReconnect(socket) {
249
243
  socket.reconnect();
250
244
  return true;
251
245
  }
246
+ /**
247
+ * True while `socket` is a reconnecting socket that will retry after the
248
+ * close event currently being handled. partysocket schedules the retry
249
+ * *before* dispatching `close`, so `retryCount` already names the attempt
250
+ * just scheduled — at `maxRetries` it has given up.
251
+ */
252
252
  function reconnectPending(socket) {
253
253
  return socket instanceof ReconnectingWebSocket && socket.shouldReconnect && socket.retryCount < RECONNECT_OPTIONS.maxRetries;
254
254
  }
@@ -706,16 +706,15 @@ function createMessageHandlers(deps) {
706
706
  case "tool.completed": {
707
707
  const tcs = getSnapshot().toolCalls;
708
708
  const idx = tcs.findIndex((tc) => tc.callId === e.toolCallId);
709
- if (idx !== -1) {
710
- const updated = [...tcs];
711
- const existing = updated[idx];
712
- if (existing) updated[idx] = {
713
- ...existing,
714
- status: "done",
715
- result: e.result
716
- };
717
- updateState({ toolCalls: updated });
718
- }
709
+ const existing = tcs[idx];
710
+ if (!existing) break;
711
+ const updated = [...tcs];
712
+ updated[idx] = {
713
+ ...existing,
714
+ status: "done",
715
+ result: e.result
716
+ };
717
+ updateState({ toolCalls: updated });
719
718
  break;
720
719
  }
721
720
  case "reply.completed":
@@ -765,7 +764,7 @@ function createMessageHandlers(deps) {
765
764
  handleErrorEvent(e);
766
765
  break;
767
766
  case "session.timed-out":
768
- deps.conn.retiredByServer = true;
767
+ conn.retiredByServer = true;
769
768
  break;
770
769
  default: break;
771
770
  }
@@ -949,13 +948,24 @@ function createSessionCore(options) {
949
948
  function resetState() {
950
949
  updateState(CLEARED_SESSION_STATE);
951
950
  }
951
+ /**
952
+ * The socket, when there is one and it can carry a frame.
953
+ *
954
+ * Returning the socket rather than a boolean because every caller needs it
955
+ * next, and a predicate does not narrow `conn.ws` across the call. One
956
+ * spelling of the readiness test, which four call sites used to repeat.
957
+ */
958
+ function openSocket() {
959
+ return conn.ws?.readyState === WS_OPEN ? conn.ws : null;
960
+ }
952
961
  function sendJson(msg) {
953
- if (conn.ws && conn.ws.readyState === WS_OPEN) conn.ws.send(JSON.stringify(msg));
962
+ openSocket()?.send(JSON.stringify(msg));
954
963
  }
955
964
  function sendAudio(bytes) {
956
- if (!conn.ws || conn.ws.readyState !== WS_OPEN) return;
957
- if (conn.ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) return;
958
- conn.ws.send(bytes);
965
+ const ws = openSocket();
966
+ if (!ws) return;
967
+ if (ws.bufferedAmount > MIC_SEND_MAX_BUFFERED_BYTES) return;
968
+ ws.send(bytes);
959
969
  }
960
970
  const { handleMessage, settleWhenAudioDrained } = createMessageHandlers({
961
971
  getSnapshot,
@@ -1090,7 +1100,7 @@ function createSessionCore(options) {
1090
1100
  }, { signal: sig });
1091
1101
  }
1092
1102
  function cancel() {
1093
- if (!conn.ws || conn.ws.readyState !== WS_OPEN) return;
1103
+ if (!openSocket()) return;
1094
1104
  conn.turn.bump();
1095
1105
  conn.voiceIO?.flush();
1096
1106
  updateState({ state: "listening" });
@@ -1099,7 +1109,7 @@ function createSessionCore(options) {
1099
1109
  function reset() {
1100
1110
  conn.turn.bump();
1101
1111
  conn.voiceIO?.flush();
1102
- if (conn.ws && conn.ws.readyState === WS_OPEN) {
1112
+ if (openSocket()) {
1103
1113
  sendJson({ type: "reset" });
1104
1114
  return;
1105
1115
  }
@@ -6,12 +6,6 @@ import type { WebSocketConstructor } from "./types.ts";
6
6
  * session started with.
7
7
  */
8
8
  export declare function openReconnectingSocket(urlProvider: () => Promise<string>): InstanceType<WebSocketConstructor>;
9
- /**
10
- * True while `socket` is a reconnecting socket that will retry after the
11
- * close event currently being handled. partysocket schedules the retry
12
- * *before* dispatching `close`, so `retryCount` already names the attempt
13
- * just scheduled — at `maxRetries` it has given up.
14
- */
15
9
  /**
16
10
  * Force a fresh connection attempt on a reconnecting socket, reporting
17
11
  * whether it was one. Used for a failure partysocket cannot see: a socket it
@@ -20,4 +14,10 @@ export declare function openReconnectingSocket(urlProvider: () => Promise<string
20
14
  * reconnect machinery — the caller then treats the failure as terminal.
21
15
  */
22
16
  export declare function forceReconnect(socket: unknown): boolean;
17
+ /**
18
+ * True while `socket` is a reconnecting socket that will retry after the
19
+ * close event currently being handled. partysocket schedules the retry
20
+ * *before* dispatching `close`, so `retryCount` already names the attempt
21
+ * just scheduled — at `maxRetries` it has given up.
22
+ */
23
23
  export declare function reconnectPending(socket: unknown): boolean;
@@ -1,2 +1,2 @@
1
- import { t as createSessionCore } from "./session-core-ClKdVgRU.js";
1
+ import { t as createSessionCore } from "./session-core-CAfYmUbg.js";
2
2
  export { createSessionCore };
@@ -1,7 +1,7 @@
1
1
  import { useTheme } from "./context.js";
2
2
  import { r as inkTint } from "./_colors-CcAi2FOU.js";
3
3
  import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
4
- import { n as truncate, r as tryParseJSON } from "./_utils-CsqbIVGI.js";
4
+ import { i as tryParseJSON, r as truncate } from "./_utils-B6498_bm.js";
5
5
  import clsx from "clsx";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { createContext, memo, useContext, useMemo, useState } from "react";
@@ -40,6 +40,7 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
40
40
  const theme = useTheme();
41
41
  const sizes = VARIANT_CLASSES[variant];
42
42
  const canExpand = children != null;
43
+ const faint = inkTint(theme.text, theme.bg, 65);
43
44
  return /* @__PURE__ */ jsxs("div", {
44
45
  className: clsx("flex flex-col rounded-md border overflow-hidden", className),
45
46
  style: {
@@ -60,7 +61,7 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
60
61
  children: icon
61
62
  }) : /* @__PURE__ */ jsx(Eyebrow, {
62
63
  className: "shrink-0",
63
- style: { color: inkTint(theme.text, theme.bg, 65) },
64
+ style: { color: faint },
64
65
  children: "Tool"
65
66
  }),
66
67
  /* @__PURE__ */ jsx("span", {
@@ -70,12 +71,12 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
70
71
  }),
71
72
  /* @__PURE__ */ jsx("span", {
72
73
  className: clsx("font-aai-mono truncate flex-1 min-w-0", sizes.title),
73
- style: { color: inkTint(theme.text, theme.bg, 65) },
74
+ style: { color: faint },
74
75
  children: detail
75
76
  }),
76
77
  canExpand && /* @__PURE__ */ jsx("span", {
77
78
  className: clsx("shrink-0 transition-transform duration-150", sizes.chevron, isOpen && "rotate-90"),
78
- style: { color: inkTint(theme.text, theme.bg, 65) },
79
+ style: { color: faint },
79
80
  children: "▶"
80
81
  })
81
82
  ]
@@ -33,7 +33,7 @@
33
33
  * followed from the same id either way.
34
34
  */
35
35
  import { type WorkflowSummary } from "@alexkroman1/aai";
36
- import type { UploadProgress } from "@alexkroman1/aai/workflow-api";
36
+ import type { UploadParallel, UploadProgress } from "@alexkroman1/aai/workflow-api";
37
37
  import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
38
38
  /** Options for {@link useWorkflows}. */
39
39
  export type UseWorkflowsOptions = {
@@ -138,6 +138,18 @@ export type UseWorkflowSubmitOptions = {
138
138
  wait?: number;
139
139
  /** How often the fallback poll re-reads a live run. */
140
140
  intervalMs?: number;
141
+ /**
142
+ * Send each chosen file as concurrent parts instead of in one request.
143
+ *
144
+ * `true` for the defaults, or `{ partBytes, concurrency }` to tune them. This is
145
+ * the wait a form with a recording in it actually spends: the run does not exist
146
+ * until its input is stored, so until the last byte lands there is no run to
147
+ * watch and nothing for `<WorkflowProgress>` to say. Splitting the file across
148
+ * connections is what makes that stretch shorter, and it degrades to the single
149
+ * request wherever it would not help — a small file, an older agent — so turning
150
+ * it on is safe for every form. See `UploadOptions.parallel`.
151
+ */
152
+ parallel?: UploadParallel;
141
153
  };
142
154
  /**
143
155
  * Start a workflow from a form, and follow the run it creates.
@@ -57,6 +57,7 @@
57
57
  * thrown away with the run; a caller who wants to resume instead drives
58
58
  * `api.uploadStream` and `api.uploadInfo` directly.
59
59
  */
60
+ import type { UploadParallel } from "@alexkroman1/aai/workflow-api";
60
61
  import type { UploadStatus } from "./use-workflow-form.ts";
61
62
  import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
62
63
  /** Options for {@link useWorkflowStream}. */
@@ -67,6 +68,19 @@ export type UseWorkflowStreamOptions = {
67
68
  key?: string;
68
69
  /** How often the fallback poll re-reads a live run. */
69
70
  intervalMs?: number;
71
+ /**
72
+ * Send the file as concurrent parts instead of in one streaming request.
73
+ *
74
+ * It COMPOSES with what this hook is for rather than competing with it: the run
75
+ * still starts before the bytes, and the store still publishes how far the file
76
+ * is readable — that number is the CONTIGUOUS prefix, so a run reading ahead of
77
+ * the uplink sees the same growing file whether one connection or four are
78
+ * filling it. What changes is only how fast it grows.
79
+ *
80
+ * `true` for the defaults, or `{ partBytes, concurrency }` to tune them. See
81
+ * `UploadOptions.parallel`.
82
+ */
83
+ parallel?: UploadParallel;
70
84
  };
71
85
  /** What {@link useWorkflowStream} returns. */
72
86
  export type WorkflowStreamSubmission<R = unknown> = {
@@ -1,7 +1,8 @@
1
1
  import { MIC_BUFFER_SECONDS, MIC_SILENCE_PROBE_MS } from "../types.js";
2
2
  import { t as workletModuleUrl } from "../_module-url-C_4gRVL0.js";
3
3
  //#region worklets/capture-processor.ts
4
- const CaptureProcessorWorklet = `
4
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
5
+ const captureProcessorSource = `
5
6
  class CaptureProcessor extends AudioWorkletProcessor {
6
7
  constructor(options) {
7
8
  super();
@@ -91,8 +92,6 @@ class CaptureProcessor extends AudioWorkletProcessor {
91
92
 
92
93
  registerProcessor('capture-processor', CaptureProcessor);
93
94
  `;
94
- /** Raw worklet source — exported so tests can evaluate the processor directly. */
95
- const captureProcessorSource = CaptureProcessorWorklet;
96
- var capture_processor_default = workletModuleUrl(CaptureProcessorWorklet);
95
+ var capture_processor_default = workletModuleUrl(captureProcessorSource);
97
96
  //#endregion
98
97
  export { captureProcessorSource, capture_processor_default as default };
@@ -1,7 +1,8 @@
1
1
  import { PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_FILL_MS, PLAYBACK_PROGRESS_INTERVAL_MS } from "../types.js";
2
2
  import { t as workletModuleUrl } from "../_module-url-C_4gRVL0.js";
3
3
  //#region worklets/playback-processor.ts
4
- const PlaybackProcessorWorklet = `
4
+ /** Raw worklet source — exported so tests can evaluate the processor directly. */
5
+ const playbackProcessorSource = `
5
6
  class PlaybackProcessor extends AudioWorkletProcessor {
6
7
  constructor(options) {
7
8
  super();
@@ -315,8 +316,6 @@ class PlaybackProcessor extends AudioWorkletProcessor {
315
316
 
316
317
  registerProcessor('playback-processor', PlaybackProcessor);
317
318
  `;
318
- /** Raw worklet source — exported so tests can evaluate the processor directly. */
319
- const playbackProcessorSource = PlaybackProcessorWorklet;
320
- var playback_processor_default = workletModuleUrl(PlaybackProcessorWorklet);
319
+ var playback_processor_default = workletModuleUrl(playbackProcessorSource);
321
320
  //#endregion
322
321
  export { playback_processor_default as default, playbackProcessorSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-ui",
3
- "version": "6.4.0",
3
+ "version": "6.5.1",
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.4.0"
32
+ "@alexkroman1/aai": "6.5.1"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^19.0.0",
@@ -1 +0,0 @@
1
- import{o as e,s as t}from"./client-audio-constants-Ck0IJO4c.js";var n={echoCancellation:!0,noiseSuppression:!1,autoGainControl:!1,voiceIsolation:!1};function r(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function i(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function a(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 o(o){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:d,onError:f,onPlaybackStats:p,onPlaybackProgress:m,onMicSilent:h}=o,g=new AudioContext({sampleRate:c,latencyHint:`playback`}),_=s===c,v=_?g:new AudioContext({sampleRate:s,latencyHint:`interactive`});async function y(){let e=_?[g]:[g,v];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let b=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),x;try{[x]=await Promise.all([b,g.resume(),v.resume(),v.audioWorklet.addModule(l),g.audioWorklet.addModule(u)]),r(v.sampleRate,s,`capture`),r(g.sampleRate,c,`playback`)}catch(e){throw i(b),await y(),e}let S=v.createMediaStreamSource(x),C=a(v,d,h);S.connect(C.node),C.node.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),f?.(e)},C.start();let w=null,T=null,E=0,D=null,O=new AbortController;function k(){T?.(),T=null,D=null}function A(e){e.stats&&e.stats.concealedSamples>0&&p?.(e.stats),e.reason!==`interrupt`&&(D===null||e.turn===D)&&k()}function j(){if(w)return w;let e=new AudioWorkletNode(g,`playback-processor`);return e.connect(g.destination),e.port.onmessage=e=>{e.data.event===`stop`?A(e.data):e.data.event===`progress`&&m?.(e.data.bufferedMs)},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),k(),f?.(e)},w=e,e}let M={enqueue(e){O.signal.aborted||e.byteLength!==0&&j().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){if(!w)return Promise.resolve();let n=++E;if(w.port.postMessage({event:`done`,turn:n}),g.state!==`running`)return D=null,Promise.resolve();let{promise:r,resolve:i}=Promise.withResolvers();T?.();let a=()=>{clearInterval(o),clearTimeout(s),T===a&&(T=null,D=null),i()},o=setInterval(()=>{g.state!==`running`&&a()},t),s=setTimeout(a,e);return T=a,D=n,r},flush(){w&&(k(),w.port.postMessage({event:`interrupt`}))},async close(){if(!O.signal.aborted){O.abort(),await C.stop(),S.disconnect(),C.node.disconnect(),w&&w.disconnect();for(let e of x.getTracks())e.stop();await y()}},async[Symbol.asyncDispose](){await M.close()}};return M}export{o as createVoiceIO};