@oshara/voice-sdk 0.1.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 (61) hide show
  1. package/README.md +198 -0
  2. package/dist/appearance-CNWT8x1G.cjs +2 -0
  3. package/dist/appearance-CNWT8x1G.cjs.map +1 -0
  4. package/dist/appearance-i6QBkpCk.js +650 -0
  5. package/dist/appearance-i6QBkpCk.js.map +1 -0
  6. package/dist/consent-CK9VXNPa.js +54 -0
  7. package/dist/consent-CK9VXNPa.js.map +1 -0
  8. package/dist/consent-D7QNSkQD.cjs +2 -0
  9. package/dist/consent-D7QNSkQD.cjs.map +1 -0
  10. package/dist/core/analytics.d.ts +30 -0
  11. package/dist/core/appearance.d.ts +113 -0
  12. package/dist/core/audioSettings.d.ts +69 -0
  13. package/dist/core/consent.d.ts +17 -0
  14. package/dist/core/createVoiceAgent.d.ts +79 -0
  15. package/dist/core/events.d.ts +103 -0
  16. package/dist/core/formController.d.ts +28 -0
  17. package/dist/core/forms.d.ts +235 -0
  18. package/dist/core/index.d.ts +29 -0
  19. package/dist/core/prevContext.d.ts +26 -0
  20. package/dist/core/transport.d.ts +30 -0
  21. package/dist/core/types.d.ts +49 -0
  22. package/dist/core/voice.d.ts +79 -0
  23. package/dist/createVoiceAgent-BM3HODS6.js +1058 -0
  24. package/dist/createVoiceAgent-BM3HODS6.js.map +1 -0
  25. package/dist/createVoiceAgent-CJWxWzz6.cjs +4 -0
  26. package/dist/createVoiceAgent-CJWxWzz6.cjs.map +1 -0
  27. package/dist/index.cjs +2 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.js +44 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/react/index.d.ts +60 -0
  32. package/dist/react.cjs +2 -0
  33. package/dist/react.cjs.map +1 -0
  34. package/dist/react.js +115 -0
  35. package/dist/react.js.map +1 -0
  36. package/dist/styles.css +1838 -0
  37. package/dist/ui/index.d.ts +21 -0
  38. package/dist/ui/ui.d.ts +165 -0
  39. package/dist/ui.cjs +284 -0
  40. package/dist/ui.cjs.map +1 -0
  41. package/dist/ui.js +1153 -0
  42. package/dist/ui.js.map +1 -0
  43. package/package.json +67 -0
  44. package/src/core/analytics.ts +111 -0
  45. package/src/core/appearance.ts +464 -0
  46. package/src/core/audioSettings.ts +180 -0
  47. package/src/core/consent.ts +78 -0
  48. package/src/core/createVoiceAgent.ts +280 -0
  49. package/src/core/events.ts +120 -0
  50. package/src/core/formController.ts +317 -0
  51. package/src/core/forms.ts +861 -0
  52. package/src/core/index.ts +121 -0
  53. package/src/core/prevContext.ts +153 -0
  54. package/src/core/transport.ts +118 -0
  55. package/src/core/types.ts +66 -0
  56. package/src/core/voice.ts +1179 -0
  57. package/src/react/index.ts +238 -0
  58. package/src/ui/index.ts +507 -0
  59. package/src/ui/styles.css +1838 -0
  60. package/src/ui/ui.ts +1672 -0
  61. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @oshara/voice-sdk/react — thin React bindings over the headless core.
