@epicbagel/saas-kit 1.10.0 → 1.11.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.
Files changed (43) hide show
  1. package/README.md +15 -4
  2. package/dist/{chunk-VBKJXNEL.js → chunk-5OF6D3GH.js} +4 -3
  3. package/dist/{chunk-UW67GSZ4.cjs → chunk-7QTFYEPE.cjs} +51 -25
  4. package/dist/{chunk-M2ZNC3TK.js → chunk-HLAHOVEI.js} +98 -2
  5. package/dist/chunk-KQBXAQAY.cjs +221 -0
  6. package/dist/{chunk-XB2D4NQK.cjs → chunk-NKWHDI4C.cjs} +4 -3
  7. package/dist/chunk-T3XMHWYV.js +221 -0
  8. package/dist/chunk-UOVA4YKW.js +153 -0
  9. package/dist/{chunk-O6F3LVZY.cjs → chunk-ZZ7ICQR2.cjs} +120 -24
  10. package/dist/hooks/use-dictation.cjs +2 -2
  11. package/dist/hooks/use-dictation.d.cts +5 -0
  12. package/dist/hooks/use-dictation.d.ts +5 -0
  13. package/dist/hooks/use-dictation.js +1 -1
  14. package/dist/primitives/agent-audio-visualizer-bar.cjs +2 -2
  15. package/dist/primitives/agent-audio-visualizer-bar.d.cts +1 -1
  16. package/dist/primitives/agent-audio-visualizer-bar.d.ts +1 -1
  17. package/dist/primitives/agent-audio-visualizer-bar.js +1 -1
  18. package/dist/primitives/alert.d.cts +1 -1
  19. package/dist/primitives/alert.d.ts +1 -1
  20. package/dist/primitives/badge.d.cts +1 -1
  21. package/dist/primitives/badge.d.ts +1 -1
  22. package/dist/primitives/bubble.d.cts +1 -1
  23. package/dist/primitives/bubble.d.ts +1 -1
  24. package/dist/primitives/button.d.cts +2 -2
  25. package/dist/primitives/button.d.ts +2 -2
  26. package/dist/primitives/chat.cjs +15 -15
  27. package/dist/primitives/chat.d.cts +2 -2
  28. package/dist/primitives/chat.d.ts +2 -2
  29. package/dist/primitives/chat.js +10 -10
  30. package/dist/primitives/dictation-provider.cjs +2 -2
  31. package/dist/primitives/dictation-provider.js +1 -1
  32. package/dist/primitives/voice-dictation-button.cjs +5 -3
  33. package/dist/primitives/voice-dictation-button.d.cts +5 -9
  34. package/dist/primitives/voice-dictation-button.d.ts +5 -9
  35. package/dist/primitives/voice-dictation-button.js +4 -2
  36. package/dist/primitives/voice-session-controls.cjs +3 -3
  37. package/dist/primitives/voice-session-controls.d.cts +4 -2
  38. package/dist/primitives/voice-session-controls.d.ts +4 -2
  39. package/dist/primitives/voice-session-controls.js +2 -2
  40. package/package.json +1 -1
  41. package/dist/chunk-HTCW6AEH.cjs +0 -223
  42. package/dist/chunk-KUG4ONYC.js +0 -127
  43. package/dist/chunk-KWV5KTOA.js +0 -223
