@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.
- package/README.md +15 -4
- package/dist/{chunk-VBKJXNEL.js → chunk-5OF6D3GH.js} +4 -3
- package/dist/{chunk-UW67GSZ4.cjs → chunk-7QTFYEPE.cjs} +51 -25
- package/dist/{chunk-M2ZNC3TK.js → chunk-HLAHOVEI.js} +98 -2
- package/dist/chunk-KQBXAQAY.cjs +221 -0
- package/dist/{chunk-XB2D4NQK.cjs → chunk-NKWHDI4C.cjs} +4 -3
- package/dist/chunk-T3XMHWYV.js +221 -0
- package/dist/chunk-UOVA4YKW.js +153 -0
- package/dist/{chunk-O6F3LVZY.cjs → chunk-ZZ7ICQR2.cjs} +120 -24
- package/dist/hooks/use-dictation.cjs +2 -2
- package/dist/hooks/use-dictation.d.cts +5 -0
- package/dist/hooks/use-dictation.d.ts +5 -0
- package/dist/hooks/use-dictation.js +1 -1
- package/dist/primitives/agent-audio-visualizer-bar.cjs +2 -2
- package/dist/primitives/agent-audio-visualizer-bar.d.cts +1 -1
- package/dist/primitives/agent-audio-visualizer-bar.d.ts +1 -1
- package/dist/primitives/agent-audio-visualizer-bar.js +1 -1
- package/dist/primitives/alert.d.cts +1 -1
- package/dist/primitives/alert.d.ts +1 -1
- package/dist/primitives/badge.d.cts +1 -1
- package/dist/primitives/badge.d.ts +1 -1
- package/dist/primitives/bubble.d.cts +1 -1
- package/dist/primitives/bubble.d.ts +1 -1
- package/dist/primitives/button.d.cts +2 -2
- package/dist/primitives/button.d.ts +2 -2
- package/dist/primitives/chat.cjs +15 -15
- package/dist/primitives/chat.d.cts +2 -2
- package/dist/primitives/chat.d.ts +2 -2
- package/dist/primitives/chat.js +10 -10
- package/dist/primitives/dictation-provider.cjs +2 -2
- package/dist/primitives/dictation-provider.js +1 -1
- package/dist/primitives/voice-dictation-button.cjs +5 -3
- package/dist/primitives/voice-dictation-button.d.cts +5 -9
- package/dist/primitives/voice-dictation-button.d.ts +5 -9
- package/dist/primitives/voice-dictation-button.js +4 -2
- package/dist/primitives/voice-session-controls.cjs +3 -3
- package/dist/primitives/voice-session-controls.d.cts +4 -2
- package/dist/primitives/voice-session-controls.d.ts +4 -2
- package/dist/primitives/voice-session-controls.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HTCW6AEH.cjs +0 -223
- package/dist/chunk-KUG4ONYC.js +0 -127
- 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',
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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',
|
|
68
|
-
const scope = _nullishCoalesce(_optionalChain([provider, 'optionalAccess',
|
|
69
|
-
const scopeGeneration = _nullishCoalesce(_optionalChain([provider, 'optionalAccess',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
238
|
-
} catch (
|
|
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',
|
|
244
|
-
} catch (
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
|