@nuancelabs/js-sdk 0.14.1 → 0.33.2

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@nuancelabs/js-sdk",
3
- "version": "0.14.1",
3
+ "version": "0.33.2",
4
4
  "description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
5
- "license": "UNLICENSED",
5
+ "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/nuance-labs-ai/nuance.git",
@@ -16,10 +16,22 @@
16
16
  ".": {
17
17
  "types": "./index.d.ts",
18
18
  "import": "./index.js"
19
+ },
20
+ "./react": {
21
+ "types": "./react/index.d.ts",
22
+ "import": "./react/index.js"
19
23
  }
20
24
  },
21
25
  "sideEffects": false,
22
26
  "dependencies": {
23
27
  "livekit-client": "^2.18.4"
28
+ },
29
+ "peerDependencies": {
30
+ "react": ">=18"
31
+ },
32
+ "peerDependenciesMeta": {
33
+ "react": {
34
+ "optional": true
35
+ }
24
36
  }
25
37
  }
@@ -0,0 +1,52 @@
1
+ import type { Conversation } from '@nuancelabs/js-sdk';
2
+ import type { ConversationStateValue } from '@nuancelabs/js-sdk';
3
+ import type { CreateNuanceConversationOptions } from '@nuancelabs/js-sdk';
4
+
5
+ /** What {@link useNuanceConversation} returns. */
6
+ export declare interface NuanceConversationHook {
7
+ /** The conversation's state; `"idle"` before the first start. */
8
+ readonly state: ConversationStateValue;
9
+ /**
10
+ * Why the last start or end failed, as thrown: test it with `instanceof`
11
+ * (`ApiError`, `MediaAccessError`, ...). Null after a start succeeds.
12
+ */
13
+ readonly error: Error | null;
14
+ /**
15
+ * The latest conversation, for anything the hook does not cover; kept after
16
+ * it ends, for its id. Null before the first start.
17
+ */
18
+ readonly conversation: Conversation | null;
19
+ /** Pass to a `<video>`'s `ref`: the avatar's video plays there. */
20
+ readonly videoRef: (element: HTMLVideoElement | null) => void;
21
+ /** Pass to an `<audio autoPlay>`'s `ref`: the avatar's voice plays there. */
22
+ readonly audioRef: (element: HTMLAudioElement | null) => void;
23
+ /** Starts a conversation, ending any current one. Call it from a click. */
24
+ readonly start: () => Promise<void>;
25
+ /** Ends the current conversation. */
26
+ readonly end: () => Promise<void>;
27
+ }
28
+
29
+ /**
30
+ * One conversation at a time in a React component: its state and error, refs
31
+ * for the elements the avatar plays in, and start and end. The hook leaves the
32
+ * conversation when the session ends (so the camera and microphone turn off),
33
+ * even with `stayConnectedOnEnd`, and when the component unmounts. Each start
34
+ * uses the options of the latest render.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * const { state, error, videoRef, audioRef, start, end } =
39
+ * useNuanceConversation({ token: fetchToken });
40
+ *
41
+ * return (
42
+ * <>
43
+ * <video ref={videoRef} autoPlay playsInline />
44
+ * <audio ref={audioRef} autoPlay />
45
+ * <button onClick={start}>Start</button>
46
+ * </>
47
+ * );
48
+ * ```
49
+ */
50
+ export declare function useNuanceConversation(options: CreateNuanceConversationOptions): NuanceConversationHook;
51
+
52
+ export { }
package/react/index.js ADDED
@@ -0,0 +1,171 @@
1
+ // src/react/index.ts
2
+ import { useEffect, useState, useSyncExternalStore } from "react";
3
+
4
+ // src/react/store.ts
5
+ import {
6
+ createNuanceConversation
7
+ } from "@nuancelabs/js-sdk";
8
+ var IDLE = {
9
+ state: "idle",
10
+ error: null,
11
+ conversation: null
12
+ };
13
+ function asError(cause) {
14
+ return cause instanceof Error ? cause : new Error(String(cause));
15
+ }
16
+
17
+ class ConversationStore {
18
+ options;
19
+ create;
20
+ snapshot = IDLE;
21
+ listeners = new Set;
22
+ current = null;
23
+ unsubscribe = null;
24
+ video = null;
25
+ audio = null;
26
+ playingVideo = null;
27
+ playingAudio = null;
28
+ leaving = Promise.resolve();
29
+ constructor(options, create = createNuanceConversation) {
30
+ this.options = options;
31
+ this.create = create;
32
+ }
33
+ setOptions(options) {
34
+ this.options = options;
35
+ }
36
+ subscribe = (listener) => {
37
+ this.listeners.add(listener);
38
+ return () => this.listeners.delete(listener);
39
+ };
40
+ getSnapshot = () => this.snapshot;
41
+ setVideo = (element) => {
42
+ if (element === this.video)
43
+ return;
44
+ this.playingVideo?.detach();
45
+ this.playingVideo = null;
46
+ this.video = element;
47
+ this.attach();
48
+ };
49
+ setAudio = (element) => {
50
+ if (element === this.audio)
51
+ return;
52
+ this.playingAudio?.detach();
53
+ this.playingAudio = null;
54
+ this.audio = element;
55
+ this.attach();
56
+ };
57
+ start = async () => {
58
+ let conversation;
59
+ try {
60
+ conversation = this.create(this.options);
61
+ } catch (cause) {
62
+ this.update({ error: asError(cause) });
63
+ return;
64
+ }
65
+ const previous = this.release();
66
+ this.current = conversation;
67
+ this.unsubscribe = conversation.onChange((event) => {
68
+ if (this.current !== conversation)
69
+ return;
70
+ if (event.type === "state-changed")
71
+ this.update({ state: event.state });
72
+ if (event.type === "tracks-changed")
73
+ this.attach();
74
+ if (event.type === "session-ended") {
75
+ if (event.error) {
76
+ this.update({ error: event.error });
77
+ return;
78
+ }
79
+ this.end();
80
+ }
81
+ });
82
+ this.update({ state: conversation.state, error: null, conversation });
83
+ if (previous)
84
+ this.leave(previous).catch(() => {});
85
+ await this.leaving;
86
+ if (this.current !== conversation)
87
+ return;
88
+ try {
89
+ await conversation.connect();
90
+ } catch (cause) {
91
+ if (this.current === conversation)
92
+ this.update({ error: asError(cause) });
93
+ }
94
+ };
95
+ end = async () => {
96
+ const conversation = this.release();
97
+ if (!conversation)
98
+ return;
99
+ try {
100
+ await this.leave(conversation);
101
+ } catch (cause) {
102
+ if (this.current === null)
103
+ this.update({ error: asError(cause) });
104
+ }
105
+ if (this.current === null)
106
+ this.update({ state: conversation.state });
107
+ };
108
+ leave(conversation) {
109
+ const done = conversation.disconnect();
110
+ this.leaving = Promise.all([this.leaving, done.catch(() => {})]);
111
+ return done;
112
+ }
113
+ release() {
114
+ const conversation = this.current;
115
+ this.unsubscribe?.();
116
+ this.unsubscribe = null;
117
+ this.current = null;
118
+ this.playingVideo?.detach();
119
+ this.playingAudio?.detach();
120
+ this.playingVideo = null;
121
+ this.playingAudio = null;
122
+ return conversation;
123
+ }
124
+ attach() {
125
+ const conversation = this.current;
126
+ if (!conversation)
127
+ return;
128
+ let avatar;
129
+ try {
130
+ avatar = conversation.avatar;
131
+ } catch {
132
+ return;
133
+ }
134
+ const { videoTrack, audioTrack } = avatar;
135
+ if (this.video && videoTrack && videoTrack !== this.playingVideo) {
136
+ this.playingVideo?.detach();
137
+ videoTrack.attach(this.video);
138
+ this.playingVideo = videoTrack;
139
+ }
140
+ if (this.audio && audioTrack && audioTrack !== this.playingAudio) {
141
+ this.playingAudio?.detach();
142
+ audioTrack.attach(this.audio);
143
+ this.playingAudio = audioTrack;
144
+ }
145
+ }
146
+ update(change) {
147
+ this.snapshot = { ...this.snapshot, ...change };
148
+ for (const listener of this.listeners)
149
+ listener();
150
+ }
151
+ }
152
+
153
+ // src/react/index.ts
154
+ function useNuanceConversation(options) {
155
+ const [store] = useState(() => new ConversationStore(options));
156
+ useEffect(() => store.setOptions(options));
157
+ const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
158
+ useEffect(() => () => {
159
+ store.end();
160
+ }, [store]);
161
+ return {
162
+ ...snapshot,
163
+ videoRef: store.setVideo,
164
+ audioRef: store.setAudio,
165
+ start: store.start,
166
+ end: store.end
167
+ };
168
+ }
169
+ export {
170
+ useNuanceConversation
171
+ };