@burdenoff/fe-libs 2026.1016.3 → 2026.1017.2

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.
Files changed (31) hide show
  1. package/README.md +25 -1
  2. package/dist/shared/assistant/i18nManifest.d.ts +1 -0
  3. package/dist/shared/assistant/i18nManifest.js +1 -0
  4. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.d.ts +6 -0
  5. package/dist/shared/assistant/speech/hostedTtsSpeechEngine.js +118 -101
  6. package/dist/shared/assistant/speech/notifyCancellationSpeechEngine.js +3 -0
  7. package/dist/shared/assistant/speech/routingSpeechEngine.js +11 -8
  8. package/dist/shared/assistant/speech/types.d.ts +11 -0
  9. package/dist/shared/assistant/ui/AssistantMessageItem.js +110 -100
  10. package/dist/shared/assistant/ui/AssistantSpeechProvider.d.ts +10 -0
  11. package/dist/shared/assistant/ui/AssistantSpeechProvider.js +278 -261
  12. package/dist/shared/assistant/ui/useVoiceSession.js +222 -227
  13. package/dist/shared/assistant/voice/handsfree/playbackReference.d.ts +15 -4
  14. package/dist/shared/assistant/voice/handsfree/playbackReference.js +9 -6
  15. package/dist/shared/pages/AiPage.d.ts +0 -8
  16. package/dist/shared/pages/AiPage.js +126 -72
  17. package/dist/shared/pages/ai/CatalogPanel.d.ts +9 -0
  18. package/dist/shared/pages/ai/CatalogPanel.js +80 -0
  19. package/dist/shared/pages/ai/ReadNotice.d.ts +4 -0
  20. package/dist/shared/pages/ai/ReadNotice.js +18 -0
  21. package/dist/shared/pages/ai/SummaryPanel.d.ts +7 -0
  22. package/dist/shared/pages/ai/SummaryPanel.js +135 -0
  23. package/dist/shared/pages/ai/contracts.d.ts +45 -0
  24. package/dist/shared/pages/ai/contracts.js +159 -0
  25. package/dist/shared/pages/ai/i18n.d.ts +1 -0
  26. package/dist/shared/pages/ai/i18n.js +10 -0
  27. package/dist/shared/pages/ai/useAiRead.d.ts +18 -0
  28. package/dist/shared/pages/ai/useAiRead.js +57 -0
  29. package/dist/shared/utils/mockData.js +212 -0
  30. package/dist/shared-assistant.js +270 -270
  31. package/package.json +1 -5
package/README.md CHANGED
@@ -21,7 +21,7 @@ This is a **Layer 1 package** - a monolithic package with subpath exports for al
21
21
 
22
22
  ```typescript
23
23
  import { Button } from '@burdenoff/fe-libs/button';
24
- import { WorkflowNode, WorkflowCanvas } from '@burdenoff/fe-libs/workflow';
24
+ import { mockProducts } from '@burdenoff/fe-libs/shared/utils/mockData';
25
25
  import { cn } from '@burdenoff/fe-libs/shared';
26
26
  ```
27
27
 
@@ -129,3 +129,27 @@ plain browser (persisted in `sessionStorage`, cleared with `off`). It is
129
129
  - TailwindCSS v4
130
130
  - Radix UI (for accessible primitives)
131
131
  - Vite 7 (for building)