@@ -0,0 +1,221 @@
1
+ "use client";
2
+ import {
3
+ AgentAudioVisualizerBar
4
+ } from "./chunk-5OF6D3GH.js";
5
+ import {
6
+ Button
7
+ } from "./chunk-W326GKML.js";
8
+ import {
9
+ cn
10
+ } from "./chunk-G6SRSWXB.js";
11
+ import {
12
+ useDictation
13
+ } from "./chunk-HLAHOVEI.js";
14
+
15
+ // src/primitives/voice-dictation-button.tsx
16
+ import {
17
+ useCallback,
18
+ useEffect,
19
+ useRef,
20
+ useState,
21
+ useSyncExternalStore
22
+ } from "react";
23
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
24
+ import { Mic, LoaderCircle } from "lucide-react";
25
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
26
+ function VoiceDictationButton({
27
+ onCommit,
28
+ onCancel,
29
+ title = "Dictate your message",
30
+ caption = "Recording",
31
+ lang = "en-US",
32
+ continuous = true,
33
+ className,
34
+ size = "sm",
35
+ showLabel = false,
36
+ children,
37
+ disabled = false,
38
+ visibility = "visible",
39
+ shrink = true
40
+ }) {
41
+ const [open, setOpen] = useState(false);
42
+ const capture = useDictation({ lang, continuous });
43
+ const { transcript, interimTranscript: interim, supported, error } = capture;
44
+ const generation = useRef(0);
45
+ const finishing = useRef(false);
46
+ const previousFocus = useRef(null);
47
+ const elapsed = useRecordingTime(capture.state);
48
+ const busy = capture.state === "requesting" || capture.state === "transcribing";
49
+ const openOverlay = useCallback(() => {
50
+ previousFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
51
+ generation.current++;
52
+ finishing.current = false;
53
+ setOpen(true);
54
+ void capture.start();
55
+ }, [capture.start]);
56
+ const closeOverlay = useCallback(() => {
57
+ generation.current++;
58
+ finishing.current = false;
59
+ capture.cancel();
60
+ setOpen(false);
61
+ }, [capture.cancel]);
62
+ const handleDone = useCallback(async () => {
63
+ if (finishing.current || busy) return;
64
+ if (capture.state === "error" && !capture.canRetry) {
65
+ void capture.start();
66
+ return;
67
+ }
68
+ finishing.current = true;
69
+ const started = generation.current;
70
+ const final = await (capture.canRetry ? capture.retry() : capture.stop());
71
+ if (started !== generation.current) return;
72
+ finishing.current = false;
73
+ if (final === null) return;
74
+ closeOverlay();
75
+ if (final) onCommit(final);
76
+ else onCancel?.();
77
+ }, [busy, capture.canRetry, capture.retry, capture.start, capture.state, capture.stop, onCommit, onCancel, closeOverlay]);
78
+ const handleCancel = useCallback(() => {
79
+ closeOverlay();
80
+ onCancel?.();
81
+ }, [closeOverlay, onCancel]);
82
+ useEffect(() => {
83
+ if (open && capture.state === "idle") handleCancel();
84
+ }, [capture.state, handleCancel, open]);
85
+ useEffect(() => {
86
+ return () => {
87
+ generation.current++;
88
+ };
89
+ }, []);
90
+ if (!supported) return null;
91
+ const displayTranscript = `${transcript}${interim ? (transcript ? " " : "") + interim : ""}`;
92
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
93
+ children ? /* @__PURE__ */ jsx(
94
+ "button",
95
+ {
96
+ type: "button",
97
+ onClick: openOverlay,
98
+ disabled,
99
+ "aria-label": "Start dictation",
100
+ className: cn(visibility === "hidden" && "hidden", className),
101
+ children
102
+ }
103
+ ) : /* @__PURE__ */ jsx(
104
+ Button,
105
+ {
106
+ type: "button",
107
+ onClick: openOverlay,
108
+ disabled,
109
+ variant: "outline",
110
+ size,
111
+ "aria-label": "Start dictation",
112
+ className: cn("transition-colors", visibility === "hidden" && "hidden", !shrink && "shrink-0", className),
113
+ children: /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-center gap-2", children: [
114
+ /* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" }),
115
+ showLabel && /* @__PURE__ */ jsx("span", { children: "Speak" })
116
+ ] })
117
+ }
118
+ ),
119
+ /* @__PURE__ */ jsx(
120
+ DialogPrimitive.Root,
121
+ {
122
+ open,
123
+ onOpenChange: (next) => {
124
+ if (!next) handleCancel();
125
+ },
126
+ children: /* @__PURE__ */ jsx(DialogPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
127
+ DialogPrimitive.Content,
128
+ {
129
+ "aria-label": "Voice dictation",
130
+ "aria-labelledby": void 0,
131
+ "data-slot": "voice-dictation-panel",
132
+ "data-state": capture.state,
133
+ onCloseAutoFocus: (event) => {
134
+ event.preventDefault();
135
+ if (previousFocus.current?.isConnected) previousFocus.current.focus();
136
+ },
137
+ onKeyDown: (event) => {
138
+ if (event.key === "Enter" && !event.shiftKey && !(event.target instanceof HTMLElement && event.target.closest("button, input, textarea, select"))) {
139
+ event.preventDefault();
140
+ void handleDone();
141
+ }
142
+ },
143
+ className: "fixed bottom-4 left-1/2 z-[200] flex max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-sm -translate-x-1/2 flex-col gap-3 overflow-y-auto rounded-2xl border border-border bg-background p-4 text-foreground shadow-lg outline-hidden sm:bottom-6",
144
+ children: [
145
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-start justify-between gap-3", children: [
146
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
147
+ /* @__PURE__ */ jsx(DialogPrimitive.Title, { className: "text-sm font-medium", children: title || "Voice dictation" }),
148
+ /* @__PURE__ */ jsxs("span", { role: "status", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
149
+ busy ? /* @__PURE__ */ jsx(LoaderCircle, { "aria-hidden": "true", className: "size-3.5 animate-spin motion-reduce:animate-none" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true", className: "size-3.5" }),
150
+ capture.state === "transcribing" ? "Transcribing\u2026" : capture.state === "requesting" ? "Waiting for microphone\u2026" : error ? "Recording not saved" : capture.state === "ready" ? "Ready" : caption
151
+ ] })
152
+ ] }),
153
+ /* @__PURE__ */ jsxs("time", { "aria-label": "Recording duration", dateTime: `PT${elapsed}S`, className: "pt-0.5 font-mono text-xs tabular-nums text-muted-foreground", children: [
154
+ Math.floor(elapsed / 60).toString().padStart(2, "0"),
155
+ ":",
156
+ (elapsed % 60).toString().padStart(2, "0")
157
+ ] })
158
+ ] }),
159
+ capture.audioLevels && /* @__PURE__ */ jsx(DictationWaveform, { source: capture.audioLevels, state: capture.state }),
160
+ /* @__PURE__ */ jsx(DialogPrimitive.Description, { className: "text-xs text-muted-foreground", children: capture.state === "transcribing" ? "Converting your recording to text\u2026" : capture.state === "requesting" ? "Allow microphone access to start." : error ? capture.canRetry ? "Retry this take or record again." : "Check microphone access, then try again." : "Speak, then choose Done to add the text." }),
161
+ displayTranscript && /* @__PURE__ */ jsxs("p", { className: "max-h-24 overflow-y-auto text-sm leading-relaxed", children: [
162
+ transcript,
163
+ interim && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
164
+ transcript ? " " : "",
165
+ interim
166
+ ] })
167
+ ] }),
168
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-xs text-destructive", children: error }),
169
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [
170
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "min-h-[44px]", onClick: handleCancel, children: "Cancel" }),
171
+ capture.canRetry && /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", className: "min-h-[44px]", onClick: () => void capture.start(), children: "Record again" }),
172
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", className: "min-h-[44px]", onClick: () => void handleDone(), disabled: busy, children: capture.state === "transcribing" ? "Transcribing\u2026" : capture.canRetry ? "Retry transcription" : error ? "Try again" : "Done" })
173
+ ] })
174
+ ]
175
+ }
176
+ ) })
177
+ }
178
+ )
179
+ ] });
180
+ }
181
+ var SILENCE = [];
182
+ var silence = () => SILENCE;
183
+ function DictationWaveform({ source, state }) {
184
+ const bands = useSyncExternalStore(source.subscribe, source.getSnapshot, silence);
185
+ return /* @__PURE__ */ jsx(
186
+ AgentAudioVisualizerBar,
187
+ {
188
+ "aria-hidden": "true",
189
+ "data-slot": "dictation-waveform",
190
+ size: "icon",
191
+ barCount: 24,
192
+ state: state === "recording" ? "listening" : state === "transcribing" ? "thinking" : state === "requesting" ? "connecting" : "disconnected",
193
+ volumeBands: state === "recording" ? bands : void 0,
194
+ className: "h-9 w-full text-foreground"
195
+ }
196
+ );
197
+ }
198
+ function useRecordingTime(state) {
199
+ const [elapsed, setElapsed] = useState(0);
200
+ const startedAt = useRef(null);
201
+ useEffect(() => {
202
+ if (state !== "recording") {
203
+ if (state === "idle" || state === "requesting") setElapsed(0);
204
+ else if (startedAt.current !== null) setElapsed(Math.floor((performance.now() - startedAt.current) / 1e3));
205
+ startedAt.current = null;
206
+ return;
207
+ }
208
+ startedAt.current = performance.now();
209
+ const update = () => setElapsed(Math.floor((performance.now() - startedAt.current) / 1e3));
210
+ update();
211
+ const timer = setInterval(update, 1e3);
212
+ return () => {
213
+ clearInterval(timer);
214
+ };
215
+ }, [state]);
216
+ return elapsed;
217
+ }
218
+
219
+ export {
220
+ VoiceDictationButton
221
+ };
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import {
3
+ AgentAudioVisualizerBar
4
+ } from "./chunk-5OF6D3GH.js";
5
+ import {
6
+ Button
7
+ } from "./chunk-W326GKML.js";
8
+ import {
9
+ cn
10
+ } from "./chunk-G6SRSWXB.js";
11
+
12
+ // src/primitives/voice-session-controls.tsx
13
+ import { useSyncExternalStore } from "react";
14
+ import { Mic, MicOff, Square, AudioLines } from "lucide-react";
15
+ import { jsx, jsxs } from "react/jsx-runtime";
16
+ var SILENCE = [];
17
+ var silence = () => SILENCE;
18
+ var noSubscription = () => () => {
19
+ };
20
+ var DEFAULT_LABELS = {
21
+ disconnected: "Voice chat",
22
+ connecting: "Connecting\u2026",
23
+ listening: "Listening\u2026",
24
+ thinking: "Thinking\u2026",
25
+ speaking: "Speaking",
26
+ reconnecting: "Reconnecting\u2026",
27
+ error: "Voice chat unavailable",
28
+ start: "Start voice chat",
29
+ end: "End voice chat",
30
+ mute: "Mute microphone",
31
+ unmute: "Unmute microphone",
32
+ muted: "Microphone muted",
33
+ retry: "Retry voice chat",
34
+ endShort: "End voice",
35
+ muteShort: "Mute",
36
+ unmuteShort: "Unmute",
37
+ transcriptPlaceholder: "Speak to continue the conversation."
38
+ };
39
+ function VoiceSessionControls({
40
+ state,
41
+ onStart,
42
+ onEnd,
43
+ muted = false,
44
+ onMutedChange,
45
+ disabled = false,
46
+ error,
47
+ transcript,
48
+ volumeBands,
49
+ audioLevels,
50
+ labels,
51
+ size = "icon",
52
+ className
53
+ }) {
54
+ const liveBands = useSyncExternalStore(audioLevels?.subscribe ?? noSubscription, audioLevels?.getSnapshot ?? silence, silence);
55
+ const copy = { ...DEFAULT_LABELS, ...labels };
56
+ const active = state !== "disconnected" && state !== "error";
57
+ const waiting = state === "connecting" || state === "reconnecting";
58
+ return /* @__PURE__ */ jsxs(
59
+ "div",
60
+ {
61
+ "data-slot": "voice-session-controls",
62
+ "data-state": state,
63
+ className: cn("min-w-0", active ? "@container/voice w-full" : "flex flex-wrap items-center gap-2", className),
64
+ children: [
65
+ active ? /* @__PURE__ */ jsxs("div", { "data-slot": "voice-session-strip", className: "flex w-full flex-wrap items-center gap-4 bg-primary/5 p-4 @2xl/voice:flex-nowrap @2xl/voice:px-5", children: [
66
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 basis-full items-center gap-3 @2xl/voice:basis-auto @2xl/voice:gap-4", children: [
67
+ /* @__PURE__ */ jsx("div", { "aria-hidden": "true", className: cn(
68
+ "flex size-12 shrink-0 items-center justify-center rounded-full ring-8 ring-primary/5 @sm/voice:size-14",
69
+ muted ? "bg-muted text-muted-foreground" : "bg-primary text-primary-foreground"
70
+ ), children: muted ? /* @__PURE__ */ jsx(MicOff, { className: "size-5" }) : /* @__PURE__ */ jsx(Mic, { className: "size-5" }) }),
71
+ /* @__PURE__ */ jsx(
72
+ AgentAudioVisualizerBar,
73
+ {
74
+ "aria-hidden": "true",
75
+ size: "sm",
76
+ barCount: 5,
77
+ className: "shrink-0 text-primary [&>div]:w-1",
78
+ state: muted && state === "listening" ? "disconnected" : state === "reconnecting" ? "connecting" : state,
79
+ volumeBands: muted && state === "listening" ? SILENCE : volumeBands ?? liveBands
80
+ }
81
+ ),
82
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
83
+ /* @__PURE__ */ jsx("span", { role: "status", className: "block text-sm font-medium text-primary", children: muted && state === "listening" ? copy.muted : copy[state] }),
84
+ /* @__PURE__ */ jsx(
85
+ "p",
86
+ {
87
+ "data-slot": "voice-session-transcript",
88
+ title: transcript || void 0,
89
+ className: cn("truncate text-sm font-normal", transcript ? "text-foreground" : "text-muted-foreground"),
90
+ children: transcript || copy.transcriptPlaceholder
91
+ }
92
+ )
93
+ ] })
94
+ ] }),
95
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [
96
+ /* @__PURE__ */ jsxs(
97
+ Button,
98
+ {
99
+ type: "button",
100
+ variant: "outline",
101
+ size: "sm",
102
+ className: "min-h-[44px] shrink-0 rounded-xl bg-background px-3 text-foreground",
103
+ "aria-label": muted ? copy.unmute : copy.mute,
104
+ title: muted ? copy.unmute : copy.mute,
105
+ "aria-pressed": muted,
106
+ disabled: waiting,
107
+ onClick: () => onMutedChange(!muted),
108
+ children: [
109
+ muted ? /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(MicOff, { "aria-hidden": "true" }),
110
+ muted ? copy.unmuteShort : copy.muteShort
111
+ ]
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsxs(
115
+ Button,
116
+ {
117
+ type: "button",
118
+ variant: "outline",
119
+ size: "sm",
120
+ className: "min-h-[44px] shrink-0 rounded-xl bg-background px-3 text-foreground",
121
+ "aria-label": copy.end,
122
+ title: copy.end,
123
+ onClick: onEnd,
124
+ children: [
125
+ /* @__PURE__ */ jsx(Square, { "aria-hidden": "true", className: "fill-current" }),
126
+ copy.endShort
127
+ ]
128
+ }
129
+ )
130
+ ] })
131
+ ] }) : /* @__PURE__ */ jsx(
132
+ Button,
133
+ {
134
+ type: "button",
135
+ size: "icon",
136
+ shape: "circle",
137
+ disabled,
138
+ onClick: onStart,
139
+ "aria-label": state === "error" ? copy.retry : copy.start,
140
+ title: state === "error" ? copy.retry : copy.start,
141
+ className: cn("min-h-[44px] min-w-[44px] shrink-0 rounded-full", size === "hero" && "size-24 [&_svg]:size-7"),
142
+ children: /* @__PURE__ */ jsx(AudioLines, { "aria-hidden": "true" })
143
+ }
144
+ ),
145
+ state === "error" && /* @__PURE__ */ jsx("span", { role: "alert", className: "basis-full text-xs text-destructive", children: error || copy.error })
146
+ ]
147
+ }
148
+ );
149
+ }
150
+
151
+ export {
152
+ VoiceSessionControls
153
+ };
@@ -12,13 +12,101 @@ function DictationProvider({ transcribe, scopeKey, children }) {
12
12
  }
