@nuancelabs/js-sdk 0.14.1 → 0.15.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/index.d.ts CHANGED
@@ -739,8 +739,8 @@ export declare interface SessionPersonality {
739
739
 
740
740
  /**
741
741
  * What your server sends to `POST /session-tokens` with its API key. Every
742
- * field is fixed for the conversation the token starts; send `{}` for the
743
- * defaults.
742
+ * field is fixed for the conversation the token starts. Send `end_user_id`
743
+ * with every token; leave the other fields out for the defaults.
744
744
  */
745
745
  export declare interface SessionTokenRequest {
746
746
  /** Who the avatar is in the conversation. Leave it out for the default personality. */
package/index.js CHANGED
@@ -4,7 +4,7 @@ function isRecord(value) {
4
4
  }
5
5
 
6
6
  // src/core/sdk-version.ts
7
- var SDK_HEADER = `js/${"0.14.1"}`;
7
+ var SDK_HEADER = `js/${"0.15.0"}`;
8
8
 
9
9
  // src/utils.ts
10
10
  function normalizeHost(host) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuancelabs/js-sdk",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -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
+ * and when the component unmounts. Each start uses the options of the latest
34
+ * 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,166 @@
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
+ this.end();
76
+ });
77
+ this.update({ state: conversation.state, error: null, conversation });
78
+ if (previous)
79
+ this.leave(previous).catch(() => {});
80
+ await this.leaving;
81
+ if (this.current !== conversation)
82
+ return;
83
+ try {
84
+ await conversation.connect();
85
+ } catch (cause) {
86
+ if (this.current === conversation)
87
+ this.update({ error: asError(cause) });
88
+ }
89
+ };
90
+ end = async () => {
91
+ const conversation = this.release();
92
+ if (!conversation)
93
+ return;
94
+ try {
95
+ await this.leave(conversation);
96
+ } catch (cause) {
97
+ if (this.current === null)
98
+ this.update({ error: asError(cause) });
99
+ }
100
+ if (this.current === null)
101
+ this.update({ state: conversation.state });
102
+ };
103
+ leave(conversation) {
104
+ const done = conversation.disconnect();
105
+ this.leaving = Promise.all([this.leaving, done.catch(() => {})]);
106
+ return done;
107
+ }
108
+ release() {
109
+ const conversation = this.current;
110
+ this.unsubscribe?.();
111
+ this.unsubscribe = null;
112
+ this.current = null;
113
+ this.playingVideo?.detach();
114
+ this.playingAudio?.detach();
115
+ this.playingVideo = null;
116
+ this.playingAudio = null;
117
+ return conversation;
118
+ }
119
+ attach() {
120
+ const conversation = this.current;
121
+ if (!conversation)
122
+ return;
123
+ let avatar;
124
+ try {
125
+ avatar = conversation.avatar;
126
+ } catch {
127
+ return;
128
+ }
129
+ const { videoTrack, audioTrack } = avatar;
130
+ if (this.video && videoTrack && videoTrack !== this.playingVideo) {
131
+ this.playingVideo?.detach();
132
+ videoTrack.attach(this.video);
133
+ this.playingVideo = videoTrack;
134
+ }
135
+ if (this.audio && audioTrack && audioTrack !== this.playingAudio) {
136
+ this.playingAudio?.detach();
137
+ audioTrack.attach(this.audio);
138
+ this.playingAudio = audioTrack;
139
+ }
140
+ }
141
+ update(change) {
142
+ this.snapshot = { ...this.snapshot, ...change };
143
+ for (const listener of this.listeners)
144
+ listener();
145
+ }
146
+ }
147
+
148
+ // src/react/index.ts
149
+ function useNuanceConversation(options) {
150
+ const [store] = useState(() => new ConversationStore(options));
151
+ useEffect(() => store.setOptions(options));
152
+ const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
153
+ useEffect(() => () => {
154
+ store.end();
155
+ }, [store]);
156
+ return {
157
+ ...snapshot,
158
+ videoRef: store.setVideo,
159
+ audioRef: store.setAudio,
160
+ start: store.start,
161
+ end: store.end
162
+ };
163
+ }
164
+ export {
165
+ useNuanceConversation
166
+ };