@epicbagel/saas-kit 1.10.0 → 1.11.1
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 +21 -3
- package/dist/{chunk-VBKJXNEL.js → chunk-5OF6D3GH.js} +4 -3
- package/dist/chunk-D47AMKT4.cjs +293 -0
- package/dist/chunk-EDEN7TAL.js +293 -0
- package/dist/{chunk-M2ZNC3TK.js → chunk-HLAHOVEI.js} +98 -2
- package/dist/{chunk-KUG4ONYC.js → chunk-KF4X37DP.js} +40 -27
- package/dist/{chunk-XB2D4NQK.cjs → chunk-NKWHDI4C.cjs} +4 -3
- package/dist/{chunk-UW67GSZ4.cjs → chunk-YMI3X2YL.cjs} +38 -25
- 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 +9 -11
- package/dist/primitives/voice-dictation-button.d.ts +9 -11
- 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 +3 -1
- package/dist/primitives/voice-session-controls.d.ts +3 -1
- package/dist/primitives/voice-session-controls.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HTCW6AEH.cjs +0 -223
- package/dist/chunk-KWV5KTOA.js +0 -223
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@ Peer dependencies: `react` and `react-dom` (18 or 19), `lucide-react`
|
|
|
20
20
|
Pass a controlled `voiceSession` to `Chat` or `ChatInput` to show start, mute,
|
|
21
21
|
end, connection and speaking controls alongside a separate dictation mic.
|
|
22
22
|
Dictation is disabled during a live call and the typed draft is preserved.
|
|
23
|
+
The active session appears as a compact horizontal strip above the text field:
|
|
24
|
+
microphone badge, audio bars, status, **Mute** and **End voice**.
|
|
25
|
+
It wraps its controls on narrow screens. Transcripts belong in the conversation
|
|
26
|
+
thread and are not repeated in the strip. The legacy `transcript` prop and
|
|
27
|
+
`labels.transcriptPlaceholder` are accepted but ignored.
|
|
23
28
|
`VoiceSessionControls` is also exported on its own.
|
|
24
29
|
The host owns audio, authentication, transcripts, retries and cleanup; the kit
|
|
25
30
|
does not import a voice provider. Stop and release audio tracks before setting
|
|
@@ -27,7 +32,7 @@ does not import a voice provider. Stop and release audio tracks before setting
|
|
|
27
32
|
|
|
28
33
|
```tsx
|
|
29
34
|
<Chat messages={messages} onSend={sendMessage} voiceSession={{
|
|
30
|
-
state: voiceState, muted, error,
|
|
35
|
+
state: voiceState, muted, error, audioLevels,
|
|
31
36
|
onStart: startVoice, onEnd: endVoice, onMutedChange: setMuted,
|
|
32
37
|
}} />
|
|
33
38
|
```
|
|
@@ -36,7 +41,16 @@ does not import a voice provider. Stop and release audio tracks before setting
|
|
|
36
41
|
|
|
37
42
|
Wrap the application in `DictationProvider` to use an authenticated transcription
|
|
38
43
|
API for every `VoiceDictationButton`, including buttons nested in chat, forms or
|
|
39
|
-
another component library. The
|
|
44
|
+
another component library. The recorder opens beside its microphone on desktop
|
|
45
|
+
and fills the screen below 768px, with elapsed time, LiveKit audio bars and
|
|
46
|
+
Done/Cancel controls. The bars use the recording stream without opening a second
|
|
47
|
+
microphone. Resizing keeps the current recording alive.
|
|
48
|
+
|
|
49
|
+
For hidden/programmatic triggers, it uses the focused visible button as the
|
|
50
|
+
desktop anchor. Hosts can pass an explicit `anchorRef` for a custom trigger.
|
|
51
|
+
Without a visible anchor it opens full-screen. Nested and sibling dialogs keep
|
|
52
|
+
their focus and cancellation behavior.
|
|
53
|
+
The recorder runs until **Done**, shows
|
|
40
54
|
**Transcribing…**, then commits the returned text once. Failed requests keep the
|
|
41
55
|
take available for retry. Cancel stops microphone tracks, aborts the request and
|
|
42
56
|
ignores a late answer. The kit never persists audio or knows provider credentials.
|
|
@@ -62,6 +76,9 @@ cannot be retried under the new scope.
|
|
|
62
76
|
`useDictation` (`hooks/use-dictation`) exposes the same capture lifecycle for a
|
|
63
77
|
custom trigger such as press-and-hold: `start()`, `stop()`, `retry()`, `cancel()`,
|
|
64
78
|
`state`, `supported`, `transcript`, `interimTranscript`, `error` and `canRetry`.
|
|
79
|
+
For API recording, `audioLevels` exposes a subscribable snapshot of real microphone
|
|
80
|
+
levels. Subscribe only the waveform to keep audio sampling out of form renders;
|
|
81
|
+
native recognition has no audio-level source.
|
|
65
82
|
`stop()` and `retry()` return the completed text, or `null` after cancellation or
|
|
66
83
|
failure. Choose either that result or `onTranscript` to commit; using both would
|
|
67
84
|
duplicate the host action. The callback receives one complete API transcript,
|
|
@@ -164,7 +181,8 @@ public in 1.1.0, and these were the decisions about its props then:
|
|
|
164
181
|
for the textarea's starting height; there is no class-name or `rows` prop.
|
|
165
182
|
The textarea grows with its text (`field-sizing: content`).
|
|
166
183
|
- **Compact mobile replies.** Opt into `Chat.mobileCompactComposer` to fold
|
|
167
|
-
an active conversation's empty composer into
|
|
184
|
+
an active conversation's empty composer into a short text row above its
|
|
185
|
+
controls below 768px, keeping all four actions clear of the text. Focusing
|
|
168
186
|
its textarea expands it; leaving the composer folds it again only when
|
|
169
187
|
there is no draft or staged file. The same textarea stays mounted, and
|
|
170
188
|
its file menu and voice dialog keep an editing session open. New
|
|
@@ -110,14 +110,15 @@ function AgentAudioVisualizerBar({
|
|
|
110
110
|
return 1e3;
|
|
111
111
|
}
|
|
112
112
|
}, [state, _barCount]);
|
|
113
|
+
const meteredListening = state === "listening" && volumeBands !== void 0;
|
|
113
114
|
const highlightedIndices = useAgentAudioVisualizerBarAnimator(
|
|
114
|
-
state,
|
|
115
|
+
meteredListening ? "speaking" : state,
|
|
115
116
|
_barCount,
|
|
116
117
|
sequencerInterval
|
|
117
118
|
);
|
|
118
119
|
const bands = useMemo(
|
|
119
|
-
() => state === "speaking" ? resolvedVolumeBands : new Array(_barCount).fill(0),
|
|
120
|
-
[state, resolvedVolumeBands, _barCount]
|
|
120
|
+
() => state === "speaking" || meteredListening ? resolvedVolumeBands : new Array(_barCount).fill(0),
|
|
121
|
+
[state, meteredListening, resolvedVolumeBands, _barCount]
|
|
121
122
|
);
|
|
122
123
|
if (children && Array.isArray(children)) {
|
|
123
124
|
throw new Error("AgentAudioVisualizerBar children must be a single element.");
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkNKWHDI4Ccjs = require('./chunk-NKWHDI4C.cjs');
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
var _chunkZZ7ICQR2cjs = require('./chunk-ZZ7ICQR2.cjs');
|
|
14
|
+
|
|
15
|
+
// src/primitives/voice-dictation-button.tsx
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
var _react = require('react');
|
|
24
|
+
var _reactdialog = require('@radix-ui/react-dialog'); var DialogPrimitive = _interopRequireWildcard(_reactdialog);
|
|
25
|
+
var _reactpopover = require('@radix-ui/react-popover'); var PopoverPrimitive = _interopRequireWildcard(_reactpopover);
|
|
26
|
+
var _lucidereact = require('lucide-react');
|
|
27
|
+
var _jsxruntime = require('react/jsx-runtime');
|
|
28
|
+
function VoiceDictationButton({
|
|
29
|
+
onCommit,
|
|
30
|
+
onCancel,
|
|
31
|
+
title = "Dictate your message",
|
|
32
|
+
caption = "Recording",
|
|
33
|
+
lang = "en-US",
|
|
34
|
+
continuous = true,
|
|
35
|
+
className,
|
|
36
|
+
size = "sm",
|
|
37
|
+
showLabel = false,
|
|
38
|
+
children,
|
|
39
|
+
disabled = false,
|
|
40
|
+
visibility = "visible",
|
|
41
|
+
shrink = true,
|
|
42
|
+
anchorRef
|
|
43
|
+
}) {
|
|
44
|
+
const [open, setOpen] = _react.useState.call(void 0, false);
|
|
45
|
+
const [anchor, setAnchor] = _react.useState.call(void 0, null);
|
|
46
|
+
const desktop = _react.useSyncExternalStore.call(void 0, subscribeDesktop, isDesktop, () => false);
|
|
47
|
+
const anchored = desktop && isVisibleAnchor(anchor);
|
|
48
|
+
const placement = _react.useRef.call(void 0, anchored);
|
|
49
|
+
placement.current = anchored;
|
|
50
|
+
const isOpen = _react.useRef.call(void 0, open);
|
|
51
|
+
isOpen.current = open;
|
|
52
|
+
const descriptionId = _react.useId.call(void 0, );
|
|
53
|
+
const capture = _chunkZZ7ICQR2cjs.useDictation.call(void 0, { lang, continuous });
|
|
54
|
+
const { transcript, interimTranscript: interim, supported, error } = capture;
|
|
55
|
+
const generation = _react.useRef.call(void 0, 0);
|
|
56
|
+
const finishing = _react.useRef.call(void 0, false);
|
|
57
|
+
const previousFocus = _react.useRef.call(void 0, null);
|
|
58
|
+
const cancelControl = _react.useRef.call(void 0, null);
|
|
59
|
+
const elapsed = useRecordingTime(capture.state);
|
|
60
|
+
const busy = capture.state === "requesting" || capture.state === "transcribing";
|
|
61
|
+
const openOverlay = _react.useCallback.call(void 0, (event) => {
|
|
62
|
+
const focused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
63
|
+
const explicit = _optionalChain([anchorRef, 'optionalAccess', _ => _.current]);
|
|
64
|
+
const invokingButton = _optionalChain([focused, 'optionalAccess', _2 => _2.matches, 'call', _3 => _3("button:not(:disabled), [role='button']:not([aria-disabled='true'])")]) ? focused : null;
|
|
65
|
+
const resolved = isVisibleAnchor(explicit) ? explicit : isVisibleAnchor(event.currentTarget) ? event.currentTarget : isVisibleAnchor(invokingButton) ? invokingButton : null;
|
|
66
|
+
previousFocus.current = _optionalChain([resolved, 'optionalAccess', _4 => _4.matches, 'call', _5 => _5("button, a[href], input, select, textarea, [tabindex]")]) ? resolved : focused;
|
|
67
|
+
setAnchor(resolved);
|
|
68
|
+
generation.current++;
|
|
69
|
+
finishing.current = false;
|
|
70
|
+
setOpen(true);
|
|
71
|
+
void capture.start();
|
|
72
|
+
}, [anchorRef, capture.start]);
|
|
73
|
+
const closeOverlay = _react.useCallback.call(void 0, () => {
|
|
74
|
+
generation.current++;
|
|
75
|
+
finishing.current = false;
|
|
76
|
+
capture.cancel();
|
|
77
|
+
setOpen(false);
|
|
78
|
+
}, [capture.cancel]);
|
|
79
|
+
const handleDone = _react.useCallback.call(void 0, async () => {
|
|
80
|
+
if (finishing.current || busy) return;
|
|
81
|
+
_optionalChain([cancelControl, 'access', _6 => _6.current, 'optionalAccess', _7 => _7.focus, 'call', _8 => _8({ preventScroll: true })]);
|
|
82
|
+
if (capture.state === "error" && !capture.canRetry) {
|
|
83
|
+
void capture.start();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
finishing.current = true;
|
|
87
|
+
const started = generation.current;
|
|
88
|
+
const final = await (capture.canRetry ? capture.retry() : capture.stop());
|
|
89
|
+
if (started !== generation.current) return;
|
|
90
|
+
finishing.current = false;
|
|
91
|
+
if (final === null) return;
|
|
92
|
+
closeOverlay();
|
|
93
|
+
if (final) onCommit(final);
|
|
94
|
+
else _optionalChain([onCancel, 'optionalCall', _9 => _9()]);
|
|
95
|
+
}, [busy, capture.canRetry, capture.retry, capture.start, capture.state, capture.stop, onCommit, onCancel, closeOverlay]);
|
|
96
|
+
const handleCancel = _react.useCallback.call(void 0, () => {
|
|
97
|
+
closeOverlay();
|
|
98
|
+
_optionalChain([onCancel, 'optionalCall', _10 => _10()]);
|
|
99
|
+
}, [closeOverlay, onCancel]);
|
|
100
|
+
_react.useEffect.call(void 0, () => {
|
|
101
|
+
if (open && capture.state === "idle") handleCancel();
|
|
102
|
+
}, [capture.state, handleCancel, open]);
|
|
103
|
+
_react.useEffect.call(void 0, () => {
|
|
104
|
+
return () => {
|
|
105
|
+
generation.current++;
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
if (!supported) return null;
|
|
109
|
+
const displayTranscript = `${transcript}${interim ? (transcript ? " " : "") + interim : ""}`;
|
|
110
|
+
const closeAutoFocus = (event) => {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
if (!isOpen.current && _optionalChain([previousFocus, 'access', _11 => _11.current, 'optionalAccess', _12 => _12.isConnected])) previousFocus.current.focus();
|
|
113
|
+
};
|
|
114
|
+
const onKeyDown = (event) => {
|
|
115
|
+
if (event.key === "Enter" && !event.shiftKey && !(event.target instanceof HTMLElement && event.target.closest("button, input, textarea, select"))) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
void handleDone();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const content = /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
121
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: _chunkKWLSBTB2cjs.cn.call(void 0, "flex min-w-0 items-start justify-between gap-3", !anchored && "w-full max-w-xl self-center"), children: [
|
|
122
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "min-w-0 space-y-1", children: [
|
|
123
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: _chunkKWLSBTB2cjs.cn.call(void 0, "font-medium", anchored ? "text-sm" : "text-lg"), children: title || "Voice dictation" }),
|
|
124
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { role: "status", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
125
|
+
busy ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.LoaderCircle, { "aria-hidden": "true", className: "size-3.5 animate-spin motion-reduce:animate-none" }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Mic, { "aria-hidden": "true", className: "size-3.5" }),
|
|
126
|
+
capture.state === "transcribing" ? "Transcribing\u2026" : capture.state === "requesting" ? "Waiting for microphone\u2026" : error ? "Recording not saved" : capture.state === "ready" ? "Ready" : caption
|
|
127
|
+
] })
|
|
128
|
+
] }),
|
|
129
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "time", { "aria-label": "Recording duration", dateTime: `PT${elapsed}S`, className: "pt-0.5 font-mono text-xs tabular-nums text-muted-foreground", children: [
|
|
130
|
+
Math.floor(elapsed / 60).toString().padStart(2, "0"),
|
|
131
|
+
":",
|
|
132
|
+
(elapsed % 60).toString().padStart(2, "0")
|
|
133
|
+
] })
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: _chunkKWLSBTB2cjs.cn.call(void 0, "flex min-h-0 flex-col gap-3", !anchored && "w-full max-w-xl flex-1 justify-center self-center py-8 text-center"), children: [
|
|
136
|
+
capture.audioLevels && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, DictationWaveform, { source: capture.audioLevels, state: capture.state, className: !anchored ? "h-20" : void 0 }),
|
|
137
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { id: descriptionId, 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." }),
|
|
138
|
+
displayTranscript && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { className: "max-h-40 overflow-y-auto text-sm leading-relaxed", children: [
|
|
139
|
+
transcript,
|
|
140
|
+
interim && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "text-muted-foreground", children: [
|
|
141
|
+
transcript ? " " : "",
|
|
142
|
+
interim
|
|
143
|
+
] })
|
|
144
|
+
] }),
|
|
145
|
+
error && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { role: "alert", className: "text-xs text-destructive", children: error })
|
|
146
|
+
] }),
|
|
147
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: _chunkKWLSBTB2cjs.cn.call(void 0, "gap-2", anchored ? "flex flex-wrap items-center justify-end" : "mt-auto grid w-full max-w-xl grid-cols-2 gap-3 self-center"), children: [
|
|
148
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.Button, { ref: cancelControl, type: "button", variant: "ghost", size: "sm", className: anchored ? "min-h-[44px]" : "min-h-[48px]", onClick: handleCancel, children: "Cancel" }),
|
|
149
|
+
capture.canRetry && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.Button, { type: "button", variant: "outline", size: "sm", className: anchored ? "min-h-[44px]" : "order-first col-span-2 min-h-[48px]", onClick: () => {
|
|
150
|
+
_optionalChain([cancelControl, 'access', _13 => _13.current, 'optionalAccess', _14 => _14.focus, 'call', _15 => _15({ preventScroll: true })]);
|
|
151
|
+
void capture.start();
|
|
152
|
+
}, children: "Record again" }),
|
|
153
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.Button, { type: "button", size: "sm", className: anchored ? "min-h-[44px]" : "min-h-[48px]", onClick: () => void handleDone(), disabled: busy, children: capture.state === "transcribing" ? "Transcribing\u2026" : capture.canRetry ? "Retry transcription" : error ? "Try again" : "Done" })
|
|
154
|
+
] })
|
|
155
|
+
] });
|
|
156
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
157
|
+
children ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
158
|
+
"button",
|
|
159
|
+
{
|
|
160
|
+
type: "button",
|
|
161
|
+
onClick: openOverlay,
|
|
162
|
+
disabled,
|
|
163
|
+
"aria-label": "Start dictation",
|
|
164
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, visibility === "hidden" && "hidden", className),
|
|
165
|
+
children
|
|
166
|
+
}
|
|
167
|
+
) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
168
|
+
_chunkJDEXZGIYcjs.Button,
|
|
169
|
+
{
|
|
170
|
+
type: "button",
|
|
171
|
+
onClick: openOverlay,
|
|
172
|
+
disabled,
|
|
173
|
+
variant: "outline",
|
|
174
|
+
size,
|
|
175
|
+
"aria-label": "Start dictation",
|
|
176
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, "transition-colors", visibility === "hidden" && "hidden", !shrink && "shrink-0", className),
|
|
177
|
+
children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "flex items-center justify-center gap-2", children: [
|
|
178
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Mic, { className: "h-4 w-4" }),
|
|
179
|
+
showLabel && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "Speak" })
|
|
180
|
+
] })
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, PopoverPrimitive.Root, { modal: true, open: open && anchored, onOpenChange: (next) => {
|
|
184
|
+
if (!next && placement.current) handleCancel();
|
|
185
|
+
}, children: [
|
|
186
|
+
anchored && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, PopoverPrimitive.Anchor, { virtualRef: { current: anchor } }),
|
|
187
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, PopoverPrimitive.Portal, { children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
188
|
+
PopoverPrimitive.Content,
|
|
189
|
+
{
|
|
190
|
+
"aria-label": "Voice dictation",
|
|
191
|
+
"aria-describedby": descriptionId,
|
|
192
|
+
"data-slot": "voice-dictation-panel",
|
|
193
|
+
"data-state": capture.state,
|
|
194
|
+
"data-placement": "anchored",
|
|
195
|
+
side: "top",
|
|
196
|
+
align: "center",
|
|
197
|
+
sideOffset: 8,
|
|
198
|
+
collisionPadding: 12,
|
|
199
|
+
avoidCollisions: true,
|
|
200
|
+
onCloseAutoFocus: closeAutoFocus,
|
|
201
|
+
onKeyDown,
|
|
202
|
+
onEscapeKeyDown: (event) => event.stopPropagation(),
|
|
203
|
+
className: "z-[200] flex max-h-[var(--radix-popover-content-available-height)] w-96 max-w-[calc(100vw-1.5rem)] flex-col gap-3 overflow-y-auto rounded-2xl border border-border bg-background p-4 text-foreground shadow-lg outline-hidden",
|
|
204
|
+
children: content
|
|
205
|
+
}
|
|
206
|
+
) })
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Root, { open: open && !anchored, onOpenChange: (next) => {
|
|
209
|
+
if (!next && !placement.current) handleCancel();
|
|
210
|
+
}, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, DialogPrimitive.Portal, { children: [
|
|
211
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Overlay, { className: "fixed inset-0 z-[199] bg-background" }),
|
|
212
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
213
|
+
DialogPrimitive.Content,
|
|
214
|
+
{
|
|
215
|
+
"aria-label": "Voice dictation",
|
|
216
|
+
"aria-labelledby": void 0,
|
|
217
|
+
"data-slot": "voice-dictation-panel",
|
|
218
|
+
"data-state": capture.state,
|
|
219
|
+
"data-placement": "fullscreen",
|
|
220
|
+
onCloseAutoFocus: closeAutoFocus,
|
|
221
|
+
onKeyDown,
|
|
222
|
+
onEscapeKeyDown: (event) => event.stopPropagation(),
|
|
223
|
+
className: "fixed inset-0 z-[200] flex h-dvh w-screen flex-col gap-6 overflow-y-auto overscroll-contain bg-background px-6 pt-[max(1.5rem,env(safe-area-inset-top))] pb-[max(1.5rem,env(safe-area-inset-bottom))] text-foreground outline-hidden",
|
|
224
|
+
children: [
|
|
225
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Title, { className: "sr-only", children: "Voice dictation" }),
|
|
226
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Description, { className: "sr-only", children: "Record your message, then choose Done to add the text. Cancel discards the recording." }),
|
|
227
|
+
content
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
] }) })
|
|
232
|
+
] });
|
|
233
|
+
}
|
|
234
|
+
var SILENCE = [];
|
|
235
|
+
var silence = () => SILENCE;
|
|
236
|
+
function DictationWaveform({ source, state, className }) {
|
|
237
|
+
const bands = _react.useSyncExternalStore.call(void 0, source.subscribe, source.getSnapshot, silence);
|
|
238
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
239
|
+
_chunkNKWHDI4Ccjs.AgentAudioVisualizerBar,
|
|
240
|
+
{
|
|
241
|
+
"aria-hidden": "true",
|
|
242
|
+
"data-slot": "dictation-waveform",
|
|
243
|
+
size: "icon",
|
|
244
|
+
barCount: 24,
|
|
245
|
+
state: state === "recording" ? "listening" : state === "transcribing" ? "thinking" : state === "requesting" ? "connecting" : "disconnected",
|
|
246
|
+
volumeBands: state === "recording" ? bands : void 0,
|
|
247
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, "h-9 w-full text-foreground", className)
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function useRecordingTime(state) {
|
|
252
|
+
const [elapsed, setElapsed] = _react.useState.call(void 0, 0);
|
|
253
|
+
const startedAt = _react.useRef.call(void 0, null);
|
|
254
|
+
_react.useEffect.call(void 0, () => {
|
|
255
|
+
if (state !== "recording") {
|
|
256
|
+
if (state === "idle" || state === "requesting") setElapsed(0);
|
|
257
|
+
else if (startedAt.current !== null) setElapsed(Math.floor((performance.now() - startedAt.current) / 1e3));
|
|
258
|
+
startedAt.current = null;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
startedAt.current = performance.now();
|
|
262
|
+
const update = () => setElapsed(Math.floor((performance.now() - startedAt.current) / 1e3));
|
|
263
|
+
update();
|
|
264
|
+
const timer = setInterval(update, 1e3);
|
|
265
|
+
return () => {
|
|
266
|
+
clearInterval(timer);
|
|
267
|
+
};
|
|
268
|
+
}, [state]);
|
|
269
|
+
return elapsed;
|
|
270
|
+
}
|
|
271
|
+
var DESKTOP_QUERY = "(min-width: 768px)";
|
|
272
|
+
function isDesktop() {
|
|
273
|
+
return typeof window !== "undefined" && !!_optionalChain([window, 'access', _16 => _16.matchMedia, 'optionalCall', _17 => _17(DESKTOP_QUERY), 'access', _18 => _18.matches]);
|
|
274
|
+
}
|
|
275
|
+
function subscribeDesktop(changed) {
|
|
276
|
+
const media = _optionalChain([window, 'access', _19 => _19.matchMedia, 'optionalCall', _20 => _20(DESKTOP_QUERY)]);
|
|
277
|
+
_optionalChain([media, 'optionalAccess', _21 => _21.addEventListener, 'call', _22 => _22("change", changed)]);
|
|
278
|
+
return () => _optionalChain([media, 'optionalAccess', _23 => _23.removeEventListener, 'call', _24 => _24("change", changed)]);
|
|
279
|
+
}
|
|
280
|
+
function isVisibleAnchor(element) {
|
|
281
|
+
if (!_optionalChain([element, 'optionalAccess', _25 => _25.isConnected]) || !element.getClientRects().length) return false;
|
|
282
|
+
const rect = element.getBoundingClientRect();
|
|
283
|
+
if (!rect.width || !rect.height) return false;
|
|
284
|
+
for (let parent = element; parent; parent = parent.parentElement) {
|
|
285
|
+
const style = getComputedStyle(parent);
|
|
286
|
+
if (parent.hidden || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || Number.parseFloat(style.opacity) === 0) return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
exports.VoiceDictationButton = VoiceDictationButton;
|
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
useId,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
useSyncExternalStore
|
|
23
|
+
} from "react";
|
|
24
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
25
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
26
|
+
import { Mic, LoaderCircle } from "lucide-react";
|
|
27
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
function VoiceDictationButton({
|
|
29
|
+
onCommit,
|
|
30
|
+
onCancel,
|
|
31
|
+
title = "Dictate your message",
|
|
32
|
+
caption = "Recording",
|
|
33
|
+
lang = "en-US",
|
|
34
|
+
continuous = true,
|
|
35
|
+
className,
|
|
36
|
+
size = "sm",
|
|
37
|
+
showLabel = false,
|
|
38
|
+
children,
|
|
39
|
+
disabled = false,
|
|
40
|
+
visibility = "visible",
|
|
41
|
+
shrink = true,
|
|
42
|
+
anchorRef
|
|
43
|
+
}) {
|
|
44
|
+
const [open, setOpen] = useState(false);
|
|
45
|
+
const [anchor, setAnchor] = useState(null);
|
|
46
|
+
const desktop = useSyncExternalStore(subscribeDesktop, isDesktop, () => false);
|
|
47
|
+
const anchored = desktop && isVisibleAnchor(anchor);
|
|
48
|
+
const placement = useRef(anchored);
|
|
49
|
+
placement.current = anchored;
|
|
50
|
+
const isOpen = useRef(open);
|
|
51
|
+
isOpen.current = open;
|
|
52
|
+
const descriptionId = useId();
|
|
53
|
+
const capture = useDictation({ lang, continuous });
|
|
54
|
+
const { transcript, interimTranscript: interim, supported, error } = capture;
|
|
55
|
+
const generation = useRef(0);
|
|
56
|
+
const finishing = useRef(false);
|
|
57
|
+
const previousFocus = useRef(null);
|
|
58
|
+
const cancelControl = useRef(null);
|
|
59
|
+
const elapsed = useRecordingTime(capture.state);
|
|
60
|
+
const busy = capture.state === "requesting" || capture.state === "transcribing";
|
|
61
|
+
const openOverlay = useCallback((event) => {
|
|
62
|
+
const focused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
63
|
+
const explicit = anchorRef?.current;
|
|
64
|
+
const invokingButton = focused?.matches("button:not(:disabled), [role='button']:not([aria-disabled='true'])") ? focused : null;
|
|
65
|
+
const resolved = isVisibleAnchor(explicit) ? explicit : isVisibleAnchor(event.currentTarget) ? event.currentTarget : isVisibleAnchor(invokingButton) ? invokingButton : null;
|
|
66
|
+
previousFocus.current = resolved?.matches("button, a[href], input, select, textarea, [tabindex]") ? resolved : focused;
|
|
67
|
+
setAnchor(resolved);
|
|
68
|
+
generation.current++;
|
|
69
|
+
finishing.current = false;
|
|
70
|
+
setOpen(true);
|
|
71
|
+
void capture.start();
|
|
72
|
+
}, [anchorRef, capture.start]);
|
|
73
|
+
const closeOverlay = useCallback(() => {
|
|
74
|
+
generation.current++;
|
|
75
|
+
finishing.current = false;
|
|
76
|
+
capture.cancel();
|
|
77
|
+
setOpen(false);
|
|
78
|
+
}, [capture.cancel]);
|
|
79
|
+
const handleDone = useCallback(async () => {
|
|
80
|
+
if (finishing.current || busy) return;
|
|
81
|
+
cancelControl.current?.focus({ preventScroll: true });
|
|
82
|
+
if (capture.state === "error" && !capture.canRetry) {
|
|
83
|
+
void capture.start();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
finishing.current = true;
|
|
87
|
+
const started = generation.current;
|
|
88
|
+
const final = await (capture.canRetry ? capture.retry() : capture.stop());
|
|
89
|
+
if (started !== generation.current) return;
|
|
90
|
+
finishing.current = false;
|
|
91
|
+
if (final === null) return;
|
|
92
|
+
closeOverlay();
|
|
93
|
+
if (final) onCommit(final);
|
|
94
|
+
else onCancel?.();
|
|
95
|
+
}, [busy, capture.canRetry, capture.retry, capture.start, capture.state, capture.stop, onCommit, onCancel, closeOverlay]);
|
|
96
|
+
const handleCancel = useCallback(() => {
|
|
97
|
+
closeOverlay();
|
|
98
|
+
onCancel?.();
|
|
99
|
+
}, [closeOverlay, onCancel]);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (open && capture.state === "idle") handleCancel();
|
|
102
|
+
}, [capture.state, handleCancel, open]);
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
return () => {
|
|
105
|
+
generation.current++;
|
|
106
|
+
};
|
|
107
|
+
}, []);
|
|
108
|
+
if (!supported) return null;
|
|
109
|
+
const displayTranscript = `${transcript}${interim ? (transcript ? " " : "") + interim : ""}`;
|
|
110
|
+
const closeAutoFocus = (event) => {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
if (!isOpen.current && previousFocus.current?.isConnected) previousFocus.current.focus();
|
|
113
|
+
};
|
|
114
|
+
const onKeyDown = (event) => {
|
|
115
|
+
if (event.key === "Enter" && !event.shiftKey && !(event.target instanceof HTMLElement && event.target.closest("button, input, textarea, select"))) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
void handleDone();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
121
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex min-w-0 items-start justify-between gap-3", !anchored && "w-full max-w-xl self-center"), children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1", children: [
|
|
123
|
+
/* @__PURE__ */ jsx("h2", { className: cn("font-medium", anchored ? "text-sm" : "text-lg"), children: title || "Voice dictation" }),
|
|
124
|
+
/* @__PURE__ */ jsxs("span", { role: "status", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
125
|
+
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" }),
|
|
126
|
+
capture.state === "transcribing" ? "Transcribing\u2026" : capture.state === "requesting" ? "Waiting for microphone\u2026" : error ? "Recording not saved" : capture.state === "ready" ? "Ready" : caption
|
|
127
|
+
] })
|
|
128
|
+
] }),
|
|
129
|
+
/* @__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: [
|
|
130
|
+
Math.floor(elapsed / 60).toString().padStart(2, "0"),
|
|
131
|
+
":",
|
|
132
|
+
(elapsed % 60).toString().padStart(2, "0")
|
|
133
|
+
] })
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsxs("div", { className: cn("flex min-h-0 flex-col gap-3", !anchored && "w-full max-w-xl flex-1 justify-center self-center py-8 text-center"), children: [
|
|
136
|
+
capture.audioLevels && /* @__PURE__ */ jsx(DictationWaveform, { source: capture.audioLevels, state: capture.state, className: !anchored ? "h-20" : void 0 }),
|
|
137
|
+
/* @__PURE__ */ jsx("p", { id: descriptionId, 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." }),
|
|
138
|
+
displayTranscript && /* @__PURE__ */ jsxs("p", { className: "max-h-40 overflow-y-auto text-sm leading-relaxed", children: [
|
|
139
|
+
transcript,
|
|
140
|
+
interim && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
141
|
+
transcript ? " " : "",
|
|
142
|
+
interim
|
|
143
|
+
] })
|
|
144
|
+
] }),
|
|
145
|
+
error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-xs text-destructive", children: error })
|
|
146
|
+
] }),
|
|
147
|
+
/* @__PURE__ */ jsxs("div", { className: cn("gap-2", anchored ? "flex flex-wrap items-center justify-end" : "mt-auto grid w-full max-w-xl grid-cols-2 gap-3 self-center"), children: [
|
|
148
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelControl, type: "button", variant: "ghost", size: "sm", className: anchored ? "min-h-[44px]" : "min-h-[48px]", onClick: handleCancel, children: "Cancel" }),
|
|
149
|
+
capture.canRetry && /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", className: anchored ? "min-h-[44px]" : "order-first col-span-2 min-h-[48px]", onClick: () => {
|
|
150
|
+
cancelControl.current?.focus({ preventScroll: true });
|
|
151
|
+
void capture.start();
|
|
152
|
+
}, children: "Record again" }),
|
|
153
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", className: anchored ? "min-h-[44px]" : "min-h-[48px]", onClick: () => void handleDone(), disabled: busy, children: capture.state === "transcribing" ? "Transcribing\u2026" : capture.canRetry ? "Retry transcription" : error ? "Try again" : "Done" })
|
|
154
|
+
] })
|
|
155
|
+
] });
|
|
156
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
157
|
+
children ? /* @__PURE__ */ jsx(
|
|
158
|
+
"button",
|
|
159
|
+
{
|
|
160
|
+
type: "button",
|
|
161
|
+
onClick: openOverlay,
|
|
162
|
+
disabled,
|
|
163
|
+
"aria-label": "Start dictation",
|
|
164
|
+
className: cn(visibility === "hidden" && "hidden", className),
|
|
165
|
+
children
|
|
166
|
+
}
|
|
167
|
+
) : /* @__PURE__ */ jsx(
|
|
168
|
+
Button,
|
|
169
|
+
{
|
|
170
|
+
type: "button",
|
|
171
|
+
onClick: openOverlay,
|
|
172
|
+
disabled,
|
|
173
|
+
variant: "outline",
|
|
174
|
+
size,
|
|
175
|
+
"aria-label": "Start dictation",
|
|
176
|
+
className: cn("transition-colors", visibility === "hidden" && "hidden", !shrink && "shrink-0", className),
|
|
177
|
+
children: /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-center gap-2", children: [
|
|
178
|
+
/* @__PURE__ */ jsx(Mic, { className: "h-4 w-4" }),
|
|
179
|
+
showLabel && /* @__PURE__ */ jsx("span", { children: "Speak" })
|
|
180
|
+
] })
|
|
181
|
+
}
|
|
182
|
+
),
|
|
183
|
+
/* @__PURE__ */ jsxs(PopoverPrimitive.Root, { modal: true, open: open && anchored, onOpenChange: (next) => {
|
|
184
|
+
if (!next && placement.current) handleCancel();
|
|
185
|
+
}, children: [
|
|
186
|
+
anchored && /* @__PURE__ */ jsx(PopoverPrimitive.Anchor, { virtualRef: { current: anchor } }),
|
|
187
|
+
/* @__PURE__ */ jsx(PopoverPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
188
|
+
PopoverPrimitive.Content,
|
|
189
|
+
{
|
|
190
|
+
"aria-label": "Voice dictation",
|
|
191
|
+
"aria-describedby": descriptionId,
|
|
192
|
+
"data-slot": "voice-dictation-panel",
|
|
193
|
+
"data-state": capture.state,
|
|
194
|
+
"data-placement": "anchored",
|
|
195
|
+
side: "top",
|
|
196
|
+
align: "center",
|
|
197
|
+
sideOffset: 8,
|
|
198
|
+
collisionPadding: 12,
|
|
199
|
+
avoidCollisions: true,
|
|
200
|
+
onCloseAutoFocus: closeAutoFocus,
|
|
201
|
+
onKeyDown,
|
|
202
|
+
onEscapeKeyDown: (event) => event.stopPropagation(),
|
|
203
|
+
className: "z-[200] flex max-h-[var(--radix-popover-content-available-height)] w-96 max-w-[calc(100vw-1.5rem)] flex-col gap-3 overflow-y-auto rounded-2xl border border-border bg-background p-4 text-foreground shadow-lg outline-hidden",
|
|
204
|
+
children: content
|
|
205
|
+
}
|
|
206
|
+
) })
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ jsx(DialogPrimitive.Root, { open: open && !anchored, onOpenChange: (next) => {
|
|
209
|
+
if (!next && !placement.current) handleCancel();
|
|
210
|
+
}, children: /* @__PURE__ */ jsxs(DialogPrimitive.Portal, { children: [
|
|
211
|
+
/* @__PURE__ */ jsx(DialogPrimitive.Overlay, { className: "fixed inset-0 z-[199] bg-background" }),
|
|
212
|
+
/* @__PURE__ */ jsxs(
|
|
213
|
+
DialogPrimitive.Content,
|
|
214
|
+
{
|
|
215
|
+
"aria-label": "Voice dictation",
|
|
216
|
+
"aria-labelledby": void 0,
|
|
217
|
+
"data-slot": "voice-dictation-panel",
|
|
218
|
+
"data-state": capture.state,
|
|
219
|
+
"data-placement": "fullscreen",
|
|
220
|
+
onCloseAutoFocus: closeAutoFocus,
|
|
221
|
+
onKeyDown,
|
|
222
|
+
onEscapeKeyDown: (event) => event.stopPropagation(),
|
|
223
|
+
className: "fixed inset-0 z-[200] flex h-dvh w-screen flex-col gap-6 overflow-y-auto overscroll-contain bg-background px-6 pt-[max(1.5rem,env(safe-area-inset-top))] pb-[max(1.5rem,env(safe-area-inset-bottom))] text-foreground outline-hidden",
|
|
224
|
+
children: [
|
|
225
|
+
/* @__PURE__ */ jsx(DialogPrimitive.Title, { className: "sr-only", children: "Voice dictation" }),
|
|
226
|
+
/* @__PURE__ */ jsx(DialogPrimitive.Description, { className: "sr-only", children: "Record your message, then choose Done to add the text. Cancel discards the recording." }),
|
|
227
|
+
content
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
] }) })
|
|
232
|
+
] });
|
|
233
|
+
}
|
|
234
|
+
var SILENCE = [];
|
|
235
|
+
var silence = () => SILENCE;
|
|
236
|
+
function DictationWaveform({ source, state, className }) {
|
|
237
|
+
const bands = useSyncExternalStore(source.subscribe, source.getSnapshot, silence);
|
|
238
|
+
return /* @__PURE__ */ jsx(
|
|
239
|
+
AgentAudioVisualizerBar,
|
|
240
|
+
{
|
|
241
|
+
"aria-hidden": "true",
|
|
242
|
+
"data-slot": "dictation-waveform",
|
|
243
|
+
size: "icon",
|
|
244
|
+
barCount: 24,
|
|
245
|
+
state: state === "recording" ? "listening" : state === "transcribing" ? "thinking" : state === "requesting" ? "connecting" : "disconnected",
|
|
246
|
+
volumeBands: state === "recording" ? bands : void 0,
|
|
247
|
+
className: cn("h-9 w-full text-foreground", className)
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function useRecordingTime(state) {
|
|
252
|
+
const [elapsed, setElapsed] = useState(0);
|
|
253
|
+
const startedAt = useRef(null);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
if (state !== "recording") {
|
|
256
|
+
if (state === "idle" || state === "requesting") setElapsed(0);
|
|
257
|
+
else if (startedAt.current !== null) setElapsed(Math.floor((performance.now() - startedAt.current) / 1e3));
|
|
258
|
+
startedAt.current = null;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
startedAt.current = performance.now();
|
|
262
|
+
const update = () => setElapsed(Math.floor((performance.now() - startedAt.current) / 1e3));
|
|
263
|
+
update();
|
|
264
|
+
const timer = setInterval(update, 1e3);
|
|
265
|
+
return () => {
|
|
266
|
+
clearInterval(timer);
|
|
267
|
+
};
|
|
268
|
+
}, [state]);
|
|
269
|
+
return elapsed;
|
|
270
|
+
}
|
|
271
|
+
var DESKTOP_QUERY = "(min-width: 768px)";
|
|
272
|
+
function isDesktop() {
|
|
273
|
+
return typeof window !== "undefined" && !!window.matchMedia?.(DESKTOP_QUERY).matches;
|
|
274
|
+
}
|
|
275
|
+
function subscribeDesktop(changed) {
|
|
276
|
+
const media = window.matchMedia?.(DESKTOP_QUERY);
|
|
277
|
+
media?.addEventListener("change", changed);
|
|
278
|
+
return () => media?.removeEventListener("change", changed);
|
|
279
|
+
}
|
|
280
|
+
function isVisibleAnchor(element) {
|
|
281
|
+
if (!element?.isConnected || !element.getClientRects().length) return false;
|
|
282
|
+
const rect = element.getBoundingClientRect();
|
|
283
|
+
if (!rect.width || !rect.height) return false;
|
|
284
|
+
for (let parent = element; parent; parent = parent.parentElement) {
|
|
285
|
+
const style = getComputedStyle(parent);
|
|
286
|
+
if (parent.hidden || style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || Number.parseFloat(style.opacity) === 0) return false;
|
|
287
|
+
}
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export {
|
|
292
|
+
VoiceDictationButton
|
|
293
|
+
};
|