3
+ *
4
+ * - `useVoiceAgent(config)` creates and owns a client, subscribes to its
5
+ * events, and returns reactive state + bound actions.
6
+ * - `<VoiceWidget config />` mounts the prebuilt UI into a div.
7
+ *
8
+ * `react`/`react-dom` are optional peer dependencies; importing this entry is
9
+ * what pulls them in.
10
+ */
11
+
12
+ import {
13
+ createElement,
14
+ useEffect,
15
+ useMemo,
16
+ useRef,
17
+ useState,
18
+ } from "react";
19
+ import {
20
+ createVoiceAgent,
21
+ type AppearanceConfig,
22
+ type AudioStateSnapshot,
23
+ type FieldValidationError,
24
+ type FormDefinition,
25
+ type OrbState,
26
+ type VoiceAgentClient,
27
+ type VoiceAgentConfig,
28
+ } from "../core";
29
+
30
+ export interface TranscriptItem {
31
+ key: string;
32
+ role: "user" | "agent";
33
+ text: string;
34
+ isFinal: boolean;
35
+ }
36
+
37
+ export interface ActiveForm {
38
+ definition: FormDefinition;
39
+ values: Record<string, string>;
40
+ stepIndex: number;
41
+ }
42
+
43
+ export interface UseVoiceAgentResult {
44
+ client: VoiceAgentClient;
45
+ ready: boolean;
46
+ orb: OrbState;
47
+ statusLabel: string | null;
48
+ callStatus: string;
49
+ connected: boolean;
50
+ muted: boolean;
51
+ appearance: AppearanceConfig;
52
+ audio: AudioStateSnapshot | null;
53
+ transcript: TranscriptItem[];
54
+ activeForm: ActiveForm | null;
55
+ formErrors: FieldValidationError[];
56
+ formSubmitting: boolean;
57
+ // actions
58
+ start: () => Promise<void>;
59
+ end: () => Promise<void>;
60
+ toggleMute: () => Promise<boolean>;
61
+ sendText: (text: string) => Promise<void>;
62
+ updateFormValues: (values: Record<string, string>) => void;
63
+ submitForm: () => void;
64
+ stepForm: (direction: "next" | "back" | number) => void;
65
+ closeForm: () => void;
66
+ }
67
+
68
+ /**
69
+ * Create and manage a voice-agent client for the lifetime of the component.
70
+ * The `config` is read once on mount (changing it later does not recreate the
71
+ * client — remount the component, e.g. with a `key`, to switch agents).
72
+ */
73
+ export function useVoiceAgent(config: VoiceAgentConfig): UseVoiceAgentResult {
74
+ // Freeze the initial config so the client is created exactly once.
75
+ const configRef = useRef(config);
76
+ const client = useMemo(() => createVoiceAgent(configRef.current), []);
77
+
78
+ const [ready, setReady] = useState(false);
79
+ const [orb, setOrb] = useState<OrbState>("idle");
80
+ const [statusLabel, setStatusLabel] = useState<string | null>(null);
81
+ const [callStatus, setCallStatus] = useState("");
82
+ const [connected, setConnected] = useState(false);
83
+ const [muted, setMuted] = useState(false);
84
+ const [appearance, setAppearance] = useState<AppearanceConfig>(
85
+ client.getAppearance(),
86
+ );
87
+ const [audio, setAudio] = useState<AudioStateSnapshot | null>(null);
88
+ const [transcript, setTranscript] = useState<TranscriptItem[]>([]);
89
+ const [activeForm, setActiveForm] = useState<ActiveForm | null>(null);
90
+ const [formErrors, setFormErrors] = useState<FieldValidationError[]>([]);
91
+ const [formSubmitting, setFormSubmitting] = useState(false);
92
+
93
+ useEffect(() => {
94
+ const offs = [
95
+ client.on("appearance", setAppearance),
96
+ client.on("state", ({ orb: o, statusLabel: s }) => {
97
+ setOrb(o);
98
+ setStatusLabel(o === "thinking" ? s : null);
99
+ }),
100
+ client.on("call:status", ({ status }) => setCallStatus(status)),
101
+ client.on("connection", ({ phase }) => {
102
+ setConnected(phase === "connected" || phase === "connecting");
103
+ if (phase === "disconnected" || phase === "failed") setConnected(false);
104
+ }),
105
+ client.on("mute", ({ muted: m }) => setMuted(m)),
106
+ client.on("audio", (a) => setAudio(a)),
107
+ client.on("transcript:clear", () => setTranscript([])),
108
+ client.on("transcript", ({ role, segmentId, text, isFinal }) => {
109
+ const key = `${role}:${segmentId}`;
110
+ setTranscript((prev) => {
111
+ const idx = prev.findIndex((t) => t.key === key);
112
+ const item: TranscriptItem = { key, role, text, isFinal };
113
+ if (idx === -1) return [...prev, item];
114
+ const next = prev.slice();
115
+ next[idx] = item;
116
+ return next;
117
+ });
118
+ }),
119
+ client.on("form:show", () => {
120
+ setFormErrors([]);
121
+ setActiveForm(client.getActiveForm());
122
+ }),
123
+ client.on("form:update", () => setActiveForm(client.getActiveForm())),
124
+ client.on("form:validation", ({ errors }) => setFormErrors(errors)),
125
+ client.on("form:submitting", () => {
126
+ setFormSubmitting(true);
127
+ setFormErrors([]);
128
+ }),
129
+ client.on("form:submitted", () => setFormSubmitting(false)),
130
+ client.on("form:error", () => setFormSubmitting(false)),
131
+ client.on("form:close", () => {
132
+ setActiveForm(null);
133
+ setFormErrors([]);
134
+ setFormSubmitting(false);
135
+ }),
136
+ ];
137
+
138
+ let cancelled = false;
139
+ void client.init().then(() => {
140
+ if (!cancelled) setReady(true);
141
+ });
142
+
143
+ return () => {
144
+ cancelled = true;
145
+ for (const off of offs) off();
146
+ client.destroy();
147
+ };
148
+ }, [client]);
149
+
150
+ return {
151
+ client,
152
+ ready,
153
+ orb,
154
+ statusLabel,
155
+ callStatus,
156
+ connected,
157
+ muted,
158
+ appearance,
159
+ audio,
160
+ transcript,
161
+ activeForm,
162
+ formErrors,
163
+ formSubmitting,
164
+ start: client.start,
165
+ end: client.end,
166
+ toggleMute: client.toggleMute,
167
+ sendText: client.sendText,
168
+ updateFormValues: client.updateFormValues,
169
+ submitForm: client.submitForm,
170
+ stepForm: client.stepForm,
171
+ closeForm: client.closeForm,
172
+ };
173
+ }
174
+
175
+ export interface VoiceWidgetProps {
176
+ config: VoiceAgentConfig;
177
+ /** Forwarded to mountVoiceUI. */
178
+ inline?: boolean;
179
+ openChat?: boolean;
180
+ closeButtonHide?: boolean;
181
+ className?: string;
182
+ style?: Record<string, string | number>;
183
+ }
184
+
185
+ /**
186
+ * Drop-in prebuilt widget for React apps. Creates a client, mounts the prebuilt
187
+ * UI into a container div, and tears both down on unmount.
188
+ */
189
+ export function VoiceWidget(props: VoiceWidgetProps) {
190
+ const hostRef = useRef<HTMLDivElement | null>(null);
191
+
192
+ useEffect(() => {
193
+ const host = hostRef.current;
194
+ if (!host) return;
195
+ let destroyed = false;
196
+ let cleanup: (() => void) | null = null;
197
+
198
+ const client = createVoiceAgent(props.config);
199
+ void client.init().then(() => {
200
+ if (destroyed) {
201
+ client.destroy();
202
+ return;
203
+ }
204
+ // Dynamically import the UI layer so a headless-only React app that never
205
+ // renders <VoiceWidget> doesn't pull the DOM UI into its bundle.
206
+ void import("../ui").then(({ mountVoiceUI }) => {
207
+ if (destroyed) {
208
+ client.destroy();
209
+ return;
210
+ }
211
+ const ui = mountVoiceUI(client, {
212
+ target: host,
213
+ inline: props.inline ?? true,
214
+ openChat: props.openChat,
215
+ closeButtonHide: props.closeButtonHide,
216
+ });
217
+ cleanup = () => {
218
+ ui.destroy();
219
+ client.destroy();
220
+ };
221
+ });
222
+ });
223
+
224
+ return () => {
225
+ destroyed = true;
226
+ if (cleanup) cleanup();
227
+ };
228
+ // eslint-disable-next-line react-hooks/exhaustive-deps
229
+ }, []);
230
+
231
+ return createElement("div", {
232
+ ref: hostRef,
233
+ className: props.className,
234
+ style: props.style,
235
+ });
236
+ }
237
+
238
+ export type { VoiceAgentConfig, VoiceAgentClient } from "../core";