@speechos/react 1.0.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/dist/index.js ADDED
@@ -0,0 +1,517 @@
1
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { events, speechOS, state } from "@speechos/core";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/context.tsx
6
+ const SpeechOSContext = createContext(void 0);
7
+ SpeechOSContext.displayName = "SpeechOSContext";
8
+ /**
9
+ * SpeechOS Provider component
10
+ *
11
+ * Wraps your app to provide SpeechOS context to all child components.
12
+ * Can optionally auto-initialize with a config.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * <SpeechOSProvider config={{ apiKey: 'your-key' }}>
17
+ * <App />
18
+ * </SpeechOSProvider>
19
+ * ```
20
+ */
21
+ function SpeechOSProvider({ config, children }) {
22
+ const currentState = useSyncExternalStore(useCallback((onStoreChange) => {
23
+ return state.subscribe(onStoreChange);
24
+ }, []), useCallback(() => state.getState(), []), useCallback(() => state.getState(), []));
25
+ const isInitialized = speechOS.isInitialized();
26
+ if (config && !isInitialized) speechOS.init(config);
27
+ const contextValue = useMemo(() => {
28
+ return {
29
+ state: currentState,
30
+ isInitialized: speechOS.isInitialized(),
31
+ init: (cfg) => speechOS.init(cfg),
32
+ dictate: () => speechOS.dictate(),
33
+ stopDictation: () => speechOS.stopDictation(),
34
+ edit: (text) => speechOS.edit(text),
35
+ stopEdit: () => speechOS.stopEdit(),
36
+ cancel: () => speechOS.cancel(),
37
+ connect: () => speechOS.connect(),
38
+ disconnect: () => speechOS.disconnect(),
39
+ enableMicrophone: () => speechOS.enableMicrophone(),
40
+ waitUntilReady: () => speechOS.waitUntilReady(),
41
+ stopAndGetTranscript: () => speechOS.stopAndGetTranscript(),
42
+ stopAndEdit: (originalText) => speechOS.stopAndEdit(originalText),
43
+ on: (event, callback) => events.on(event, callback),
44
+ off: (event, callback) => {
45
+ console.warn("SpeechOS: Use the unsubscribe function returned by on() instead of off()");
46
+ }
47
+ };
48
+ }, [currentState]);
49
+ return /* @__PURE__ */ jsx(SpeechOSContext.Provider, {
50
+ value: contextValue,
51
+ children
52
+ });
53
+ }
54
+ /**
55
+ * Hook to access the SpeechOS context
56
+ *
57
+ * @throws Error if used outside of SpeechOSProvider
58
+ * @returns The SpeechOS context value
59
+ */
60
+ function useSpeechOSContext() {
61
+ const context = useContext(SpeechOSContext);
62
+ if (context === void 0) throw new Error("useSpeechOSContext must be used within a SpeechOSProvider");
63
+ return context;
64
+ }
65
+
66
+ //#endregion
67
+ //#region src/hooks/useSpeechOS.ts
68
+ /**
69
+ * Main hook for accessing the full SpeechOS context
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * function MyComponent() {
74
+ * const { state, dictate, cancel, connect, enableMicrophone } = useSpeechOS();
75
+ *
76
+ * // High-level usage
77
+ * const handleDictate = async () => {
78
+ * const text = await dictate();
79
+ * console.log('Transcribed:', text);
80
+ * };
81
+ *
82
+ * // Low-level usage
83
+ * const handleCustomFlow = async () => {
84
+ * await connect();
85
+ * await waitUntilReady();
86
+ * await enableMicrophone();
87
+ * // ... custom logic
88
+ * const text = await stopAndGetTranscript();
89
+ * };
90
+ * }
91
+ * ```
92
+ *
93
+ * @returns The full SpeechOS context value
94
+ */
95
+ function useSpeechOS() {
96
+ return useSpeechOSContext();
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/hooks/useSpeechOSState.ts
101
+ /**
102
+ * Hook to access just the SpeechOS state
103
+ *
104
+ * Use this when you only need to read state values like
105
+ * isConnected, recordingState, etc. without needing the API methods.
106
+ *
107
+ * @example
108
+ * ```tsx
109
+ * function RecordingIndicator() {
110
+ * const state = useSpeechOSState();
111
+ *
112
+ * return (
113
+ * <div>
114
+ * {state.recordingState === 'recording' && <span>Recording...</span>}
115
+ * {state.isConnected && <span>Connected</span>}
116
+ * </div>
117
+ * );
118
+ * }
119
+ * ```
120
+ *
121
+ * @returns The current SpeechOS state
122
+ */
123
+ function useSpeechOSState() {
124
+ const context = useSpeechOSContext();
125
+ return context.state;
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/hooks/useSpeechOSEvents.ts
130
+ /**
131
+ * Hook to subscribe to SpeechOS events
132
+ *
133
+ * Automatically subscribes on mount and unsubscribes on unmount.
134
+ * The callback is stable - changes to it will update the subscription.
135
+ *
136
+ * @example
137
+ * ```tsx
138
+ * function TranscriptionListener() {
139
+ * useSpeechOSEvents('transcription:complete', (payload) => {
140
+ * console.log('Transcription received:', payload.text);
141
+ * });
142
+ *
143
+ * useSpeechOSEvents('error', (payload) => {
144
+ * console.error('Error:', payload.message);
145
+ * });
146
+ *
147
+ * return <div>Listening for events...</div>;
148
+ * }
149
+ * ```
150
+ *
151
+ * @param event - The event name to subscribe to
152
+ * @param callback - The callback to invoke when the event fires
153
+ * @returns Cleanup function (automatically called on unmount)
154
+ */
155
+ function useSpeechOSEvents(event, callback) {
156
+ useEffect(() => {
157
+ const unsubscribe = events.on(event, callback);
158
+ return unsubscribe;
159
+ }, [event, callback]);
160
+ return () => {};
161
+ }
162
+
163
+ //#endregion
164
+ //#region src/hooks/useDictation.ts
165
+ /**
166
+ * Simplified hook for dictation workflows
167
+ *
168
+ * Provides an easy-to-use interface for voice-to-text dictation
169
+ * with automatic state management.
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * function VoiceInput() {
174
+ * const { start, stop, isRecording, isProcessing, transcript, error } = useDictation();
175
+ *
176
+ * return (
177
+ * <div>
178
+ * <button onClick={isRecording ? stop : start} disabled={isProcessing}>
179
+ * {isRecording ? 'Stop' : 'Start'} Recording
180
+ * </button>
181
+ * {isProcessing && <span>Processing...</span>}
182
+ * {transcript && <p>You said: {transcript}</p>}
183
+ * {error && <p style={{ color: 'red' }}>{error}</p>}
184
+ * </div>
185
+ * );
186
+ * }
187
+ * ```
188
+ *
189
+ * @returns Dictation controls and state
190
+ */
191
+ function useDictation() {
192
+ const { state: state$1, dictate, stopDictation, cancel } = useSpeechOSContext();
193
+ const [transcript, setTranscript] = useState(null);
194
+ const [error, setError] = useState(null);
195
+ const isRecording = state$1.recordingState === "recording";
196
+ const isProcessing = state$1.recordingState === "processing";
197
+ const start = useCallback(async () => {
198
+ setError(null);
199
+ try {
200
+ await dictate();
201
+ } catch (err) {
202
+ const message = err instanceof Error ? err.message : "Failed to start dictation";
203
+ setError(message);
204
+ }
205
+ }, [dictate]);
206
+ const stop = useCallback(async () => {
207
+ try {
208
+ const result = await stopDictation();
209
+ setTranscript(result);
210
+ setError(null);
211
+ return result;
212
+ } catch (err) {
213
+ const message = err instanceof Error ? err.message : "Failed to get transcript";
214
+ setError(message);
215
+ throw err;
216
+ }
217
+ }, [stopDictation]);
218
+ const clear = useCallback(() => {
219
+ setTranscript(null);
220
+ setError(null);
221
+ }, []);
222
+ return {
223
+ start,
224
+ stop,
225
+ isRecording,
226
+ isProcessing,
227
+ transcript,
228
+ error,
229
+ clear
230
+ };
231
+ }
232
+
233
+ //#endregion
234
+ //#region src/hooks/useEdit.ts
235
+ /**
236
+ * Simplified hook for voice editing workflows
237
+ *
238
+ * Provides an easy-to-use interface for voice-based text editing
239
+ * with automatic state management.
240
+ *
241
+ * @example
242
+ * ```tsx
243
+ * function TextEditor() {
244
+ * const [text, setText] = useState('Hello world');
245
+ * const { start, stop, isEditing, isProcessing, result, error } = useEdit();
246
+ *
247
+ * const handleEdit = async () => {
248
+ * await start(text);
249
+ * };
250
+ *
251
+ * const handleStop = async () => {
252
+ * const edited = await stop();
253
+ * setText(edited);
254
+ * };
255
+ *
256
+ * return (
257
+ * <div>
258
+ * <textarea value={text} onChange={(e) => setText(e.target.value)} />
259
+ * <button onClick={isEditing ? handleStop : handleEdit} disabled={isProcessing}>
260
+ * {isEditing ? 'Apply Edit' : 'Edit with Voice'}
261
+ * </button>
262
+ * {isProcessing && <span>Processing...</span>}
263
+ * {error && <p style={{ color: 'red' }}>{error}</p>}
264
+ * </div>
265
+ * );
266
+ * }
267
+ * ```
268
+ *
269
+ * @returns Edit controls and state
270
+ */
271
+ function useEdit() {
272
+ const { state: state$1, edit, stopEdit, cancel } = useSpeechOSContext();
273
+ const [originalText, setOriginalText] = useState(null);
274
+ const [result, setResult] = useState(null);
275
+ const [error, setError] = useState(null);
276
+ const isEditing = state$1.recordingState === "recording" && state$1.activeAction === "edit";
277
+ const isProcessing = state$1.recordingState === "processing";
278
+ const start = useCallback(async (textToEdit) => {
279
+ setError(null);
280
+ setOriginalText(textToEdit);
281
+ try {
282
+ await edit(textToEdit);
283
+ } catch (err) {
284
+ const message = err instanceof Error ? err.message : "Failed to start edit";
285
+ setError(message);
286
+ }
287
+ }, [edit]);
288
+ const stop = useCallback(async () => {
289
+ try {
290
+ const editedResult = await stopEdit();
291
+ setResult(editedResult);
292
+ setError(null);
293
+ return editedResult;
294
+ } catch (err) {
295
+ const message = err instanceof Error ? err.message : "Failed to apply edit";
296
+ setError(message);
297
+ throw err;
298
+ }
299
+ }, [stopEdit]);
300
+ const clear = useCallback(() => {
301
+ setOriginalText(null);
302
+ setResult(null);
303
+ setError(null);
304
+ }, []);
305
+ return {
306
+ start,
307
+ stop,
308
+ isEditing,
309
+ isProcessing,
310
+ originalText,
311
+ result,
312
+ error,
313
+ clear
314
+ };
315
+ }
316
+
317
+ //#endregion
318
+ //#region src/hooks/useTranscription.ts
319
+ /**
320
+ * Low-level hook for granular transcription control
321
+ *
322
+ * Use this when you need fine-grained control over the LiveKit
323
+ * connection lifecycle. For most use cases, prefer useDictation()
324
+ * or useEdit() which provide simpler interfaces.
325
+ *
326
+ * @example
327
+ * ```tsx
328
+ * function CustomVoiceUI() {
329
+ * const {
330
+ * connect,
331
+ * waitUntilReady,
332
+ * enableMicrophone,
333
+ * getTranscript,
334
+ * disconnect,
335
+ * isConnected,
336
+ * isMicEnabled,
337
+ * recordingState,
338
+ * } = useTranscription();
339
+ *
340
+ * const handleRecord = async () => {
341
+ * // Step 1: Connect to LiveKit
342
+ * await connect();
343
+ *
344
+ * // Step 2: Wait for agent to be ready
345
+ * await waitUntilReady();
346
+ *
347
+ * // Step 3: Enable microphone
348
+ * await enableMicrophone();
349
+ *
350
+ * // ... user speaks ...
351
+ *
352
+ * // Step 4: Get transcript
353
+ * const text = await getTranscript();
354
+ * console.log('Transcribed:', text);
355
+ *
356
+ * // Step 5: Cleanup
357
+ * await disconnect();
358
+ * };
359
+ *
360
+ * return (
361
+ * <div>
362
+ * <p>Connected: {isConnected ? 'Yes' : 'No'}</p>
363
+ * <p>Mic: {isMicEnabled ? 'On' : 'Off'}</p>
364
+ * <p>State: {recordingState}</p>
365
+ * <button onClick={handleRecord}>Record</button>
366
+ * </div>
367
+ * );
368
+ * }
369
+ * ```
370
+ *
371
+ * @returns Low-level transcription controls and state
372
+ */
373
+ function useTranscription() {
374
+ const { state: state$1, connect: contextConnect, waitUntilReady: contextWaitUntilReady, enableMicrophone: contextEnableMicrophone, stopAndGetTranscript, stopAndEdit, disconnect: contextDisconnect, cancel } = useSpeechOSContext();
375
+ const connect = useCallback(async () => {
376
+ await contextConnect();
377
+ }, [contextConnect]);
378
+ const waitUntilReady = useCallback(async () => {
379
+ await contextWaitUntilReady();
380
+ }, [contextWaitUntilReady]);
381
+ const enableMicrophone = useCallback(async () => {
382
+ await contextEnableMicrophone();
383
+ }, [contextEnableMicrophone]);
384
+ const getTranscript = useCallback(async () => {
385
+ return await stopAndGetTranscript();
386
+ }, [stopAndGetTranscript]);
387
+ const getEdit = useCallback(async (originalText) => {
388
+ return await stopAndEdit(originalText);
389
+ }, [stopAndEdit]);
390
+ const disconnect = useCallback(async () => {
391
+ await contextDisconnect();
392
+ }, [contextDisconnect]);
393
+ return {
394
+ connect,
395
+ waitUntilReady,
396
+ enableMicrophone,
397
+ getTranscript,
398
+ getEdit,
399
+ disconnect,
400
+ cancel,
401
+ isConnected: state$1.isConnected,
402
+ isMicEnabled: state$1.isMicEnabled,
403
+ recordingState: state$1.recordingState
404
+ };
405
+ }
406
+
407
+ //#endregion
408
+ //#region src/components/SpeechOSWidget.tsx
409
+ /**
410
+ * SpeechOSWidget - React wrapper for the SpeechOS Web Component
411
+ *
412
+ * This component mounts the existing <speechos-widget> Lit Web Component
413
+ * and bridges React props to the Web Component's attributes and events.
414
+ *
415
+ * Note: Requires @speechos/client to be installed and imported somewhere
416
+ * in your app to register the Web Component.
417
+ *
418
+ * @example
419
+ * ```tsx
420
+ * import { SpeechOSProvider, SpeechOSWidget } from '@speechos/react';
421
+ * import '@speechos/client'; // Registers the Web Component
422
+ *
423
+ * function App() {
424
+ * return (
425
+ * <SpeechOSProvider>
426
+ * <MyForm />
427
+ * <SpeechOSWidget
428
+ * apiKey="your-key"
429
+ * onTranscription={(text) => console.log('Transcribed:', text)}
430
+ * onError={(error) => console.error(error)}
431
+ * />
432
+ * </SpeechOSProvider>
433
+ * );
434
+ * }
435
+ * ```
436
+ */
437
+ function SpeechOSWidget({ apiKey, userId, position = "bottom-center", host, zIndex, debug, onTranscription, onEdit, onError, onShow, onHide, className }) {
438
+ const containerRef = useRef(null);
439
+ const widgetRef = useRef(null);
440
+ const { init, isInitialized } = useSpeechOSContext();
441
+ useEffect(() => {
442
+ if (!isInitialized && apiKey) init({
443
+ apiKey,
444
+ userId,
445
+ host,
446
+ position,
447
+ zIndex,
448
+ debug
449
+ });
450
+ }, [
451
+ isInitialized,
452
+ init,
453
+ apiKey,
454
+ userId,
455
+ host,
456
+ position,
457
+ zIndex,
458
+ debug
459
+ ]);
460
+ useEffect(() => {
461
+ const unsubscribers = [];
462
+ if (onTranscription) unsubscribers.push(events.on("transcription:inserted", (payload) => {
463
+ onTranscription(payload.text, payload.element);
464
+ }));
465
+ if (onEdit) unsubscribers.push(events.on("edit:applied", (payload) => {
466
+ onEdit(payload.editedContent, payload.originalContent, payload.element);
467
+ }));
468
+ if (onError) unsubscribers.push(events.on("error", (payload) => {
469
+ onError(payload);
470
+ }));
471
+ if (onShow) unsubscribers.push(events.on("widget:show", () => {
472
+ onShow();
473
+ }));
474
+ if (onHide) unsubscribers.push(events.on("widget:hide", () => {
475
+ onHide();
476
+ }));
477
+ return () => {
478
+ unsubscribers.forEach((unsub) => unsub());
479
+ };
480
+ }, [
481
+ onTranscription,
482
+ onEdit,
483
+ onError,
484
+ onShow,
485
+ onHide
486
+ ]);
487
+ useEffect(() => {
488
+ if (!containerRef.current) return;
489
+ const isWebComponentRegistered = customElements.get("speechos-widget") !== void 0;
490
+ if (!isWebComponentRegistered) {
491
+ console.warn("SpeechOSWidget: <speechos-widget> Web Component is not registered. Make sure to import @speechos/client in your app.");
492
+ return;
493
+ }
494
+ const widget = document.createElement("speechos-widget");
495
+ widget.setAttribute("position", position);
496
+ containerRef.current.appendChild(widget);
497
+ widgetRef.current = widget;
498
+ return () => {
499
+ if (widgetRef.current && containerRef.current) {
500
+ containerRef.current.removeChild(widgetRef.current);
501
+ widgetRef.current = null;
502
+ }
503
+ };
504
+ }, [position]);
505
+ return /* @__PURE__ */ jsx("div", {
506
+ ref: containerRef,
507
+ className,
508
+ style: { display: "contents" }
509
+ });
510
+ }
511
+
512
+ //#endregion
513
+ //#region src/index.ts
514
+ const VERSION = "0.1.0";
515
+
516
+ //#endregion
517
+ export { SpeechOSContext, SpeechOSProvider, SpeechOSWidget, VERSION, useDictation, useEdit, useSpeechOS, useSpeechOSContext, useSpeechOSEvents, useSpeechOSState, useTranscription };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@speechos/react",
3
+ "version": "1.0.0",
4
+ "description": "React hooks and components for SpeechOS voice integration",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/speechos-org/speechos.git",
26
+ "directory": "speechos-client/packages/react"
27
+ },
28
+ "homepage": "https://speechos.ai",
29
+ "bugs": {
30
+ "url": "https://github.com/speechos-org/speechos/issues"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "dev": "tsdown --watch",
35
+ "type-check": "tsc --noEmit",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest"
38
+ },
39
+ "keywords": [
40
+ "speechos",
41
+ "react",
42
+ "hooks",
43
+ "voice",
44
+ "ai",
45
+ "speech"
46
+ ],
47
+ "author": "SpeechOS",
48
+ "license": "MIT",
49
+ "peerDependencies": {
50
+ "@speechos/client": "^0.2.0",
51
+ "@speechos/core": "^0.2.0",
52
+ "react": ">=18.0.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@speechos/client": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@testing-library/jest-dom": "^6.9.1",
61
+ "@testing-library/react": "^16.1.0",
62
+ "@types/react": "^19.0.7",
63
+ "happy-dom": "^20.1.0",
64
+ "react": "^19.0.0",
65
+ "tsdown": "^0.2.0",
66
+ "tslib": "^2.8.1",
67
+ "typescript": "^5.7.3",
68
+ "vitest": "^4.0.16"
69
+ },
70
+ "dependencies": {
71
+ "@speechos/core": "^0.2.0"
72
+ }
73
+ }