13
13
  var EMPTY = { state: "idle", transcript: "", interimTranscript: "", error: null, canRetry: false };
14
14
  var MIME_TYPES = ["audio/webm;codecs=opus", "audio/mp4", "audio/ogg;codecs=opus", "audio/webm"];
15
+ var SILENCE = [];
16
+ function createAudioLevels() {
17
+ let bands = SILENCE;
18
+ const listeners = /* @__PURE__ */ new Set();
19
+ return {
20
+ subscribe: (listener) => {
21
+ listeners.add(listener);
22
+ return () => {
23
+ listeners.delete(listener);
24
+ };
25
+ },
26
+ getSnapshot: () => bands,
27
+ update: (next) => {
28
+ if (next.length === bands.length && next.every((value, index) => value === bands[index])) return;
29
+ bands = next;
30
+ listeners.forEach((listener) => listener());
31
+ }
32
+ };
33
+ }
34
+ function meterStream(stream, levels, active) {
35
+ let context;
36
+ let source;
37
+ let analyser;
38
+ let frame;
39
+ let stopped = false;
40
+ const stop = () => {
41
+ if (stopped) return;
42
+ stopped = true;
43
+ if (frame !== void 0) cancelAnimationFrame(frame);
44
+ try {
45
+ _optionalChain([source, 'optionalAccess', _2 => _2.disconnect, 'call', _3 => _3()]);
46
+ } catch (e) {
47
+ }
48
+ try {
49
+ _optionalChain([analyser, 'optionalAccess', _4 => _4.disconnect, 'call', _5 => _5()]);
50
+ } catch (e2) {
51
+ }
52
+ try {
53
+ void _optionalChain([context, 'optionalAccess', _6 => _6.close, 'call', _7 => _7(), 'access', _8 => _8.catch, 'call', _9 => _9(() => {
54
+ })]);
55
+ } catch (e3) {
56
+ }
57
+ levels.update(SILENCE);
58
+ };
59
+ try {
60
+ const Constructor = _nullishCoalesce(window.AudioContext, () => ( window.webkitAudioContext));
61
+ if (!Constructor || typeof requestAnimationFrame !== "function") return stop;
62
+ context = new Constructor();
63
+ source = context.createMediaStreamSource(stream);
64
+ analyser = context.createAnalyser();
65
+ analyser.fftSize = 512;
66
+ analyser.smoothingTimeConstant = 0.7;
67
+ source.connect(analyser);
68
+ const bins = new Uint8Array(analyser.frequencyBinCount);
69
+ const binWidth = context.sampleRate / analyser.fftSize;
70
+ const highest = Math.min(8e3, context.sampleRate / 2);
71
+ let sampledAt = -Infinity;
72
+ const sample = (time) => {
73
+ if (stopped) return;
74
+ if (!active()) {
75
+ stop();
76
+ return;
77
+ }
78
+ try {
79
+ if (time - sampledAt >= 40) {
80
+ sampledAt = time;
81
+ analyser.getByteFrequencyData(bins);
82
+ const bands = Array.from({ length: 24 }, (_, index) => {
83
+ const from = Math.min(bins.length - 1, Math.floor(80 * (highest / 80) ** (index / 24) / binWidth));
84
+ const to = Math.min(bins.length, Math.max(from + 1, Math.ceil(80 * (highest / 80) ** ((index + 1) / 24) / binWidth)));
85
+ let total = 0;
86
+ for (let bin = from; bin < to; bin++) total += bins[bin];
87
+ return Math.round(total / (to - from) / 255 * 1e3) / 1e3;
88
+ });
89
+ levels.update(bands);
90
+ }
91
+ frame = requestAnimationFrame(sample);
92
+ } catch (e4) {
93
+ stop();
94
+ }
95
+ };
96
+ if (context.state === "suspended") void context.resume().catch(stop);
97
+ frame = requestAnimationFrame(sample);
98
+ } catch (e5) {
99
+ stop();
100
+ }
101
+ return stop;
102
+ }
15
103
  function recognitionConstructor() {
16
104
  if (typeof window === "undefined") return void 0;
17
105
  const speech = window;
18
106
  return _nullishCoalesce(speech.SpeechRecognition, () => ( speech.webkitSpeechRecognition));
19
107
  }
