@epicbagel/saas-kit 1.7.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +60 -0
  2. package/THIRD_PARTY_NOTICES.md +14 -0
  3. package/dist/chunk-2BSFYU22.cjs +71 -0
  4. package/dist/{chunk-GTVSRRE3.cjs → chunk-BGCNGYPL.cjs} +2 -2
  5. package/dist/{chunk-QTWJQQDH.cjs → chunk-FBVZRRA4.cjs} +6 -6
  6. package/dist/chunk-GJ7S3L6R.js +71 -0
  7. package/dist/{chunk-LKPM6DQP.cjs → chunk-HTCW6AEH.cjs} +51 -72
  8. package/dist/{chunk-F3B4M6DU.js → chunk-HTPPNW74.js} +1 -1
  9. package/dist/chunk-KUG4ONYC.js +127 -0
  10. package/dist/{chunk-TI57GAQP.js → chunk-KWV5KTOA.js} +48 -69
  11. package/dist/{chunk-JN6JOSZG.js → chunk-LQPJGZIE.js} +7 -7
  12. package/dist/chunk-M2ZNC3TK.js +316 -0
  13. package/dist/chunk-O6F3LVZY.cjs +316 -0
  14. package/dist/chunk-UW67GSZ4.cjs +127 -0
  15. package/dist/chunk-VBKJXNEL.js +157 -0
  16. package/dist/chunk-XB2D4NQK.cjs +157 -0
  17. package/dist/hooks/use-agent-audio-visualizer-bar.cjs +8 -0
  18. package/dist/hooks/use-agent-audio-visualizer-bar.d.cts +4 -0
  19. package/dist/hooks/use-agent-audio-visualizer-bar.d.ts +4 -0
  20. package/dist/hooks/use-agent-audio-visualizer-bar.js +8 -0
  21. package/dist/hooks/use-dictation.cjs +10 -0
  22. package/dist/hooks/use-dictation.d.cts +56 -0
  23. package/dist/hooks/use-dictation.d.ts +56 -0
  24. package/dist/hooks/use-dictation.js +10 -0
  25. package/dist/primitives/activity-feed.cjs +3 -3
  26. package/dist/primitives/activity-feed.js +4 -4
  27. package/dist/primitives/agent-audio-visualizer-bar.cjs +16 -0
  28. package/dist/primitives/agent-audio-visualizer-bar.d.cts +75 -0
  29. package/dist/primitives/agent-audio-visualizer-bar.d.ts +75 -0
  30. package/dist/primitives/agent-audio-visualizer-bar.js +16 -0
  31. package/dist/primitives/archive-confirmation-dialog.cjs +2 -2
  32. package/dist/primitives/archive-confirmation-dialog.js +3 -3
  33. package/dist/primitives/attachment.d.cts +1 -1
  34. package/dist/primitives/attachment.d.ts +1 -1
  35. package/dist/primitives/badge.d.cts +1 -1
  36. package/dist/primitives/badge.d.ts +1 -1
  37. package/dist/primitives/board-view.cjs +6 -6
  38. package/dist/primitives/board-view.js +5 -5
  39. package/dist/primitives/bubble.d.cts +1 -1
  40. package/dist/primitives/bubble.d.ts +1 -1
  41. package/dist/primitives/button.d.cts +3 -3
  42. package/dist/primitives/button.d.ts +3 -3
  43. package/dist/primitives/chat.cjs +55 -24
  44. package/dist/primitives/chat.d.cts +6 -1
  45. package/dist/primitives/chat.d.ts +6 -1
  46. package/dist/primitives/chat.js +52 -21
  47. package/dist/primitives/code-block.cjs +4 -4
  48. package/dist/primitives/code-block.js +3 -3
  49. package/dist/primitives/connection.cjs +6 -6
  50. package/dist/primitives/connection.js +7 -7
  51. package/dist/primitives/copy-button.cjs +3 -3
  52. package/dist/primitives/copy-button.js +2 -2
  53. package/dist/primitives/dictation-provider.cjs +8 -0
  54. package/dist/primitives/dictation-provider.d.cts +2 -0
  55. package/dist/primitives/dictation-provider.d.ts +2 -0
  56. package/dist/primitives/dictation-provider.js +8 -0
  57. package/dist/primitives/editable-list.cjs +1 -1
  58. package/dist/primitives/editable-list.js +1 -1
  59. package/dist/primitives/file-dropzone.cjs +3 -3
  60. package/dist/primitives/file-dropzone.js +2 -2
  61. package/dist/primitives/file-preview.cjs +6 -6
  62. package/dist/primitives/file-preview.js +5 -5
  63. package/dist/primitives/filter-bar.cjs +4 -4
  64. package/dist/primitives/filter-bar.js +3 -3
  65. package/dist/primitives/input-group.d.cts +1 -1
  66. package/dist/primitives/input-group.d.ts +1 -1
  67. package/dist/primitives/item.d.cts +2 -2
  68. package/dist/primitives/item.d.ts +2 -2
  69. package/dist/primitives/resource-list.cjs +6 -6
  70. package/dist/primitives/resource-list.js +5 -5
  71. package/dist/primitives/section-header.d.cts +1 -1
  72. package/dist/primitives/section-header.d.ts +1 -1
  73. package/dist/primitives/section.cjs +2 -2
  74. package/dist/primitives/section.js +3 -3
  75. package/dist/primitives/sheet.d.cts +1 -1
  76. package/dist/primitives/sheet.d.ts +1 -1
  77. package/dist/primitives/switch-field.cjs +2 -2
  78. package/dist/primitives/switch-field.js +3 -3
  79. package/dist/primitives/theme-selector.cjs +3 -3
  80. package/dist/primitives/theme-selector.js +4 -4
  81. package/dist/primitives/toggle-group.d.cts +2 -2
  82. package/dist/primitives/toggle-group.d.ts +2 -2
  83. package/dist/primitives/toggle.d.cts +2 -2
  84. package/dist/primitives/toggle.d.ts +2 -2
  85. package/dist/primitives/voice-dictation-button.cjs +3 -2
  86. package/dist/primitives/voice-dictation-button.js +2 -1
  87. package/dist/primitives/voice-session-controls.cjs +12 -0
  88. package/dist/primitives/voice-session-controls.d.cts +29 -0
  89. package/dist/primitives/voice-session-controls.d.ts +29 -0
  90. package/dist/primitives/voice-session-controls.js +12 -0
  91. package/package.json +51 -1
  92. package/dist/{chunk-HREEZUKL.js → chunk-DILYV4FI.js} +3 -3
  93. package/dist/{chunk-MU4F3DSW.cjs → chunk-FARCLWLU.cjs} +2 -2
  94. package/dist/{chunk-CRTH5GJ5.cjs → chunk-MW67PHAQ.cjs} +3 -3
  95. package/dist/{chunk-KNKV25WS.js → chunk-UFFHGKI7.js} +3 -3
  96. package/dist/{chunk-HBRVOGLA.js → chunk-WHDHUYS6.js} +3 -3
  97. package/dist/{chunk-Z3JJSNR5.cjs → chunk-ZLEM2SAU.cjs} +2 -2
