@epicbagel/saas-kit 1.11.1 → 1.13.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 +41 -4
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/{chunk-ZZ7ICQR2.cjs → chunk-7Q4EFCUI.cjs} +93 -28
- package/dist/{chunk-EDEN7TAL.js → chunk-JKVVTFAP.js} +5 -4
- package/dist/chunk-KI7SWNIN.js +53 -0
- package/dist/{chunk-D47AMKT4.cjs → chunk-QWUMIAYD.cjs} +6 -5
- package/dist/{chunk-HLAHOVEI.js → chunk-RPSRCB5O.js} +79 -14
- package/dist/chunk-SJB4DPGQ.cjs +51 -0
- package/dist/chunk-SRIGG5I5.js +51 -0
- package/dist/chunk-VKYRB37O.cjs +53 -0
- package/dist/hooks/use-dictation.cjs +2 -2
- package/dist/hooks/use-dictation.d.cts +27 -5
- package/dist/hooks/use-dictation.d.ts +27 -5
- package/dist/hooks/use-dictation.js +1 -1
- package/dist/primitives/chat.cjs +16 -12
- package/dist/primitives/chat.d.cts +13 -6
- package/dist/primitives/chat.d.ts +13 -6
- package/dist/primitives/chat.js +13 -9
- package/dist/primitives/dictation-provider.cjs +2 -2
- package/dist/primitives/dictation-provider.d.cts +1 -1
- package/dist/primitives/dictation-provider.d.ts +1 -1
- package/dist/primitives/dictation-provider.js +1 -1
- package/dist/primitives/notes.cjs +259 -0
- package/dist/primitives/notes.d.cts +123 -0
- package/dist/primitives/notes.d.ts +123 -0
- package/dist/primitives/notes.js +259 -0
- package/dist/primitives/panel-section.cjs +4 -48
- package/dist/primitives/panel-section.js +3 -47
- package/dist/primitives/section-header.cjs +4 -45
- package/dist/primitives/section-header.js +4 -45
- package/dist/primitives/voice-dictation-button.cjs +3 -3
- package/dist/primitives/voice-dictation-button.js +2 -2
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -50,8 +50,7 @@ For hidden/programmatic triggers, it uses the focused visible button as the
|
|
|
50
50
|
desktop anchor. Hosts can pass an explicit `anchorRef` for a custom trigger.
|
|
51
51
|
Without a visible anchor it opens full-screen. Nested and sibling dialogs keep
|
|
52
52
|
their focus and cancellation behavior.
|
|
53
|
-
The recorder runs until **Done**,
|
|
54
|
-
**Transcribing…**, then commits the returned text once. Failed requests keep the
|
|
53
|
+
The recorder runs until **Done**, finishes the last words, then commits the returned text once. Failed requests keep the
|
|
55
54
|
take available for retry. Cancel stops microphone tracks, aborts the request and
|
|
56
55
|
ignores a late answer. The kit never persists audio or knows provider credentials.
|
|
57
56
|
|
|
@@ -67,6 +66,21 @@ import { DictationProvider } from "@epicbagel/saas-kit/primitives/dictation-prov
|
|
|
67
66
|
</DictationProvider>
|
|
68
67
|
```
|
|
69
68
|
|
|
69
|
+
For words that appear while speaking, supply `stream` as well (or on its own).
|
|
70
|
+
The host connects the provided `MediaStream` to its speech service; the kit
|
|
71
|
+
captures the microphone only once. `onTranscript` replaces the live preview
|
|
72
|
+
with `{ transcript, interimTranscript }`. The adapter resolves when ready and
|
|
73
|
+
returns `{ finish, cancel }`: `finish()` drains the final audio and returns the
|
|
74
|
+
complete transcript; `cancel()` releases the transport without saving. The
|
|
75
|
+
adapter must honor `signal` during startup, streaming and finalization, and
|
|
76
|
+
report unexpected disconnection through `onError`.
|
|
77
|
+
|
|
78
|
+
Preview updates never invoke a control's commit callback. Done waits for
|
|
79
|
+
`finish()` before saving. If streaming fails and `transcribe` is also supplied,
|
|
80
|
+
the recorder shows a warning and uses its complete recording on Done. Retry
|
|
81
|
+
uses that same recording. With no batch adapter, an interrupted stream is an
|
|
82
|
+
error; partial words are never presented as a complete note.
|
|
83
|
+
|
|
70
84
|
Set `scopeKey` from the acting user and workspace (including impersonation, if
|
|
71
85
|
the app supports it). A changed scope discards the active take, stops recording
|
|
72
86
|
and aborts transcription without remounting the application. Responses from the
|
|
@@ -83,8 +97,9 @@ native recognition has no audio-level source.
|
|
|
83
97
|
failure. Choose either that result or `onTranscript` to commit; using both would
|
|
84
98
|
duplicate the host action. The callback receives one complete API transcript,
|
|
85
99
|
or finalized chunks with native recognition. API recording requires HTTPS (or
|
|
86
|
-
localhost), microphone permission and MediaRecorder.
|
|
87
|
-
|
|
100
|
+
localhost), microphone permission and MediaRecorder. Live interim text requires
|
|
101
|
+
the host's `stream` adapter; the batch `transcribe` adapter alone returns text
|
|
102
|
+
after Done. Live voice conversation controls remain a separate feature.
|
|
88
103
|
|
|
89
104
|
Without a provider, controls retain browser-native speech recognition. The
|
|
90
105
|
private ui-kit's `VoiceInput` uses this same provider and capture hook, and its
|
|
@@ -424,3 +439,25 @@ export function Appearance() {
|
|
|
424
439
|
|
|
425
440
|
MIT. Some components and styles are adapted from shadcn/ui, and the file
|
|
426
441
|
drop handling from AI SDK Elements (Apache-2.0); see `THIRD_PARTY_NOTICES.md`.
|
|
442
|
+
|
|
443
|
+
## Notes
|
|
444
|
+
|
|
445
|
+
`primitives/notes` provides a reusable `Notes` area and `NoteComposer` for any
|
|
446
|
+
record. Pass display data with stable IDs; the list owns rendering and count,
|
|
447
|
+
preserves order, and accepts author/date nodes and optional context, pinned,
|
|
448
|
+
voice/assistant and deletion metadata. Hosts resolve identity, authorize saves
|
|
449
|
+
and confirm deletions.
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
<Notes
|
|
453
|
+
notes={[{ id: "note-1", author: "You", date: "Today", text: "Follow up next week." }]}
|
|
454
|
+
composer={<NoteComposer onSubmit={saveNote} onVoice={openRecorder} />}
|
|
455
|
+
/>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Omit `onVoice` for a text-only composer. A rejected save retains the draft; all
|
|
459
|
+
conflicting actions are disabled during a save. `value` / `onValueChange` and
|
|
460
|
+
`editing` / `onEditingChange` support host-owned drafts, including failed voice
|
|
461
|
+
transcripts. A controlled host clears or replaces `value` after saving. Set
|
|
462
|
+
`maxLength` to the application's limit. `NoteList` and `NoteCard` are available
|
|
463
|
+
for existing compositions.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -52,6 +52,26 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
52
52
|
SOFTWARE.
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
## Attributed notes
|
|
56
|
+
|
|
57
|
+
`primitives/notes` adapts the attributed note cards and voice/text composer
|
|
58
|
+
from the public `@recruiteros/recruit-ui` package, version 0.16.0. The adaptation
|
|
59
|
+
adds a generic notes-array API, controlled drafts, accessible labels and
|
|
60
|
+
serialized asynchronous saves. It contains no application data or persistence.
|
|
61
|
+
|
|
62
|
+
These adapted portions are licensed under the Apache License, Version 2.0,
|
|
63
|
+
reproduced below. The upstream notice is retained here:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Recruit UI
|
|
67
|
+
Copyright 2026 RecruiterOS
|
|
68
|
+
|
|
69
|
+
RecruiterOS-authored portions are licensed under the Apache License,
|
|
70
|
+
Version 2.0. The UI primitives come from the separately installed
|
|
71
|
+
@epicbagel/saas-kit package (MIT). Third-party runtime dependencies retain
|
|
72
|
+
their own licenses and copyright notices. See THIRD_PARTY_NOTICES.md.
|
|
73
|
+
```
|
|
74
|
+
|
|
55
75
|
## AI SDK Elements
|
|
56
76
|
|
|
57
77
|
Source: https://github.com/vercel/ai-elements (`packages/elements/src/prompt-input.tsx`,
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// src/hooks/use-dictation.ts
|
|
4
4
|
var _react = require('react');
|
|
5
5
|
var DictationContext = _react.createContext.call(void 0, null);
|
|
6
|
-
function DictationProvider({ transcribe, scopeKey, children }) {
|
|
6
|
+
function DictationProvider({ transcribe, stream, scopeKey, children }) {
|
|
7
7
|
const scope = _react.useRef.call(void 0, { key: scopeKey, generation: 0 });
|
|
8
8
|
if (scope.current.key !== scopeKey) scope.current = { key: scopeKey, generation: scope.current.generation + 1 };
|
|
9
9
|
const generation = scope.current.generation;
|
|
10
|
-
const value = _react.useMemo.call(void 0, () => ({ transcribe, scope, generation }), [transcribe, generation]);
|
|
10
|
+
const value = _react.useMemo.call(void 0, () => ({ transcribe, stream, scope, generation }), [transcribe, stream, generation]);
|
|
11
11
|
return _react.createElement.call(void 0, DictationContext.Provider, { value }, children);
|
|
12
12
|
}
|
|
13
13
|
var EMPTY = { state: "idle", transcript: "", interimTranscript: "", error: null, canRetry: false };
|
|
@@ -128,6 +128,8 @@ function releaseTracks(session) {
|
|
|
128
128
|
}
|
|
129
129
|
function dispose(session) {
|
|
130
130
|
session.controller.abort();
|
|
131
|
+
_optionalChain([session, 'access', _18 => _18.streamController, 'optionalAccess', _19 => _19.abort, 'call', _20 => _20()]);
|
|
132
|
+
_optionalChain([session, 'access', _21 => _21.live, 'optionalAccess', _22 => _22.cancel, 'call', _23 => _23()]);
|
|
131
133
|
if (session.nativeTimer) clearTimeout(session.nativeTimer);
|
|
132
134
|
const recorder = session.recorder;
|
|
133
135
|
if (recorder) {
|
|
@@ -157,9 +159,10 @@ function dispose(session) {
|
|
|
157
159
|
}
|
|
158
160
|
function useDictation({ lang = "en-US", continuous = true, onTranscript } = {}) {
|
|
159
161
|
const provider = _react.useContext.call(void 0, DictationContext);
|
|
160
|
-
const transcribe = _nullishCoalesce(_optionalChain([provider, 'optionalAccess',
|
|
161
|
-
const
|
|
162
|
-
const
|
|
162
|
+
const transcribe = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _24 => _24.transcribe]), () => ( null));
|
|
163
|
+
const startStream = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _25 => _25.stream]), () => ( null));
|
|
164
|
+
const scope = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _26 => _26.scope]), () => ( null));
|
|
165
|
+
const scopeGeneration = _nullishCoalesce(_optionalChain([provider, 'optionalAccess', _27 => _27.generation]), () => ( 0));
|
|
163
166
|
const [snapshot, setSnapshot] = _react.useState.call(void 0, EMPTY);
|
|
164
167
|
const [supported, setSupported] = _react.useState.call(void 0, false);
|
|
165
168
|
const current = _react.useRef.call(void 0, null);
|
|
@@ -169,8 +172,8 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
169
172
|
activeScope.current = scope;
|
|
170
173
|
callback.current = onTranscript;
|
|
171
174
|
_react.useEffect.call(void 0, () => {
|
|
172
|
-
setSupported(transcribe ? typeof navigator !== "undefined" && !!_optionalChain([navigator, 'access',
|
|
173
|
-
}, [transcribe]);
|
|
175
|
+
setSupported(transcribe || startStream ? typeof navigator !== "undefined" && !!_optionalChain([navigator, 'access', _28 => _28.mediaDevices, 'optionalAccess', _29 => _29.getUserMedia]) && typeof MediaRecorder !== "undefined" : typeof recognitionConstructor() === "function");
|
|
176
|
+
}, [transcribe, startStream]);
|
|
174
177
|
const cancel = _react.useCallback.call(void 0, () => {
|
|
175
178
|
const session = current.current;
|
|
176
179
|
current.current = null;
|
|
@@ -190,9 +193,9 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
190
193
|
if (session) dispose(session);
|
|
191
194
|
};
|
|
192
195
|
}, []);
|
|
193
|
-
const isCurrent = _react.useCallback.call(void 0, (session) => mounted.current && current.current === session && session.scope === activeScope.current && session.scopeGeneration === (_nullishCoalesce(_optionalChain([session, 'access',
|
|
196
|
+
const isCurrent = _react.useCallback.call(void 0, (session) => mounted.current && current.current === session && session.scope === activeScope.current && session.scopeGeneration === (_nullishCoalesce(_optionalChain([session, 'access', _30 => _30.scope, 'optionalAccess', _31 => _31.current, 'access', _32 => _32.generation]), () => ( 0))) && !session.controller.signal.aborted, []);
|
|
194
197
|
const start = _react.useCallback.call(void 0, async () => {
|
|
195
|
-
if (scope !== activeScope.current || scopeGeneration !== (_nullishCoalesce(_optionalChain([scope, 'optionalAccess',
|
|
198
|
+
if (scope !== activeScope.current || scopeGeneration !== (_nullishCoalesce(_optionalChain([scope, 'optionalAccess', _33 => _33.current, 'access', _34 => _34.generation]), () => ( 0)))) return;
|
|
196
199
|
if (current.current && isCurrent(current.current) && ["requesting", "recording", "transcribing"].includes(current.current.phase)) return;
|
|
197
200
|
cancel();
|
|
198
201
|
let resolveCapture;
|
|
@@ -200,9 +203,12 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
200
203
|
const session = {
|
|
201
204
|
language: lang,
|
|
202
205
|
transcribe,
|
|
206
|
+
startStream,
|
|
207
|
+
liveFailed: false,
|
|
203
208
|
scope,
|
|
204
209
|
scopeGeneration,
|
|
205
210
|
controller: new AbortController(),
|
|
211
|
+
streamController: startStream ? new AbortController() : void 0,
|
|
206
212
|
phase: "requesting",
|
|
207
213
|
chunks: [],
|
|
208
214
|
capture: new Promise((resolve) => {
|
|
@@ -217,20 +223,20 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
217
223
|
transcript: "",
|
|
218
224
|
interim: "",
|
|
219
225
|
notified: /* @__PURE__ */ new Set(),
|
|
220
|
-
audioLevels: transcribe ? createAudioLevels() : void 0
|
|
226
|
+
audioLevels: transcribe || startStream ? createAudioLevels() : void 0
|
|
221
227
|
};
|
|
222
228
|
current.current = session;
|
|
223
229
|
setSnapshot({ ...EMPTY, state: "requesting" });
|
|
224
|
-
if (transcribe) {
|
|
230
|
+
if (transcribe || startStream) {
|
|
225
231
|
try {
|
|
226
|
-
if (!_optionalChain([navigator, 'access',
|
|
232
|
+
if (!_optionalChain([navigator, 'access', _35 => _35.mediaDevices, 'optionalAccess', _36 => _36.getUserMedia]) || typeof MediaRecorder === "undefined") throw new Error("unsupported");
|
|
227
233
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
228
234
|
if (!isCurrent(session)) {
|
|
229
235
|
stream.getTracks().forEach((track) => track.stop());
|
|
230
236
|
return;
|
|
231
237
|
}
|
|
232
238
|
session.stream = stream;
|
|
233
|
-
const mimeType = MIME_TYPES.find((mime) => _optionalChain([MediaRecorder, 'access',
|
|
239
|
+
const mimeType = MIME_TYPES.find((mime) => _optionalChain([MediaRecorder, 'access', _37 => _37.isTypeSupported, 'optionalCall', _38 => _38(mime)]));
|
|
234
240
|
const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream);
|
|
235
241
|
session.recorder = recorder;
|
|
236
242
|
recorder.ondataavailable = (event) => {
|
|
@@ -242,7 +248,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
242
248
|
session.resolveCapture(null);
|
|
243
249
|
return;
|
|
244
250
|
}
|
|
245
|
-
const type = recorder.mimeType || _optionalChain([session, 'access',
|
|
251
|
+
const type = recorder.mimeType || _optionalChain([session, 'access', _39 => _39.chunks, 'access', _40 => _40[0], 'optionalAccess', _41 => _41.type]) || mimeType || "audio/webm";
|
|
246
252
|
const blob = new Blob(session.chunks, { type });
|
|
247
253
|
session.blob = blob;
|
|
248
254
|
session.chunks = [];
|
|
@@ -250,16 +256,53 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
250
256
|
};
|
|
251
257
|
recorder.onerror = () => {
|
|
252
258
|
if (!isCurrent(session)) return;
|
|
259
|
+
session.liveFailed = true;
|
|
260
|
+
_optionalChain([session, 'access', _42 => _42.live, 'optionalAccess', _43 => _43.cancel, 'call', _44 => _44()]);
|
|
253
261
|
releaseTracks(session);
|
|
254
262
|
session.phase = "error";
|
|
255
263
|
session.resolveCapture(null);
|
|
256
264
|
setSnapshot({ ...EMPTY, state: "error", error: "Recording was interrupted. Try again." });
|
|
257
265
|
};
|
|
258
266
|
recorder.start();
|
|
267
|
+
if (startStream) {
|
|
268
|
+
const failed = (error) => {
|
|
269
|
+
if (!isCurrent(session) || session.phase === "ready" || session.phase === "error") return;
|
|
270
|
+
session.liveFailed = true;
|
|
271
|
+
_optionalChain([session, 'access', _45 => _45.live, 'optionalAccess', _46 => _46.cancel, 'call', _47 => _47()]);
|
|
272
|
+
if (transcribe) {
|
|
273
|
+
setSnapshot((previous) => ({ ...previous, previewError: "Live text is unavailable. Choose Done to transcribe this recording." }));
|
|
274
|
+
} else {
|
|
275
|
+
releaseTracks(session);
|
|
276
|
+
if (recorder.state !== "inactive") recorder.stop();
|
|
277
|
+
session.phase = "error";
|
|
278
|
+
setSnapshot((previous) => ({ ...previous, state: "error", error: error.message || "Live transcription stopped. Try again." }));
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
try {
|
|
282
|
+
const live = await startStream(stream, {
|
|
283
|
+
language: lang,
|
|
284
|
+
signal: session.streamController.signal,
|
|
285
|
+
onTranscript: (preview) => {
|
|
286
|
+
if (!isCurrent(session) || session.liveFailed || session.phase === "error" || session.phase === "ready") return;
|
|
287
|
+
session.transcript = preview.transcript;
|
|
288
|
+
session.interim = preview.interimTranscript;
|
|
289
|
+
setSnapshot((previous) => ({ ...previous, transcript: preview.transcript, interimTranscript: preview.interimTranscript }));
|
|
290
|
+
},
|
|
291
|
+
onError: failed
|
|
292
|
+
});
|
|
293
|
+
if (!isCurrent(session) || session.liveFailed || session.phase === "error") {
|
|
294
|
+
live.cancel();
|
|
295
|
+
} else session.live = live;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
failed(error instanceof Error ? error : new Error("Live transcription could not start. Try again."));
|
|
298
|
+
}
|
|
299
|
+
if (!isCurrent(session) || session.phase !== "requesting") return;
|
|
300
|
+
}
|
|
259
301
|
session.phase = "recording";
|
|
260
302
|
session.stopMeter = meterStream(stream, session.audioLevels, () => isCurrent(session) && session.phase === "recording");
|
|
261
|
-
setSnapshot({ ...
|
|
303
|
+
setSnapshot((previous) => ({ ...previous, state: "recording" }));
|
|
262
304
|
} catch (error) {
|
|
305
|
+
_optionalChain([session, 'access', _48 => _48.live, 'optionalAccess', _49 => _49.cancel, 'call', _50 => _50()]);
|
|
263
306
|
releaseTracks(session);
|
|
264
307
|
if (!isCurrent(session)) return;
|
|
265
308
|
session.phase = "error";
|
|
@@ -295,7 +338,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
295
338
|
session.transcript = final.join(" ");
|
|
296
339
|
session.interim = interim.join(" ");
|
|
297
340
|
setSnapshot((previous) => ({ ...previous, transcript: session.transcript, interimTranscript: session.interim }));
|
|
298
|
-
if (chunks.length) _optionalChain([callback, 'access',
|
|
341
|
+
if (chunks.length) _optionalChain([callback, 'access', _51 => _51.current, 'optionalCall', _52 => _52(chunks.join(" "))]);
|
|
299
342
|
};
|
|
300
343
|
recognition.onend = () => {
|
|
301
344
|
session.nativeEnded = true;
|
|
@@ -320,23 +363,23 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
320
363
|
session.phase = "error";
|
|
321
364
|
setSnapshot({ ...EMPTY, state: "error", error: microphoneError(error) });
|
|
322
365
|
}
|
|
323
|
-
}, [cancel, continuous, isCurrent, lang, scope, scopeGeneration, transcribe]);
|
|
366
|
+
}, [cancel, continuous, isCurrent, lang, scope, scopeGeneration, transcribe, startStream]);
|
|
324
367
|
const finish = _react.useCallback.call(void 0, async (session) => {
|
|
325
368
|
if (!isCurrent(session)) return null;
|
|
326
369
|
releaseMeter(session);
|
|
327
370
|
session.phase = "transcribing";
|
|
328
371
|
setSnapshot((previous) => ({ ...previous, state: "transcribing", error: null, canRetry: false }));
|
|
329
|
-
if (!session.transcribe) {
|
|
372
|
+
if (!session.transcribe && !session.startStream) {
|
|
330
373
|
if (!session.nativeEnded) {
|
|
331
374
|
session.nativeTimer = setTimeout(() => {
|
|
332
375
|
try {
|
|
333
|
-
_optionalChain([session, 'access',
|
|
376
|
+
_optionalChain([session, 'access', _53 => _53.recognition, 'optionalAccess', _54 => _54.abort, 'call', _55 => _55()]);
|
|
334
377
|
} catch (e8) {
|
|
335
378
|
}
|
|
336
379
|
session.resolveNativeEnd();
|
|
337
380
|
}, 1e3);
|
|
338
381
|
try {
|
|
339
|
-
_optionalChain([session, 'access',
|
|
382
|
+
_optionalChain([session, 'access', _56 => _56.recognition, 'optionalAccess', _57 => _57.stop, 'call', _58 => _58()]);
|
|
340
383
|
} catch (e9) {
|
|
341
384
|
session.resolveNativeEnd();
|
|
342
385
|
}
|
|
@@ -352,13 +395,29 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
352
395
|
let text;
|
|
353
396
|
try {
|
|
354
397
|
if (!session.blob) {
|
|
355
|
-
if (_optionalChain([session, 'access',
|
|
398
|
+
if (_optionalChain([session, 'access', _59 => _59.recorder, 'optionalAccess', _60 => _60.state]) !== "inactive") _optionalChain([session, 'access', _61 => _61.recorder, 'optionalAccess', _62 => _62.stop, 'call', _63 => _63()]);
|
|
356
399
|
const blob = await session.capture;
|
|
357
400
|
if (!isCurrent(session) || !blob) return null;
|
|
358
401
|
session.blob = blob;
|
|
359
402
|
}
|
|
360
|
-
if (
|
|
361
|
-
|
|
403
|
+
if (session.live && !session.liveFailed) {
|
|
404
|
+
try {
|
|
405
|
+
const final = await session.live.finish();
|
|
406
|
+
if (session.liveFailed) throw new Error("Live transcription was interrupted. Please retry this recording.");
|
|
407
|
+
text = final.trim();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
session.liveFailed = true;
|
|
410
|
+
if (!session.transcribe) throw error;
|
|
411
|
+
} finally {
|
|
412
|
+
session.live.cancel();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!isCurrent(session)) return null;
|
|
416
|
+
if (text === void 0) {
|
|
417
|
+
if (!session.transcribe) throw new Error("Live transcription could not finish. Please record again.");
|
|
418
|
+
if (!session.blob.size) throw new Error("No audio was recorded. Try again.");
|
|
419
|
+
text = (await session.transcribe(session.blob, { language: session.language, signal: session.controller.signal })).trim();
|
|
420
|
+
}
|
|
362
421
|
if (!isCurrent(session)) return null;
|
|
363
422
|
if (!text) throw new Error("No speech was detected. Record again.");
|
|
364
423
|
} catch (error) {
|
|
@@ -366,13 +425,14 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
366
425
|
releaseTracks(session);
|
|
367
426
|
session.phase = "error";
|
|
368
427
|
const message = error instanceof Error && error.message ? error.message : "Transcription failed. Try again.";
|
|
369
|
-
|
|
428
|
+
_optionalChain([session, 'access', _64 => _64.live, 'optionalAccess', _65 => _65.cancel, 'call', _66 => _66()]);
|
|
429
|
+
setSnapshot((previous) => ({ ...previous, state: "error", error: message, canRetry: !!session.transcribe && !!_optionalChain([session, 'access', _67 => _67.blob, 'optionalAccess', _68 => _68.size]) }));
|
|
370
430
|
return null;
|
|
371
431
|
}
|
|
372
432
|
session.transcript = text;
|
|
373
433
|
session.phase = "ready";
|
|
374
434
|
setSnapshot({ ...EMPTY, state: "ready", transcript: text });
|
|
375
|
-
_optionalChain([callback, 'access',
|
|
435
|
+
_optionalChain([callback, 'access', _69 => _69.current, 'optionalCall', _70 => _70(text)]);
|
|
376
436
|
return text;
|
|
377
437
|
}, [isCurrent]);
|
|
378
438
|
const stop = _react.useCallback.call(void 0, () => {
|
|
@@ -383,8 +443,13 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
383
443
|
return Promise.resolve(null);
|
|
384
444
|
}
|
|
385
445
|
if (session.phase === "requesting") {
|
|
386
|
-
|
|
387
|
-
|
|
446
|
+
if (!session.transcribe || _optionalChain([session, 'access', _71 => _71.recorder, 'optionalAccess', _72 => _72.state]) !== "recording") {
|
|
447
|
+
cancel();
|
|
448
|
+
return Promise.resolve(null);
|
|
449
|
+
}
|
|
450
|
+
session.liveFailed = true;
|
|
451
|
+
_optionalChain([session, 'access', _73 => _73.streamController, 'optionalAccess', _74 => _74.abort, 'call', _75 => _75()]);
|
|
452
|
+
_optionalChain([session, 'access', _76 => _76.live, 'optionalAccess', _77 => _77.cancel, 'call', _78 => _78()]);
|
|
388
453
|
}
|
|
389
454
|
if (session.finish) return session.finish;
|
|
390
455
|
if (session.phase === "error") return Promise.resolve(null);
|
|
@@ -397,7 +462,7 @@ function useDictation({ lang = "en-US", continuous = true, onTranscript } = {})
|
|
|
397
462
|
cancel();
|
|
398
463
|
return Promise.resolve(null);
|
|
399
464
|
}
|
|
400
|
-
if (!_optionalChain([session, 'optionalAccess',
|
|
465
|
+
if (!_optionalChain([session, 'optionalAccess', _79 => _79.transcribe]) || !_optionalChain([session, 'access', _80 => _80.blob, 'optionalAccess', _81 => _81.size])) return Promise.resolve(null);
|
|
401
466
|
if (session.phase !== "error") return _nullishCoalesce(session.finish, () => ( Promise.resolve(null)));
|
|
402
467
|
session.finish = finish(session);
|
|
403
468
|
return session.finish;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./chunk-G6SRSWXB.js";
|
|
11
11
|
import {
|
|
12
12
|
useDictation
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-RPSRCB5O.js";
|
|
14
14
|
|
|
15
15
|
// src/primitives/voice-dictation-button.tsx
|
|
16
16
|
import {
|
|
@@ -123,7 +123,7 @@ function VoiceDictationButton({
|
|
|
123
123
|
/* @__PURE__ */ jsx("h2", { className: cn("font-medium", anchored ? "text-sm" : "text-lg"), children: title || "Voice dictation" }),
|
|
124
124
|
/* @__PURE__ */ jsxs("span", { role: "status", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
125
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" ? "
|
|
126
|
+
capture.state === "transcribing" ? "Finishing transcript\u2026" : capture.state === "requesting" ? "Starting dictation\u2026" : error ? "Recording not saved" : capture.state === "ready" ? "Ready" : caption
|
|
127
127
|
] })
|
|
128
128
|
] }),
|
|
129
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: [
|
|
@@ -134,7 +134,7 @@ function VoiceDictationButton({
|
|
|
134
134
|
] }),
|
|
135
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
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" ? "
|
|
137
|
+
/* @__PURE__ */ jsx("p", { id: descriptionId, className: "text-xs text-muted-foreground", children: capture.state === "transcribing" ? "Finishing your last words\u2026" : capture.state === "requesting" ? "Allow microphone access to start." : error ? capture.canRetry ? "Retry this take or record again." : "Try recording again." : "Speak, then choose Done to add the text." }),
|
|
138
138
|
displayTranscript && /* @__PURE__ */ jsxs("p", { className: "max-h-40 overflow-y-auto text-sm leading-relaxed", children: [
|
|
139
139
|
transcript,
|
|
140
140
|
interim && /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
@@ -142,7 +142,8 @@ function VoiceDictationButton({
|
|
|
142
142
|
interim
|
|
143
143
|
] })
|
|
144
144
|
] }),
|
|
145
|
-
error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-xs text-destructive", children: error })
|
|
145
|
+
error && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-xs text-destructive", children: error }),
|
|
146
|
+
!error && capture.previewError && /* @__PURE__ */ jsx("p", { role: "status", className: "text-xs text-muted-foreground", children: capture.previewError })
|
|
146
147
|
] }),
|
|
147
148
|
/* @__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
149
|
/* @__PURE__ */ jsx(Button, { ref: cancelControl, type: "button", variant: "ghost", size: "sm", className: anchored ? "min-h-[44px]" : "min-h-[48px]", onClick: handleCancel, children: "Cancel" }),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
cn
|
|
4
|
+
} from "./chunk-G6SRSWXB.js";
|
|
5
|
+
|
|
6
|
+
// src/primitives/panel-section.tsx
|
|
7
|
+
import { forwardRef } from "react";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
9
|
+
var borders = {
|
|
10
|
+
none: "",
|
|
11
|
+
top: "border-t",
|
|
12
|
+
bottom: "border-b"
|
|
13
|
+
};
|
|
14
|
+
var paddings = {
|
|
15
|
+
none: "",
|
|
16
|
+
compact: "px-[22px] py-[18px]",
|
|
17
|
+
default: "px-[26px] py-[26px]"
|
|
18
|
+
};
|
|
19
|
+
var PanelSection = forwardRef(
|
|
20
|
+
function PanelSection2({ as, border = "none", padding = "default", paddingX = "none", paddingTop = "none", paddingBottom = "none", paddingY = "none", surface = "default", layout = "block", gap = "none", shrink = true, className, ...props }, ref) {
|
|
21
|
+
const Component = as ?? "section";
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
Component,
|
|
24
|
+
{
|
|
25
|
+
ref,
|
|
26
|
+
className: cn(
|
|
27
|
+
borders[border],
|
|
28
|
+
paddings[padding],
|
|
29
|
+
paddingX === "md" && "px-[22px]",
|
|
30
|
+
paddingX === "lg" && "px-[26px]",
|
|
31
|
+
paddingTop === "sm" && "pt-2",
|
|
32
|
+
paddingTop === "md" && "pt-3",
|
|
33
|
+
paddingTop === "lg" && "pt-5",
|
|
34
|
+
paddingBottom === "xs" && "pb-1",
|
|
35
|
+
paddingBottom === "sm" && "pb-2",
|
|
36
|
+
paddingBottom === "md" && "pb-3",
|
|
37
|
+
paddingBottom === "lg" && "pb-4",
|
|
38
|
+
paddingY === "xl" && "py-12",
|
|
39
|
+
surface === "muted" && "bg-muted/40",
|
|
40
|
+
layout === "row" && "flex",
|
|
41
|
+
gap === "sm" && "gap-2",
|
|
42
|
+
!shrink && "shrink-0",
|
|
43
|
+
className
|
|
44
|
+
),
|
|
45
|
+
...props
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
PanelSection
|
|
53
|
+
};
|
|
@@ -10,7 +10,7 @@ var _chunkJDEXZGIYcjs = require('./chunk-JDEXZGIY.cjs');
|
|
|
10
10
|
var _chunkKWLSBTB2cjs = require('./chunk-KWLSBTB2.cjs');
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
var
|
|
13
|
+
var _chunk7Q4EFCUIcjs = require('./chunk-7Q4EFCUI.cjs');
|
|
14
14
|
|
|
15
15
|
// src/primitives/voice-dictation-button.tsx
|
|
16
16
|
|
|
@@ -50,7 +50,7 @@ function VoiceDictationButton({
|
|
|
50
50
|
const isOpen = _react.useRef.call(void 0, open);
|
|
51
51
|
isOpen.current = open;
|
|
52
52
|
const descriptionId = _react.useId.call(void 0, );
|
|
53
|
-
const capture =
|
|
53
|
+
const capture = _chunk7Q4EFCUIcjs.useDictation.call(void 0, { lang, continuous });
|
|
54
54
|
const { transcript, interimTranscript: interim, supported, error } = capture;
|
|
55
55
|
const generation = _react.useRef.call(void 0, 0);
|
|
56
56
|
const finishing = _react.useRef.call(void 0, false);
|
|
@@ -123,7 +123,7 @@ function VoiceDictationButton({
|
|
|
123
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
124
|
/* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { role: "status", className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
|
|
125
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" ? "
|
|
126
|
+
capture.state === "transcribing" ? "Finishing transcript\u2026" : capture.state === "requesting" ? "Starting dictation\u2026" : error ? "Recording not saved" : capture.state === "ready" ? "Ready" : caption
|
|
127
127
|
] })
|
|
128
128
|
] }),
|
|
129
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: [
|
|
@@ -134,7 +134,7 @@ function VoiceDictationButton({
|
|
|
134
134
|
] }),
|
|
135
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
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" ? "
|
|
137
|
+
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { id: descriptionId, className: "text-xs text-muted-foreground", children: capture.state === "transcribing" ? "Finishing your last words\u2026" : capture.state === "requesting" ? "Allow microphone access to start." : error ? capture.canRetry ? "Retry this take or record again." : "Try recording again." : "Speak, then choose Done to add the text." }),
|
|
138
138
|
displayTranscript && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "p", { className: "max-h-40 overflow-y-auto text-sm leading-relaxed", children: [
|
|
139
139
|
transcript,
|
|
140
140
|
interim && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "text-muted-foreground", children: [
|
|
@@ -142,7 +142,8 @@ function VoiceDictationButton({
|
|
|
142
142
|
interim
|
|
143
143
|
] })
|
|
144
144
|
] }),
|
|
145
|
-
error && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { role: "alert", className: "text-xs text-destructive", children: error })
|
|
145
|
+
error && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { role: "alert", className: "text-xs text-destructive", children: error }),
|
|
146
|
+
!error && capture.previewError && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { role: "status", className: "text-xs text-muted-foreground", children: capture.previewError })
|
|
146
147
|
] }),
|
|
147
148
|
/* @__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
149
|
/* @__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" }),
|