20
108
  function microphoneError(reason) {
21
- const name = _optionalChain([reason, 'optionalAccess', _ => _.name]);
109
+ const name = _optionalChain([reason, 'optionalAccess', _10 => _10.name]);
22
110
  if (name === "NotAllowedError" || name === "SecurityError") return "Microphone access is blocked. Allow it in your browser and try again.";
23
111
  if (name === "NotFoundError" || name === "NotReadableError") return "The microphone is unavailable. Check your microphone and try again.";
24
112
  return "Recording could not start. Check your microphone and try again.";
@@ -29,8 +117,13 @@ function recognitionError(code) {
29
117
  if (code === "no-speech") return "No speech was detected. Try again.";
30
118
  return "Speech recognition stopped. Try again.";
31
119
  }
120
+ function releaseMeter(session) {
121
+ _optionalChain([session, 'access', _11 => _11.stopMeter, 'optionalCall', _12 => _12()]);
122
+ session.stopMeter = void 0;
123
+ }
32
124
  function releaseTracks(session) {
33
- _optionalChain([session, 'access', _2 => _2.stream, 'optionalAccess', _3 => _3.getTracks, 'call', _4 => _4(), 'access', _5 => _5.forEach, 'call', _6 => _6((track) => track.stop())]);
125
+ releaseMeter(session);
126
+ _optionalChain([session, 'access', _13 => _13.stream, 'optionalAccess', _14 => _14.getTracks, 'call', _15 => _15(), 'access', _16 => _16.forEach, 'call', _17 => _17((track) => track.stop())]);
34
127
  session.stream = void 0;
35
128
  }
36
129
  function dispose(session) {
@@ -44,7 +137,7 @@ function dispose(session) {
44
137
  if (recorder.state !== "inactive") {
45
138
  try {
46
139
  recorder.stop();
47
- } catch (e) {
140
+ } catch (e6) {
48
141
  }
49
142
  }
50
143
  }
@@ -55,7 +148,7 @@ function dispose(session) {
55
148
  recognition.onend = null;
56
149
  try {
57
150
  recognition.abort();
58
- } catch (e2) {
151
+ } catch (e7) {
59
152
  }
60
153
  }
61
154
  releaseTracks(session);
@@ -64,9 +157,9 @@ function dispose(session) {
64
157
  }
65
158
  function useDictation({ lang = "en-US", continuous = true, onTranscript } = {}) {
66
159
  const provider = _react.useContext.call(void 0, DictationContext);
67
- const transcribe = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _7 => _7.transcribe]), () => ( null));
68
- const scope = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _8 => _8.scope]), () => ( null));
69
- const scopeGeneration = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _9 => _9.generation]), () => ( 0));
160
+ const transcribe = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _18 => _18.transcribe]), () => ( null));
161
+ const scope = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _19 => _19.scope]), () => ( null));
162
+ const scopeGeneration = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _20 => _20.generation]), () => ( 0));
70
163
  const [snapshot, setSnapshot] = _react.useState.call(void 0, EMPTY);