132
+
133
+ ## Export migration (BOFF-9417)
134
+
135
+ Beginning with the first CalVer release containing BOFF-9417, the already
136
+ nonfunctional `@burdenoff/fe-libs/workflow` export metadata is explicitly retired. Both advertised
137
+ targets (`dist/workflow.js` and `dist/workflow.d.ts`) are absent from the verified
138
+ published 2026.1016.3 package. The former WorkflowCanvas/WorkflowNode implementation
139
+ was deliberately removed in source commit `f0920f9a` on 2026-01-15; this change does
140
+ not remove a working runtime implementation or restore that obsolete UI. Existing
141
+ published releases are unchanged. Usage outside the bounded local repository
142
+ inventory is unknown; this is not a claim that external consumers do not exist.
143
+
144
+ Current workflow-builder functionality belongs to `@burdenoff/fluidgrids-fe-sdk`
145
+ (WorkflowBuilder and WorkflowBuilderCore), not another microfrontend import. Those
146
+ APIs differ from the deleted generic canvas/node props and require an explicit
147
+ consumer migration; they are not a drop-in compatibility adapter. Consumers should
148
+ review their intended workflow behavior before choosing that API.
149
+
150
+ The existing `@burdenoff/fe-libs/shared/utils/mockData` subpath is retained. Its five
151
+ fixture arrays now have an explicit runtime build entry alongside their declarations.
152
+ The archive verifier checks literal export/bin targets in the actual npm tar before
153
+ publication; wildcard export patterns and universal consumer compatibility are not
154
+ certified by that existence check. Normal release automation assigns the concrete
155
+ CalVer version; this source note does not claim publication or host adoption.
@@ -499,6 +499,7 @@ export declare const ASSISTANT_I18N_LITERAL_COPY: {
499
499
  readonly "sheet.collapse": "Shrink the assistant";
500
500
  readonly "sheet.expand": "Expand the assistant";
501
501
  readonly "speech.codeBlock": "Code block";
502
+ readonly "speech.deviceFallback": "Device voice used: the selected voice was unavailable.";
502
503
  readonly "speech.emailAt": "at";
503
504
  readonly "speech.emailDot": "dot";
504
505
  readonly "speech.image": "image";
@@ -393,6 +393,7 @@ var m = {
393
393
  "sheet.collapse": "Shrink the assistant",
394
394
  "sheet.expand": "Expand the assistant",
395
395
  "speech.codeBlock": "Code block",
396
+ "speech.deviceFallback": "Device voice used: the selected voice was unavailable.",
396
397
  "speech.emailAt": "at",
397
398
  "speech.emailDot": "dot",
398
399
  "speech.image": "image",
@@ -60,4 +60,10 @@ export declare const HOSTED_OUTPUT_STALL_AUDIO_RATIO = 2;
60
60
  export declare const HOSTED_OUTPUT_MIN_AUDIO_BYTES_PER_SECOND = 1000;
61
61
  /** The watchdog's window once `bytesHanded` bytes of audio went to the output. */
62
62
  export declare function hostedOutputStallMs(bytesHanded: number): number;
63
+ /** 44 bytes of silence: what a tap plays to unlock an engine's element. */
64
+ export declare const SILENT_WAV_DATA_URI = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=";
65
+ /** Unlock every hosted engine's playback element. Call inside a user gesture. */
66
+ export declare function primeHostedPlayback(): void;
67
+ /** Test seam: forget every engine registered so far. */
68
+ export declare function resetHostedPlaybackForTest(): void;
63
69
  export declare function createHostedTtsEngine(options: HostedTtsOptions): HostedSpeechEngine;
@@ -1,39 +1,39 @@
1
- import { tapPlaybackElement as e } from "../voice/handsfree/playbackReference.js";
2
- import { HOSTED_TTS_TIER as t, HOSTED_VOICES as n, hostedLocaleFor as r } from "./hostedVoiceCatalogue.js";
3
- import { buildRequest as i, classifyHttpFailure as a, decodeAudioResponse as o, disableDurationMs as s, shouldDisableEngine as c, speakableChunks as l } from "./hostedTtsProtocol.js";
4
- import { createPieceSink as u, isOutputRefusal as d } from "./pieceSink.js";
1
+ import { isPlaybackReferenceAttached as e, tapPlaybackElement as t } from "../voice/handsfree/playbackReference.js";
2
+ import { HOSTED_TTS_TIER as n, HOSTED_VOICES as r, hostedLocaleFor as i } from "./hostedVoiceCatalogue.js";
3
+ import { buildRequest as a, classifyHttpFailure as o, decodeAudioResponse as s, disableDurationMs as c, shouldDisableEngine as l, speakableChunks as u } from "./hostedTtsProtocol.js";
4
+ import { createPieceSink as d, isOutputRefusal as f } from "./pieceSink.js";
5
5
  //#region src/shared/assistant/speech/hostedTtsSpeechEngine.ts
6
- function f(e) {
6
+ function p(e) {
7
7
  let t = Object.entries(e).map(([e, t]) => `${e.toLowerCase()}:${t}`).sort().join("\n"), n = 5381;
8
8
  for (let e = 0; e < t.length; e += 1) n = (n << 5) + n + t.charCodeAt(e) | 0;
9
9
  return `${t.length}:${n >>> 0}`;
10
10
  }
11
- var p = new Set([
11
+ var m = new Set([
12
12
  429,
13
13
  502,
14
14
  503,
15
15
  504
16
- ]), m = 3, h = 400, g = 2e3;
17
- function _(e, t) {
16
+ ]), h = 3, g = 400, _ = 2e3;
17
+ function v(e, t) {
18
18
  let n = e === null ? NaN : Number(e);
19
- return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : h * 2 ** (t - 1), g);
19
+ return Math.min(Number.isFinite(n) && n >= 0 ? n * 1e3 : g * 2 ** (t - 1), _);
20
20
  }
21
- var v = 6e4, y = 2, b = 1e3;
22
- function x(e) {
23
- return v + e / b * 2 * 1e3;
21
+ var y = 6e4, b = 2, x = 1e3;
22
+ function S(e) {
23
+ return y + e / x * 2 * 1e3;
24
24
  }
25
- var S = class extends Error {
25
+ var C = class extends Error {
26
26
  reason = "unknown";
27
27
  constructor() {
28
28
  super("audio output made no progress"), this.name = "OutputStalledError";
29
29
  }
30
30
  };
31
- function C(e) {
31
+ function w(e) {
32
32
  let t = 0, n = !1, r = null, i = () => {
33
33
  let n = r;
34
34
  n !== null && (n.cancel(), n.cancel = e(() => {
35
- r === n && (r = null, n.fail(new S()));
36
- }, x(t)));
35
+ r === n && (r = null, n.fail(new C()));
36
+ }, S(t)));
37
37
  };
38
38
  return {
39
39
  handed(e) {
@@ -64,7 +64,14 @@ function C(e) {
64
64
  }
65
65
  };
66
66
  }
67
- function w(e, t) {
67
+ var T = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=", E = [], D = 4;
68
+ function O() {
69
+ for (let e of [...E]) e();
70
+ }
71
+ function k() {
72
+ E.length = 0;
73
+ }
74
+ function A(e, t) {
68
75
  return new Promise((n) => {
69
76
  if (t.aborted) {
70
77
  n();
@@ -79,183 +86,193 @@ function w(e, t) {
79
86
  t.addEventListener("abort", i, { once: !0 });
80
87
  });
81
88
  }
82
- function T(h) {
83
- let g = h.fetchImpl ?? ((...e) => fetch(...e)), v = h.now ?? (() => Date.now()), y = h.setTimer ?? ((e, t) => {
89
+ function j(g) {
90
+ let _ = g.fetchImpl ?? ((...e) => fetch(...e)), y = g.now ?? (() => Date.now()), b = g.setTimer ?? ((e, t) => {
84
91
  let n = setTimeout(e, t);
85
92
  return () => clearTimeout(n);
86
- }), b = null, x = null, T = 0, E = null, D = null, O = null, k = 0, A = null, j = null, M = () => {
93
+ }), x = null, S = null, O = null, k = !1, j = () => {
94
+ if (!(k || typeof Audio != "function")) try {
95
+ let e = O ?? new Audio();
96
+ if (O = e, x === e) return;
97
+ e.muted = !0, e.src = T, Promise.resolve(e.play()).then(() => {
98
+ O === e && x !== e && e.pause();
99
+ }, () => {});
100
+ } catch {}
101
+ };
102
+ E.push(j), E.length > D && E.shift();
103
+ let M = 0, N = null, P = null, F = null, I = 0, L = null, R = null, z = () => {
87
104
  try {
88
- return h.getHeaders?.() ?? {};
105
+ return g.getHeaders?.() ?? {};
89
106
  } catch {
90
107
  return {};
91
108
  }
92
- }, N = () => v() >= k ? !1 : j === null ? !0 : f(M()) === j, P = /* @__PURE__ */ new Set(), F = () => {
93
- for (let e of [...P]) e();
94
- }, I = () => {
95
- A !== null && clearTimeout(A), A = null;
96
- }, L = (e) => {
97
- I(), A = setTimeout(() => {
98
- A = null;
99
- let e = k - v();
109
+ }, B = () => y() >= I ? !1 : R === null ? !0 : p(z()) === R, V = /* @__PURE__ */ new Set(), H = () => {
110
+ for (let e of [...V]) e();
111
+ }, U = () => {
112
+ L !== null && clearTimeout(L), L = null;
113
+ }, W = (e) => {
114
+ U(), L = setTimeout(() => {
115
+ L = null;
116
+ let e = I - y();
100
117
  if (e > 0) {
101
- L(e);
118
+ W(e);
102
119
  return;
103
120
  }
104
- F();
121
+ H();
105
122
  }, e);
106
- }, R = (e, t) => {
107
- k = v() + e, j = t, L(e), F();
108
- }, z = () => {
109
- x &&= (URL.revokeObjectURL(x), null);
110
- }, B = () => {
111
- if (b) {
112
- b.onended = null, b.onerror = null;
123
+ }, G = (e, t) => {
124
+ I = y() + e, R = t, W(e), H();
125
+ }, K = () => {
126
+ S &&= (URL.revokeObjectURL(S), null);
127
+ }, q = () => {
128
+ if (x) {
129
+ x.onended = null, x.onerror = null;
113
130
  try {
114
- b.pause();
131
+ x.pause();
115
132
  } catch {}
116
- b = null;
133
+ x = null;
117
134
  }
118
- z();
135
+ K();
119
136
  };
120
- async function V(e, n, s) {
121
- let c = i(e, r(n.lang) ?? n.lang, t, n.voice?.name ?? null, n.rate), l = h.getHeaders?.() ?? {}, u;
122
- for (let e = 1; u = await g(h.endpoint, {
137
+ async function J(e, t, r) {
138
+ let c = a(e, i(t.lang) ?? t.lang, n, t.voice?.name ?? null, t.rate), l = g.getHeaders?.() ?? {}, u;
139
+ for (let e = 1; u = await _(g.endpoint, {
123
140
  method: "POST",
124
- signal: s,
141
+ signal: r,
125
142
  headers: {
126
143
  "content-type": "application/json",
127
144
  ...l
128
145
  },
129
146
  body: JSON.stringify(c)
130
147
  }), !u.ok; e += 1) {
131
- if (!(p.has(u.status) && e < m && !s.aborted)) {
148
+ if (!(m.has(u.status) && e < h && !r.aborted)) {
132
149
  let e = /* @__PURE__ */ Error(`hosted tts ${u.status}`);
133
- throw e.reason = a(u.status), e.headers = f(l), e;
150
+ throw e.reason = o(u.status), e.headers = p(l), e;
134
151
  }
135
- await w(_(u.headers.get("retry-after"), e), s);
152
+ await A(v(u.headers.get("retry-after"), e), r);
136
153
  }
137
- let d = u.headers.get("content-type"), v = o(d, d?.includes("json") ? await u.json() : await u.arrayBuffer());
138
- if (!v) {
154
+ let d = u.headers.get("content-type"), f = s(d, d?.includes("json") ? await u.json() : await u.arrayBuffer());
155
+ if (!f) {
139
156
  let e = /* @__PURE__ */ Error("hosted tts returned no audio");
140
157
  throw e.reason = "unknown", e;
141
158
  }
142
- return v;
159
+ return f;
143
160
  }
144
- function H(t, n, r) {
145
- return new Promise((i, a) => {
146
- if (n !== T) {
147
- i();
161
+ function Y(n, r, i) {
162
+ return new Promise((a, o) => {
163
+ if (r !== M) {
164
+ a();
148
165
  return;
149
166
  }
150
- let o = new Blob([t.slice().buffer], { type: "audio/mpeg" });
151
- z(), x = URL.createObjectURL(o);
152
- let s = new Audio(x);
153
- e(s), b = s, s.onended = () => {
154
- n === T && i();
155
- }, s.onerror = () => {
156
- if (n !== T) {
157
- i();
167
+ let s = new Blob([n.slice().buffer], { type: "audio/mpeg" });
168
+ K(), S = URL.createObjectURL(s);
169
+ let c;
170
+ e() ? (c = new Audio(S), t(c)) : (c = O ?? new Audio(), O = c, c.muted = !1, c.src = S), x = c, c.onended = () => {
171
+ r === M && a();
172
+ }, c.onerror = () => {
173
+ if (r !== M) {
174
+ a();
158
175
  return;
159
176
  }
160
177
  let e = /* @__PURE__ */ Error("audio playback failed");
161
- e.reason = "unknown", a(e);
162
- }, s.play().then(() => {
163
- n === T && r?.();
178
+ e.reason = "unknown", o(e);
179
+ }, c.play().then(() => {
180
+ c === O && !c.muted && (k = !0), r === M && i?.();
164
181
  }, (e) => {
165
- if (n !== T) {
166
- i();
182
+ if (r !== M) {
183
+ a();
167
184
  return;
168
185
  }
169
- let t = /* @__PURE__ */ Error("audio play rejected"), r = e?.name === "NotAllowedError";
170
- t.reason = r ? "not-allowed" : "unknown", t.transient = r, a(t);
186
+ let t = /* @__PURE__ */ Error("audio play rejected"), n = e?.name === "NotAllowedError";
187
+ t.reason = n ? "not-allowed" : "unknown", t.transient = n, o(t);
171
188
  });
172
189
  });
173
190
  }
174
191
  return {
175
192
  id: "hosted-tts",
176
193
  isSupported() {
177
- return N() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
194
+ return B() ? !1 : typeof window < "u" && typeof window.Audio == "function" && typeof fetch == "function";
178
195
  },
179
196
  listVoices() {
180
- return N() ? [] : [...n];
197
+ return B() ? [] : [...r];
181
198
  },
182
199
  subscribeVoices(e) {
183
- return P.add(e), () => {
184
- P.delete(e);
200
+ return V.add(e), () => {
201
+ V.delete(e);
185
202
  };
186
203
  },
187
204
  speak(e, t, n = {}) {
188
- let r = l(e);
205
+ let r = u(e);
189
206
  if (r.length === 0) {
190
207
  n.onEnd?.();
191
208
  return;
192
209
  }
193
- T += 1;
194
- let i = T;
195
- B(), D?.cancel(), O?.stop();
210
+ M += 1;
211
+ let i = M;
212
+ q(), P?.cancel(), F?.stop(), j();
196
213
  let a = new AbortController();
197
- E = a;
198
- let o = !1, f = () => {
214
+ N = a;
215
+ let o = !1, s = () => {
199
216
  o || (o = !0, n.onStart?.());
200
217
  }, p = (e) => {
201
- let n = V(e, t, a.signal);
218
+ let n = J(e, t, a.signal);
202
219
  return n.catch(() => {}), n;
203
- }, m = t.audioOutput ? C(y) : null, h = t.audioOutput ? u(t.audioOutput, {
204
- play: (e) => H(e, i, f),
205
- isCurrent: () => i === T,
220
+ }, m = t.audioOutput ? w(b) : null, h = t.audioOutput ? d(t.audioOutput, {
221
+ play: (e) => Y(e, i, s),
222
+ isCurrent: () => i === M,
206
223
  onChunkStart: (e) => {
207
224
  m?.progress(), n.onChunkStart?.(e);
208
225
  },
209
- onStart: f
226
+ onStart: s
210
227
  }) : null;
211
- D = h, O = m;
228
+ P = h, F = m;
212
229
  let g = (e) => m ? m.guard(e) : e, _ = () => {
213
- h && (m?.stop(), O === m && (O = null), D === h && (D = null));
230
+ h && (m?.stop(), F === m && (F = null), P === h && (P = null));
214
231
  };
215
232
  (async () => {
216
233
  try {
217
234
  let e = p(r[0]);
218
235
  for (let t = 0; t < r.length; t += 1) {
219
- if (i !== T) return;
236
+ if (i !== M) return;
220
237
  let a = await e;
221
- if (i !== T) return;
238
+ if (i !== M) return;
222
239
  let o = r[t + 1];
223
240
  if (o !== void 0 && (e = p(o)), h) {
224
241
  m?.handed(a.byteLength), await g(h.write(a));
225
242
  continue;
226
243
  }
227
- n.onChunkStart?.(t), await H(a, i, f);
244
+ n.onChunkStart?.(t), await Y(a, i, s);
228
245
  }
229
246
  if (h) {
230
247
  let e = await g(h.drain());
231
- if (i !== T) return;
248
+ if (i !== M) return;
232
249
  if (!e) throw Object.assign(/* @__PURE__ */ Error("audio output did not finish"), { reason: "unknown" });
233
250
  }
234
- i === T && (_(), n.onEnd?.());
251
+ i === M && (_(), n.onEnd?.());
235
252
  } catch (e) {
236
- if (a.abort(), i !== T) return;
253
+ if (a.abort(), i !== M) return;
237
254
  let t = null;
238
255
  if (h) {
239
- let n = e instanceof S ? null : await g(h.drain()).then(() => null, (e) => e);
240
- if (i !== T) return;
241
- h.cancel(), d(e) ? t = e : d(n) && (t = n);
256
+ let n = e instanceof C ? null : await g(h.drain()).then(() => null, (e) => e);
257
+ if (i !== M) return;
258
+ h.cancel(), f(e) ? t = e : f(n) && (t = n);
242
259
  }
243
- if (T += 1, B(), _(), t) {
260
+ if (M += 1, q(), _(), t) {
244
261
  n.onError?.("output-not-cleared");
245
262
  return;
246
263
  }
247
264
  let r = e?.reason;
248
- c(r, e?.transient === !0) && R(s(r), r === "not-allowed" ? e.headers ?? null : null), n.onError?.(r === "not-allowed" || r === "unsupported" || r === "cancelled" ? r : "unknown");
265
+ l(r, e?.transient === !0) && G(c(r), r === "not-allowed" ? e.headers ?? null : null), n.onError?.(r === "not-allowed" || r === "unsupported" || r === "cancelled" ? r : "unknown");
249
266
  }
250
267
  })();
251
268
  },
252
269
  headersChanged() {
253
- v() >= k || j === null || N() || (k = 0, j = null, I(), F());
270
+ y() >= I || R === null || B() || (I = 0, R = null, U(), H());
254
271
  },
255
272
  cancel() {
256
- T += 1, E?.abort(), E = null, B(), D?.cancel(), D = null, O?.stop(), O = null;
273
+ M += 1, N?.abort(), N = null, q(), P?.cancel(), P = null, F?.stop(), F = null;
257
274
  }
258
275
  };
259
276
  }
260
277
  //#endregion
261
- export { b as HOSTED_OUTPUT_MIN_AUDIO_BYTES_PER_SECOND, y as HOSTED_OUTPUT_STALL_AUDIO_RATIO, v as HOSTED_OUTPUT_STALL_BASE_MS, T as createHostedTtsEngine, x as hostedOutputStallMs, _ as retryDelayMs };
278
+ export { x as HOSTED_OUTPUT_MIN_AUDIO_BYTES_PER_SECOND, b as HOSTED_OUTPUT_STALL_AUDIO_RATIO, y as HOSTED_OUTPUT_STALL_BASE_MS, T as SILENT_WAV_DATA_URI, j as createHostedTtsEngine, S as hostedOutputStallMs, O as primeHostedPlayback, k as resetHostedPlaybackForTest, v as retryDelayMs };
@@ -25,6 +25,9 @@ function e(e) {
25
25
  onChunkStart: (e) => {
26
26
  t === o && a.onChunkStart?.(e);
27
27
  },
28
+ onDeviceFallback: () => {
29
+ t === o && a.onDeviceFallback?.();
30
+ },
28
31
  onEnd: () => s(),
29
32
  onError: (e) => s(e)
30
33
  });
@@ -15,23 +15,26 @@ function r(e) {
15
15
  }
16
16
  function i({ device: i, hosted: a }) {
17
17
  if (!a) return i;
18
- let o = new Set(a.listVoices().map((e) => e.name)), s = (e) => e.source === "hosted" || o.has(e.name), c = (e, t, r) => {
18
+ let o = new Set(a.listVoices().map((e) => e.name)), s = (e) => e.source === "hosted" || o.has(e.name), c = (e, t, r, a = !1) => {
19
+ let o = () => {
20
+ a && r?.onDeviceFallback?.();
21
+ };
19
22
  if (!t.voice || !s(t.voice)) {
20
- i.speak(e, t, r);
23
+ o(), i.speak(e, t, r);
21
24
  return;
22
25
  }
23
26
  if (t.fallback === !1) {
24
27
  r?.onError?.("unsupported");
25
28
  return;
26
29
  }
27
- let a = n(i.listVoices(), t.lang);
28
- if (!a) {
30
+ let c = n(i.listVoices(), t.lang);
31
+ if (!c) {
29
32
  r?.onError?.("unsupported");
30
33
  return;
31
34
  }
32
- i.speak(e, {
35
+ o(), i.speak(e, {
33
36
  ...t,
34
- voice: a
37
+ voice: c
35
38
  }, r);
36
39
  }, l = (t) => a.isSupported() ? t.voice ? o.has(t.voice.name) ? a : i : e(t.lang) ? a : i : i;
37
40
  return {
@@ -68,7 +71,7 @@ function i({ device: i, hosted: a }) {
68
71
  return;
69
72
  }
70
73
  if (!s) {
71
- c(e, n, o);
74
+ c(e, n, o, !0);
72
75
  return;
73
76
  }
74
77
  let a = u >= 0 ? t(e).slice(u + 1) : [];
@@ -81,7 +84,7 @@ function i({ device: i, hosted: a }) {
81
84
  ...o,
82
85
  onStart: void 0,
83
86
  onChunkStart: (e) => o?.onChunkStart?.(l + e)
84
- });
87
+ }, !0);
85
88
  }
86
89
  });
87
90
  },
@@ -191,6 +191,17 @@ export interface SpeechSpeakHandlers {
191
191
  /** The whole queue finished on its own. */
192
192
  onEnd?: () => void;
193
193
  onError?: (reason: SpeechErrorReason) => void;
194
+ /**
195
+ * ★ BOFF-9408: the hosted voice could not play this reply, so the rest of it
196
+ * (or all of it) is about to be spoken on the DEVICE voice.
197
+ *
198
+ * The routing engine calls it once, just before the device voice starts, so
199
+ * the caller can say so on screen instead of the user hearing a different
200
+ * voice with no explanation. The fallback changes nothing the user chose:
201
+ * their saved voice is untouched and the next reply tries the hosted voice
202
+ * again. Optional; an engine with no fallback never calls it.
203
+ */
204
+ onDeviceFallback?: () => void;
194
205
  }
195
206
  export interface SpeechEngine {
196
207
  /** Stable id, for logging and for telling engines apart in a future swap. */