@epicbagel/saas-kit 1.7.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.
- package/README.md +60 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/chunk-2BSFYU22.cjs +71 -0
- package/dist/{chunk-GTVSRRE3.cjs → chunk-BGCNGYPL.cjs} +2 -2
- package/dist/{chunk-QTWJQQDH.cjs → chunk-FBVZRRA4.cjs} +6 -6
- package/dist/chunk-GJ7S3L6R.js +71 -0
- package/dist/{chunk-LKPM6DQP.cjs → chunk-HTCW6AEH.cjs} +51 -72
- package/dist/{chunk-F3B4M6DU.js → chunk-HTPPNW74.js} +1 -1
- package/dist/chunk-KUG4ONYC.js +127 -0
- package/dist/{chunk-TI57GAQP.js → chunk-KWV5KTOA.js} +48 -69
- package/dist/{chunk-JN6JOSZG.js → chunk-LQPJGZIE.js} +7 -7
- package/dist/chunk-M2ZNC3TK.js +316 -0
- package/dist/chunk-O6F3LVZY.cjs +316 -0
- package/dist/chunk-UW67GSZ4.cjs +127 -0
- package/dist/chunk-VBKJXNEL.js +157 -0
- package/dist/chunk-XB2D4NQK.cjs +157 -0
- package/dist/hooks/use-agent-audio-visualizer-bar.cjs +8 -0
- package/dist/hooks/use-agent-audio-visualizer-bar.d.cts +4 -0
- package/dist/hooks/use-agent-audio-visualizer-bar.d.ts +4 -0
- package/dist/hooks/use-agent-audio-visualizer-bar.js +8 -0
- package/dist/hooks/use-dictation.cjs +10 -0
- package/dist/hooks/use-dictation.d.cts +56 -0
- package/dist/hooks/use-dictation.d.ts +56 -0
- package/dist/hooks/use-dictation.js +10 -0
- package/dist/primitives/activity-feed.cjs +3 -3
- package/dist/primitives/activity-feed.js +4 -4
- package/dist/primitives/agent-audio-visualizer-bar.cjs +16 -0
- package/dist/primitives/agent-audio-visualizer-bar.d.cts +75 -0
- package/dist/primitives/agent-audio-visualizer-bar.d.ts +75 -0
- package/dist/primitives/agent-audio-visualizer-bar.js +16 -0
- package/dist/primitives/archive-confirmation-dialog.cjs +2 -2
- package/dist/primitives/archive-confirmation-dialog.js +3 -3
- package/dist/primitives/attachment.d.cts +1 -1
- package/dist/primitives/attachment.d.ts +1 -1
- package/dist/primitives/badge.d.cts +1 -1
- package/dist/primitives/badge.d.ts +1 -1
- package/dist/primitives/board-view.cjs +6 -6
- package/dist/primitives/board-view.js +5 -5
- package/dist/primitives/bubble.d.cts +1 -1
- package/dist/primitives/bubble.d.ts +1 -1
- package/dist/primitives/button.d.cts +3 -3
- package/dist/primitives/button.d.ts +3 -3
- package/dist/primitives/chat.cjs +55 -24
- package/dist/primitives/chat.d.cts +6 -1
- package/dist/primitives/chat.d.ts +6 -1
- package/dist/primitives/chat.js +52 -21
- package/dist/primitives/code-block.cjs +4 -4
- package/dist/primitives/code-block.js +3 -3
- package/dist/primitives/connection.cjs +6 -6
- package/dist/primitives/connection.js +7 -7
- package/dist/primitives/copy-button.cjs +3 -3
- package/dist/primitives/copy-button.js +2 -2
- package/dist/primitives/dictation-provider.cjs +8 -0
- package/dist/primitives/dictation-provider.d.cts +2 -0
- package/dist/primitives/dictation-provider.d.ts +2 -0
- package/dist/primitives/dictation-provider.js +8 -0
- package/dist/primitives/editable-list.cjs +1 -1
- package/dist/primitives/editable-list.js +1 -1
- package/dist/primitives/file-dropzone.cjs +3 -3
- package/dist/primitives/file-dropzone.js +2 -2
- package/dist/primitives/file-preview.cjs +6 -6
- package/dist/primitives/file-preview.js +5 -5
- package/dist/primitives/filter-bar.cjs +4 -4
- package/dist/primitives/filter-bar.js +3 -3
- package/dist/primitives/input-group.d.cts +1 -1
- package/dist/primitives/input-group.d.ts +1 -1
- package/dist/primitives/item.d.cts +2 -2
- package/dist/primitives/item.d.ts +2 -2
- package/dist/primitives/resource-list.cjs +6 -6
- package/dist/primitives/resource-list.js +5 -5
- package/dist/primitives/section-header.d.cts +1 -1
- package/dist/primitives/section-header.d.ts +1 -1
- package/dist/primitives/section.cjs +2 -2
- package/dist/primitives/section.js +3 -3
- package/dist/primitives/sheet.d.cts +1 -1
- package/dist/primitives/sheet.d.ts +1 -1
- package/dist/primitives/switch-field.cjs +2 -2
- package/dist/primitives/switch-field.js +3 -3
- package/dist/primitives/theme-selector.cjs +3 -3
- package/dist/primitives/theme-selector.js +4 -4
- package/dist/primitives/toggle-group.d.cts +2 -2
- package/dist/primitives/toggle-group.d.ts +2 -2
- package/dist/primitives/toggle.d.cts +2 -2
- package/dist/primitives/toggle.d.ts +2 -2
- package/dist/primitives/voice-dictation-button.cjs +3 -2
- package/dist/primitives/voice-dictation-button.js +2 -1
- package/dist/primitives/voice-session-controls.cjs +12 -0
- package/dist/primitives/voice-session-controls.d.cts +29 -0
- package/dist/primitives/voice-session-controls.d.ts +29 -0
- package/dist/primitives/voice-session-controls.js +12 -0
- package/package.json +51 -1
- package/dist/{chunk-HREEZUKL.js → chunk-DILYV4FI.js} +3 -3
- package/dist/{chunk-MU4F3DSW.cjs → chunk-FARCLWLU.cjs} +2 -2
- package/dist/{chunk-CRTH5GJ5.cjs → chunk-MW67PHAQ.cjs} +3 -3
- package/dist/{chunk-KNKV25WS.js → chunk-UFFHGKI7.js} +3 -3
- package/dist/{chunk-HBRVOGLA.js → chunk-WHDHUYS6.js} +3 -3
- 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
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
-
|
|
85
|
-
|
|
44
|
+
generation.current++;
|
|
45
|
+
finishing.current = false;
|
|
86
46
|
setOpen(true);
|
|
87
|
-
|
|
88
|
-
}, [
|
|
47
|
+
void capture.start();
|
|
48
|
+
}, [capture.start]);
|
|
89
49
|
const closeOverlay = useCallback(() => {
|
|
90
|
-
|
|
50
|
+
generation.current++;
|
|
51
|
+
finishing.current = false;
|
|
52
|
+
capture.cancel();
|
|
91
53
|
setOpen(false);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}, [
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
|
|
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:
|
|
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-
|
|
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
|
+
};
|