@epicbagel/saas-kit 1.6.0 → 1.10.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 (81) hide show
  1. package/README.md +71 -1
  2. package/THIRD_PARTY_NOTICES.md +14 -0
  3. package/dist/chunk-2BSFYU22.cjs +71 -0
  4. package/dist/{chunk-GTVSRRE3.cjs → chunk-BGCNGYPL.cjs} +2 -2
  5. package/dist/{chunk-QTWJQQDH.cjs → chunk-FBVZRRA4.cjs} +6 -6
  6. package/dist/chunk-GJ7S3L6R.js +71 -0
  7. package/dist/{chunk-LKPM6DQP.cjs → chunk-HTCW6AEH.cjs} +51 -72
  8. package/dist/{chunk-F3B4M6DU.js → chunk-HTPPNW74.js} +1 -1
  9. package/dist/chunk-KUG4ONYC.js +127 -0
  10. package/dist/{chunk-TI57GAQP.js → chunk-KWV5KTOA.js} +48 -69
  11. package/dist/{chunk-JN6JOSZG.js → chunk-LQPJGZIE.js} +7 -7
  12. package/dist/chunk-M2ZNC3TK.js +316 -0
  13. package/dist/chunk-O6F3LVZY.cjs +316 -0
  14. package/dist/chunk-UW67GSZ4.cjs +127 -0
  15. package/dist/chunk-VBKJXNEL.js +157 -0
  16. package/dist/chunk-XB2D4NQK.cjs +157 -0
  17. package/dist/hooks/use-agent-audio-visualizer-bar.cjs +8 -0
  18. package/dist/hooks/use-agent-audio-visualizer-bar.d.cts +4 -0
  19. package/dist/hooks/use-agent-audio-visualizer-bar.d.ts +4 -0
  20. package/dist/hooks/use-agent-audio-visualizer-bar.js +8 -0
  21. package/dist/hooks/use-dictation.cjs +10 -0
  22. package/dist/hooks/use-dictation.d.cts +56 -0
  23. package/dist/hooks/use-dictation.d.ts +56 -0
  24. package/dist/hooks/use-dictation.js +10 -0
  25. package/dist/primitives/activity-feed.cjs +3 -3
  26. package/dist/primitives/activity-feed.js +4 -4
  27. package/dist/primitives/agent-audio-visualizer-bar.cjs +16 -0
  28. package/dist/primitives/agent-audio-visualizer-bar.d.cts +75 -0
  29. package/dist/primitives/agent-audio-visualizer-bar.d.ts +75 -0
  30. package/dist/primitives/agent-audio-visualizer-bar.js +16 -0
  31. package/dist/primitives/archive-confirmation-dialog.cjs +2 -2
  32. package/dist/primitives/archive-confirmation-dialog.js +3 -3
  33. package/dist/primitives/board-view.cjs +6 -6
  34. package/dist/primitives/board-view.js +5 -5
  35. package/dist/primitives/chat.cjs +127 -48
  36. package/dist/primitives/chat.d.cts +25 -3
  37. package/dist/primitives/chat.d.ts +25 -3
  38. package/dist/primitives/chat.js +122 -43
  39. package/dist/primitives/code-block.cjs +4 -4
  40. package/dist/primitives/code-block.js +3 -3
  41. package/dist/primitives/connection.cjs +6 -6
  42. package/dist/primitives/connection.js +7 -7
  43. package/dist/primitives/copy-button.cjs +3 -3
  44. package/dist/primitives/copy-button.js +2 -2
  45. package/dist/primitives/dictation-provider.cjs +8 -0
  46. package/dist/primitives/dictation-provider.d.cts +2 -0
  47. package/dist/primitives/dictation-provider.d.ts +2 -0
  48. package/dist/primitives/dictation-provider.js +8 -0
  49. package/dist/primitives/editable-list.cjs +1 -1
  50. package/dist/primitives/editable-list.js +1 -1
  51. package/dist/primitives/file-dropzone.cjs +3 -3
  52. package/dist/primitives/file-dropzone.js +2 -2
  53. package/dist/primitives/file-preview.cjs +6 -6
  54. package/dist/primitives/file-preview.js +5 -5
  55. package/dist/primitives/filter-bar.cjs +4 -4
  56. package/dist/primitives/filter-bar.js +3 -3
  57. package/dist/primitives/input-group.d.cts +1 -1
  58. package/dist/primitives/input-group.d.ts +1 -1
  59. package/dist/primitives/resource-list.cjs +6 -6
  60. package/dist/primitives/resource-list.js +5 -5
  61. package/dist/primitives/section.cjs +2 -2
  62. package/dist/primitives/section.js +3 -3
  63. package/dist/primitives/sheet.d.cts +1 -1
  64. package/dist/primitives/sheet.d.ts +1 -1
  65. package/dist/primitives/switch-field.cjs +2 -2
  66. package/dist/primitives/switch-field.js +3 -3
  67. package/dist/primitives/theme-selector.cjs +3 -3
  68. package/dist/primitives/theme-selector.js +4 -4
  69. package/dist/primitives/voice-dictation-button.cjs +3 -2
  70. package/dist/primitives/voice-dictation-button.js +2 -1
  71. package/dist/primitives/voice-session-controls.cjs +12 -0
  72. package/dist/primitives/voice-session-controls.d.cts +29 -0
  73. package/dist/primitives/voice-session-controls.d.ts +29 -0
  74. package/dist/primitives/voice-session-controls.js +12 -0
  75. package/package.json +51 -1
  76. package/dist/{chunk-HREEZUKL.js → chunk-DILYV4FI.js} +3 -3
  77. package/dist/{chunk-MU4F3DSW.cjs → chunk-FARCLWLU.cjs} +2 -2
  78. package/dist/{chunk-CRTH5GJ5.cjs → chunk-MW67PHAQ.cjs} +3 -3
  79. package/dist/{chunk-KNKV25WS.js → chunk-UFFHGKI7.js} +3 -3
  80. package/dist/{chunk-HBRVOGLA.js → chunk-WHDHUYS6.js} +3 -3
  81. package/dist/{chunk-Z3JJSNR5.cjs → chunk-ZLEM2SAU.cjs} +2 -2
