@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
package/README.md
CHANGED
|
@@ -20,14 +20,18 @@ 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
|
-
|
|
23
|
+
The active session appears as a compact horizontal strip above the text field:
|
|
24
|
+
microphone badge, audio bars, status, latest utterance, **Mute** and **End voice**.
|
|
25
|
+
It wraps its controls on narrow screens. Pass the latest user utterance as
|
|
26
|
+
`voiceSession.transcript`; this is plain preview text and never submits or replaces
|
|
27
|
+
the typed draft. `VoiceSessionControls` is also exported on its own.
|
|
24
28
|
The host owns audio, authentication, transcripts, retries and cleanup; the kit
|
|
25
29
|
does not import a voice provider. Stop and release audio tracks before setting
|
|
26
30
|
`state: "error"` or unmounting. Every label is replaceable through `labels`.
|
|
27
31
|
|
|
28
32
|
```tsx
|
|
29
33
|
<Chat messages={messages} onSend={sendMessage} voiceSession={{
|
|
30
|
-
state: voiceState, muted, error,
|
|
34
|
+
state: voiceState, muted, error, transcript: latestUserUtterance, audioLevels,
|
|
31
35
|
onStart: startVoice, onEnd: endVoice, onMutedChange: setMuted,
|
|
32
36
|
}} />
|
|
33
37
|
```
|
|
@@ -36,7 +40,10 @@ does not import a voice provider. Stop and release audio tracks before setting
|
|
|
36
40
|
|
|
37
41
|
Wrap the application in `DictationProvider` to use an authenticated transcription
|
|
38
42
|
API for every `VoiceDictationButton`, including buttons nested in chat, forms or
|
|
39
|
-
another component library.
|
|
43
|
+
another component library. A compact recorder shows the elapsed time and LiveKit
|
|
44
|
+
audio bars measured from the recording stream, without opening a second microphone.
|
|
45
|
+
It also works with hidden/programmatic triggers and inside another dialog.
|
|
46
|
+
The recorder runs until **Done**, shows
|
|
40
47
|
**Transcribing…**, then commits the returned text once. Failed requests keep the
|
|
41
48
|
take available for retry. Cancel stops microphone tracks, aborts the request and
|
|
42
49
|
ignores a late answer. The kit never persists audio or knows provider credentials.
|
|
@@ -62,6 +69,9 @@ cannot be retried under the new scope.
|
|
|
62
69
|
`useDictation` (`hooks/use-dictation`) exposes the same capture lifecycle for a
|
|
63
70
|
custom trigger such as press-and-hold: `start()`, `stop()`, `retry()`, `cancel()`,
|
|
64
71
|
`state`, `supported`, `transcript`, `interimTranscript`, `error` and `canRetry`.
|
|
72
|
+
For API recording, `audioLevels` exposes a subscribable snapshot of real microphone
|
|
73
|
+
levels. Subscribe only the waveform to keep audio sampling out of form renders;
|
|
74
|
+
native recognition has no audio-level source.
|
|
65
75
|
`stop()` and `retry()` return the completed text, or `null` after cancellation or
|
|
66
76
|
failure. Choose either that result or `onTranscript` to commit; using both would
|
|
67
77
|
duplicate the host action. The callback receives one complete API transcript,
|
|
@@ -164,7 +174,8 @@ public in 1.1.0, and these were the decisions about its props then:
|
|
|
164
174
|
for the textarea's starting height; there is no class-name or `rows` prop.
|
|
165
175
|
The textarea grows with its text (`field-sizing: content`).
|
|
166
176
|
- **Compact mobile replies.** Opt into `Chat.mobileCompactComposer` to fold
|
|
167
|
-
an active conversation's empty composer into
|
|
177
|
+
an active conversation's empty composer into a short text row above its
|
|
178
|
+
controls below 768px, keeping all four actions clear of the text. Focusing
|
|
168
179
|
its textarea expands it; leaving the composer folds it again only when
|
|
169
180
|
there is no draft or staged file. The same textarea stays mounted, and
|
|
170
181
|
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.");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } 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
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkNKWHDI4Ccjs = require('./chunk-NKWHDI4C.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
|
|
@@ -20,7 +20,7 @@ var noSubscription = () => () => {
|
|
|
20
20
|
var DEFAULT_LABELS = {
|
|
21
21
|
disconnected: "Voice chat",
|
|
22
22
|
connecting: "Connecting\u2026",
|
|
23
|
-
listening: "Listening",
|
|
23
|
+
listening: "Listening\u2026",
|
|
24
24
|
thinking: "Thinking\u2026",
|
|
25
25
|
speaking: "Speaking",
|
|
26
26
|
reconnecting: "Reconnecting\u2026",
|
|
@@ -30,7 +30,11 @@ var DEFAULT_LABELS = {
|
|
|
30
30
|
mute: "Mute microphone",
|
|
31
31
|
unmute: "Unmute microphone",
|
|
32
32
|
muted: "Microphone muted",
|
|
33
|
-
retry: "Retry voice chat"
|
|
33
|
+
retry: "Retry voice chat",
|
|
34
|
+
endShort: "End voice",
|
|
35
|
+
muteShort: "Mute",
|
|
36
|
+
unmuteShort: "Unmute",
|
|
37
|
+
transcriptPlaceholder: "Speak to continue the conversation."
|
|
34
38
|
};
|
|
35
39
|
function VoiceSessionControls({
|
|
36
40
|
state,
|
|
@@ -40,6 +44,7 @@ function VoiceSessionControls({
|
|
|
40
44
|
onMutedChange,
|
|
41
45
|
disabled = false,
|
|
42
46
|
error,
|
|
47
|
+
transcript,
|
|
43
48
|
volumeBands,
|
|
44
49
|
audioLevels,
|
|
45
50
|
labels,
|
|
@@ -55,34 +60,55 @@ function VoiceSessionControls({
|
|
|
55
60
|
{
|
|
56
61
|
"data-slot": "voice-session-controls",
|
|
57
62
|
"data-state": state,
|
|
58
|
-
className: _chunkKWLSBTB2cjs.cn.call(void 0, "
|
|
63
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, "min-w-0", active ? "@container/voice w-full" : "flex flex-wrap items-center gap-2", className),
|
|
59
64
|
children: [
|
|
60
|
-
active ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
61
|
-
/* @__PURE__ */ _jsxruntime.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
barCount: 5,
|
|
67
|
-
state: state === "reconnecting" ? "connecting" : state,
|
|
68
|
-
volumeBands: _nullishCoalesce(volumeBands, () => ( liveBands))
|
|
69
|
-
}
|
|
70
|
-
),
|
|
71
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { role: "status", className: "text-sm text-muted-foreground", children: muted && state === "listening" ? copy.muted : copy[state] }),
|
|
72
|
-
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [
|
|
65
|
+
active ? /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "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__ */ _jsxruntime.jsxs.call(void 0, "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__ */ _jsxruntime.jsx.call(void 0, "div", { "aria-hidden": "true", className: _chunkKWLSBTB2cjs.cn.call(void 0,
|
|
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__ */ _jsxruntime.jsx.call(void 0, _lucidereact.MicOff, { className: "size-5" }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Mic, { className: "size-5" }) }),
|
|
73
71
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
72
|
+
_chunkNKWHDI4Ccjs.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 : _nullishCoalesce(volumeBands, () => ( liveBands))
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "min-w-0 flex-1 space-y-1", children: [
|
|
83
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { role: "status", className: "block text-sm font-medium text-primary", children: muted && state === "listening" ? copy.muted : copy[state] }),
|
|
84
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
85
|
+
"p",
|
|
86
|
+
{
|
|
87
|
+
"data-slot": "voice-session-transcript",
|
|
88
|
+
title: transcript || void 0,
|
|
89
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, "truncate text-sm font-normal", transcript ? "text-foreground" : "text-muted-foreground"),
|
|
90
|
+
children: transcript || copy.transcriptPlaceholder
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
] })
|
|
94
|
+
] }),
|
|
95
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [
|
|
96
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
74
97
|
_chunkJDEXZGIYcjs.Button,
|
|
75
98
|
{
|
|
76
99
|
type: "button",
|
|
77
|
-
variant: "
|
|
78
|
-
size: "
|
|
79
|
-
className: "min-h-[44px]
|
|
100
|
+
variant: "outline",
|
|
101
|
+
size: "sm",
|
|
102
|
+
className: "min-h-[44px] shrink-0 rounded-xl bg-background px-3 text-foreground",
|
|
80
103
|
"aria-label": muted ? copy.unmute : copy.mute,
|
|
81
104
|
title: muted ? copy.unmute : copy.mute,
|
|
82
105
|
"aria-pressed": muted,
|
|
83
106
|
disabled: waiting,
|
|
84
107
|
onClick: () => onMutedChange(!muted),
|
|
85
|
-
children:
|
|
108
|
+
children: [
|
|
109
|
+
muted ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Mic, { "aria-hidden": "true" }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.MicOff, { "aria-hidden": "true" }),
|
|
110
|
+
muted ? copy.unmuteShort : copy.muteShort
|
|
111
|
+
]
|
|
86
112
|
}
|
|
87
113
|
),
|
|
88
114
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
@@ -90,14 +116,14 @@ function VoiceSessionControls({
|
|
|
90
116
|
{
|
|
91
117
|
type: "button",
|
|
92
118
|
variant: "outline",
|
|
93
|
-
|
|
94
|
-
className: "min-h-[44px] shrink-0",
|
|
119
|
+
size: "sm",
|
|
120
|
+
className: "min-h-[44px] shrink-0 rounded-xl bg-background px-3 text-foreground",
|
|
95
121
|
"aria-label": copy.end,
|
|
96
122
|
title: copy.end,
|
|
97
123
|
onClick: onEnd,
|
|
98
124
|
children: [
|
|
99
|
-
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.
|
|
100
|
-
copy.
|
|
125
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Square, { "aria-hidden": "true", className: "fill-current" }),
|
|
126
|
+
copy.endShort
|
|
101
127
|
]
|
|
102
128
|
}
|
|
103
129
|
)
|
|
@@ -12,6 +12,94 @@ 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
|
+
source?.disconnect();
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
analyser?.disconnect();
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
void context?.close().catch(() => {
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
levels.update(SILENCE);
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const Constructor = 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 {
|
|
93
|
+
stop();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
if (context.state === "suspended") void context.resume().catch(stop);
|
|
97
|
+
frame = requestAnimationFrame(sample);
|
|
98
|
+
} catch {
|
|
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;
|
|
@@ -29,7 +117,12 @@ 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
|
+
session.stopMeter?.();
|
|
122
|
+
session.stopMeter = void 0;
|
|
123
|
+
}
|
|
32
124
|
function releaseTracks(session) {
|
|
125
|
+
releaseMeter(session);
|
|
33
126
|
session.stream?.getTracks().forEach((track) => track.stop());
|
|
34
127
|
session.stream = void 0;
|
|
35
128
|
}
|
|
@@ -123,7 +216,8 @@ 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" });
|
|
@@ -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);
|
|
@@ -228,6 +323,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
228
323
|
}, [cancel, continuous, isCurrent, lang, scope, scopeGeneration, transcribe]);
|
|
229
324
|
const finish = useCallback(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) {
|
|
@@ -307,7 +403,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
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
|
export {
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
var _react = require('react');
|
|
23
|
+
var _reactdialog = require('@radix-ui/react-dialog'); var DialogPrimitive = _interopRequireWildcard(_reactdialog);
|
|
24
|
+
var _lucidereact = require('lucide-react');
|
|
25
|
+
var _jsxruntime = require('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] = _react.useState.call(void 0, false);
|
|
42
|
+
const capture = _chunkZZ7ICQR2cjs.useDictation.call(void 0, { lang, continuous });
|
|
43
|
+
const { transcript, interimTranscript: interim, supported, error } = capture;
|
|
44
|
+
const generation = _react.useRef.call(void 0, 0);
|
|
45
|
+
const finishing = _react.useRef.call(void 0, false);
|
|
46
|
+
const previousFocus = _react.useRef.call(void 0, null);
|
|
47
|
+
const elapsed = useRecordingTime(capture.state);
|
|
48
|
+
const busy = capture.state === "requesting" || capture.state === "transcribing";
|
|
49
|
+
const openOverlay = _react.useCallback.call(void 0, () => {
|
|
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 = _react.useCallback.call(void 0, () => {
|
|
57
|
+
generation.current++;
|
|
58
|
+
finishing.current = false;
|
|
59
|
+
capture.cancel();
|
|
60
|
+
setOpen(false);
|
|
61
|
+
}, [capture.cancel]);
|
|
62
|
+
const handleDone = _react.useCallback.call(void 0, 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 _optionalChain([onCancel, 'optionalCall', _ => _()]);
|
|
77
|
+
}, [busy, capture.canRetry, capture.retry, capture.start, capture.state, capture.stop, onCommit, onCancel, closeOverlay]);
|
|
78
|
+
const handleCancel = _react.useCallback.call(void 0, () => {
|
|
79
|
+
closeOverlay();
|
|
80
|
+
_optionalChain([onCancel, 'optionalCall', _2 => _2()]);
|
|
81
|
+
}, [closeOverlay, onCancel]);
|
|
82
|
+
_react.useEffect.call(void 0, () => {
|
|
83
|
+
if (open && capture.state === "idle") handleCancel();
|
|
84
|
+
}, [capture.state, handleCancel, open]);
|
|
85
|
+
_react.useEffect.call(void 0, () => {
|
|
86
|
+
return () => {
|
|
87
|
+
generation.current++;
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
if (!supported) return null;
|
|
91
|
+
const displayTranscript = `${transcript}${interim ? (transcript ? " " : "") + interim : ""}`;
|
|
92
|
+
return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
|
|
93
|
+
children ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
94
|
+
"button",
|
|
95
|
+
{
|
|
96
|
+
type: "button",
|
|
97
|
+
onClick: openOverlay,
|
|
98
|
+
disabled,
|
|
99
|
+
"aria-label": "Start dictation",
|
|
100
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, visibility === "hidden" && "hidden", className),
|
|
101
|
+
children
|
|
102
|
+
}
|
|
103
|
+
) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
104
|
+
_chunkJDEXZGIYcjs.Button,
|
|
105
|
+
{
|
|
106
|
+
type: "button",
|
|
107
|
+
onClick: openOverlay,
|
|
108
|
+
disabled,
|
|
109
|
+
variant: "outline",
|
|
110
|
+
size,
|
|
111
|
+
"aria-label": "Start dictation",
|
|
112
|
+
className: _chunkKWLSBTB2cjs.cn.call(void 0, "transition-colors", visibility === "hidden" && "hidden", !shrink && "shrink-0", className),
|
|
113
|
+
children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "flex items-center justify-center gap-2", children: [
|
|
114
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _lucidereact.Mic, { className: "h-4 w-4" }),
|
|
115
|
+
showLabel && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "Speak" })
|
|
116
|
+
] })
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
120
|
+
DialogPrimitive.Root,
|
|
121
|
+
{
|
|
122
|
+
open,
|
|
123
|
+
onOpenChange: (next) => {
|
|
124
|
+
if (!next) handleCancel();
|
|
125
|
+
},
|
|
126
|
+
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Portal, { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
|
|
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 (_optionalChain([previousFocus, 'access', _3 => _3.current, 'optionalAccess', _4 => _4.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__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex min-w-0 items-start justify-between gap-3", children: [
|
|
146
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "min-w-0 space-y-1", children: [
|
|
147
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Title, { className: "text-sm font-medium", children: title || "Voice dictation" }),
|
|
148
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { role: "status", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
149
|
+
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" }),
|
|
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__ */ _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: [
|
|
154
|
+
Math.floor(elapsed / 60).toString().padStart(2, "0"),
|
|
155
|
+
":",
|
|
156
|
+
(elapsed % 60).toString().padStart(2, "0")
|
|
157
|
+
] })
|
|
158
|
+
] }),
|
|
159
|
+
capture.audioLevels && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, DictationWaveform, { source: capture.audioLevels, state: capture.state }),
|
|
160
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, 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__ */ _jsxruntime.jsxs.call(void 0, "p", { className: "max-h-24 overflow-y-auto text-sm leading-relaxed", children: [
|
|
162
|
+
transcript,
|
|
163
|
+
interim && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "text-muted-foreground", children: [
|
|
164
|
+
transcript ? " " : "",
|
|
165
|
+
interim
|
|
166
|
+
] })
|
|
167
|
+
] }),
|
|
168
|
+
error && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { role: "alert", className: "text-xs text-destructive", children: error }),
|
|
169
|
+
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex flex-wrap items-center justify-end gap-2", children: [
|
|
170
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.Button, { type: "button", variant: "ghost", size: "sm", className: "min-h-[44px]", onClick: handleCancel, children: "Cancel" }),
|
|
171
|
+
capture.canRetry && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.Button, { type: "button", variant: "outline", size: "sm", className: "min-h-[44px]", onClick: () => void capture.start(), children: "Record again" }),
|
|
172
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.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 = _react.useSyncExternalStore.call(void 0, source.subscribe, source.getSnapshot, silence);
|
|
185
|
+
return /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
|
|
186
|
+
_chunkNKWHDI4Ccjs.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] = _react.useState.call(void 0, 0);
|
|
200
|
+
const startedAt = _react.useRef.call(void 0, null);
|
|
201
|
+
_react.useEffect.call(void 0, () => {
|
|
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
|
+
|
|
220
|
+
|
|
221
|
+
exports.VoiceDictationButton = VoiceDictationButton;
|
|
@@ -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 = _chunk2BSFYU22cjs.useAgentAudioVisualizerBarAnimator.call(void 0,
|
|
114
|
-
state,
|
|
115
|
+
meteredListening ? "speaking" : state,
|
|
115
116
|
_barCount,
|
|
116
117
|
sequencerInterval
|
|
117
118
|
);
|
|
118
119
|
const bands = _react.useMemo.call(void 0,
|
|
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.");
|