71
164
  const [supported, setSupported] = _react.useState.call(void 0, false);
72
165
  const current = _react.useRef.call(void 0, null);
@@ -76,7 +169,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
76
169
  activeScope.current = scope;
77
170
  callback.current = onTranscript;
78
171
  _react.useEffect.call(void 0, () => {
79
- setSupported(transcribe ? typeof navigator !== "undefined" && !!_optionalChain([navigator, 'access', _10 => _10.mediaDevices, 'optionalAccess', _11 => _11.getUserMedia]) && typeof MediaRecorder !== "undefined" : typeof recognitionConstructor() === "function");
172
+ setSupported(transcribe ? typeof navigator !== "undefined" && !!_optionalChain([navigator, 'access', _21 => _21.mediaDevices, 'optionalAccess', _22 => _22.getUserMedia]) && typeof MediaRecorder !== "undefined" : typeof recognitionConstructor() === "function");
80
173
  }, [transcribe]);
81
174
  const cancel = _react.useCallback.call(void 0, () => {
82
175
  const session = current.current;
@@ -97,9 +190,9 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
97
190
  if (session) dispose(session);
98
191
  };
99
192
  }, []);
100
- const isCurrent = _react.useCallback.call(void 0, (session) => mounted.current && current.current === session && session.scope === activeScope.current && session.scopeGeneration === (_nullishCoalesce(_optionalChain([session, 'access', _12 => _12.scope, 'optionalAccess', _13 => _13.current, 'access', _14 => _14.generation]), () => ( 0))) && !session.controller.signal.aborted, []);
193
+ const isCurrent = _react.useCallback.call(void 0, (session) => mounted.current && current.current === session && session.scope === activeScope.current && session.scopeGeneration === (_nullishCoalesce(_optionalChain([session, 'access', _23 => _23.scope, 'optionalAccess', _24 => _24.current, 'access', _25 => _25.generation]), () => ( 0))) && !session.controller.signal.aborted, []);
101
194
  const start = _react.useCallback.call(void 0, async () => {
102
- if (scope !== activeScope.current || scopeGeneration !== (_nullishCoalesce(_optionalChain([scope, 'optionalAccess', _15 => _15.current, 'access', _16 => _16.generation]), () => ( 0)))) return;
195
+ if (scope !== activeScope.current || scopeGeneration !== (_nullishCoalesce(_optionalChain([scope, 'optionalAccess', _26 => _26.current, 'access', _27 => _27.generation]), () => ( 0)))) return;
103
196
  if (current.current && isCurrent(current.current) && ["requesting", "recording", "transcribing"].includes(current.current.phase)) return;
104
197
  cancel();
105
198
  let resolveCapture;
@@ -123,20 +216,21 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
123
216
  nativeEnded: false,
124
217
  transcript: "",
125
218
  interim: "",
126
- notified: /* @__PURE__ */ new Set()
219
+ notified: /* @__PURE__ */ new Set(),
220
+ audioLevels: transcribe ? createAudioLevels() : void 0
127
221
  };
128
222
  current.current = session;
129
223
  setSnapshot({ ...EMPTY, state: "requesting" });
130
224
  if (transcribe) {
131
225
  try {
132
- if (!_optionalChain([navigator, 'access', _17 => _17.mediaDevices, 'optionalAccess', _18 => _18.getUserMedia]) || typeof MediaRecorder === "undefined") throw new Error("unsupported");
226
+ if (!_optionalChain([navigator, 'access', _28 => _28.mediaDevices, 'optionalAccess', _29 => _29.getUserMedia]) || typeof MediaRecorder === "undefined") throw new Error("unsupported");
133
227
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
134
228
  if (!isCurrent(session)) {
135
229
  stream.getTracks().forEach((track) => track.stop());
136
230
  return;
137
231
  }
138
232
  session.stream = stream;
139
- const mimeType = MIME_TYPES.find((mime) => _optionalChain([MediaRecorder, 'access', _19 => _19.isTypeSupported, 'optionalCall', _20 => _20(mime)]));
233
+ const mimeType = MIME_TYPES.find((mime) => _optionalChain([MediaRecorder, 'access', _30 => _30.isTypeSupported, 'optionalCall', _31 => _31(mime)]));
140
234
  const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
141
235
  session.recorder = recorder;
142
236
  recorder.ondataavailable = (event) => {
@@ -148,7 +242,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
148
242
  session.resolveCapture(null);
149
243
  return;
150
244
  }
151
- const type = recorder.mimeType || _optionalChain([session, 'access', _21 => _21.chunks, 'access', _22 => _22[0], 'optionalAccess', _23 => _23.type]) || mimeType || "audio/webm";
245
+ const type = recorder.mimeType || _optionalChain([session, 'access', _32 => _32.chunks, 'access', _33 => _33[0], 'optionalAccess', _34 => _34.type]) || mimeType || "audio/webm";
152
246
  const blob = new Blob(session.chunks, { type });
153
247
  session.blob = blob;
154
248
  session.chunks = [];
@@ -163,6 +257,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
163
257
  };
164
258
  recorder.start();
165
259
  session.phase = "recording";
260
+ session.stopMeter = meterStream(stream, session.audioLevels, () => isCurrent(session) && session.phase === "recording");
166
261
  setSnapshot({ ...EMPTY, state: "recording" });
167
262
  } catch (error) {
168
263
  releaseTracks(session);
@@ -200,7 +295,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
200
295
  session.transcript = final.join(" ");
201
296
  session.interim = interim.join(" ");
202
297
  setSnapshot((previous) => ({ ...previous, transcript: session.transcript, interimTranscript: session.interim }));
203
- if (chunks.length) _optionalChain([callback, 'access', _24 => _24.current, 'optionalCall', _25 => _25(chunks.join(" "))]);
298
+ if (chunks.length) _optionalChain([callback, 'access', _35 => _35.current, 'optionalCall', _36 => _36(chunks.join(" "))]);
204
299
  };
205
300
  recognition.onend = () => {
206
301
  session.nativeEnded = true;
@@ -228,20 +323,21 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
228
323
  }, [cancel, continuous, isCurrent, lang, scope, scopeGeneration, transcribe]);