@@ -5,6 +5,9 @@ import {
5
5
  import {
6
6
  cn
7
7
  } from "./chunk-G6SRSWXB.js";
8
+ import {
9
+ useDictation
10
+ } from "./chunk-M2ZNC3TK.js";
8
11
 
9
12
  // src/primitives/voice-dictation-button.tsx
10
13
  import {
@@ -31,87 +34,61 @@ function VoiceDictationButton({
31
34
  visibility = "visible",
32
35
  shrink = true
33
36
  }) {
34
- const [supported, setSupported] = useState(false);
35
37
  const [open, setOpen] = useState(false);
36
- const [transcript, setTranscript] = useState("");
37
- const [interim, setInterim] = useState("");
38
- const [errored, setErrored] = useState(false);
39
- const recognitionRef = useRef(null);
40
- useEffect(() => {
41
- const w = window;
42
- const Ctor = w.SpeechRecognition ?? w.webkitSpeechRecognition;
43
- setSupported(typeof Ctor === "function");
44
- }, []);
45
- const stop = useCallback(() => {
46
- recognitionRef.current?.stop();
47
- recognitionRef.current = null;
48
- }, []);
49
- const startRecognition = useCallback(() => {
50
- const w = window;
51
- const Ctor = w.SpeechRecognition ?? w.webkitSpeechRecognition;
52
- if (!Ctor) return;
53
- const rec = new Ctor();
54
- rec.continuous = continuous;
55
- rec.interimResults = true;
56
- rec.lang = lang;
57
- rec.onresult = (event) => {
58
- let finalChunk = "";
59
- let interimChunk = "";
60
- for (let i = event.resultIndex; i < event.results.length; i++) {
61
- const r = event.results[i];
62
- if (!r) continue;
63
- if (r.isFinal) finalChunk += r[0].transcript;
64
- else interimChunk += r[0].transcript;
65
- }
66
- if (finalChunk) {
67
- setTranscript((prev) => prev ? prev + " " + finalChunk : finalChunk);
68
- setInterim("");
69
- } else if (interimChunk) {
70
- setInterim(interimChunk);
71
- }
72
- };
73
- rec.onerror = () => {
74
- setErrored(true);
75
- };
76
- rec.onend = () => {
77
- recognitionRef.current = null;
78
- };
79
- recognitionRef.current = rec;
80
- setErrored(false);
81
- rec.start();
82
- }, [continuous, lang]);
38
+ const capture = useDictation({ lang, continuous });
39
+ const { transcript, interimTranscript: interim, supported, error } = capture;
40
+ const generation = useRef(0);
41
+ const finishing = useRef(false);
42
+ const busy = capture.state === "requesting" || capture.state === "transcribing";
83
43
  const openOverlay = useCallback(() => {
84
- setTranscript("");
85
- setInterim("");
44
+ generation.current++;
45
+ finishing.current = false;
86
46
  setOpen(true);
87
- startRecognition();
88
- }, [startRecognition]);
47
+ void capture.start();
48
+ }, [capture.start]);
89
49
  const closeOverlay = useCallback(() => {
90
- stop();
50
+ generation.current++;
51
+ finishing.current = false;
52
+ capture.cancel();
91
53
  setOpen(false);
92
- setInterim("");
93
- }, [stop]);
94
- const handleDone = useCallback(() => {
95
- const final = `${transcript} ${interim}`.trim();
54
+ }, [capture.cancel]);
55
+ const handleDone = useCallback(async () => {
56
+ if (finishing.current || busy) return;
57
+ if (capture.state === "error" && !capture.canRetry) {
58
+ void capture.start();
59
+ return;
60
+ }
61
+ finishing.current = true;
62
+ const started = generation.current;
63
+ const final = await (capture.canRetry ? capture.retry() : capture.stop());
64
+ if (started !== generation.current) return;
65
+ finishing.current = false;
66
+ if (final === null) return;
96
67
  closeOverlay();
97
68
  if (final) onCommit(final);
98
69
  else onCancel?.();
99
- }, [transcript, interim, onCommit, onCancel, closeOverlay]);
70
+ }, [busy, capture.canRetry, capture.retry, capture.start, capture.state, capture.stop, onCommit, onCancel, closeOverlay]);
100
71
  const handleCancel = useCallback(() => {
101
72
  closeOverlay();
102
73
  onCancel?.();
103
74
  }, [closeOverlay, onCancel]);
