@voiceinput/react 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/index.cjs +701 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +110 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +690 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/styles.css +157 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
let _voiceinput_core = require("@voiceinput/core");
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
//#region src/coordinator.ts
|
|
7
|
+
var VoiceInputCoordinator = class {
|
|
8
|
+
#active;
|
|
9
|
+
#requested;
|
|
10
|
+
#generation = 0;
|
|
11
|
+
#queue = Promise.resolve();
|
|
12
|
+
activate(session) {
|
|
13
|
+
const generation = ++this.#generation;
|
|
14
|
+
this.#requested = session;
|
|
15
|
+
const activation = this.#queue.then(async () => {
|
|
16
|
+
if (!this.#isCurrentRequest(session, generation)) return false;
|
|
17
|
+
const previous = this.#active;
|
|
18
|
+
if (previous !== void 0 && previous !== session) {
|
|
19
|
+
try {
|
|
20
|
+
await previous.stop("replaced");
|
|
21
|
+
} catch (error) {
|
|
22
|
+
reportUnhandledError(error);
|
|
23
|
+
}
|
|
24
|
+
if (this.#active === previous) this.#active = void 0;
|
|
25
|
+
}
|
|
26
|
+
if (!this.#isCurrentRequest(session, generation)) return false;
|
|
27
|
+
this.#active = session;
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
this.#queue = activation.then(() => {}, () => {});
|
|
31
|
+
return activation;
|
|
32
|
+
}
|
|
33
|
+
release(session) {
|
|
34
|
+
this.cancel(session);
|
|
35
|
+
if (this.#active === session) this.#active = void 0;
|
|
36
|
+
}
|
|
37
|
+
cancel(session) {
|
|
38
|
+
if (this.#requested === session) {
|
|
39
|
+
this.#requested = void 0;
|
|
40
|
+
this.#generation += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
#isCurrentRequest(session, generation) {
|
|
44
|
+
return this.#requested === session && this.#generation === generation;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function reportUnhandledError(error) {
|
|
48
|
+
const reportError = globalThis.reportError;
|
|
49
|
+
reportError?.(error);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/context.tsx
|
|
53
|
+
const VoiceInputContext = (0, react.createContext)(null);
|
|
54
|
+
function VoiceInputProvider({ provider, audioSource, children }) {
|
|
55
|
+
const browserAudioSource = (0, react.useMemo)(() => (0, _voiceinput_core.createBrowserAudioSource)(), []);
|
|
56
|
+
const resolvedAudioSource = audioSource ?? browserAudioSource;
|
|
57
|
+
const coordinator = (0, react.useMemo)(() => new VoiceInputCoordinator(), []);
|
|
58
|
+
const value = (0, react.useMemo)(() => ({
|
|
59
|
+
provider,
|
|
60
|
+
audioSource: resolvedAudioSource,
|
|
61
|
+
coordinator
|
|
62
|
+
}), [
|
|
63
|
+
coordinator,
|
|
64
|
+
provider,
|
|
65
|
+
resolvedAudioSource
|
|
66
|
+
]);
|
|
67
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceInputContext.Provider, {
|
|
68
|
+
value,
|
|
69
|
+
children
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/use-voice-input.ts
|
|
74
|
+
const ACTIVE_KEYS = /* @__PURE__ */ new Set(["Enter", " "]);
|
|
75
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
|
|
76
|
+
function useVoiceInput(options = {}) {
|
|
77
|
+
return useVoiceInputInternal(options);
|
|
78
|
+
}
|
|
79
|
+
function useVoiceInputInternal(options = {}, dispatchInput = false) {
|
|
80
|
+
const context = (0, react.useContext)(VoiceInputContext);
|
|
81
|
+
const provider = options.provider ?? context?.provider;
|
|
82
|
+
if (provider === void 0) throw new _voiceinput_core.VoiceInputError({
|
|
83
|
+
code: "invalid-configuration",
|
|
84
|
+
message: "useVoiceInput requires a provider option or a parent VoiceInputProvider."
|
|
85
|
+
});
|
|
86
|
+
const controlled = options.value !== void 0 || options.onValueChange !== void 0;
|
|
87
|
+
if (controlled && (typeof options.value !== "string" || typeof options.onValueChange !== "function")) throw new _voiceinput_core.VoiceInputError({
|
|
88
|
+
code: "invalid-configuration",
|
|
89
|
+
message: "Controlled voice input requires both value and onValueChange options."
|
|
90
|
+
});
|
|
91
|
+
if ((0, react.useRef)(controlled).current !== controlled) throw new _voiceinput_core.VoiceInputError({
|
|
92
|
+
code: "invalid-configuration",
|
|
93
|
+
message: "A voice field cannot switch between controlled and uncontrolled modes. Remount it with a new key."
|
|
94
|
+
});
|
|
95
|
+
const latest = (0, react.useRef)(options);
|
|
96
|
+
latest.current = options;
|
|
97
|
+
const disabled = options.disabled ?? false;
|
|
98
|
+
const disabledRef = (0, react.useRef)(disabled);
|
|
99
|
+
disabledRef.current = disabled;
|
|
100
|
+
const disposedRef = (0, react.useRef)(false);
|
|
101
|
+
const selectionCapturedRef = (0, react.useRef)(false);
|
|
102
|
+
const heldPointerRef = (0, react.useRef)(null);
|
|
103
|
+
const heldKeyRef = (0, react.useRef)(null);
|
|
104
|
+
const targetNodeRef = (0, react.useRef)(null);
|
|
105
|
+
const browserAudioSource = (0, react.useMemo)(() => (0, _voiceinput_core.createBrowserAudioSource)(), []);
|
|
106
|
+
const audioSource = options.audioSource ?? context?.audioSource ?? browserAudioSource;
|
|
107
|
+
const standaloneCoordinator = (0, react.useMemo)(() => new VoiceInputCoordinator(), []);
|
|
108
|
+
const coordinator = context?.coordinator ?? standaloneCoordinator;
|
|
109
|
+
const vocabularyKey = JSON.stringify(options.vocabulary ?? null);
|
|
110
|
+
const vocabulary = (0, react.useMemo)(() => options.vocabulary === void 0 ? void 0 : [...options.vocabulary], [vocabularyKey]);
|
|
111
|
+
const endpointingKey = JSON.stringify(options.endpointing ?? null);
|
|
112
|
+
const endpointing = (0, react.useMemo)(() => typeof options.endpointing === "object" ? { ...options.endpointing } : options.endpointing, [endpointingKey]);
|
|
113
|
+
const hasTransform = options.transformTranscript !== void 0;
|
|
114
|
+
const [textEngine] = (0, react.useState)(() => {
|
|
115
|
+
return (0, _voiceinput_core.createVoiceInputTextEngine)({
|
|
116
|
+
...controlled ? { controlled: {
|
|
117
|
+
getValue: () => latest.current.value ?? "",
|
|
118
|
+
dispatchInput,
|
|
119
|
+
onValueChange: (value) => latest.current.onValueChange?.(value)
|
|
120
|
+
} } : {},
|
|
121
|
+
...options.interimBehavior === void 0 ? {} : { interimBehavior: options.interimBehavior },
|
|
122
|
+
...!hasTransform ? {} : { transformTranscript: (text) => {
|
|
123
|
+
const transform = latest.current.transformTranscript;
|
|
124
|
+
if (transform === void 0) throw new TypeError("transformTranscript is unavailable.");
|
|
125
|
+
return transform(text);
|
|
126
|
+
} },
|
|
127
|
+
...options.transformTimeoutMs === void 0 ? {} : { transformTimeoutMs: options.transformTimeoutMs }
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
const [session] = (0, react.useState)(() => (0, _voiceinput_core.createVoiceInputSession)({
|
|
131
|
+
provider,
|
|
132
|
+
audioSource,
|
|
133
|
+
textEngine
|
|
134
|
+
}));
|
|
135
|
+
useIsomorphicLayoutEffect(() => {
|
|
136
|
+
textEngine.updateOptions({
|
|
137
|
+
...options.interimBehavior === void 0 ? {} : { interimBehavior: options.interimBehavior },
|
|
138
|
+
...options.transformTranscript === void 0 ? {} : { transformTranscript: options.transformTranscript },
|
|
139
|
+
...options.transformTimeoutMs === void 0 ? {} : { transformTimeoutMs: options.transformTimeoutMs }
|
|
140
|
+
});
|
|
141
|
+
session.updateOptions({
|
|
142
|
+
provider,
|
|
143
|
+
audioSource,
|
|
144
|
+
...options.language === void 0 ? {} : { language: options.language },
|
|
145
|
+
...vocabulary === void 0 ? {} : { vocabulary },
|
|
146
|
+
...endpointing === void 0 ? {} : { endpointing },
|
|
147
|
+
...options.maxDurationMs === void 0 ? {} : { maxDurationMs: options.maxDurationMs },
|
|
148
|
+
...options.connectionTimeoutMs === void 0 ? {} : { connectionTimeoutMs: options.connectionTimeoutMs }
|
|
149
|
+
});
|
|
150
|
+
}, [
|
|
151
|
+
session,
|
|
152
|
+
provider,
|
|
153
|
+
audioSource,
|
|
154
|
+
textEngine,
|
|
155
|
+
options.language,
|
|
156
|
+
vocabulary,
|
|
157
|
+
endpointing,
|
|
158
|
+
options.maxDurationMs,
|
|
159
|
+
options.connectionTimeoutMs,
|
|
160
|
+
options.interimBehavior,
|
|
161
|
+
options.transformTranscript,
|
|
162
|
+
options.transformTimeoutMs
|
|
163
|
+
]);
|
|
164
|
+
const currentTextEngineRef = (0, react.useRef)(textEngine);
|
|
165
|
+
currentTextEngineRef.current = textEngine;
|
|
166
|
+
const undo = (0, react.useCallback)(() => textEngine.undo(), [textEngine]);
|
|
167
|
+
const redo = (0, react.useCallback)(() => textEngine.redo(), [textEngine]);
|
|
168
|
+
const getTextSnapshot = (0, react.useCallback)(() => textEngine.getSnapshot(), [textEngine]);
|
|
169
|
+
(0, react.useEffect)(() => () => {
|
|
170
|
+
queueMicrotask(() => {
|
|
171
|
+
if (disposedRef.current || currentTextEngineRef.current !== textEngine) textEngine.destroy();
|
|
172
|
+
});
|
|
173
|
+
}, [textEngine]);
|
|
174
|
+
const coordinatedSession = (0, react.useMemo)(() => ({ stop: (reason) => session.stop(reason) }), [session]);
|
|
175
|
+
const subscribe = (0, react.useCallback)((listener) => session.subscribe(() => listener()), [session]);
|
|
176
|
+
const getSnapshot = (0, react.useCallback)(() => session.getSnapshot(), [session]);
|
|
177
|
+
const snapshot = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
178
|
+
const [isSupported, setIsSupported] = (0, react.useState)(false);
|
|
179
|
+
(0, react.useEffect)(() => {
|
|
180
|
+
setIsSupported((0, _voiceinput_core.getBrowserVoiceInputSupport)().isSupported);
|
|
181
|
+
}, []);
|
|
182
|
+
useIsomorphicLayoutEffect(() => {
|
|
183
|
+
if (controlled) textEngine.reconcileControlledValue(latest.current.value ?? "");
|
|
184
|
+
}, [
|
|
185
|
+
controlled,
|
|
186
|
+
options.value,
|
|
187
|
+
textEngine
|
|
188
|
+
]);
|
|
189
|
+
(0, react.useEffect)(() => {
|
|
190
|
+
return session.subscribe((event) => {
|
|
191
|
+
dispatchCallback(latest.current, event);
|
|
192
|
+
if (event.type === "status-change" && event.status === "error" || event.type === "stop" || event.type === "cancel") coordinator.release(coordinatedSession);
|
|
193
|
+
});
|
|
194
|
+
}, [
|
|
195
|
+
coordinatedSession,
|
|
196
|
+
coordinator,
|
|
197
|
+
session
|
|
198
|
+
]);
|
|
199
|
+
const captureSelection = (0, react.useCallback)(() => {
|
|
200
|
+
selectionCapturedRef.current = textEngine.captureSelection() !== null;
|
|
201
|
+
}, [textEngine]);
|
|
202
|
+
const startInternal = (0, react.useCallback)(async (capture = true) => {
|
|
203
|
+
if (disabledRef.current || disposedRef.current || targetNodeRef.current !== null && !textEngine.isWritable()) return;
|
|
204
|
+
if (capture && !selectionCapturedRef.current) captureSelection();
|
|
205
|
+
selectionCapturedRef.current = false;
|
|
206
|
+
if (!await coordinator.activate(coordinatedSession)) return;
|
|
207
|
+
if (disabledRef.current || disposedRef.current || targetNodeRef.current !== null && !textEngine.isWritable()) {
|
|
208
|
+
coordinator.release(coordinatedSession);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
await session.start();
|
|
212
|
+
const status = session.getSnapshot().status;
|
|
213
|
+
if (status === "idle" || status === "error") coordinator.release(coordinatedSession);
|
|
214
|
+
}, [
|
|
215
|
+
captureSelection,
|
|
216
|
+
coordinatedSession,
|
|
217
|
+
coordinator,
|
|
218
|
+
session,
|
|
219
|
+
textEngine
|
|
220
|
+
]);
|
|
221
|
+
const start = (0, react.useCallback)(() => startInternal(true), [startInternal]);
|
|
222
|
+
const stop = (0, react.useCallback)(async (reason = "user") => {
|
|
223
|
+
await session.stop(reason);
|
|
224
|
+
coordinator.release(coordinatedSession);
|
|
225
|
+
}, [
|
|
226
|
+
coordinatedSession,
|
|
227
|
+
coordinator,
|
|
228
|
+
session
|
|
229
|
+
]);
|
|
230
|
+
const cancel = (0, react.useCallback)(async () => {
|
|
231
|
+
await session.cancel();
|
|
232
|
+
coordinator.release(coordinatedSession);
|
|
233
|
+
}, [
|
|
234
|
+
coordinatedSession,
|
|
235
|
+
coordinator,
|
|
236
|
+
session
|
|
237
|
+
]);
|
|
238
|
+
const toggle = (0, react.useCallback)(async () => {
|
|
239
|
+
const status = session.getSnapshot().status;
|
|
240
|
+
if (status === "idle" || status === "error") await startInternal(true);
|
|
241
|
+
else await stop();
|
|
242
|
+
}, [
|
|
243
|
+
session,
|
|
244
|
+
startInternal,
|
|
245
|
+
stop
|
|
246
|
+
]);
|
|
247
|
+
const targetRef = (0, react.useCallback)((target) => {
|
|
248
|
+
const previousTarget = targetNodeRef.current;
|
|
249
|
+
targetNodeRef.current = target;
|
|
250
|
+
if (target === null && previousTarget !== null) stop("replaced");
|
|
251
|
+
textEngine.setTarget(target);
|
|
252
|
+
}, [stop, textEngine]);
|
|
253
|
+
(0, react.useEffect)(() => {
|
|
254
|
+
disposedRef.current = false;
|
|
255
|
+
return () => {
|
|
256
|
+
disposedRef.current = true;
|
|
257
|
+
coordinator.cancel(coordinatedSession);
|
|
258
|
+
session.stop("replaced").then(() => coordinator.release(coordinatedSession), () => coordinator.release(coordinatedSession));
|
|
259
|
+
};
|
|
260
|
+
}, [
|
|
261
|
+
coordinatedSession,
|
|
262
|
+
coordinator,
|
|
263
|
+
session
|
|
264
|
+
]);
|
|
265
|
+
const releaseHold = (0, react.useCallback)(() => {
|
|
266
|
+
if (heldPointerRef.current === null && heldKeyRef.current === null) return;
|
|
267
|
+
heldPointerRef.current = null;
|
|
268
|
+
heldKeyRef.current = null;
|
|
269
|
+
stop();
|
|
270
|
+
}, [stop]);
|
|
271
|
+
(0, react.useEffect)(() => {
|
|
272
|
+
if (disabled) releaseHold();
|
|
273
|
+
}, [disabled, releaseHold]);
|
|
274
|
+
(0, react.useEffect)(() => {
|
|
275
|
+
window.addEventListener("blur", releaseHold);
|
|
276
|
+
return () => window.removeEventListener("blur", releaseHold);
|
|
277
|
+
}, [releaseHold]);
|
|
278
|
+
const [targetWritable, setTargetWritable] = (0, react.useState)(true);
|
|
279
|
+
useIsomorphicLayoutEffect(() => {
|
|
280
|
+
const target = targetNodeRef.current;
|
|
281
|
+
const update = () => setTargetWritable(target === null || textEngine.isWritable());
|
|
282
|
+
update();
|
|
283
|
+
if (target === null) return;
|
|
284
|
+
const observer = new MutationObserver(update);
|
|
285
|
+
observer.observe(target, {
|
|
286
|
+
attributes: true,
|
|
287
|
+
attributeFilter: [
|
|
288
|
+
"disabled",
|
|
289
|
+
"readonly",
|
|
290
|
+
"type"
|
|
291
|
+
]
|
|
292
|
+
});
|
|
293
|
+
for (let ancestor = target.parentElement; ancestor; ancestor = ancestor.parentElement) observer.observe(ancestor, {
|
|
294
|
+
attributes: true,
|
|
295
|
+
attributeFilter: ["disabled"]
|
|
296
|
+
});
|
|
297
|
+
return () => observer.disconnect();
|
|
298
|
+
});
|
|
299
|
+
(0, react.useEffect)(() => {
|
|
300
|
+
if (disabled || !targetWritable) stop("target-unavailable");
|
|
301
|
+
}, [
|
|
302
|
+
disabled,
|
|
303
|
+
targetWritable,
|
|
304
|
+
stop
|
|
305
|
+
]);
|
|
306
|
+
const resolvedDisabled = disabled || !targetWritable || !isSupported;
|
|
307
|
+
const active = snapshot.status !== "idle" && snapshot.status !== "error";
|
|
308
|
+
const activationMode = options.activationMode ?? "toggle";
|
|
309
|
+
const triggerProps = (0, react.useMemo)(() => ({
|
|
310
|
+
type: "button",
|
|
311
|
+
disabled: resolvedDisabled,
|
|
312
|
+
"aria-pressed": active,
|
|
313
|
+
onPointerDown(event) {
|
|
314
|
+
if (resolvedDisabled || event.button !== 0) return;
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
if (isStartable(session.getSnapshot().status)) captureSelection();
|
|
317
|
+
if (activationMode === "hold") {
|
|
318
|
+
heldPointerRef.current = event.pointerId;
|
|
319
|
+
try {
|
|
320
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
321
|
+
} catch {}
|
|
322
|
+
startInternal(false);
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
onPointerUp(event) {
|
|
326
|
+
if (activationMode === "hold" && heldPointerRef.current === event.pointerId) {
|
|
327
|
+
heldPointerRef.current = null;
|
|
328
|
+
try {
|
|
329
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
330
|
+
} catch {}
|
|
331
|
+
stop();
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
onPointerCancel(event) {
|
|
335
|
+
if (activationMode === "hold" && heldPointerRef.current === event.pointerId) {
|
|
336
|
+
heldPointerRef.current = null;
|
|
337
|
+
stop();
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
onLostPointerCapture() {
|
|
341
|
+
releaseHold();
|
|
342
|
+
},
|
|
343
|
+
onBlur() {
|
|
344
|
+
if (heldKeyRef.current !== null) releaseHold();
|
|
345
|
+
},
|
|
346
|
+
onClick(event) {
|
|
347
|
+
if (resolvedDisabled || activationMode === "hold") {
|
|
348
|
+
event.preventDefault();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
toggle();
|
|
352
|
+
},
|
|
353
|
+
onKeyDown(event) {
|
|
354
|
+
if (resolvedDisabled || !ACTIVE_KEYS.has(event.key) || event.repeat) return;
|
|
355
|
+
if (isStartable(session.getSnapshot().status)) captureSelection();
|
|
356
|
+
if (activationMode === "hold") {
|
|
357
|
+
event.preventDefault();
|
|
358
|
+
heldKeyRef.current = event.key;
|
|
359
|
+
startInternal(false);
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
onKeyUp(event) {
|
|
363
|
+
if (activationMode === "hold" && heldKeyRef.current === event.key) {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
heldKeyRef.current = null;
|
|
366
|
+
stop();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}), [
|
|
370
|
+
activationMode,
|
|
371
|
+
active,
|
|
372
|
+
captureSelection,
|
|
373
|
+
releaseHold,
|
|
374
|
+
resolvedDisabled,
|
|
375
|
+
session,
|
|
376
|
+
startInternal,
|
|
377
|
+
stop,
|
|
378
|
+
toggle
|
|
379
|
+
]);
|
|
380
|
+
const getTriggerProps = (0, react.useCallback)((props = {}) => composeTriggerProps(triggerProps, props), [triggerProps]);
|
|
381
|
+
return (0, react.useMemo)(() => ({
|
|
382
|
+
...snapshot,
|
|
383
|
+
targetRef,
|
|
384
|
+
triggerProps,
|
|
385
|
+
getTriggerProps,
|
|
386
|
+
isSupported,
|
|
387
|
+
getTextSnapshot,
|
|
388
|
+
undo,
|
|
389
|
+
redo,
|
|
390
|
+
start,
|
|
391
|
+
stop,
|
|
392
|
+
cancel,
|
|
393
|
+
toggle
|
|
394
|
+
}), [
|
|
395
|
+
cancel,
|
|
396
|
+
getTriggerProps,
|
|
397
|
+
getTextSnapshot,
|
|
398
|
+
undo,
|
|
399
|
+
redo,
|
|
400
|
+
isSupported,
|
|
401
|
+
snapshot,
|
|
402
|
+
start,
|
|
403
|
+
stop,
|
|
404
|
+
targetRef,
|
|
405
|
+
toggle,
|
|
406
|
+
triggerProps
|
|
407
|
+
]);
|
|
408
|
+
}
|
|
409
|
+
function composeTriggerProps(trigger, props) {
|
|
410
|
+
return {
|
|
411
|
+
...props,
|
|
412
|
+
type: props.type ?? trigger.type,
|
|
413
|
+
disabled: props.disabled === true || trigger.disabled,
|
|
414
|
+
"aria-pressed": trigger["aria-pressed"],
|
|
415
|
+
onBlur: composeEventHandlers$1(props.onBlur, trigger.onBlur),
|
|
416
|
+
onClick: composeEventHandlers$1(props.onClick, trigger.onClick),
|
|
417
|
+
onKeyDown: composeEventHandlers$1(props.onKeyDown, trigger.onKeyDown),
|
|
418
|
+
onKeyUp: composeEventHandlers$1(props.onKeyUp, trigger.onKeyUp),
|
|
419
|
+
onLostPointerCapture: composeEventHandlers$1(props.onLostPointerCapture, trigger.onLostPointerCapture),
|
|
420
|
+
onPointerCancel: composeEventHandlers$1(props.onPointerCancel, trigger.onPointerCancel),
|
|
421
|
+
onPointerDown: composeEventHandlers$1(props.onPointerDown, trigger.onPointerDown),
|
|
422
|
+
onPointerUp: composeEventHandlers$1(props.onPointerUp, trigger.onPointerUp)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function composeEventHandlers$1(consumer, internal) {
|
|
426
|
+
return (event) => {
|
|
427
|
+
consumer?.(event);
|
|
428
|
+
if (!event.defaultPrevented) internal(event);
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function isStartable(status) {
|
|
432
|
+
return status === "idle" || status === "error";
|
|
433
|
+
}
|
|
434
|
+
function dispatchCallback(callbacks, event) {
|
|
435
|
+
callbacks.onEvent?.(event);
|
|
436
|
+
switch (event.type) {
|
|
437
|
+
case "status-change":
|
|
438
|
+
callbacks.onStatusChange?.(event.status, event.previousStatus);
|
|
439
|
+
break;
|
|
440
|
+
case "interim":
|
|
441
|
+
callbacks.onInterimTranscript?.(event.text);
|
|
442
|
+
if (event.transcriptChanged) callbacks.onTranscriptChange?.(event.transcript);
|
|
443
|
+
break;
|
|
444
|
+
case "final":
|
|
445
|
+
callbacks.onFinalTranscriptPart?.(event.text);
|
|
446
|
+
if (event.finalTranscriptChanged) callbacks.onFinalTranscript?.(event.transcript);
|
|
447
|
+
if (event.transcriptChanged) callbacks.onTranscriptChange?.(event.transcript);
|
|
448
|
+
break;
|
|
449
|
+
case "text-limit":
|
|
450
|
+
callbacks.onTextLimit?.(event);
|
|
451
|
+
break;
|
|
452
|
+
case "duration-warning":
|
|
453
|
+
callbacks.onDurationWarning?.(event.remainingMs, event.maxDurationMs);
|
|
454
|
+
break;
|
|
455
|
+
case "stop":
|
|
456
|
+
callbacks.onStop?.(event.reason);
|
|
457
|
+
break;
|
|
458
|
+
case "error": callbacks.onError?.(event.error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/components.tsx
|
|
463
|
+
const visuallyHiddenStyle = {
|
|
464
|
+
border: 0,
|
|
465
|
+
clip: "rect(0 0 0 0)",
|
|
466
|
+
clipPath: "inset(50%)",
|
|
467
|
+
height: 1,
|
|
468
|
+
margin: -1,
|
|
469
|
+
overflow: "hidden",
|
|
470
|
+
padding: 0,
|
|
471
|
+
position: "absolute",
|
|
472
|
+
whiteSpace: "nowrap",
|
|
473
|
+
width: 1
|
|
474
|
+
};
|
|
475
|
+
const VoiceButton = /* @__PURE__ */ (0, react.forwardRef)(function VoiceButton({ voice: options, ...props }, forwardedRef) {
|
|
476
|
+
const voice = useVoiceInput({
|
|
477
|
+
...options,
|
|
478
|
+
disabled: props.disabled === true || options?.disabled === true
|
|
479
|
+
});
|
|
480
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceButtonElement, {
|
|
481
|
+
...props,
|
|
482
|
+
ref: forwardedRef,
|
|
483
|
+
voice
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
const VoiceInput = /* @__PURE__ */ (0, react.forwardRef)(function VoiceInput({ className, containerClassName, disabled, readOnly, onChange, onValueChange, type = "text", value, voice: options, voiceButtonProps, ...props }, forwardedRef) {
|
|
487
|
+
const voice = useVoiceField({
|
|
488
|
+
disabled: disabled === true || readOnly === true || voiceButtonProps?.disabled === true,
|
|
489
|
+
onValueChange,
|
|
490
|
+
options,
|
|
491
|
+
value
|
|
492
|
+
});
|
|
493
|
+
const targetRef = useComposedTargetRef(voice, forwardedRef);
|
|
494
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
495
|
+
className: joinClassNames("voiceinput-field", containerClassName),
|
|
496
|
+
...voiceDataAttributes(voice),
|
|
497
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
498
|
+
...props,
|
|
499
|
+
ref: targetRef,
|
|
500
|
+
className: joinClassNames("voiceinput-field__input", className),
|
|
501
|
+
disabled,
|
|
502
|
+
readOnly,
|
|
503
|
+
onChange: (event) => {
|
|
504
|
+
onValueChange?.(event.currentTarget.value);
|
|
505
|
+
onChange?.(event);
|
|
506
|
+
},
|
|
507
|
+
type,
|
|
508
|
+
value
|
|
509
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceButtonElement, {
|
|
510
|
+
...voiceButtonProps,
|
|
511
|
+
className: joinClassNames("voiceinput-field__button", voiceButtonProps?.className),
|
|
512
|
+
voice
|
|
513
|
+
})]
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
const VoiceTextarea = /* @__PURE__ */ (0, react.forwardRef)(function VoiceTextarea({ className, containerClassName, disabled, readOnly, onChange, onValueChange, value, voice: options, voiceButtonProps, ...props }, forwardedRef) {
|
|
517
|
+
const voice = useVoiceField({
|
|
518
|
+
disabled: disabled === true || readOnly === true || voiceButtonProps?.disabled === true,
|
|
519
|
+
onValueChange,
|
|
520
|
+
options,
|
|
521
|
+
value
|
|
522
|
+
});
|
|
523
|
+
const targetRef = useComposedTargetRef(voice, forwardedRef);
|
|
524
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
525
|
+
className: joinClassNames("voiceinput-field voiceinput-field--textarea", containerClassName),
|
|
526
|
+
...voiceDataAttributes(voice),
|
|
527
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
528
|
+
...props,
|
|
529
|
+
ref: targetRef,
|
|
530
|
+
className: joinClassNames("voiceinput-field__input", className),
|
|
531
|
+
disabled,
|
|
532
|
+
readOnly,
|
|
533
|
+
onChange: (event) => {
|
|
534
|
+
onValueChange?.(event.currentTarget.value);
|
|
535
|
+
onChange?.(event);
|
|
536
|
+
},
|
|
537
|
+
value
|
|
538
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VoiceButtonElement, {
|
|
539
|
+
...voiceButtonProps,
|
|
540
|
+
className: joinClassNames("voiceinput-field__button", voiceButtonProps?.className),
|
|
541
|
+
voice
|
|
542
|
+
})]
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
const VoiceButtonElement = /* @__PURE__ */ (0, react.forwardRef)(function VoiceButtonElement({ announce = true, children, className, disabled, getAnnouncement = defaultAnnouncement, onBlur, onClick, onKeyDown, onKeyUp, onLostPointerCapture, onPointerCancel, onPointerDown, onPointerUp, type, voice, ...props }, forwardedRef) {
|
|
546
|
+
const announcementId = (0, react.useId)();
|
|
547
|
+
const active = isActive(voice);
|
|
548
|
+
const label = defaultButtonLabel(voice);
|
|
549
|
+
const describedBy = [props["aria-describedby"], announce && announcementId].filter(Boolean).join(" ");
|
|
550
|
+
const trigger = voice.triggerProps;
|
|
551
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
552
|
+
...props,
|
|
553
|
+
...voiceDataAttributes(voice),
|
|
554
|
+
ref: forwardedRef,
|
|
555
|
+
"aria-describedby": describedBy || void 0,
|
|
556
|
+
"aria-label": props["aria-label"] ?? label,
|
|
557
|
+
"aria-pressed": trigger["aria-pressed"],
|
|
558
|
+
className: joinClassNames("voiceinput-button", className),
|
|
559
|
+
disabled: disabled === true || trigger.disabled,
|
|
560
|
+
type: type ?? trigger.type,
|
|
561
|
+
onBlur: composeEventHandlers(onBlur, trigger.onBlur),
|
|
562
|
+
onClick: composeEventHandlers(onClick, trigger.onClick),
|
|
563
|
+
onKeyDown: composeEventHandlers(onKeyDown, trigger.onKeyDown),
|
|
564
|
+
onKeyUp: composeEventHandlers(onKeyUp, trigger.onKeyUp),
|
|
565
|
+
onLostPointerCapture: composeEventHandlers(onLostPointerCapture, trigger.onLostPointerCapture),
|
|
566
|
+
onPointerCancel: composeEventHandlers(onPointerCancel, trigger.onPointerCancel),
|
|
567
|
+
onPointerDown: composeEventHandlers(onPointerDown, trigger.onPointerDown),
|
|
568
|
+
onPointerUp: composeEventHandlers(onPointerUp, trigger.onPointerUp),
|
|
569
|
+
children: typeof children === "function" ? children(voice) : children ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultButtonContent, {
|
|
570
|
+
active,
|
|
571
|
+
label
|
|
572
|
+
})
|
|
573
|
+
}), announce ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
574
|
+
id: announcementId,
|
|
575
|
+
"aria-live": voice.error === null ? "polite" : "assertive",
|
|
576
|
+
className: "voiceinput-sr-only",
|
|
577
|
+
role: voice.error === null ? "status" : "alert",
|
|
578
|
+
style: visuallyHiddenStyle,
|
|
579
|
+
children: getAnnouncement(voice)
|
|
580
|
+
}) : null] });
|
|
581
|
+
});
|
|
582
|
+
function DefaultButtonContent({ active, label }) {
|
|
583
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
584
|
+
"aria-hidden": "true",
|
|
585
|
+
className: "voiceinput-button__icon",
|
|
586
|
+
children: active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StopIcon, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MicrophoneIcon, {})
|
|
587
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
588
|
+
className: "voiceinput-button__label",
|
|
589
|
+
children: label
|
|
590
|
+
})] });
|
|
591
|
+
}
|
|
592
|
+
function MicrophoneIcon() {
|
|
593
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
594
|
+
viewBox: "0 0 24 24",
|
|
595
|
+
width: "18",
|
|
596
|
+
height: "18",
|
|
597
|
+
fill: "none",
|
|
598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
599
|
+
d: "M12 3a3 3 0 0 0-3 3v6a3 3 0 1 0 6 0V6a3 3 0 0 0-3-3Z",
|
|
600
|
+
fill: "currentColor"
|
|
601
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
602
|
+
d: "M6.5 11.5a5.5 5.5 0 0 0 11 0M12 17v4m-3 0h6",
|
|
603
|
+
stroke: "currentColor",
|
|
604
|
+
strokeWidth: "1.75",
|
|
605
|
+
strokeLinecap: "round"
|
|
606
|
+
})]
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function StopIcon() {
|
|
610
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
611
|
+
viewBox: "0 0 24 24",
|
|
612
|
+
width: "18",
|
|
613
|
+
height: "18",
|
|
614
|
+
fill: "none",
|
|
615
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
616
|
+
x: "6",
|
|
617
|
+
y: "6",
|
|
618
|
+
width: "12",
|
|
619
|
+
height: "12",
|
|
620
|
+
rx: "2",
|
|
621
|
+
fill: "currentColor"
|
|
622
|
+
})
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
function useVoiceField(options) {
|
|
626
|
+
const controlled = options.value !== void 0 || options.onValueChange !== void 0;
|
|
627
|
+
return useVoiceInputInternal({
|
|
628
|
+
...options.options,
|
|
629
|
+
disabled: options.disabled === true || options.options?.disabled === true,
|
|
630
|
+
...controlled && options.value !== void 0 ? { value: options.value } : {},
|
|
631
|
+
...controlled && options.onValueChange !== void 0 ? { onValueChange: options.onValueChange } : {}
|
|
632
|
+
}, true);
|
|
633
|
+
}
|
|
634
|
+
function useComposedTargetRef(voice, forwardedRef) {
|
|
635
|
+
const { targetRef } = voice;
|
|
636
|
+
const nodeRef = (0, react.useRef)(null);
|
|
637
|
+
(0, react.useImperativeHandle)(forwardedRef, () => nodeRef.current, []);
|
|
638
|
+
return (0, react.useCallback)((node) => {
|
|
639
|
+
nodeRef.current = node;
|
|
640
|
+
targetRef(node);
|
|
641
|
+
}, [targetRef]);
|
|
642
|
+
}
|
|
643
|
+
function voiceDataAttributes(voice) {
|
|
644
|
+
return {
|
|
645
|
+
"data-voiceinput-active": String(isActive(voice)),
|
|
646
|
+
"data-voiceinput-error": voice.error?.code ?? "",
|
|
647
|
+
"data-voiceinput-status": voice.status,
|
|
648
|
+
"data-voiceinput-supported": String(voice.isSupported)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function isActive(voice) {
|
|
652
|
+
return voice.status !== "error" && voice.status !== "idle";
|
|
653
|
+
}
|
|
654
|
+
function defaultButtonLabel(voice) {
|
|
655
|
+
if (!voice.isSupported) return "Voice input unavailable";
|
|
656
|
+
switch (voice.status) {
|
|
657
|
+
case "requesting-permission": return "Requesting access";
|
|
658
|
+
case "connecting": return "Connecting";
|
|
659
|
+
case "listening": return "Stop voice input";
|
|
660
|
+
case "stopping": return "Finishing";
|
|
661
|
+
case "processing": return "Processing";
|
|
662
|
+
case "error":
|
|
663
|
+
case "idle": return "Start voice input";
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function defaultAnnouncement(voice) {
|
|
667
|
+
if (!voice.isSupported) return "Voice input is unavailable in this browser.";
|
|
668
|
+
if (voice.error !== null) return `Voice input error: ${voice.error.message}`;
|
|
669
|
+
switch (voice.status) {
|
|
670
|
+
case "idle": return "Voice input ready.";
|
|
671
|
+
case "requesting-permission": return "Requesting microphone permission.";
|
|
672
|
+
case "connecting": return "Connecting voice input.";
|
|
673
|
+
case "listening": return "Voice input is listening.";
|
|
674
|
+
case "stopping": return "Finishing voice input.";
|
|
675
|
+
case "processing": return "Processing the transcript.";
|
|
676
|
+
case "error": return "Voice input stopped with an error.";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function composeEventHandlers(consumer, internal) {
|
|
680
|
+
return (event) => {
|
|
681
|
+
consumer?.(event);
|
|
682
|
+
if (!event.defaultPrevented) internal(event);
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function joinClassNames(...values) {
|
|
686
|
+
return values.filter(Boolean).join(" ");
|
|
687
|
+
}
|
|
688
|
+
//#endregion
|
|
689
|
+
exports.VoiceButton = VoiceButton;
|
|
690
|
+
exports.VoiceInput = VoiceInput;
|
|
691
|
+
Object.defineProperty(exports, "VoiceInputError", {
|
|
692
|
+
enumerable: true,
|
|
693
|
+
get: function() {
|
|
694
|
+
return _voiceinput_core.VoiceInputError;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
exports.VoiceInputProvider = VoiceInputProvider;
|
|
698
|
+
exports.VoiceTextarea = VoiceTextarea;
|
|
699
|
+
exports.useVoiceInput = useVoiceInput;
|
|
700
|
+
|
|
701
|
+
//# sourceMappingURL=index.cjs.map
|