@burdenoff/fe-libs 2026.1004.4 → 2026.1004.5

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 (37) hide show
  1. package/dist/shared/assistant/i18nManifest.d.ts +10 -0
  2. package/dist/shared/assistant/i18nManifest.d.ts.map +1 -1
  3. package/dist/shared/assistant/i18nManifest.js +22 -12
  4. package/dist/shared/assistant/index.d.ts +3 -0
  5. package/dist/shared/assistant/index.d.ts.map +1 -1
  6. package/dist/shared/assistant/store.d.ts +10 -0
  7. package/dist/shared/assistant/store.d.ts.map +1 -1
  8. package/dist/shared/assistant/store.js +52 -44
  9. package/dist/shared/assistant/ui/AssistantChatArea.d.ts.map +1 -1
  10. package/dist/shared/assistant/ui/AssistantChatArea.js +761 -746
  11. package/dist/shared/assistant/ui/AssistantMoreMenu.d.ts.map +1 -1
  12. package/dist/shared/assistant/ui/AssistantMoreMenu.js +58 -56
  13. package/dist/shared/assistant/ui/AssistantSpeechProvider.d.ts.map +1 -1
  14. package/dist/shared/assistant/ui/AssistantSpeechProvider.js +106 -105
  15. package/dist/shared/assistant/ui/composer/AssistantVoiceGraceSelector.d.ts +20 -0
  16. package/dist/shared/assistant/ui/composer/AssistantVoiceGraceSelector.d.ts.map +1 -0
  17. package/dist/shared/assistant/ui/composer/AssistantVoiceGraceSelector.js +39 -0
  18. package/dist/shared/assistant/ui/useVoiceSession.d.ts +35 -2
  19. package/dist/shared/assistant/ui/useVoiceSession.d.ts.map +1 -1
  20. package/dist/shared/assistant/ui/useVoiceSession.js +232 -171
  21. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.d.ts +15 -1
  22. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.d.ts.map +1 -1
  23. package/dist/shared/assistant/ui/voice/AssistantVoiceSession.js +121 -101
  24. package/dist/shared/assistant/voice/handsFree.d.ts +160 -0
  25. package/dist/shared/assistant/voice/handsFree.d.ts.map +1 -0
  26. package/dist/shared/assistant/voice/handsFree.js +104 -0
  27. package/dist/shared/assistant/voice/supersededTurns.d.ts +29 -0
  28. package/dist/shared/assistant/voice/supersededTurns.d.ts.map +1 -0
  29. package/dist/shared/assistant/voice/supersededTurns.js +17 -0
  30. package/dist/shared/assistant/voice/useVoiceInput.d.ts +7 -0
  31. package/dist/shared/assistant/voice/useVoiceInput.d.ts.map +1 -1
  32. package/dist/shared/assistant/voice/useVoiceInput.js +13 -10
  33. package/dist/shared/assistant/voice/voiceSession.d.ts +64 -20
  34. package/dist/shared/assistant/voice/voiceSession.d.ts.map +1 -1
  35. package/dist/shared/assistant/voice/voiceSession.js +120 -67
  36. package/dist/shared-assistant.js +206 -203
  37. package/package.json +1 -1
@@ -3,44 +3,44 @@ import { useAssistantTr as t } from "../../i18n.js";
3
3
  import { micIsInterrupt as n, showsAssistantCaption as r, voiceStageState as i } from "../../voice/voiceStageState.js";
4
4
  import { VOICE_THINKING_LABEL as a, composeVoiceStatus as o, voiceStatusSuffix as ee } from "../../voice/voiceStatus.js";
5
5
  import { spokenSoFar as s } from "../../voice/voiceSession.js";
6
- import { ASSISTANT_PANEL_ATTR as c, ASSISTANT_VOICE_SESSION_ATTR as l } from "../panelOverlays.js";
7
- import { AssistantAttachmentButton as u } from "../composer/AssistantAttachmentButton.js";
8
- import { AssistantAttachmentChips as te } from "../composer/AssistantAttachmentChips.js";
9
- import { AssistantVoiceOrb as ne } from "./AssistantVoiceOrb.js";
10
- import { VoiceMiniController as re } from "./VoiceMiniController.js";
11
- import { VoiceStage as ie } from "./VoiceStage.js";
12
- import { VoiceTranscriptDrawer as ae } from "./VoiceTranscriptDrawer.js";
13
- import { escapeBelongsToLayerAbove as oe, voiceSessionEscapeAction as se } from "./voiceSessionEscape.js";
14
- import { useCallback as d, useEffect as f, useRef as p, useState as m } from "react";
15
- import { Maximize2 as ce, Minimize2 as le, ScrollText as ue, X as h } from "lucide-react";
16
- import { jsx as g, jsxs as _ } from "react/jsx-runtime";
6
+ import { ASSISTANT_PANEL_ATTR as te, ASSISTANT_VOICE_SESSION_ATTR as ne } from "../panelOverlays.js";
7
+ import { AssistantAttachmentButton as re } from "../composer/AssistantAttachmentButton.js";
8
+ import { AssistantAttachmentChips as ie } from "../composer/AssistantAttachmentChips.js";
9
+ import { AssistantVoiceOrb as ae } from "./AssistantVoiceOrb.js";
10
+ import { VoiceMiniController as oe } from "./VoiceMiniController.js";
11
+ import { VoiceStage as se } from "./VoiceStage.js";
12
+ import { VoiceTranscriptDrawer as ce } from "./VoiceTranscriptDrawer.js";
13
+ import { escapeBelongsToLayerAbove as le, voiceSessionEscapeAction as ue } from "./voiceSessionEscape.js";
14
+ import { useCallback as c, useEffect as l, useRef as u, useState as d } from "react";
15
+ import { Maximize2 as de, Minimize2 as fe, ScrollText as pe, X as f } from "lucide-react";
16
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
17
17
  //#region src/shared/assistant/ui/voice/AssistantVoiceSession.tsx