75
+ useEffect(() => {
76
+ if (open && capture.state === "idle") handleCancel();
77
+ }, [capture.state, handleCancel, open]);
104
78
  useEffect(() => {
105
79
  if (!open) return;
106
80
  const onKey = (e) => {
107
- if (e.key === "Enter" && !e.shiftKey) handleDone();
81
+ if (e.key === "Enter" && !e.shiftKey) {
82
+ e.preventDefault();
83
+ void handleDone();
84
+ }
108
85
  };
109
86
  document.addEventListener("keydown", onKey);
110
87
  return () => document.removeEventListener("keydown", onKey);
111
88
  }, [open, handleDone]);
112
89
  useEffect(() => {
113
90
  return () => {
114
- recognitionRef.current?.abort();
91
+ generation.current++;
115
92
  };
116
93
  }, []);
117
94
  if (!supported) return null;
@@ -148,7 +125,7 @@ function VoiceDictationButton({
148
125
  {
149
126
  open,
150
127
  onOpenChange: (next) => {
151
- if (!next) closeOverlay();
128
+ if (!next) handleCancel();
152
129
  },
153
130
  children: /* @__PURE__ */ jsx(DialogPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
154
131
  DialogPrimitive.Content,
@@ -161,9 +138,10 @@ function VoiceDictationButton({
161
138
  className: "fixed inset-0 z-[200] flex flex-col bg-background outline-hidden md:items-center md:justify-center md:bg-black/40 md:backdrop-blur-sm",
162
139
  children: [
163
140
  /* @__PURE__ */ jsx(DialogPrimitive.Title, { className: "sr-only", children: "Voice dictation" }),
141
+ /* @__PURE__ */ jsx(DialogPrimitive.Description, { className: "sr-only", children: "Record your message, then choose Done to add the text. Cancel discards the recording." }),
164
142
  /* @__PURE__ */ jsxs("div", { className: "flex-1 md:flex-initial w-full md:max-w-lg md:rounded-xl bg-background flex flex-col md:max-h-[80vh] md:shadow-2xl md:border md:border-border", children: [
165
143
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-border md:rounded-t-xl", children: [
166
- /* @__PURE__ */ jsx("span", { className: "font-mono text-xs uppercase tracking-wider text-muted-foreground", children: caption }),
144
+ /* @__PURE__ */ jsx("span", { role: "status", className: "font-mono text-xs uppercase tracking-wider text-muted-foreground", children: capture.state === "transcribing" ? "transcribing" : capture.state === "requesting" ? "microphone" : caption }),
167
145
  /* @__PURE__ */ jsx(
168
146
  "button",
169
147
  {
@@ -183,7 +161,7 @@ function VoiceDictationButton({
183
161
  "aria-hidden": true,
184
162
  className: cn(
185
163
  "absolute inset-0 rounded-full",
186
- errored ? "bg-destructive/20" : "bg-destructive/20 animate-ping"
164
+ error || busy || capture.state !== "recording" ? "bg-destructive/20" : "bg-destructive/20 animate-ping"
187
165
  )
188
166
  }
189
167
  ),
@@ -193,7 +171,7 @@ function VoiceDictationButton({
193
171
  "aria-hidden": true,
194
172
  className: cn(
195
173
  "absolute inset-2 rounded-full",
196
- errored ? "bg-destructive/40" : "bg-destructive/40"
174
+ "bg-destructive/40"
197
175
  )
198
176
  }
199
177
  ),
@@ -206,8 +184,8 @@ function VoiceDictationButton({
206
184
  transcript ? " " : "",
207
185
  interim
208
186
  ] })
209
- ] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground italic text-base", children: "Start speaking \u2014 I'm listening." }) }),
210
- errored && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive mt-4", children: "Couldn't access the microphone. Check permissions." })
187
+ ] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground italic text-base", children: capture.state === "transcribing" ? "Converting your recording to text\u2026" : capture.state === "requesting" ? "Allow microphone access to start." : "Start speaking, then choose Done." }) }),
188
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-sm text-destructive mt-4", children: error })
211
189
  ] }),
212
190
  /* @__PURE__ */ jsxs("div", { className: "border-t border-border px-4 py-4 pb-[calc(1rem+env(safe-area-inset-bottom))] md:pb-4 md:rounded-b-xl flex flex-col items-stretch gap-2", children: [
213
191
  /* @__PURE__ */ jsx(
@@ -215,11 +193,12 @@ function VoiceDictationButton({
215
193
  {
216
194
  size: "lg",
217
195
  className: "h-12 w-full",
218
- onClick: handleDone,
219
- disabled: !displayTranscript.trim(),
220
- children: "Done"
196
+ onClick: () => void handleDone(),
197
+ disabled: busy,
198
+ children: capture.state === "transcribing" ? "Transcribing\u2026" : capture.canRetry ? "Retry transcription" : error ? "Try again" : "Done"
221
199
  }
222
200
  ),
201
+ capture.canRetry && /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: () => void capture.start(), children: "Record again" }),
223
202
  /* @__PURE__ */ jsx(
224
203
  "button",
225
204
  {
@@ -7,16 +7,16 @@ import {
7
7
  } from "./chunk-EEHIMPD7.js";
8
8
  import {
9
9
  FilterBar
10
- } from "./chunk-KNKV25WS.js";
11
- import {
12
- Heading
13
- } from "./chunk-H2O6NMLZ.js";
10
+ } from "./chunk-UFFHGKI7.js";
14
11
  import {
15
12
  Tooltip,
16
13
  TooltipContent,
17
14
  TooltipProvider,
18
15
  TooltipTrigger
19
16
  } from "./chunk-ZRLUUZHK.js";
17
+ import {
18
+ Heading
19
+ } from "./chunk-H2O6NMLZ.js";
20
20
  import {
21
21
  BoardColumnComponent,
22
22
  MobileBoardView
@@ -30,12 +30,12 @@ import {
30
30
  import {
31
31
  Button
32
32
  } from "./chunk-W326GKML.js";
33
- import {
34
- useIsMobile
35
- } from "./chunk-PIU5SSUV.js";
36
33
  import {
37
34
  cn
38
35
  } from "./chunk-G6SRSWXB.js";
36
+ import {
37
+ useIsMobile
38
+ } from "./chunk-PIU5SSUV.js";
39
39
 
40
40
  // src/primitives/resource-list.tsx
41
41
  import { ChevronLeft, ChevronRight, Grid3X3, Kanban, List, Search, X } from "lucide-react";
@@ -0,0 +1,316 @@
1
+ "use client";
2
+
3
+ // src/hooks/use-dictation.ts
4
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
+ var DictationContext = createContext(null);
6
+ function DictationProvider({ transcribe, scopeKey, children }) {
7
+ const scope = useRef({ key: scopeKey, generation: 0 });
8
+ if (scope.current.key !== scopeKey) scope.current = { key: scopeKey, generation: scope.current.generation + 1 };
9
+ const generation = scope.current.generation;
10
+ const value = useMemo(() => ({ transcribe, scope, generation }), [transcribe, generation]);
11
+ return createElement(DictationContext.Provider, { value }, children);
12
+ }
13
+ var EMPTY = { state: "idle", transcript: "", interimTranscript: "", error: null, canRetry: false };
14
+ var MIME_TYPES = ["audio/webm;codecs=opus", "audio/mp4", "audio/ogg;codecs=opus", "audio/webm"];
15
+ function recognitionConstructor() {
16
+ if (typeof window === "undefined") return void 0;
17
+ const speech = window;
18
+ return speech.SpeechRecognition ?? speech.webkitSpeechRecognition;
19
+ }
20
+ function microphoneError(reason) {
21
+ const name = reason?.name;
22
+ if (name === "NotAllowedError" || name === "SecurityError") return "Microphone access is blocked. Allow it in your browser and try again.";
23
+ if (name === "NotFoundError" || name === "NotReadableError") return "The microphone is unavailable. Check your microphone and try again.";
24
+ return "Recording could not start. Check your microphone and try again.";
25
+ }
26
+ function recognitionError(code) {
27
+ if (code === "not-allowed" || code === "service-not-allowed") return microphoneError({ name: "NotAllowedError" });
28
+ if (code === "audio-capture") return microphoneError({ name: "NotFoundError" });
29
+ if (code === "no-speech") return "No speech was detected. Try again.";
30
+ return "Speech recognition stopped. Try again.";
31
+ }
32
+ function releaseTracks(session) {
33
+ session.stream?.getTracks().forEach((track) => track.stop());
34
+ session.stream = void 0;
35
+ }
36
+ function dispose(session) {
37
+ session.controller.abort();
38
+ if (session.nativeTimer) clearTimeout(session.nativeTimer);
39
+ const recorder = session.recorder;
40
+ if (recorder) {
41
+ recorder.ondataavailable = null;
42
+ recorder.onstop = null;
43
+ recorder.onerror = null;
44
+ if (recorder.state !== "inactive") {
45
+ try {
46
+ recorder.stop();
47
+ } catch {
48
+ }
49
+ }
50
+ }
51
+ const recognition = session.recognition;
52
+ if (recognition) {
53
+ recognition.onresult = null;
54
+ recognition.onerror = null;
55
+ recognition.onend = null;
56
+ try {
57
+ recognition.abort();
58
+ } catch {
59
+ }
60
+ }
61
+ releaseTracks(session);
62
+ session.resolveCapture(null);
63
+ session.resolveNativeEnd();
64
+ }
65
+ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {}) {
66
+ const provider = useContext(DictationContext);
67
+ const transcribe = provider?.transcribe ?? null;
68
+ const scope = provider?.scope ?? null;
69
+ const scopeGeneration = provider?.generation ?? 0;
70
+ const [snapshot, setSnapshot] = useState(EMPTY);
71
+ const [supported, setSupported] = useState(false);
72
+ const current = useRef(null);
73
+ const mounted = useRef(true);
74
+ const callback = useRef(onTranscript);
75
+ const activeScope = useRef(scope);
76
+ activeScope.current = scope;
77
+ callback.current = onTranscript;
78
+ useEffect(() => {
79
+ setSupported(transcribe ? typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof MediaRecorder !== "undefined" : typeof recognitionConstructor() === "function");
80
+ }, [transcribe]);
81
+ const cancel = useCallback(() => {
82
+ const session = current.current;
83
+ current.current = null;
84
+ if (session) dispose(session);
85
+ if (mounted.current) setSnapshot(EMPTY);
86
+ }, []);
87
+ useEffect(() => {
88
+ const session = current.current;
89
+ if (session && (session.scope !== scope || session.scopeGeneration !== scopeGeneration)) cancel();
90
+ }, [cancel, scope, scopeGeneration]);
91
+ useEffect(() => {
92
+ mounted.current = true;
93
+ return () => {
94
+ mounted.current = false;
95
+ const session = current.current;
96
+ current.current = null;
97
+ if (session) dispose(session);
98
+ };
99
+ }, []);
100
+ const isCurrent = useCallback((session) => mounted.current && current.current === session && session.scope === activeScope.current && session.scopeGeneration === (session.scope?.current.generation ?? 0) && !session.controller.signal.aborted, []);
101
+ const start = useCallback(async () => {
102
+ if (scope !== activeScope.current || scopeGeneration !== (scope?.current.generation ?? 0)) return;
103
+ if (current.current && isCurrent(current.current) && ["requesting", "recording", "transcribing"].includes(current.current.phase)) return;
104
+ cancel();
105
+ let resolveCapture;
106
+ let resolveNativeEnd;
107
+ const session = {
108
+ language: lang,
109
+ transcribe,
110
+ scope,
111
+ scopeGeneration,
112
+ controller: new AbortController(),
113
+ phase: "requesting",
114
+ chunks: [],
115
+ capture: new Promise((resolve) => {
116
+ resolveCapture = resolve;
117
+ }),
118
+ resolveCapture: (blob) => resolveCapture(blob),
119
+ nativeEnd: new Promise((resolve) => {
120
+ resolveNativeEnd = resolve;
121
+ }),
122
+ resolveNativeEnd: () => resolveNativeEnd(),
123
+ nativeEnded: false,
124
+ transcript: "",
125
+ interim: "",
126
+ notified: /* @__PURE__ */ new Set()
127
+ };
128
+ current.current = session;
129
+ setSnapshot({ ...EMPTY, state: "requesting" });
130
+ if (transcribe) {
131
+ try {
132
+ if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") throw new Error("unsupported");
133
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
134
+ if (!isCurrent(session)) {
135
+ stream.getTracks().forEach((track) => track.stop());
136
+ return;
137
+ }
138
+ session.stream = stream;
139
+ const mimeType = MIME_TYPES.find((mime) => MediaRecorder.isTypeSupported?.(mime));
140
+ const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
141
+ session.recorder = recorder;
142
+ recorder.ondataavailable = (event) => {
143
+ if (isCurrent(session) && event.data.size) session.chunks.push(event.data);
144
+ };
145
+ recorder.onstop = () => {
146
+ releaseTracks(session);
147
+ if (!isCurrent(session)) {
148
+ session.resolveCapture(null);
149
+ return;
150
+ }
151
+ const type = recorder.mimeType || session.chunks[0]?.type || mimeType || "audio/webm";
152
+ const blob = new Blob(session.chunks, { type });
153
+ session.blob = blob;
154
+ session.chunks = [];
155
+ session.resolveCapture(blob);
156
+ };
157
+ recorder.onerror = () => {
158
+ if (!isCurrent(session)) return;
159
+ releaseTracks(session);
160
+ session.phase = "error";
161
+ session.resolveCapture(null);
162
+ setSnapshot({ ...EMPTY, state: "error", error: "Recording was interrupted. Try again." });
163
+ };
164
+ recorder.start();
165
+ session.phase = "recording";
166
+ setSnapshot({ ...EMPTY, state: "recording" });
167
+ } catch (error) {
168
+ releaseTracks(session);
169
+ if (!isCurrent(session)) return;
170
+ session.phase = "error";
171
+ setSnapshot({ ...EMPTY, state: "error", error: microphoneError(error) });
172
+ }
173
+ return;
174
+ }
175
+ try {
176
+ const Constructor = recognitionConstructor();
177
+ if (!Constructor) throw new Error("unsupported");
178
+ const recognition = new Constructor();
179
+ session.recognition = recognition;
180
+ recognition.lang = lang;
181
+ recognition.continuous = continuous;
182
+ recognition.interimResults = true;
183
+ recognition.onresult = (event) => {
184
+ if (!isCurrent(session) || session.phase === "error") return;
185
+ const final = [];
186
+ const interim = [];
187
+ const chunks = [];
188
+ for (let i = 0; i < event.results.length; i++) {
189
+ const result = event.results[i];
190
+ if (!result) continue;
191
+ const text = result[0].transcript.trim();
192
+ if (result.isFinal) {
193
+ final.push(text);
194
+ if (!session.notified.has(i)) {
195
+ session.notified.add(i);
196
+ if (text) chunks.push(text);
197
+ }
198
+ } else interim.push(text);
199
+ }
200
+ session.transcript = final.join(" ");
201
+ session.interim = interim.join(" ");
202
+ setSnapshot((previous) => ({ ...previous, transcript: session.transcript, interimTranscript: session.interim }));
203
+ if (chunks.length) callback.current?.(chunks.join(" "));
204
+ };
205
+ recognition.onend = () => {
206
+ session.nativeEnded = true;
207
+ session.resolveNativeEnd();
208
+ if (isCurrent(session) && session.phase === "recording") {
209
+ session.phase = "ready";
210
+ setSnapshot((previous) => ({ ...previous, state: "ready" }));
211
+ }
212
+ };
213
+ recognition.onerror = (event) => {
214
+ if (!isCurrent(session)) return;
215
+ session.phase = "error";
216
+ session.nativeEnded = true;
217
+ session.resolveNativeEnd();
218
+ setSnapshot((previous) => ({ ...previous, state: "error", error: recognitionError(event.error) }));
219
+ };
220
+ recognition.start();
221
+ session.phase = "recording";
222
+ setSnapshot({ ...EMPTY, state: "recording" });
223
+ } catch (error) {
224
+ if (!isCurrent(session)) return;
225
+ session.phase = "error";
226
+ setSnapshot({ ...EMPTY, state: "error", error: microphoneError(error) });
227
+ }
228
+ }, [cancel, continuous, isCurrent, lang, scope, scopeGeneration, transcribe]);
229
+ const finish = useCallback(async (session) => {
230
+ if (!isCurrent(session)) return null;
231
+ session.phase = "transcribing";
232
+ setSnapshot((previous) => ({ ...previous, state: "transcribing", error: null, canRetry: false }));
233
+ if (!session.transcribe) {
234
+ if (!session.nativeEnded) {
235
+ session.nativeTimer = setTimeout(() => {
236
+ try {
237
+ session.recognition?.abort();
238
+ } catch {
239
+ }
240
+ session.resolveNativeEnd();
241
+ }, 1e3);
242
+ try {
243
+ session.recognition?.stop();
244
+ } catch {
245
+ session.resolveNativeEnd();
246
+ }
247
+ await session.nativeEnd;
248
+ clearTimeout(session.nativeTimer);
249
+ }
250
+ if (!isCurrent(session) || session.phase === "error") return null;
251
+ const text2 = [session.transcript, session.interim].filter(Boolean).join(" ").trim();
252
+ session.phase = "ready";
253
+ setSnapshot({ ...EMPTY, state: "ready", transcript: text2 });
254
+ return text2;
255
+ }
256
+ let text;
257
+ try {
258
+ if (!session.blob) {
259
+ if (session.recorder?.state !== "inactive") session.recorder?.stop();
260
+ const blob = await session.capture;
261
+ if (!isCurrent(session) || !blob) return null;
262
+ session.blob = blob;
263
+ }
264
+ if (!session.blob.size) throw new Error("No audio was recorded. Try again.");
265
+ text = (await session.transcribe(session.blob, { language: session.language, signal: session.controller.signal })).trim();
266
+ if (!isCurrent(session)) return null;
267
+ if (!text) throw new Error("No speech was detected. Record again.");
268
+ } catch (error) {
269
+ if (!isCurrent(session)) return null;
270
+ releaseTracks(session);
271
+ session.phase = "error";
272
+ const message = error instanceof Error && error.message ? error.message : "Transcription failed. Try again.";
273
+ setSnapshot({ ...EMPTY, state: "error", error: message, canRetry: !!session.blob?.size });
274
+ return null;
275
+ }
276
+ session.transcript = text;
277
+ session.phase = "ready";
278
+ setSnapshot({ ...EMPTY, state: "ready", transcript: text });
279
+ callback.current?.(text);
280
+ return text;
281
+ }, [isCurrent]);
282
+ const stop = useCallback(() => {
283
+ const session = current.current;
284
+ if (!session) return Promise.resolve(null);
285
+ if (!isCurrent(session)) {
286
+ cancel();
287
+ return Promise.resolve(null);
288
+ }
289
+ if (session.phase === "requesting") {
290
+ cancel();
291
+ return Promise.resolve(null);
292
+ }
293
+ if (session.finish) return session.finish;
294
+ if (session.phase === "error") return Promise.resolve(null);
295
+ session.finish = finish(session);
296
+ return session.finish;
297
+ }, [cancel, finish, isCurrent]);
298
+ const retry = useCallback(() => {
299
+ const session = current.current;
300
+ if (session && !isCurrent(session)) {
301
+ cancel();
302
+ return Promise.resolve(null);
303
+ }
304
+ if (!session?.transcribe || !session.blob?.size) return Promise.resolve(null);
305
+ if (session.phase !== "error") return session.finish ?? Promise.resolve(null);
306
+ session.finish = finish(session);
307
+ return session.finish;
308
+ }, [cancel, finish, isCurrent]);
309
+ const visibleSnapshot = current.current && !isCurrent(current.current) ? EMPTY : snapshot;
310
+ return { ...visibleSnapshot, supported, start, stop, retry, cancel };
311
+ }
312
+
313
+ export {
314
+ DictationProvider,
315
+ useDictation
316
+ };