package/README.md CHANGED
@@ -7,6 +7,7 @@ dark modes.
7
7
 
8
8
  ## Install
9
9
 
10
+
10
11
  ```bash
11
12
  npm install @epicbagel/saas-kit
12
13
  ```
@@ -14,6 +15,65 @@ npm install @epicbagel/saas-kit
14
15
  Peer dependencies: `react` and `react-dom` (18 or 19), `lucide-react`
15
16
  (`>=0.400.0 <2`), `react-icons` (5) and `tailwindcss` (4.3 or later).
16
17
 
18
+ ## Live voice in Chat
19
+
20
+ Pass a controlled `voiceSession` to `Chat` or `ChatInput` to show start, mute,
21
+ end, connection and speaking controls alongside a separate dictation mic.
22
+ Dictation is disabled during a live call and the typed draft is preserved.
23
+ `VoiceSessionControls` is also exported on its own.
24
+ The host owns audio, authentication, transcripts, retries and cleanup; the kit
25
+ does not import a voice provider. Stop and release audio tracks before setting
26
+ `state: "error"` or unmounting. Every label is replaceable through `labels`.
27
+
28
+ ```tsx
29
+ <Chat messages={messages} onSend={sendMessage} voiceSession={{
30
+ state: voiceState, muted, error,
31
+ onStart: startVoice, onEnd: endVoice, onMutedChange: setMuted,
32
+ }} />
33
+ ```
34
+
35
+ ## API dictation
36
+
37
+ Wrap the application in `DictationProvider` to use an authenticated transcription
38
+ API for every `VoiceDictationButton`, including buttons nested in chat, forms or
39
+ another component library. The existing dialog records until **Done**, shows
40
+ **Transcribing…**, then commits the returned text once. Failed requests keep the
41
+ take available for retry. Cancel stops microphone tracks, aborts the request and
42
+ ignores a late answer. The kit never persists audio or knows provider credentials.
43
+
44
+ ```tsx
45
+ import { DictationProvider } from "@epicbagel/saas-kit/primitives/dictation-provider";
46
+
47
+ <DictationProvider scopeKey={`${actorId}:${workspaceId}`} transcribe={async (audio, { language, signal }) => {
48
+ // The host sends the Blob to its authenticated API. Preserve audio.type
49
+ // and pass signal to fetch; keep provider keys on the server.
50
+ return transcribeRecording(audio, { language, signal });
51
+ }}>
52
+ <App />
53
+ </DictationProvider>
54
+ ```
55
+
56
+ Set `scopeKey` from the acting user and workspace (including impersonation, if
57
+ the app supports it). A changed scope discards the active take, stops recording
58
+ and aborts transcription without remounting the application. Responses from the
59
+ previous scope are ignored even before the cleanup effect runs; a failed take
60
+ cannot be retried under the new scope.
61
+
62
+ `useDictation` (`hooks/use-dictation`) exposes the same capture lifecycle for a
63
+ custom trigger such as press-and-hold: `start()`, `stop()`, `retry()`, `cancel()`,
64
+ `state`, `supported`, `transcript`, `interimTranscript`, `error` and `canRetry`.
65
+ `stop()` and `retry()` return the completed text, or `null` after cancellation or
66
+ failure. Choose either that result or `onTranscript` to commit; using both would
67
+ duplicate the host action. The callback receives one complete API transcript,
68
+ or finalized chunks with native recognition. API recording requires HTTPS (or
69
+ localhost), microphone permission and MediaRecorder. It does not produce live
70
+ interim text; the live voice controls are a separate feature.
71
+
72
+ Without a provider, controls retain browser-native speech recognition. The
73
+ private ui-kit's `VoiceInput` uses this same provider and capture hook, and its
74
+ provider export is a shim of this exact context.
75
+
76
+
17
77
  ## Styles