18
- var v = ({ session: v, turns: pe = [], onSaveTranscript: me, onContinueInChat: he, notice: y = null, spoken: b, onInterrupt: ge, onToggleMute: x, attachments: S = [], onAttachFiles: C, onRemoveAttachment: _e, onRetryAttachment: ve, canAttachMore: ye = !0, preparingAttachments: w = 0, spokenChunks: T = null, onEnd: E, capturing: be = !0, interrupting: xe = !1, awaitingEndpoint: Se = !1, proposalChip: D = null, proposalTray: O = null }) => {
19
- let k = t(), { muted: A, error: j } = v, M = p(null), N = p(null), [P, F] = m("mini"), I = P === "full", L = P === "mini", R = P === "mini" ? "panel" : P === "panel" ? "full" : "mini", [z, B] = m(!1), V = O !== null && !L, H = p(!1), U = p(x);
20
- U.current = x;
21
- let W = d(() => {
22
- v.phase === "listening" && (A || H.current || s(v) || (H.current = !0, U.current()));
23
- }, [A, v]), G = d(() => {
18
+ var h = ({ session: h, turns: ge = [], onSaveTranscript: _e, onContinueInChat: ve, notice: g = null, spoken: _, onInterrupt: ye, onToggleMute: v, attachments: y = [], onAttachFiles: b, onRemoveAttachment: be, onRetryAttachment: xe, canAttachMore: Se = !0, preparingAttachments: x = 0, spokenChunks: S = null, onEnd: C, capturing: Ce = !0, interrupting: we = !1, awaitingEndpoint: Te = !1, holding: w = !1, correcting: Ee = !1, onLetMeFinish: T, onImDone: E, proposalChip: D = null, proposalTray: O = null }) => {
19
+ let k = t(), { muted: A, error: j } = h, M = u(null), N = u(null), [P, F] = d("mini"), I = P === "full", L = P === "mini", R = P === "mini" ? "panel" : P === "panel" ? "full" : "mini", [z, B] = d(!1), V = O !== null && !L, H = u(!1), U = u(v);
20
+ U.current = v;
21
+ let W = c(() => {
22
+ h.phase !== "listening" && h.phase !== "thinking" || A || H.current || s(h) || (H.current = !0, U.current());
23
+ }, [A, h]), G = c(() => {
24
24
  H.current && (H.current = !1, U.current());
25
- }, []), K = W;
26
- f(() => {
25
+ }, []), De = W;
26
+ l(() => {
27
27
  let e = () => {
28
28
  document.visibilityState === "hidden" ? W() : G();
29
29
  };
30
30
  return document.addEventListener("visibilitychange", e), () => document.removeEventListener("visibilitychange", e);
31
- }, [W, G]), f(() => {
31
+ }, [W, G]), l(() => {
32
32
  let e = G;
33
33
  return window.addEventListener("focus", e), () => {
34
34
  window.removeEventListener("focus", e), H.current = !1;
35
35
  };
36
- }, []), f(() => {
36
+ }, []), l(() => {
37
37
  M.current?.focus();
38
- }, []), e(N, { fallbackSelector: `[${c}]` }), f(() => {
38
+ }, []), e(N, { fallbackSelector: `[${te}]` }), l(() => {
39
39
  let e = (e) => {
40
40
  if (e.key === "Escape") {
41
- let t = se({
41
+ let t = ue({
42
42
  defaultPrevented: e.defaultPrevented,
43
- layerAbove: oe({
43
+ layerAbove: le({
44
44
  target: e.target,
45
45
  session: N.current,
46
46
  doc: document
@@ -48,7 +48,7 @@ var v = ({ session: v, turns: pe = [], onSaveTranscript: me, onContinueInChat: h
48
48
  transcriptOpen: z
49
49
  });
50
50
  if (t === "ignore") return;
51
- e.preventDefault(), t === "close-transcript" ? B(!1) : V ? O?.onClose() : E();
51
+ e.preventDefault(), t === "close-transcript" ? B(!1) : V ? O?.onClose() : C();
52
52
  return;
53
53
  }
54
54
  if (e.key !== "Tab" || !I) return;
@@ -59,19 +59,28 @@ var v = ({ session: v, turns: pe = [], onSaveTranscript: me, onContinueInChat: h
59
59
  };
60
60
  return window.addEventListener("keydown", e), () => window.removeEventListener("keydown", e);
61
61
  }, [
62
- E,
62
+ C,
63
63
  I,
64
64
  z,
65
65
  V,
66
66
  O
67
67
  ]);
68
- let q = y !== null && j === null, J = s(v), Y = i({
69
- snapshot: v,
70
- capturing: be,
71
- interrupting: xe,
72
- awaitingEndpoint: Se
73
- }), X = (() => {
74
- switch (Y) {
68
+ let K = g !== null && j === null, q = s(h), J = T && E && h.phase === "listening" && !A && j === null ? {
69
+ letMeFinish: T,
70
+ imDone: E
71
+ } : null, Y = J !== null;
72
+ l(() => {
73
+ if (Y) return;
74
+ let e = document.activeElement;
75
+ (!e || e === document.body) && M.current?.focus();
76
+ }, [Y]);
77
+ let X = i({
78
+ snapshot: h,
79
+ capturing: Ce,
80
+ interrupting: we,
81
+ awaitingEndpoint: Te
82
+ }), Oe = (() => {
83
+ switch (X) {
75
84
  case "connecting": return k("voiceSession.connecting", "Starting the microphone…");
76
85
  case "endpoint-grace": return k("voiceSession.endpointGrace", "Still listening — take your time");
77
86
  case "interrupting": return k("voiceSession.interrupting", "Stopped — go ahead");
@@ -80,79 +89,90 @@ var v = ({ session: v, turns: pe = [], onSaveTranscript: me, onContinueInChat: h
80
89
  case "mic-error":
81
90
  case "ended": return "";
82
91
  case "listening": return A ? k("voiceSession.muted", "Microphone off") : k("voiceSession.listening", "Listening…");
83
- default: return Y;
92
+ default: return X;
84
93
  }
85
- })(), Z = ee({
94
+ })(), Z = !A && (X === "listening" || X === "endpoint-grace"), Q = Z && w ? k("voiceSession.holding", "Take your time — tap “I’m done” when you finish") : Z && Ee ? k("voiceSession.correcting", "Stopped that answer — listening to your correction") : Oe, $ = ee({
86
95
  tr: k,
87
- stage: Y,
96
+ stage: X,
88
97
  muted: A,
89
- label: X
90
- }), Q = o(X, Z), $ = Y === "thinking";
91
- return /* @__PURE__ */ _("div", {
98
+ label: Q
99
+ }), ke = o(Q, $), Ae = X === "thinking";
100
+ return /* @__PURE__ */ m("div", {
92
101
  ref: N,
93
102
  className: `flex flex-col items-center justify-between bg-bg-sunken ${P === "full" ? "fixed inset-x-0 bottom-0 top-14 z-[60] px-6 py-8" : P === "panel" ? "absolute inset-0 z-20 px-4 py-6" : "absolute inset-x-0 bottom-0 z-20 h-[11.25rem] border-t border-border-subtle px-3 py-3 shadow-lg"}`,
94
103
  role: "dialog",
95
104
  "aria-modal": I || void 0,
96
105
  "aria-label": k("voiceSession.title", "Voice conversation"),
97
106
  "data-testid": "assistant-voice-session",
98
- [l]: "",
107
+ [ne]: "",
99
108
  children: [
100
- /* @__PURE__ */ _("button", {
109
+ /* @__PURE__ */ m("button", {
101
110
  ref: M,
102
111
  type: "button",
103
- onClick: E,
112
+ onClick: C,
104
113
  className: `absolute ${I ? "right-6 top-6 h-14 gap-2 px-5 text-base" : L ? "right-12 top-2 h-8 gap-1.5 px-2.5 text-xs" : "right-3 top-3 h-11 gap-2 px-4 text-sm"} flex items-center justify-center rounded-full border border-border-default bg-bg-elevated text-text-secondary hover:bg-bg-sunken transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong`,
105
114
  title: k("voiceSession.end", "End the voice conversation"),
106
115
  "data-testid": "assistant-voice-end",
107
- children: [/* @__PURE__ */ g(h, {
116
+ children: [/* @__PURE__ */ p(f, {
108
117
  className: I ? "size-6" : L ? "size-4" : "size-5",
109
118
  "aria-hidden": "true"
110
- }), /* @__PURE__ */ g("span", {
119
+ }), /* @__PURE__ */ p("span", {
111
120
  className: `truncate whitespace-nowrap font-medium ${L ? "max-w-[3.5rem] text-xs" : "max-w-[7rem]"}`,
112
121
  children: k("voiceSession.endShort", "End")
113
122
  })]
114
123
  }),
115
- /* @__PURE__ */ g("div", {
116
- className: `flex flex-col items-center ${L ? "px-3" : `min-h-[1.5rem] ${q ? I ? "pt-14" : "pt-10" : ""}`}`,
117
- children: q ? /* @__PURE__ */ g("p", {
124
+ /* @__PURE__ */ p("div", {
125
+ className: `flex flex-col items-center ${L ? "px-3" : `min-h-[1.5rem] ${K ? I ? "pt-14" : "pt-10" : ""}`}`,
126
+ children: K ? /* @__PURE__ */ p("p", {
118
127
  className: "max-w-xs text-center text-xs text-status-warning",
119
128
  "data-testid": "assistant-voice-notice",
120
- children: de(y, k)
129
+ children: me(g, k)
121
130
  }) : null
122
131
  }),
123
132
  (() => {
124
- let e = /* @__PURE__ */ g(ne, {
125
- session: v,
133
+ let e = /* @__PURE__ */ p(ae, {
134
+ session: h,
126
135
  compact: L,
127
- connecting: Y === "connecting",
128
- onPress: n(Y) ? ge : x,
129
- label: n(Y) ? k("voiceSession.interrupt", "Stop the assistant and speak") : A ? k("voiceSession.unmute", "Turn the microphone on") : k("voiceSession.mute", "Turn the microphone off")
130
- }), t = r(Y) ? b : J || null, i = j === null ? null : fe(j, k);
131
- return L ? /* @__PURE__ */ g(re, {
132
- spokenChunks: T,
133
- state: Y,
134
- caption: t,
135
- statusText: Q,
136
- statusSuffix: Z,
137
- rotateStatus: $,
138
- errorText: i,
139
- control: e
140
- }) : /* @__PURE__ */ g(ie, {
141
- state: Y,
142
- userCaption: J || null,
143
- spokenChunks: T,
144
- assistantCaption: b,
136
+ connecting: X === "connecting",
137
+ onPress: n(X) ? ye : v,
138
+ label: n(X) ? k("voiceSession.interrupt", "Stop the assistant and speak") : A ? k("voiceSession.unmute", "Turn the microphone on") : k("voiceSession.mute", "Turn the microphone off")
139
+ }), t = J ? /* @__PURE__ */ p("button", {
140
+ type: "button",
141
+ onClick: w ? J.imDone : J.letMeFinish,
142
+ className: `flex items-center justify-center rounded-full border font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong ${L ? "h-7 px-3 text-xs" : I ? "h-11 px-6 text-base" : "h-9 px-4 text-sm"} ${w ? "border-transparent bg-action-primary-bg text-action-primary-text hover:opacity-90" : "border-border-default bg-bg-elevated text-text-secondary hover:bg-bg-sunken"}`,
143
+ title: w ? k("voiceSession.imDoneHint", "Send what you said now") : k("voiceSession.letMeFinishHint", "Pause sending until you tap “I’m done”"),
144
+ "data-testid": "assistant-voice-hold",
145
+ "data-holding": w ? "true" : "false",
146
+ children: w ? k("voiceSession.imDone", "I’m done") : k("voiceSession.letMeFinish", "Let me finish")
147
+ }) : null, i = t ? /* @__PURE__ */ m("div", {
148
+ className: "flex flex-col items-center gap-2",
149
+ children: [e, t]
150
+ }) : e, a = r(X) ? _ : q || null, o = j === null ? null : he(j, k);
151
+ return L ? /* @__PURE__ */ p(oe, {
152
+ spokenChunks: S,
153
+ state: X,
154
+ caption: a,
155
+ statusText: ke,
156
+ statusSuffix: $,
157
+ rotateStatus: Ae,
158
+ errorText: o,
159
+ control: i
160
+ }) : /* @__PURE__ */ p(se, {
161
+ state: X,
162
+ userCaption: q || null,
163
+ spokenChunks: S,
164
+ assistantCaption: _,
145
165
  expanded: I,
146
- statusText: Q,
147
- statusSuffix: Z,
148
- rotateStatus: $,
149
- errorText: i,
150
- control: e
166
+ statusText: ke,
167
+ statusSuffix: $,
168
+ rotateStatus: Ae,
169
+ errorText: o,
170
+ control: i
151
171
  });
152
172
  })(),
153
- D && !V ? /* @__PURE__ */ g("div", {
173
+ D && !V ? /* @__PURE__ */ p("div", {
154
174
  className: L ? "absolute left-20 right-36 top-2 flex justify-center" : `flex w-full justify-center ${I ? "max-w-[calc(100%-12rem)]" : "max-w-[calc(100%-8rem)]"}`,
155
- children: /* @__PURE__ */ g("button", {
175
+ children: /* @__PURE__ */ p("button", {
156
176
  type: "button",
157
177
  onClick: () => {
158
178
  F("panel"), D.onOpen();
@@ -162,72 +182,72 @@ var v = ({ session: v, turns: pe = [], onSaveTranscript: me, onContinueInChat: h
162
182
  children: D.label
163
183
  })
164
184
  }) : null,
165
- O && V ? /* @__PURE__ */ _("div", {
185
+ O && V ? /* @__PURE__ */ m("div", {
166
186
  className: `absolute z-10 flex flex-col gap-2 overflow-y-auto rounded-lg border border-border-subtle bg-bg-elevated shadow-lg ${I ? "inset-x-6 bottom-24 top-24 mx-auto max-w-xl p-4" : "inset-x-3 bottom-16 top-16 p-3"}`,
167
187
  "data-testid": "assistant-voice-proposal-tray",
168
- children: [/* @__PURE__ */ g("div", {
188
+ children: [/* @__PURE__ */ p("div", {
169
189
  className: "flex justify-end",
170
- children: /* @__PURE__ */ g("button", {
190
+ children: /* @__PURE__ */ p("button", {
171
191
  type: "button",
172
192
  onClick: O.onClose,
173
193
  className: "flex size-8 items-center justify-center rounded-full text-text-secondary transition-colors hover:bg-bg-raised hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong",
174
194
  "aria-label": O.closeLabel,
175
195
  "data-testid": "assistant-voice-proposal-tray-close",
176
- children: /* @__PURE__ */ g(h, {
196
+ children: /* @__PURE__ */ p(f, {
177
197
  className: "size-4",
178
198
  "aria-hidden": "true"
179
199
  })
180
200
  })
181
201
  }), O.content]
182
202
  }) : null,
183
- z ? /* @__PURE__ */ g(ae, {
184
- turns: pe,
203
+ z ? /* @__PURE__ */ p(ce, {
204
+ turns: ge,
185
205
  onClose: () => B(!1),
186
- onSave: me,
187
- onContinueInChat: he
206
+ onSave: _e,
207
+ onContinueInChat: ve
188
208
  }) : null,
189
- C ? /* @__PURE__ */ g("div", {
209
+ b ? /* @__PURE__ */ p("div", {
190
210
  className: `absolute ${I ? "left-6 top-6" : L ? "left-11 top-2" : "left-3 top-3"}`,
191
- children: /* @__PURE__ */ g(u, {
211
+ children: /* @__PURE__ */ p(re, {
192
212
  onFiles: (e) => {
193
- C(e), F((e) => e === "mini" ? "panel" : e);
213
+ b(e), F((e) => e === "mini" ? "panel" : e);
194
214
  },
195
- onPickerOpen: K,
215
+ onPickerOpen: De,
196
216
  direction: "down",
197
- canAddMore: ye
217
+ canAddMore: Se
198
218
  })
199
219
  }) : null,
200
- (S.length > 0 || w > 0) && !L ? /* @__PURE__ */ g("div", {
220
+ (y.length > 0 || x > 0) && !L ? /* @__PURE__ */ p("div", {
201
221
  className: `absolute ${I ? "bottom-24 left-24 right-24" : "bottom-16 left-16 right-16"}`,
202
222
  "data-testid": "assistant-voice-attachments",
203
- children: /* @__PURE__ */ g(te, {
204
- attachments: S,
205
- onRemove: _e ?? (() => void 0),
206
- onRetry: ve ?? (() => void 0),
207
- preparingCount: w
223
+ children: /* @__PURE__ */ p(ie, {
224
+ attachments: y,
225
+ onRemove: be ?? (() => void 0),
226
+ onRetry: xe ?? (() => void 0),
227
+ preparingCount: x
208
228
  })
209
229
  }) : null,
210
- /* @__PURE__ */ g("button", {
230
+ /* @__PURE__ */ p("button", {
211
231
  type: "button",
212
232
  onClick: () => B((e) => !e),
213
233
  className: `absolute ${I ? "bottom-6 right-6 size-14" : L ? "right-2 top-2 size-8" : "bottom-3 right-3 size-11"} flex items-center justify-center rounded-full bg-bg-elevated text-text-secondary transition-colors hover:bg-bg-raised hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong`,
214
234
  "aria-pressed": z,
215
235
  "aria-label": k("voiceSession.transcriptTitle", "Voice transcript"),
216
236
  "data-testid": "assistant-voice-transcript-toggle",
217
- children: /* @__PURE__ */ g(ue, { className: I ? "size-6" : L ? "size-4" : "size-5" })
237
+ children: /* @__PURE__ */ p(pe, { className: I ? "size-6" : L ? "size-4" : "size-5" })
218
238
  }),
219
- /* @__PURE__ */ g("button", {
239
+ /* @__PURE__ */ p("button", {
220
240
  type: "button",
221
241
  onClick: () => F(R),
222
242
  className: `absolute ${I ? "bottom-6 left-6 size-14" : L ? "left-2 top-2 size-8" : "bottom-3 left-3 size-11"} flex items-center justify-center rounded-full bg-bg-elevated text-text-secondary transition-colors hover:bg-bg-raised hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-border-strong`,
223
243
  "aria-label": R === "panel" ? k("voiceSession.expandPanel", "Show the conversation in the panel") : R === "full" ? k("voiceSession.expand", "Expand the conversation to full screen") : k("voiceSession.collapse", "Shrink to the compact controller"),
224
244
  "data-testid": "assistant-voice-expand",
225
- children: I ? /* @__PURE__ */ g(le, { className: "size-5" }) : /* @__PURE__ */ g(ce, { className: L ? "size-4" : "size-5" })
245
+ children: I ? /* @__PURE__ */ p(fe, { className: "size-5" }) : /* @__PURE__ */ p(de, { className: L ? "size-4" : "size-5" })
226
246
  })
227
247
  ]
228
248
  });
229
249
  };
230
- function de(e, t) {
250
+ function me(e, t) {
231
251
  switch (e) {
232
252
  case "network": return t("voiceSession.noticeNetwork", "Speech recognition lost its connection. Check your network — it will retry.");
233
253
  case "not-capturing": return t("voiceSession.noticeNotCapturing", "The microphone did not start. Check that this site is allowed to use it, then try again.");
@@ -236,7 +256,7 @@ function de(e, t) {
236
256
  default: return e;
237
257
  }
238
258
  }
239
- function fe(e, t) {
259
+ function he(e, t) {
240
260
  switch (e) {
241
261
  case "mic-denied": return t("voiceSession.micDenied", "Microphone access is blocked. Allow it in your browser settings to talk.");
242
262
  case "no-speech-support": return t("voiceSession.noSpeechSupport", "This browser cannot listen. Try a different browser, or type instead.");
@@ -245,4 +265,4 @@ function fe(e, t) {
245
265
  }
246
266
  }
247
267
  //#endregion
248
- export { v as AssistantVoiceSession };
268
+ export { h as AssistantVoiceSession };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Hands-free voice, steps H1 and H2 — the pure rules (BOFF hands-free slice).
3
+ *
4
+ * Everything here is data in, decision out: no DOM, no timers, no React, for
5
+ * the reason `voiceSession.ts` gives (this repo's unit runner has no browser).
6
+ * The hook (`useVoiceSession`) owns the clock and the recogniser; this file
7
+ * owns what the words and the preference MEAN.
8
+ *
9
+ * ★ H1 — the end-of-speech grace is a USER PREFERENCE (2 s default, 3 s, 5 s),
10
+ * and there is exactly ONE timing owner: `planSubmitTimer`. The old
11
+ * `SILENCE_SUBMIT_MS` constant is gone; the value the timer waits is the
12
+ * preference, resolved by `resolveVoiceGraceMs`. Nothing else in the package
13
+ * waits before submitting a spoken turn (`ENDPOINT_GRACE_MS` only decides when
14
+ * the stage SHOWS the pause, and is clamped inside the grace).
15
+ *
16
+ * ★ H2 — corrections while the assistant is still PREPARING an answer. The
17
+ * microphone stays open in `thinking`, so a recogniser can deliver three kinds
18
+ * of result there, and telling them apart is the whole difficulty:
19
+ * 1. a trailing result of the turn JUST submitted (Chrome finalises late;
20
+ * Chrome Android re-sends the whole utterance, sometimes revised) — must
21
+ * be ignored, or every spoken question is aborted and sent twice;
22
+ * 2. noise and fillers ("uh", "hmm") — must not abort a model turn;
23
+ * 3. a person speaking — a correction: the turn is superseded.
24
+ * `planThinkingSpeech` is that classification.
25
+ *
26
+ * A spoken "yes" never commits a business write. In `thinking` a bare
27
+ * affirmation is therefore NOT a correction at all: it neither aborts the turn
28
+ * nor withdraws anything (`classifyWriteReply` is the same classifier the 5a
29
+ * binder uses). The binder, and the card's button, stay the only way a change
30
+ * is confirmed.
31
+ */
32
+ /** The choices offered in the voice settings, shortest first. */
33
+ export declare const VOICE_GRACE_CHOICES_MS: readonly [2000, 3000, 5000];
34
+ export type VoiceGraceMs = (typeof VOICE_GRACE_CHOICES_MS)[number];
35
+ /**
36
+ * How long the user must stop talking before the turn is submitted, until they
37
+ * choose otherwise.
38
+ *
39
+ * ★ 2000, not 1200: 1.2 s does not survive an ordinary pause. Once a turn is
40
+ * submitted the reducer discards the rest of the sentence, so cutting someone
41
+ * off costs the whole round trip AND makes them repeat themselves — waiting
42
+ * 800 ms longer costs only a moment of silence. The asymmetry is what settled
43
+ * the number, and it is why the longer choices exist for people who pause more.
44
+ */
45
+ export declare const DEFAULT_VOICE_GRACE_MS: VoiceGraceMs;
46
+ /**
47
+ * Read a stored or passed value as one of the offered choices.
48
+ *
49
+ * Anything else — a stale number from an older build, a string, `null`, a value
50
+ * hand-edited into storage — is the default, never a nearby choice: a grace of
51
+ * 0 or 600000 ms read back from storage would either submit mid-word or never
52
+ * submit at all.
53
+ */
54
+ export declare function resolveVoiceGraceMs(value: unknown): VoiceGraceMs;
55
+ /**
56
+ * The ONE decision about the submit timer.
57
+ *
58
+ * `arm` is true only for a listening, unmuted session with something worth
59
+ * sending, and NOT while the person asked to finish in their own time
60
+ * ("Let me finish", `holding`). `delayMs` is the preference. The hook re-asks
61
+ * on every change of the session, so new speech cancels a pending submit and
62
+ * the turn continues — there is no second timer to forget.
63
+ */
64
+ export declare function planSubmitTimer(input: {
65
+ phase: string;
66
+ muted: boolean;
67
+ /** Everything heard this turn (`spokenSoFar`). */
68
+ spoken: string;
69
+ /** "Let me finish" is on: nothing is submitted until the person says done. */
70
+ holding: boolean;
71
+ /** The preference, in ms. */
72
+ graceMs: number;
73
+ }): {
74
+ arm: boolean;
75
+ delayMs: number;
76
+ };
77
+ /**
78
+ * The comparable words of a transcript: letters, marks and digits only (marks
79
+ * kept — Tamil and Hindi vowel signs are marks), lower-cased. Punctuation and
80
+ * capitals a final adds are not speech.
81
+ */
82
+ export declare function speechWords(text: string): string[];
83
+ /**
84
+ * Add a newly finalised phrase to the turn heard so far.
85
+ *
86
+ * ★ Chrome Android duplicates finals. With `continuous` on, a restarted
87
+ * recogniser can re-send the phrase it just finalised, and it often re-sends
88
+ * the WHOLE utterance so far with a few words added. Plain concatenation turned
89
+ * "when should I sow" into "when should I sow when should I sow ragi" and sent
90
+ * that. So:
91
+ * - a phrase that STARTS WITH everything heard is the same utterance, longer
92
+ * or equal: it replaces what was heard;
93
+ * - a phrase of two or more words that the turn already ENDS with is a
94
+ * repeat: dropped;
95
+ * - anything else is new speech: appended.
96
+ * A single repeated word is kept unless it is the whole turn — "no no" is
97
+ * emphasis, but a lone "hello" arriving twice is the recogniser.
98
+ */
99
+ export declare function mergeFinalPhrase(pending: string, phrase: string): string;
100
+ /**
101
+ * A transcript without the words of the turn it follows, when it starts with
102
+ * them (a cumulative result: "when should I sow ragi" after "when should I
103
+ * sow"). Anything else is returned unchanged.
104
+ */
105
+ export declare function stripSubmittedPrefix(transcript: string, submitted: string): string;
106
+ /**
107
+ * How long after a turn is submitted its OWN trailing results may still arrive.
108
+ *
109
+ * The person was silent for the whole grace (2 s at least), so anything
110
+ * arriving this soon after the submit and resembling what was sent is the
111
+ * recogniser finishing the sentence, not a correction. Past it, only an exact
112
+ * repeat is ignored — a person correcting "plant ragi in June" with "no plant
113
+ * ragi in July" is speaking, however alike the words are.
114
+ */
115
+ export declare const TRAILING_RESULT_WINDOW_MS = 1500;
116
+ /**
117
+ * Is this transcript the recogniser finishing, repeating or revising the turn
118
+ * that was just submitted?
119
+ *
120
+ * `sinceSubmitMs` is null when unknown (no clock): then only an exact repeat
121
+ * counts, which can only ever err towards treating speech as speech.
122
+ */
123
+ export declare function isTrailingResultOfSubmitted(transcript: string, submitted: string, sinceSubmitMs: number | null): boolean;
124
+ /**
125
+ * Is this a person speaking, rather than noise?
126
+ *
127
+ * Aborting a model turn costs the turn, so the bar is two letters or digits
128
+ * that are not all filler. "no" and "ok" pass; "a", "uh" and "hmm" do not. A
129
+ * single Han, kana or Hangul character passes — "不" is a whole word.
130
+ */
131
+ export declare function isGenuineUserSpeech(text: string): boolean;
132
+ /** What a result heard while the answer is being prepared amounts to. */
133
+ export type ThinkingSpeechPlan = {
134
+ kind: "ignore";
135
+ reason: "empty" | "trailing" | "noise" | "affirmation";
136
+ } | {
137
+ kind: "supersede";
138
+ /** What the person said, without the words of the turn it corrects. */
139
+ text: string;
140
+ };
141
+ /**
142
+ * Classify a result that arrives while the assistant is PREPARING an answer.
143
+ *
144
+ * Ignored: nothing; the recogniser finishing, repeating or revising the turn
145
+ * just submitted; noise; and a bare affirmation (a spoken "yes" is never a
146
+ * correction, and never commits a write — see the header). Everything else is
147
+ * a correction.
148
+ *
149
+ * Echo is not checked here: nothing plays during `thinking`. The caller still
150
+ * passes the previous reply through `isEcho` first, because its tail can sound
151
+ * for a moment after playback ends.
152
+ */
153
+ export declare function planThinkingSpeech(input: {
154
+ transcript: string;
155
+ /** The turn in flight, as it was sent. */
156
+ submitted: string;
157
+ /** Milliseconds since it was sent, or null when no clock was given. */
158
+ sinceSubmitMs: number | null;
159
+ }): ThinkingSpeechPlan;
160
+ //# sourceMappingURL=handsFree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handsFree.d.ts","sourceRoot":"","sources":["../../../../src/shared/assistant/voice/handsFree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAQH,iEAAiE;AACjE,eAAO,MAAM,sBAAsB,6BAA8B,CAAC;AAElE,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,EAAE,YAAmB,CAAC;AAEzD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAEhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,OAAO,CAAC;IACjB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAIpC;AAMD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMlD;AA6BD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAQxE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAelF;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAK9C;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAYT;AAyBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,OAAO,GAAG,aAAa,CAAA;CAAE,GAC1E;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEN;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,kBAAkB,CASrB"}
@@ -0,0 +1,104 @@
1
+ import { classifyWriteReply as e } from "../writeReplyBinder.js";
2
+ //#region src/shared/assistant/voice/handsFree.ts
3
+ var t = [
4
+ 2e3,
5
+ 3e3,
6
+ 5e3
7
+ ], n = 2e3;
8
+ function r(e) {
9
+ return t.find((t) => t === e) ?? 2e3;
10
+ }
11
+ function i(e) {
12
+ let t = r(e.graceMs);
13
+ return {
14
+ arm: e.phase === "listening" && !e.muted && !e.holding && e.spoken.trim().length > 0,
15
+ delayMs: t
16
+ };
17
+ }
18
+ function a(e) {
19
+ let t = e.toLowerCase().replace(/[^\p{L}\p{M}\p{N}]+/gu, " ").trim();
20
+ return t ? t.split(" ") : [];
21
+ }
22
+ var o = (e, t) => t.length <= e.length && t.every((t, n) => e[n] === t), s = (e, t) => t.length <= e.length && t.every((n, r) => e[e.length - t.length + r] === n), c = (e, t) => {
23
+ if (t.length === 0) return !0;
24
+ for (let n = 0; n + t.length <= e.length; n += 1) if (t.every((t, r) => e[n + r] === t)) return !0;
25
+ return !1;
26
+ };
27
+ function l(e, t) {
28
+ let n = Array(t.length + 1).fill(0);
29
+ for (let r of e) {
30
+ let e = Array(t.length + 1).fill(0);
31
+ for (let i = 1; i <= t.length; i += 1) e[i] = r === t[i - 1] ? (n[i - 1] ?? 0) + 1 : Math.max(n[i] ?? 0, e[i - 1] ?? 0);
32
+ n = e;
33
+ }
34
+ return n[t.length] ?? 0;
35
+ }
36
+ function u(e, t) {
37
+ let n = a(e), r = a(t);
38
+ return r.length === 0 ? e : n.length === 0 || o(r, n) ? t.replace(/\s+/g, " ").trim() : r.length >= 2 && s(n, r) ? e : `${e} ${t}`.replace(/\s+/g, " ").trim();
39
+ }
40
+ function d(e, t) {
41
+ let n = a(t);
42
+ if (n.length === 0) return e.replace(/\s+/g, " ").trim();
43
+ let r = e.split(/\s+/).filter(Boolean), i = 0;
44
+ for (let t = 0; t < r.length; t += 1) {
45
+ let o = a(r[t] ?? "");
46
+ if (o.length !== 0) for (let a of o) {
47
+ if (a !== n[i]) return e.replace(/\s+/g, " ").trim();
48
+ if (i += 1, i === n.length) return r.slice(t + 1).join(" ").trim();
49
+ }
50
+ }
51
+ return e.replace(/\s+/g, " ").trim();
52
+ }
53
+ var f = 1500, p = .6;
54
+ function m(e, t, n) {
55
+ let r = a(e), i = a(t);
56
+ return r.length === 0 ? !0 : i.length === 0 ? !1 : r.length === i.length && r.every((e, t) => e === i[t]) ? !0 : n === null || n >= 1500 ? !1 : c(i, r) ? !0 : o(r, i) && r.length - i.length >= 2 ? !1 : l(r, i) / Math.max(r.length, i.length) >= p;
57
+ }
58
+ var h = new Set([
59
+ "uh",
60
+ "um",
61
+ "uhm",
62
+ "umm",
63
+ "er",
64
+ "erm",
65
+ "eh",
66
+ "ah",
67
+ "hm",
68
+ "hmm",
69
+ "hmmm",
70
+ "mm",
71
+ "mmm",
72
+ "mhm",
73
+ "huh",
74
+ "oh"
75
+ ]), g = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
76
+ function _(e) {
77
+ let t = a(e);
78
+ if (t.length === 0 || t.every((e) => h.has(e))) return !1;
79
+ let n = [...t.join("")];
80
+ return n.length >= 2 ? !0 : n.length === 1 && g.test(n[0] ?? "");
81
+ }
82
+ function v(t) {
83
+ if (a(t.transcript).length === 0) return {
84
+ kind: "ignore",
85
+ reason: "empty"
86
+ };
87
+ if (m(t.transcript, t.submitted, t.sinceSubmitMs)) return {
88
+ kind: "ignore",
89
+ reason: "trailing"
90
+ };
91
+ let n = d(t.transcript, t.submitted);
92
+ return _(n) ? e(n, "en") === "affirm" ? {
93
+ kind: "ignore",
94
+ reason: "affirmation"
95
+ } : {
96
+ kind: "supersede",
97
+ text: n
98
+ } : {
99
+ kind: "ignore",
100
+ reason: "noise"
101
+ };
102
+ }
103
+ //#endregion
104
+ export { n as DEFAULT_VOICE_GRACE_MS, f as TRAILING_RESULT_WINDOW_MS, t as VOICE_GRACE_CHOICES_MS, _ as isGenuineUserSpeech, m as isTrailingResultOfSubmitted, u as mergeFinalPhrase, i as planSubmitTimer, v as planThinkingSpeech, r as resolveVoiceGraceMs, a as speechWords, d as stripSubmittedPrefix };