229
324
  const finish = _react.useCallback.call(void 0, async (session) => {
230
325
  if (!isCurrent(session)) return null;
326
+ releaseMeter(session);
231
327
  session.phase = "transcribing";
232
328
  setSnapshot((previous) => ({ ...previous, state: "transcribing", error: null, canRetry: false }));
233
329
  if (!session.transcribe) {
234
330
  if (!session.nativeEnded) {
235
331
  session.nativeTimer = setTimeout(() => {
236
332
  try {
237
- _optionalChain([session, 'access', _26 => _26.recognition, 'optionalAccess', _27 => _27.abort, 'call', _28 => _28()]);
238
- } catch (e3) {
333
+ _optionalChain([session, 'access', _37 => _37.recognition, 'optionalAccess', _38 => _38.abort, 'call', _39 => _39()]);
334
+ } catch (e8) {
239
335
  }
240
336
  session.resolveNativeEnd();
241
337
  }, 1e3);
242
338
  try {
243
- _optionalChain([session, 'access', _29 => _29.recognition, 'optionalAccess', _30 => _30.stop, 'call', _31 => _31()]);
244
- } catch (e4) {
339
+ _optionalChain([session, 'access', _40 => _40.recognition, 'optionalAccess', _41 => _41.stop, 'call', _42 => _42()]);
340
+ } catch (e9) {
245
341
  session.resolveNativeEnd();
246
342
  }
247
343
  await session.nativeEnd;
@@ -256,7 +352,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
256
352
  let text;
257
353
  try {
258
354
  if (!session.blob) {
259
- if (_optionalChain([session, 'access', _32 => _32.recorder, 'optionalAccess', _33 => _33.state]) !== "inactive") _optionalChain([session, 'access', _34 => _34.recorder, 'optionalAccess', _35 => _35.stop, 'call', _36 => _36()]);
355
+ if (_optionalChain([session, 'access', _43 => _43.recorder, 'optionalAccess', _44 => _44.state]) !== "inactive") _optionalChain([session, 'access', _45 => _45.recorder, 'optionalAccess', _46 => _46.stop, 'call', _47 => _47()]);
260
356
  const blob = await session.capture;
261
357
  if (!isCurrent(session) || !blob) return null;
262
358
  session.blob = blob;
@@ -270,13 +366,13 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
270
366
  releaseTracks(session);
271
367
  session.phase = "error";
272
368
  const message = error instanceof Error && error.message ? error.message : "Transcription failed. Try again.";
273
- setSnapshot({ ...EMPTY, state: "error", error: message, canRetry: !!_optionalChain([session, 'access', _37 => _37.blob, 'optionalAccess', _38 => _38.size]) });
369
+ setSnapshot({ ...EMPTY, state: "error", error: message, canRetry: !!_optionalChain([session, 'access', _48 => _48.blob, 'optionalAccess', _49 => _49.size]) });
274
370
  return null;
275
371
  }
276
372
  session.transcript = text;
277
373
  session.phase = "ready";
278
374
  setSnapshot({ ...EMPTY, state: "ready", transcript: text });
279
- _optionalChain([callback, 'access', _39 => _39.current, 'optionalCall', _40 => _40(text)]);
375
+ _optionalChain([callback, 'access', _50 => _50.current, 'optionalCall', _51 => _51(text)]);
280
376
  return text;
281
377
  }, [isCurrent]);
282
378
  const stop = _react.useCallback.call(void 0, () => {
@@ -301,13 +397,13 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
301
397
  cancel();
302
398
  return Promise.resolve(null);
303
399
  }
304
- if (!_optionalChain([session, 'optionalAccess', _41 => _41.transcribe]) || !_optionalChain([session, 'access', _42 => _42.blob, 'optionalAccess', _43 => _43.size])) return Promise.resolve(null);
400
+ if (!_optionalChain([session, 'optionalAccess', _52 => _52.transcribe]) || !_optionalChain([session, 'access', _53 => _53.blob, 'optionalAccess', _54 => _54.size])) return Promise.resolve(null);
305
401
  if (session.phase !== "error") return _nullishCoalesce(session.finish, () => ( Promise.resolve(null)));
306
402
  session.finish = finish(session);
307
403
  return session.finish;
308
404
  }, [cancel, finish, isCurrent]);
309
405
  const visibleSnapshot = current.current && !isCurrent(current.current) ? EMPTY : snapshot;
310
- return { ...visibleSnapshot, supported, start, stop, retry, cancel };
406
+ return { ...visibleSnapshot, audioLevels: current.current && isCurrent(current.current) ? current.current.audioLevels : void 0, supported, start, stop, retry, cancel };
311
407
  }
312
408
 
313
409