@live-assistant/react-native 0.3.0 → 0.3.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/README.md +6 -2
- package/dist/live-assistant.d.ts +8 -0
- package/dist/live-assistant.d.ts.map +1 -1
- package/dist/live-assistant.js +31 -7
- package/dist/live-assistant.js.map +1 -1
- package/package.json +6 -6
- package/src/live-assistant.tsx +31 -8
package/README.md
CHANGED
|
@@ -15,8 +15,10 @@ same way in every app — so they are not yours to write. The one value it canno
|
|
|
15
15
|
invent is the route on **your** server that mints a short-lived token, because
|
|
16
16
|
your API key must never ship in an app bundle.
|
|
17
17
|
|
|
18
|
-
This page is the whole of it:
|
|
19
|
-
|
|
18
|
+
This page is the whole of it: five steps, then a table for every value you can
|
|
19
|
+
pass. **Building for the web only?** Steps 1 and 2 are the native setup — there,
|
|
20
|
+
the install is the whole of it and nothing needs rebuilding; skip to
|
|
21
|
+
[step 3](#3-mint-tokens-on-your-server). If you would rather read code than prose,
|
|
20
22
|
[`examples/expo-app`](https://github.com/Recipely-Team/live-assistant/tree/main/examples/expo-app)
|
|
21
23
|
is a working app in one file, and CI builds it on every change.
|
|
22
24
|
|
|
@@ -39,6 +41,8 @@ On the web nothing native is needed, but `getUserMedia` only exists in a
|
|
|
39
41
|
|
|
40
42
|
## 2. Configure the microphone — one line
|
|
41
43
|
|
|
44
|
+
*Native only. A web build needs nothing from this step.*
|
|
45
|
+
|
|
42
46
|
```json
|
|
43
47
|
{
|
|
44
48
|
"expo": {
|
package/dist/live-assistant.d.ts
CHANGED
|
@@ -49,6 +49,14 @@ export type LiveAssistantProps = LiveAssistantEndpointProps | LiveAssistantConne
|
|
|
49
49
|
* - **The controller is built once**, in a `useState` initialiser. It owns a
|
|
50
50
|
* socket and two devices; rebuilding it on a render would start a session per
|
|
51
51
|
* render. The session is stopped when this unmounts.
|
|
52
|
+
* - **Handlers are held, not depended on.** `onReady` and `onFailure` are read
|
|
53
|
+
* through a ref, so passing them inline — which is how anyone passes them —
|
|
54
|
+
* cannot re-run the lifecycle effect. It once could, and its cleanup hung up
|
|
55
|
+
* on a live session whenever a parent re-rendered.
|
|
56
|
+
* - **`tools`, `page` and `timing` are read once**, when the assistant is
|
|
57
|
+
* created: they configure a controller that outlives any one render. Register
|
|
58
|
+
* a tool later through the controller `onReady` hands you. `theme`, `strings`,
|
|
59
|
+
* `headers` and `language` are live.
|
|
52
60
|
* - **`headers` and `language` are read at connection time, not at mount.** An
|
|
53
61
|
* app whose `Authorization` header is refreshed mid-session would otherwise
|
|
54
62
|
* reconnect with the token it had when the component first rendered — which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-assistant.d.ts","sourceRoot":"","sources":["../src/live-assistant.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,KAAK,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG1H,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE,mEAAmE;AACnE,UAAU,wBAAyB,SAAQ,oBAAoB;IAC7D,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oHAAoH;IACpH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,YAAY,CAAC;IACzD,oFAAoF;IACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,0BAA0B,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9E,wGAAwG;IACxG,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,CAAC,qBAAqB,CAAC,KAAK,IAAI,CAAC;IACpF,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC1D;AAED,wCAAwC;AACxC,UAAU,0BAA2B,SAAQ,wBAAwB;IACnE,sGAAsG;IACtG,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;CAChC;AAED,mFAAmF;AACnF,UAAU,4BAA6B,SAAQ,wBAAwB;IACrE,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5G,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;CAChC;AAED,MAAM,MAAM,kBAAkB,GAAG,0BAA0B,GAAG,4BAA4B,CAAC;AAK3F
|
|
1
|
+
{"version":3,"file":"live-assistant.d.ts","sourceRoot":"","sources":["../src/live-assistant.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzE,OAAO,KAAK,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG1H,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAGpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAEnE,mEAAmE;AACnE,UAAU,wBAAyB,SAAQ,oBAAoB;IAC7D,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,oHAAoH;IACpH,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,YAAY,CAAC;IACzD,oFAAoF;IACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,0BAA0B,CAAC,qBAAqB,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9E,wGAAwG;IACxG,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,CAAC,qBAAqB,CAAC,KAAK,IAAI,CAAC;IACpF,+FAA+F;IAC/F,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC1D;AAED,wCAAwC;AACxC,UAAU,0BAA2B,SAAQ,wBAAwB;IACnE,sGAAsG;IACtG,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;CAChC;AAED,mFAAmF;AACnF,UAAU,4BAA6B,SAAQ,wBAAwB;IACrE,QAAQ,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5G,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;CAChC;AAED,MAAM,MAAM,kBAAkB,GAAG,0BAA0B,GAAG,4BAA4B,CAAC;AAK3F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,aAAa,EACb,QAA2B,EAC3B,OAAO,EACP,KAAK,EACL,IAAI,EACJ,MAAM,EACN,OAAO,EACP,SAAS,EACT,GAAG,MAAM,EACV,EAAE,kBAAkB,+BA8CpB"}
|
package/dist/live-assistant.js
CHANGED
|
@@ -27,6 +27,14 @@ const JSON_TYPE = 'application/json';
|
|
|
27
27
|
* - **The controller is built once**, in a `useState` initialiser. It owns a
|
|
28
28
|
* socket and two devices; rebuilding it on a render would start a session per
|
|
29
29
|
* render. The session is stopped when this unmounts.
|
|
30
|
+
* - **Handlers are held, not depended on.** `onReady` and `onFailure` are read
|
|
31
|
+
* through a ref, so passing them inline — which is how anyone passes them —
|
|
32
|
+
* cannot re-run the lifecycle effect. It once could, and its cleanup hung up
|
|
33
|
+
* on a live session whenever a parent re-rendered.
|
|
34
|
+
* - **`tools`, `page` and `timing` are read once**, when the assistant is
|
|
35
|
+
* created: they configure a controller that outlives any one render. Register
|
|
36
|
+
* a tool later through the controller `onReady` hands you. `theme`, `strings`,
|
|
37
|
+
* `headers` and `language` are live.
|
|
30
38
|
* - **`headers` and `language` are read at connection time, not at mount.** An
|
|
31
39
|
* app whose `Authorization` header is refreshed mid-session would otherwise
|
|
32
40
|
* reconnect with the token it had when the component first rendered — which
|
|
@@ -42,8 +50,8 @@ const JSON_TYPE = 'application/json';
|
|
|
42
50
|
*/
|
|
43
51
|
function LiveAssistant({ tokenEndpoint, getConnection, language = DEFAULT_LANGUAGE, headers, tools, page, timing, onReady, onFailure, ...widget }) {
|
|
44
52
|
// Read at connection time rather than captured at mount: see the doc block.
|
|
45
|
-
const latest = (0, react_1.useRef)({ tokenEndpoint, getConnection, language, headers });
|
|
46
|
-
latest.current = { tokenEndpoint, getConnection, language, headers };
|
|
53
|
+
const latest = (0, react_1.useRef)({ tokenEndpoint, getConnection, language, headers, onReady });
|
|
54
|
+
latest.current = { tokenEndpoint, getConnection, language, headers, onReady };
|
|
47
55
|
const [controller] = (0, react_1.useState)(() => new core_1.AssistantController({
|
|
48
56
|
session: new gemini_1.GeminiLiveSession(),
|
|
49
57
|
microphone: new audio_1.Microphone(),
|
|
@@ -55,6 +63,10 @@ function LiveAssistant({ tokenEndpoint, getConnection, language = DEFAULT_LANGUA
|
|
|
55
63
|
const current = latest.current;
|
|
56
64
|
if (current.getConnection !== undefined)
|
|
57
65
|
return current.getConnection({ resumptionHandle });
|
|
66
|
+
// Types make this unreachable; JavaScript does not, and `fetch(undefined)`
|
|
67
|
+
// fails as a network error nobody can place.
|
|
68
|
+
if (current.tokenEndpoint === undefined)
|
|
69
|
+
throw new Error('LiveAssistant needs tokenEndpoint or getConnection');
|
|
58
70
|
const response = await fetch(current.tokenEndpoint, {
|
|
59
71
|
method: 'POST',
|
|
60
72
|
headers: { 'content-type': JSON_TYPE, ...current.headers },
|
|
@@ -65,20 +77,32 @@ function LiveAssistant({ tokenEndpoint, getConnection, language = DEFAULT_LANGUA
|
|
|
65
77
|
return (await response.json());
|
|
66
78
|
},
|
|
67
79
|
}));
|
|
80
|
+
// Only the controller is a dependency. `onReady` used to be one too, and an
|
|
81
|
+
// inline arrow — the ordinary way to pass it — is a new function on every
|
|
82
|
+
// render, so the effect re-ran and its cleanup hung up on a live session
|
|
83
|
+
// whenever anything above this re-rendered.
|
|
68
84
|
(0, react_1.useEffect)(() => {
|
|
69
|
-
onReady?.(controller);
|
|
85
|
+
latest.current.onReady?.(controller);
|
|
70
86
|
return () => void controller.stop();
|
|
71
|
-
}, [controller
|
|
87
|
+
}, [controller]);
|
|
72
88
|
return ((0, jsx_runtime_1.jsxs)(react_2.AssistantProvider, { controller: controller, children: [(0, jsx_runtime_1.jsx)(FailureReporter, { onFailure: onFailure }), (0, jsx_runtime_1.jsx)(widget_1.AssistantWidget, { ...widget })] }));
|
|
73
89
|
}
|
|
74
90
|
const selectError = (state) => state.error;
|
|
75
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* Reports failures to the app without re-rendering the widget for them.
|
|
93
|
+
*
|
|
94
|
+
* The handler is held in a ref for the same reason the lifecycle effect takes
|
|
95
|
+
* only the controller: an inline arrow changes identity every render, and a
|
|
96
|
+
* handler in the dependencies would report the same failure again each time.
|
|
97
|
+
*/
|
|
76
98
|
function FailureReporter({ onFailure }) {
|
|
77
99
|
const error = (0, react_2.useAssistantState)(selectError);
|
|
100
|
+
const report = (0, react_1.useRef)(onFailure);
|
|
101
|
+
report.current = onFailure;
|
|
78
102
|
(0, react_1.useEffect)(() => {
|
|
79
103
|
if (error !== null)
|
|
80
|
-
|
|
81
|
-
}, [error
|
|
104
|
+
report.current?.(error);
|
|
105
|
+
}, [error]);
|
|
82
106
|
return null;
|
|
83
107
|
}
|
|
84
108
|
//# sourceMappingURL=live-assistant.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-assistant.js","sourceRoot":"","sources":["../src/live-assistant.tsx"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"live-assistant.js","sourceRoot":"","sources":["../src/live-assistant.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-assistant/react-native",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "One install for a voice assistant in a React Native or Expo app: the session, a Gemini Live connection, microphone and playback, React bindings and a ready-made widget, all re-exported from one place.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Recep Tayyip Ekşi",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@live-assistant/audio": "0.3.
|
|
45
|
-
"@live-assistant/core": "0.3.
|
|
46
|
-
"@live-assistant/gemini": "0.3.
|
|
47
|
-
"@live-assistant/react": "0.3.
|
|
48
|
-
"@live-assistant/widget": "0.3.
|
|
44
|
+
"@live-assistant/audio": "0.3.2",
|
|
45
|
+
"@live-assistant/core": "0.3.2",
|
|
46
|
+
"@live-assistant/gemini": "0.3.2",
|
|
47
|
+
"@live-assistant/react": "0.3.2",
|
|
48
|
+
"@live-assistant/widget": "0.3.2"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"react": ">=18",
|
package/src/live-assistant.tsx
CHANGED
|
@@ -60,6 +60,14 @@ const JSON_TYPE = 'application/json';
|
|
|
60
60
|
* - **The controller is built once**, in a `useState` initialiser. It owns a
|
|
61
61
|
* socket and two devices; rebuilding it on a render would start a session per
|
|
62
62
|
* render. The session is stopped when this unmounts.
|
|
63
|
+
* - **Handlers are held, not depended on.** `onReady` and `onFailure` are read
|
|
64
|
+
* through a ref, so passing them inline — which is how anyone passes them —
|
|
65
|
+
* cannot re-run the lifecycle effect. It once could, and its cleanup hung up
|
|
66
|
+
* on a live session whenever a parent re-rendered.
|
|
67
|
+
* - **`tools`, `page` and `timing` are read once**, when the assistant is
|
|
68
|
+
* created: they configure a controller that outlives any one render. Register
|
|
69
|
+
* a tool later through the controller `onReady` hands you. `theme`, `strings`,
|
|
70
|
+
* `headers` and `language` are live.
|
|
63
71
|
* - **`headers` and `language` are read at connection time, not at mount.** An
|
|
64
72
|
* app whose `Authorization` header is refreshed mid-session would otherwise
|
|
65
73
|
* reconnect with the token it had when the component first rendered — which
|
|
@@ -86,8 +94,8 @@ export function LiveAssistant({
|
|
|
86
94
|
...widget
|
|
87
95
|
}: LiveAssistantProps) {
|
|
88
96
|
// Read at connection time rather than captured at mount: see the doc block.
|
|
89
|
-
const latest = useRef({ tokenEndpoint, getConnection, language, headers });
|
|
90
|
-
latest.current = { tokenEndpoint, getConnection, language, headers };
|
|
97
|
+
const latest = useRef({ tokenEndpoint, getConnection, language, headers, onReady });
|
|
98
|
+
latest.current = { tokenEndpoint, getConnection, language, headers, onReady };
|
|
91
99
|
|
|
92
100
|
const [controller] = useState(
|
|
93
101
|
() =>
|
|
@@ -101,7 +109,10 @@ export function LiveAssistant({
|
|
|
101
109
|
getConnection: async ({ resumptionHandle }) => {
|
|
102
110
|
const current = latest.current;
|
|
103
111
|
if (current.getConnection !== undefined) return current.getConnection({ resumptionHandle });
|
|
104
|
-
|
|
112
|
+
// Types make this unreachable; JavaScript does not, and `fetch(undefined)`
|
|
113
|
+
// fails as a network error nobody can place.
|
|
114
|
+
if (current.tokenEndpoint === undefined) throw new Error('LiveAssistant needs tokenEndpoint or getConnection');
|
|
115
|
+
const response = await fetch(current.tokenEndpoint, {
|
|
105
116
|
method: 'POST',
|
|
106
117
|
headers: { 'content-type': JSON_TYPE, ...current.headers },
|
|
107
118
|
body: JSON.stringify({ resumptionHandle, languageCode: current.language }),
|
|
@@ -112,10 +123,14 @@ export function LiveAssistant({
|
|
|
112
123
|
}),
|
|
113
124
|
);
|
|
114
125
|
|
|
126
|
+
// Only the controller is a dependency. `onReady` used to be one too, and an
|
|
127
|
+
// inline arrow — the ordinary way to pass it — is a new function on every
|
|
128
|
+
// render, so the effect re-ran and its cleanup hung up on a live session
|
|
129
|
+
// whenever anything above this re-rendered.
|
|
115
130
|
useEffect(() => {
|
|
116
|
-
onReady?.(controller);
|
|
131
|
+
latest.current.onReady?.(controller);
|
|
117
132
|
return () => void controller.stop();
|
|
118
|
-
}, [controller
|
|
133
|
+
}, [controller]);
|
|
119
134
|
|
|
120
135
|
return (
|
|
121
136
|
<AssistantProvider controller={controller}>
|
|
@@ -127,11 +142,19 @@ export function LiveAssistant({
|
|
|
127
142
|
|
|
128
143
|
const selectError = (state: { readonly error: AssistantFailure | null }): AssistantFailure | null => state.error;
|
|
129
144
|
|
|
130
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Reports failures to the app without re-rendering the widget for them.
|
|
147
|
+
*
|
|
148
|
+
* The handler is held in a ref for the same reason the lifecycle effect takes
|
|
149
|
+
* only the controller: an inline arrow changes identity every render, and a
|
|
150
|
+
* handler in the dependencies would report the same failure again each time.
|
|
151
|
+
*/
|
|
131
152
|
function FailureReporter({ onFailure }: { readonly onFailure?: (failure: AssistantFailure) => void }) {
|
|
132
153
|
const error = useAssistantState(selectError);
|
|
154
|
+
const report = useRef(onFailure);
|
|
155
|
+
report.current = onFailure;
|
|
133
156
|
useEffect(() => {
|
|
134
|
-
if (error !== null)
|
|
135
|
-
}, [error
|
|
157
|
+
if (error !== null) report.current?.(error);
|
|
158
|
+
}, [error]);
|
|
136
159
|
return null;
|
|
137
160
|
}
|