@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.
- package/README.md +198 -0
- package/dist/appearance-CNWT8x1G.cjs +2 -0
- package/dist/appearance-CNWT8x1G.cjs.map +1 -0
- package/dist/appearance-i6QBkpCk.js +650 -0
- package/dist/appearance-i6QBkpCk.js.map +1 -0
- package/dist/consent-CK9VXNPa.js +54 -0
- package/dist/consent-CK9VXNPa.js.map +1 -0
- package/dist/consent-D7QNSkQD.cjs +2 -0
- package/dist/consent-D7QNSkQD.cjs.map +1 -0
- package/dist/core/analytics.d.ts +30 -0
- package/dist/core/appearance.d.ts +113 -0
- package/dist/core/audioSettings.d.ts +69 -0
- package/dist/core/consent.d.ts +17 -0
- package/dist/core/createVoiceAgent.d.ts +79 -0
- package/dist/core/events.d.ts +103 -0
- package/dist/core/formController.d.ts +28 -0
- package/dist/core/forms.d.ts +235 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/prevContext.d.ts +26 -0
- package/dist/core/transport.d.ts +30 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/voice.d.ts +79 -0
- package/dist/createVoiceAgent-BM3HODS6.js +1058 -0
- package/dist/createVoiceAgent-BM3HODS6.js.map +1 -0
- package/dist/createVoiceAgent-CJWxWzz6.cjs +4 -0
- package/dist/createVoiceAgent-CJWxWzz6.cjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +60 -0
- package/dist/react.cjs +2 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.js +115 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +1838 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/ui.d.ts +165 -0
- package/dist/ui.cjs +284 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.js +1153 -0
- package/dist/ui.js.map +1 -0
- package/package.json +67 -0
- package/src/core/analytics.ts +111 -0
- package/src/core/appearance.ts +464 -0
- package/src/core/audioSettings.ts +180 -0
- package/src/core/consent.ts +78 -0
- package/src/core/createVoiceAgent.ts +280 -0
- package/src/core/events.ts +120 -0
- package/src/core/formController.ts +317 -0
- package/src/core/forms.ts +861 -0
- package/src/core/index.ts +121 -0
- package/src/core/prevContext.ts +153 -0
- package/src/core/transport.ts +118 -0
- package/src/core/types.ts +66 -0
- package/src/core/voice.ts +1179 -0
- package/src/react/index.ts +238 -0
- package/src/ui/index.ts +507 -0
- package/src/ui/styles.css +1838 -0
- package/src/ui/ui.ts +1672 -0
- 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";
|