18
78
 
19
79
  If the app has no Tailwind entry of its own, import the complete stylesheet:
@@ -268,3 +268,17 @@ The Apache License, Version 2.0:
268
268
 
269
269
  END OF TERMS AND CONDITIONS
270
270
  ```
271
+
272
+
273
+ ## LiveKit Agents UI
274
+
275
+ Source: https://github.com/livekit/components-js/tree/3f74806f1c8484b2eb2fd40773dd1be80760750c/packages/shadcn
276
+
277
+ `primitives/agent-audio-visualizer-bar` and
278
+ `hooks/use-agent-audio-visualizer-bar` are adapted from LiveKit's
279
+ `components/agents-ui/agent-audio-visualizer-bar.tsx` and
280
+ `hooks/agents-ui/use-agent-audio-visualizer-bar.ts` at that commit.
281
+ LiveKit contributors license them under Apache-2.0 (full license above).
282
+ The compiled output includes these adaptations. We removed the LiveKit React
283
+ track hook and accept host-supplied volume bands, clamp band values/counts,
284
+ and respect reduced motion and stop idle animation loops.
@@ -0,0 +1,71 @@
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
+
3
+ // src/hooks/use-agent-audio-visualizer-bar.ts
4
+ var _react = require('react');
5
+ function generateConnectingSequenceBar(columns) {
6
+ const seq = [];
7
+ for (let x = 0; x < columns; x++) {
8
+ seq.push([x, columns - 1 - x]);
9
+ }
10
+ return seq;
11
+ }
12
+ function generateListeningSequenceBar(columns) {
13
+ const center = Math.floor(columns / 2);
14
+ const noIndex = -1;
15
+ return [[center], [noIndex]];
16
+ }
17
+ function useAgentAudioVisualizerBarAnimator(state, columns, interval) {
18
+ const [index, setIndex] = _react.useState.call(void 0, 0);
19
+ const [sequence, setSequence] = _react.useState.call(void 0, [[]]);
20
+ _react.useEffect.call(void 0, () => {
21
+ if (state === "thinking") {
22
+ setSequence(generateListeningSequenceBar(columns));
23
+ } else if (state === "connecting" || state === "initializing") {
24
+ const sequence2 = [...generateConnectingSequenceBar(columns)];
25
+ setSequence(sequence2);
26
+ } else if (state === "listening") {
27
+ setSequence(generateListeningSequenceBar(columns));
28
+ } else if (state === void 0 || state === "speaking") {
29
+ setSequence([new Array(columns).fill(0).map((_, idx) => idx)]);
30
+ } else {
31
+ setSequence([[]]);
32
+ }
33
+ setIndex(0);
34
+ }, [state, columns]);
35
+ const animationFrameId = _react.useRef.call(void 0, null);
36
+ _react.useEffect.call(void 0, () => {
37
+ if (sequence.length < 2) return;
38
+ const preference = _optionalChain([window, 'access', _2 => _2.matchMedia, 'optionalCall', _3 => _3("(prefers-reduced-motion: reduce)")]);
39
+ const stop = () => {
40
+ if (animationFrameId.current !== null) cancelAnimationFrame(animationFrameId.current);
41
+ animationFrameId.current = null;
42
+ };
43
+ const start = () => {
44
+ stop();
45
+ if (_optionalChain([preference, 'optionalAccess', _4 => _4.matches])) {
46
+ setIndex(0);
47
+ return;
48
+ }
49
+ let startTime = performance.now();
50
+ const animate = (time) => {
51
+ if (time - startTime >= interval) {
52
+ setIndex((prev) => prev + 1);
53
+ startTime = time;
54
+ }
55
+ animationFrameId.current = requestAnimationFrame(animate);
56
+ };
57
+ animationFrameId.current = requestAnimationFrame(animate);
58
+ };
59
+ start();
60
+ _optionalChain([preference, 'optionalAccess', _5 => _5.addEventListener, 'call', _6 => _6("change", start)]);
61
+ return () => {
62
+ stop();
63
+ _optionalChain([preference, 'optionalAccess', _7 => _7.removeEventListener, 'call', _8 => _8("change", start)]);
64
+ };
65
+ }, [interval, columns, state, sequence.length]);
66
+ return _nullishCoalesce(sequence[index % sequence.length], () => ( []));
67
+ }
68
+
69
+
70
+
71
+ exports.useAgentAudioVisualizerBarAnimator = useAgentAudioVisualizerBarAnimator;
@@ -1,7 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});"use client";
2
2
 
3
3
 
4
- var _chunkMU4F3DSWcjs = require('./chunk-MU4F3DSW.cjs');
4
+ var _chunkFARCLWLUcjs = require('./chunk-FARCLWLU.cjs');
5
5
 
6
6
 
7
7
  var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
@@ -38,7 +38,7 @@ function CodeBlock({
38
38
  }
39
39
  ),
40
40
  copy ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
41
- _chunkMU4F3DSWcjs.CopyButton,
41
+ _chunkFARCLWLUcjs.CopyButton,
42
42
  {
43
43
  text: copyText,
44
44
  label: copyLabel,
@@ -7,16 +7,16 @@ var _chunkEUFV25KLcjs = require('./chunk-EUFV25KL.cjs');
7
7
  var _chunk7YCKII7Gcjs = require('./chunk-7YCKII7G.cjs');
8
8
 
9
9
 
10
- var _chunkCRTH5GJ5cjs = require('./chunk-CRTH5GJ5.cjs');
10
+ var _chunkMW67PHAQcjs = require('./chunk-MW67PHAQ.cjs');
11
11
 
12
12
 
13
- var _chunkLELBCBDTcjs = require('./chunk-LELBCBDT.cjs');
14
13
 
15
14
 
16
15
 
16
+ var _chunkU2NES7VRcjs = require('./chunk-U2NES7VR.cjs');
17
17
 
18
18
 
19
- var _chunkU2NES7VRcjs = require('./chunk-U2NES7VR.cjs');
19
+ var _chunkLELBCBDTcjs = require('./chunk-LELBCBDT.cjs');
20
20
 
21
21
 
22
22
 
@@ -32,10 +32,10 @@ var _chunkPQUSJEXLcjs = require('./chunk-PQUSJEXL.cjs');
32
32
  var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
33
33
 
34
34
 
35
- var _chunkR35FT53Gcjs = require('./chunk-R35FT53G.cjs');
35
+ var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
36
36
 
37
37
 
38
- var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
38
+ var _chunkR35FT53Gcjs = require('./chunk-R35FT53G.cjs');
39
39
 
40
40
  // src/primitives/resource-list.tsx
41
41
  var _lucidereact = require('lucide-react');
@@ -268,7 +268,7 @@ function ResourceList({
268
268
  const controlsVisible = !(hideControlsWhenEmpty && !loading && !error && _optionalChain([data, 'optionalAccess', _5 => _5.length]) === 0);
269
269
  const filterOnQuickRow = filterBarVisible && !toolbarHasOtherControls && hasQuickFilters;
270
270
  const filterBarNode = filterBarVisible ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
271
- _chunkCRTH5GJ5cjs.FilterBar,
271
+ _chunkMW67PHAQcjs.FilterBar,
272
272
  {
273
273
  filters: filterSchema,
274
274
  onFiltersChange,
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ // src/hooks/use-agent-audio-visualizer-bar.ts
4
+ import { useEffect, useRef, useState } from "react";
5
+ function generateConnectingSequenceBar(columns) {
6
+ const seq = [];
7
+ for (let x = 0; x < columns; x++) {
8
+ seq.push([x, columns - 1 - x]);
9
+ }
10
+ return seq;
11
+ }
12
+ function generateListeningSequenceBar(columns) {
13
+ const center = Math.floor(columns / 2);
14
+ const noIndex = -1;
15
+ return [[center], [noIndex]];
16
+ }
17
+ function useAgentAudioVisualizerBarAnimator(state, columns, interval) {
18
+ const [index, setIndex] = useState(0);
19
+ const [sequence, setSequence] = useState([[]]);
20
+ useEffect(() => {
21
+ if (state === "thinking") {
22
+ setSequence(generateListeningSequenceBar(columns));
23
+ } else if (state === "connecting" || state === "initializing") {
24
+ const sequence2 = [...generateConnectingSequenceBar(columns)];
25
+ setSequence(sequence2);
26
+ } else if (state === "listening") {
27
+ setSequence(generateListeningSequenceBar(columns));
28
+ } else if (state === void 0 || state === "speaking") {
29
+ setSequence([new Array(columns).fill(0).map((_, idx) => idx)]);
30
+ } else {
31
+ setSequence([[]]);
32
+ }
33
+ setIndex(0);
34
+ }, [state, columns]);
35
+ const animationFrameId = useRef(null);
36
+ useEffect(() => {
37
+ if (sequence.length < 2) return;
38
+ const preference = window.matchMedia?.("(prefers-reduced-motion: reduce)");
39
+ const stop = () => {
40
+ if (animationFrameId.current !== null) cancelAnimationFrame(animationFrameId.current);
41
+ animationFrameId.current = null;
42
+ };
43
+ const start = () => {
44
+ stop();
45
+ if (preference?.matches) {
46
+ setIndex(0);
47
+ return;
48
+ }
49
+ let startTime = performance.now();
50
+ const animate = (time) => {
51
+ if (time - startTime >= interval) {
52
+ setIndex((prev) => prev + 1);
53
+ startTime = time;
54
+ }
55
+ animationFrameId.current = requestAnimationFrame(animate);
56
+ };
57
+ animationFrameId.current = requestAnimationFrame(animate);
58
+ };
59
+ start();
60
+ preference?.addEventListener("change", start);
61
+ return () => {
62
+ stop();
63
+ preference?.removeEventListener("change", start);
64
+ };
65
+ }, [interval, columns, state, sequence.length]);
66
+ return sequence[index % sequence.length] ?? [];
67
+ }
68
+
69
+ export {
70
+ useAgentAudioVisualizerBarAnimator
71
+ };
@@ -1,4 +1,4 @@
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 _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";
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
2
 
3
3
 
4
4
  var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
@@ -6,6 +6,9 @@ var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
6
6
 
7
7
  var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
8
8
 
9
+
10
+ var _chunkO6F3LVZYcjs = require('./chunk-O6F3LVZY.cjs');
11
+
9
12
  // src/primitives/voice-dictation-button.tsx
10
13
 
11
14
 
@@ -31,87 +34,61 @@ function VoiceDictationButton({
31
34
  visibility = "visible",
32
35
  shrink = true
33
36
  }) {
34
- const [supported, setSupported] = _react.useState.call(void 0, false);
35
37
  const [open, setOpen] = _react.useState.call(void 0, false);
36
- const [transcript, setTranscript] = _react.useState.call(void 0, "");
37
- const [interim, setInterim] = _react.useState.call(void 0, "");
38
- const [errored, setErrored] = _react.useState.call(void 0, false);
39
- const recognitionRef = _react.useRef.call(void 0, null);
40
- _react.useEffect.call(void 0, () => {
41
- const w = window;
42
- const Ctor = _nullishCoalesce(w.SpeechRecognition, () => ( w.webkitSpeechRecognition));
43
- setSupported(typeof Ctor === "function");
44
- }, []);
45
- const stop = _react.useCallback.call(void 0, () => {
46
- _optionalChain([recognitionRef, 'access', _ => _.current, 'optionalAccess', _2 => _2.stop, 'call', _3 => _3()]);
47
- recognitionRef.current = null;
48
- }, []);
49
- const startRecognition = _react.useCallback.call(void 0, () => {
50
- const w = window;
51
- const Ctor = _nullishCoalesce(w.SpeechRecognition, () => ( w.webkitSpeechRecognition));
52
- if (!Ctor) return;
53
- const rec = new Ctor();
54
- rec.continuous = continuous;
55
- rec.interimResults = true;
56
- rec.lang = lang;
57
- rec.onresult = (event) => {
58
- let finalChunk = "";
59
- let interimChunk = "";
60
- for (let i = event.resultIndex; i < event.results.length; i++) {
61
- const r = event.results[i];
62
- if (!r) continue;
63
- if (r.isFinal) finalChunk += r[0].transcript;
64
- else interimChunk += r[0].transcript;
65
- }
66
- if (finalChunk) {
67
- setTranscript((prev) => prev ? prev + " " + finalChunk : finalChunk);
68
- setInterim("");
69
- } else if (interimChunk) {
70
- setInterim(interimChunk);
71
- }
72
- };
73
- rec.onerror = () => {
74
- setErrored(true);
75
- };
76
- rec.onend = () => {
77
- recognitionRef.current = null;
78
- };
79
- recognitionRef.current = rec;
80
- setErrored(false);
81
- rec.start();
82
- }, [continuous, lang]);
38
+ const capture = _chunkO6F3LVZYcjs.useDictation.call(void 0, { lang, continuous });
39
+ const { transcript, interimTranscript: interim, supported, error } = capture;
40
+ const generation = _react.useRef.call(void 0, 0);
41
+ const finishing = _react.useRef.call(void 0, false);
42
+ const busy = capture.state === "requesting" || capture.state === "transcribing";
83
43
  const openOverlay = _react.useCallback.call(void 0, () => {
84
- setTranscript("");
85
- setInterim("");
44
+ generation.current++;
45
+ finishing.current = false;
86
46
  setOpen(true);
87
- startRecognition();
88
- }, [startRecognition]);
47
+ void capture.start();
48
+ }, [capture.start]);
89
49
  const closeOverlay = _react.useCallback.call(void 0, () => {
90
- stop();
50
+ generation.current++;
51
+ finishing.current = false;
52
+ capture.cancel();
91
53
  setOpen(false);
92
- setInterim("");
93
- }, [stop]);
94
- const handleDone = _react.useCallback.call(void 0, () => {
95
- const final = `${transcript} ${interim}`.trim();
54
+ }, [capture.cancel]);
55
+ const handleDone = _react.useCallback.call(void 0, async () => {
56
+ if (finishing.current || busy) return;
57
+ if (capture.state === "error" && !capture.canRetry) {
58
+ void capture.start();
59
+ return;
60
+ }
61
+ finishing.current = true;
62
+ const started = generation.current;
63
+ const final = await (capture.canRetry ? capture.retry() : capture.stop());
64
+ if (started !== generation.current) return;
65
+ finishing.current = false;
66
+ if (final === null) return;
96
67
  closeOverlay();
97
68
  if (final) onCommit(final);
98
- else _optionalChain([onCancel, 'optionalCall', _4 => _4()]);
99
- }, [transcript, interim, onCommit, onCancel, closeOverlay]);
69
+ else _optionalChain([onCancel, 'optionalCall', _ => _()]);
70
+ }, [busy, capture.canRetry, capture.retry, capture.start, capture.state, capture.stop, onCommit, onCancel, closeOverlay]);
100
71
  const handleCancel = _react.useCallback.call(void 0, () => {
101
72
  closeOverlay();
102
- _optionalChain([onCancel, 'optionalCall', _5 => _5()]);
73
+ _optionalChain([onCancel, 'optionalCall', _2 => _2()]);
103
74
  }, [closeOverlay, onCancel]);
75
+ _react.useEffect.call(void 0, () => {
76
+ if (open && capture.state === "idle") handleCancel();
77
+ }, [capture.state, handleCancel, open]);
104
78
  _react.useEffect.call(void 0, () => {
105
79
  if (!open) return;
106
80
  const onKey = (e) => {
107
- if (e.key === "Enter" && !e.shiftKey) handleDone();
81
+ if (e.key === "Enter" && !e.shiftKey) {
82
+ e.preventDefault();
83
+ void handleDone();
84
+ }
108
85
  };
109
86
  document.addEventListener("keydown", onKey);
110
87
  return () => document.removeEventListener("keydown", onKey);
111
88
  }, [open, handleDone]);
112
89
  _react.useEffect.call(void 0, () => {
113
90
  return () => {
114
- _optionalChain([recognitionRef, 'access', _6 => _6.current, 'optionalAccess', _7 => _7.abort, 'call', _8 => _8()]);
91
+ generation.current++;
115
92
  };
116
93
  }, []);
117
94
  if (!supported) return null;
@@ -148,7 +125,7 @@ function VoiceDictationButton({
148
125
  {
149
126
  open,
150
127
  onOpenChange: (next) => {
151
- if (!next) closeOverlay();
128
+ if (!next) handleCancel();
152
129
  },
153
130
  children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Portal, { children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0,
154
131
  DialogPrimitive.Content,
@@ -161,9 +138,10 @@ function VoiceDictationButton({
161
138
  className: "fixed inset-0 z-[200] flex flex-col bg-background outline-hidden md:items-center md:justify-center md:bg-black/40 md:backdrop-blur-sm",
162
139
  children: [
163
140
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, DialogPrimitive.Title, { className: "sr-only", children: "Voice dictation" }),
141
+ /* @__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." }),
164
142
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex-1 md:flex-initial w-full md:max-w-lg md:rounded-xl bg-background flex flex-col md:max-h-[80vh] md:shadow-2xl md:border md:border-border", children: [
165
143
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-between px-4 py-3 border-b border-border md:rounded-t-xl", children: [
166
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "font-mono text-xs uppercase tracking-wider text-muted-foreground", children: caption }),
144
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { role: "status", className: "font-mono text-xs uppercase tracking-wider text-muted-foreground", children: capture.state === "transcribing" ? "transcribing" : capture.state === "requesting" ? "microphone" : caption }),
167
145
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
168
146
  "button",
169
147
  {
@@ -183,7 +161,7 @@ function VoiceDictationButton({
183
161
  "aria-hidden": true,
184
162
  className: _chunkKWLSBTB2cjs.cn.call(void 0,
185
163
  "absolute inset-0 rounded-full",
186
- errored ? "bg-destructive/20" : "bg-destructive/20 animate-ping"
164
+ error || busy || capture.state !== "recording" ? "bg-destructive/20" : "bg-destructive/20 animate-ping"
187
165
  )
188
166
  }
189
167
  ),
@@ -193,7 +171,7 @@ function VoiceDictationButton({
193
171
  "aria-hidden": true,
194
172
  className: _chunkKWLSBTB2cjs.cn.call(void 0,
195
173
  "absolute inset-2 rounded-full",
196
- errored ? "bg-destructive/40" : "bg-destructive/40"
174
+ "bg-destructive/40"
197
175
  )
198
176
  }
199
177
  ),
@@ -206,8 +184,8 @@ function VoiceDictationButton({
206
184
  transcript ? " " : "",
207
185
  interim
208
186
  ] })
209
- ] }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-muted-foreground italic text-base", children: "Start speaking \u2014 I'm listening." }) }),
210
- errored && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { className: "text-sm text-destructive mt-4", children: "Couldn't access the microphone. Check permissions." })
187
+ ] }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-muted-foreground italic text-base", children: capture.state === "transcribing" ? "Converting your recording to text\u2026" : capture.state === "requesting" ? "Allow microphone access to start." : "Start speaking, then choose Done." }) }),
188
+ error && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { role: "alert", className: "text-sm text-destructive mt-4", children: error })
211
189
  ] }),
212
190
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "border-t border-border px-4 py-4 pb-[calc(1rem+env(safe-area-inset-bottom))] md:pb-4 md:rounded-b-xl flex flex-col items-stretch gap-2", children: [
213
191
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
@@ -215,11 +193,12 @@ function VoiceDictationButton({
215
193
  {
216
194
  size: "lg",
217
195
  className: "h-12 w-full",
218
- onClick: handleDone,
219
- disabled: !displayTranscript.trim(),
220
- children: "Done"
196
+ onClick: () => void handleDone(),
197
+ disabled: busy,
198
+ children: capture.state === "transcribing" ? "Transcribing\u2026" : capture.canRetry ? "Retry transcription" : error ? "Try again" : "Done"
221
199
  }
222
200
  ),
201
+ capture.canRetry && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _chunkJDEXZGIYcjs.Button, { variant: "ghost", onClick: () => void capture.start(), children: "Record again" }),
223
202
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
224
203
  "button",
225
204
  {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  CopyButton
4
- } from "./chunk-HREEZUKL.js";
4
+ } from "./chunk-DILYV4FI.js";
5
5
  import {
6
6
  cn
7
7
  } from "./chunk-G6SRSWXB.js";
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ import {
3
+ AgentAudioVisualizerBar
4
+ } from "./chunk-VBKJXNEL.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, PhoneOff, AudioLines } from "lucide-react";
15
+ import { Fragment, 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",
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
+ };
35
+ function VoiceSessionControls({
36
+ state,
37
+ onStart,
38
+ onEnd,
39
+ muted = false,
40
+ onMutedChange,
41
+ disabled = false,
42
+ error,
43
+ volumeBands,
44
+ audioLevels,
45
+ labels,
46
+ size = "icon",
47
+ className
48
+ }) {
49
+ const liveBands = useSyncExternalStore(audioLevels?.subscribe ?? noSubscription, audioLevels?.getSnapshot ?? silence, silence);
50
+ const copy = { ...DEFAULT_LABELS, ...labels };
51
+ const active = state !== "disconnected" && state !== "error";
52
+ const waiting = state === "connecting" || state === "reconnecting";
53
+ return /* @__PURE__ */ jsxs(
54
+ "div",
55
+ {
56
+ "data-slot": "voice-session-controls",
57
+ "data-state": state,
58
+ className: cn("flex min-w-0 items-center gap-2", active && "w-full flex-col gap-3 py-4", !active && "flex-wrap", className),
59
+ children: [
60
+ active ? /* @__PURE__ */ jsxs(Fragment, { children: [
61
+ /* @__PURE__ */ jsx(
62
+ AgentAudioVisualizerBar,
63
+ {
64
+ "aria-hidden": "true",
65
+ size: "sm",
66
+ barCount: 5,
67
+ state: state === "reconnecting" ? "connecting" : state,
68
+ volumeBands: volumeBands ?? liveBands
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx("span", { role: "status", className: "text-sm text-muted-foreground", children: muted && state === "listening" ? copy.muted : copy[state] }),
72
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
73
+ /* @__PURE__ */ jsx(
74
+ Button,
75
+ {
76
+ type: "button",
77
+ variant: "ghost",
78
+ size: "icon",
79
+ className: "min-h-[44px] min-w-[44px] shrink-0",
80
+ "aria-label": muted ? copy.unmute : copy.mute,
81
+ title: muted ? copy.unmute : copy.mute,
82
+ "aria-pressed": muted,
83
+ disabled: waiting,
84
+ onClick: () => onMutedChange(!muted),
85
+ children: muted ? /* @__PURE__ */ jsx(MicOff, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Mic, { "aria-hidden": "true" })
86
+ }
87
+ ),
88
+ /* @__PURE__ */ jsxs(
89
+ Button,
90
+ {
91
+ type: "button",
92
+ variant: "outline",
93
+ shape: "pill",
94
+ className: "min-h-[44px] shrink-0",
95
+ "aria-label": copy.end,
96
+ title: copy.end,
97
+ onClick: onEnd,
98
+ children: [
99
+ /* @__PURE__ */ jsx(PhoneOff, { "aria-hidden": "true" }),
100
+ copy.end
101
+ ]
102
+ }
103
+ )
104
+ ] })
105
+ ] }) : /* @__PURE__ */ jsx(
106
+ Button,
107
+ {
108
+ type: "button",
109
+ size: "icon",
110
+ shape: "circle",
111
+ disabled,
112
+ onClick: onStart,
113
+ "aria-label": state === "error" ? copy.retry : copy.start,
114
+ title: state === "error" ? copy.retry : copy.start,
115
+ className: cn("min-h-[44px] min-w-[44px] shrink-0 rounded-full", size === "hero" && "size-24 [&_svg]:size-7"),
116
+ children: /* @__PURE__ */ jsx(AudioLines, { "aria-hidden": "true" })
117
+ }
118
+ ),
119
+ state === "error" && /* @__PURE__ */ jsx("span", { role: "alert", className: "basis-full text-xs text-destructive", children: error || copy.error })
120
+ ]
121
+ }
122
+ );
123
+ }
124
+
125
+ export {
126
+ VoiceSessionControls
127
+ };