@graineai/inapp-react-native 0.11.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/INTEGRATION.md +131 -0
- package/dist/audio.js +2 -1
- package/dist/client.d.ts +7 -1
- package/dist/client.js +9 -2
- package/dist/index.d.ts +10 -9
- package/dist/index.js +9 -8
- package/dist/react-native/index.d.ts +4 -4
- package/dist/react-native/index.js +3 -3
- package/dist/react-native/ui.d.ts +12 -1
- package/dist/react-native/ui.js +53 -26
- package/dist/react-native/voice-launcher.js +25 -5
- package/dist/react-native/voice-rtc.d.ts +41 -0
- package/dist/react-native/voice-rtc.js +143 -0
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +1 -1
- package/package.json +9 -2
package/INTEGRATION.md
CHANGED
|
@@ -135,6 +135,34 @@ agent just does not know their name.
|
|
|
135
135
|
|
|
136
136
|
---
|
|
137
137
|
|
|
138
|
+
### Metadata before the first word
|
|
139
|
+
|
|
140
|
+
Two places, and the difference is when you know the value.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Known at build time — carried in the init frame, before the agent speaks.
|
|
144
|
+
<GraineProvider variables={{ tier: 'gold', region: 'IN' }} />
|
|
145
|
+
|
|
146
|
+
// Known after sign-in — merged into that same init frame if it lands before the
|
|
147
|
+
// socket opens, sent on the next context frame if it lands after.
|
|
148
|
+
identify({ name: user.name, plan: user.plan });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`identify()` wins over `variables` on the same key: a signed-in customer is more
|
|
152
|
+
specific than a default. Both are masked on the device before they leave it.
|
|
153
|
+
|
|
154
|
+
### It reaches the call record, not only the prompt
|
|
155
|
+
|
|
156
|
+
Traits are stored on the conversation as `app_user`, and the name labels the row
|
|
157
|
+
in Call History. A web call has no phone number to identify it by, so without
|
|
158
|
+
this every in-app conversation shows a session id.
|
|
159
|
+
|
|
160
|
+
Read from the LIVE identity rather than the init frame, so a customer who signs
|
|
161
|
+
in halfway through is identified from that moment rather than from a frame that
|
|
162
|
+
predates them.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
138
166
|
## Step 3 — Report what is ON the screen
|
|
139
167
|
|
|
140
168
|
The route gives the agent a screen *name*. This gives it the contents.
|
|
@@ -218,6 +246,23 @@ updating the screen.
|
|
|
218
246
|
|
|
219
247
|
---
|
|
220
248
|
|
|
249
|
+
### Declaring them: paste, do not retype
|
|
250
|
+
|
|
251
|
+
Keep the list in your repo — `docs/agent-actions.json` is the convention — and
|
|
252
|
+
load it with **Embed & Widgets → App actions → Import from JSON**. A bare array
|
|
253
|
+
works, so does `{ "appActions": [...] }`.
|
|
254
|
+
|
|
255
|
+
Names must match your `useGraineAction` handlers exactly, and that is the whole
|
|
256
|
+
argument for importing: a name one underscore out declares a tool your app will
|
|
257
|
+
refuse for the life of the release, and the agent keeps trying it. `enum`
|
|
258
|
+
constraints are preserved, so a value the model cannot get wrong stays that way.
|
|
259
|
+
|
|
260
|
+
Import replaces the panel's contents rather than merging — the file is the
|
|
261
|
+
source of truth, and a merge leaves actions declared here and implemented
|
|
262
|
+
nowhere. Save is what pushes the catalogue to the agent.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
221
266
|
## Step 5 — Product events, without interrupting
|
|
222
267
|
|
|
223
268
|
```tsx
|
|
@@ -235,6 +280,30 @@ and the runtime decides whether it is worth interrupting for.
|
|
|
235
280
|
|
|
236
281
|
---
|
|
237
282
|
|
|
283
|
+
### When something should be spoken about
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
const { client } = useGraineAgent();
|
|
287
|
+
client.reportEvent({
|
|
288
|
+
name: 'mandate_failed',
|
|
289
|
+
detail: 'Their mandate was declined twice. Offer to switch to a card.',
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`track()` is "know this when they next ask". `reportEvent()` is "this may be
|
|
294
|
+
worth interrupting for". `detail` tells the agent what to OFFER — without it the
|
|
295
|
+
agent reads an event name aloud, which is worse than saying nothing.
|
|
296
|
+
|
|
297
|
+
Reserve it for a dead end the customer would want addressed without asking: a
|
|
298
|
+
save that never reached the server, an upload that failed twice, a payment
|
|
299
|
+
declined. Not a tap, not a navigation, not a save that worked.
|
|
300
|
+
|
|
301
|
+
The runtime still decides whether to speak: silent while it is talking, while a
|
|
302
|
+
reply is generating, for 20s after the last intervention, and after three in a
|
|
303
|
+
session.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
238
307
|
## Step 6 — React to the conversation
|
|
239
308
|
|
|
240
309
|
```tsx
|
|
@@ -318,6 +387,62 @@ part. Everything else is unchanged.
|
|
|
318
387
|
|
|
319
388
|
---
|
|
320
389
|
|
|
390
|
+
## Making the bar yours
|
|
391
|
+
|
|
392
|
+
Three levels, and most apps stop at the first.
|
|
393
|
+
|
|
394
|
+
**1. The dashboard.** Accent, title and logo come from Appearance on the agent
|
|
395
|
+
and reach every surface without an app release. Set the colour in Graine and it
|
|
396
|
+
changes here. This is the level a brand team should be working at.
|
|
397
|
+
|
|
398
|
+
**2. Props, when the dashboard cannot express it.** Any prop passed wins over
|
|
399
|
+
the dashboard, so an app that needs something per-build still can.
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
<GraineAgentBar
|
|
403
|
+
scheme="auto" // "dark" | "light" | "auto" (default) — follows the device
|
|
404
|
+
accent="#318CE7" // overrides the dashboard
|
|
405
|
+
name="Ring"
|
|
406
|
+
bottomInset={110} // clear of your tab bar
|
|
407
|
+
captionsOn
|
|
408
|
+
/>
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`scheme` defaults to `auto` rather than dark: an agent that ignores the system
|
|
412
|
+
setting is the one piece of the screen that looks like it came from somewhere
|
|
413
|
+
else. Pin it only if your app pins its own.
|
|
414
|
+
|
|
415
|
+
**3. The palette, when your brand is not one of the two.** `theme` is merged
|
|
416
|
+
over the scheme's colours, so pass the one you care about:
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
import type { GraineBarTheme } from "@graineai/inapp-react-native";
|
|
420
|
+
|
|
421
|
+
<GraineAgentBar theme={{ surface: "#0E1116", border: "#232A33" }} />
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
| Key | Where it lands |
|
|
425
|
+
|---|---|
|
|
426
|
+
| `surface` | The bar, the expanded panel, the caption bubble |
|
|
427
|
+
| `border` | Hairline around all three |
|
|
428
|
+
| `text` | The agent's name, its replies, what the customer types |
|
|
429
|
+
| `sub` | Status line, placeholder, empty state |
|
|
430
|
+
| `chip` | Quiet round buttons, and the agent's message bubbles |
|
|
431
|
+
| `onAccent` | Text and glyphs sitting on the accent, and on `danger` |
|
|
432
|
+
| `danger` | Muted — the one state worth its own colour |
|
|
433
|
+
|
|
434
|
+
**4. Or draw your own.** Everything the bar does is built on the public hooks,
|
|
435
|
+
so an app with a design system should ignore the component entirely:
|
|
436
|
+
|
|
437
|
+
```tsx
|
|
438
|
+
const { connected, muted, setMuted, messages, send, caption } = useGraineAgent();
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
That is the expected path for anyone with a brand team, and nothing else in the
|
|
442
|
+
SDK depends on this component.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
321
446
|
## Session lifecycle
|
|
322
447
|
|
|
323
448
|
One session per client, and it follows the app.
|
|
@@ -350,6 +475,12 @@ than opening a second socket. This matters under React 18 strict mode, which
|
|
|
350
475
|
mounts effects twice: previously the first socket was orphaned with no reference
|
|
351
476
|
to close it, and the runtime kept it open and billing until its own timeout.
|
|
352
477
|
|
|
478
|
+
**Nothing is left holding the call.** An outstanding action's waiter is released
|
|
479
|
+
in a `finally`, so a customer closing the app mid-action releases it too, and
|
|
480
|
+
every wait has a deadline — an app that never answers cannot hold the turn open
|
|
481
|
+
against someone who has already gone. A timeout is reported rather than
|
|
482
|
+
swallowed, because silence would have the agent claim the change landed.
|
|
483
|
+
|
|
353
484
|
**`close()` sends a stop frame** before dropping the socket, so the conversation
|
|
354
485
|
is filed as ended rather than as a customer who vanished mid-turn. It is called
|
|
355
486
|
for you on unmount and on backgrounding.
|
package/dist/audio.js
CHANGED
|
@@ -60,7 +60,8 @@ export function decodeAgentAudio(bytes, declared, latched, fallbackPcmRate = 160
|
|
|
60
60
|
const pcm16 = muLawWins ? asMuLaw : asPcm;
|
|
61
61
|
const kind = muLawWins ? "mulaw" : "pcm";
|
|
62
62
|
const sampleRate = muLawWins ? 8000 : declared?.sampleRate || fallbackPcmRate;
|
|
63
|
-
const confident = rms(
|
|
63
|
+
const confident = rms(asPcm) >= CONFIDENT_RMS &&
|
|
64
|
+
zeroCrossingRate(asMuLaw) !== zeroCrossingRate(asPcm);
|
|
64
65
|
return { pcm16, sampleRate, kind, latch: confident ? { kind, sampleRate } : null };
|
|
65
66
|
}
|
|
66
67
|
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext } from "./protocol";
|
|
1
|
+
import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext } from "./protocol.js";
|
|
2
2
|
export declare const SUBPROTOCOL = "graine.embed.v1";
|
|
3
3
|
export interface GraineInAppOptions {
|
|
4
4
|
baseUrl: string;
|
|
@@ -54,6 +54,12 @@ export declare class GraineInAppClient {
|
|
|
54
54
|
private runAction;
|
|
55
55
|
private invokeAction;
|
|
56
56
|
getScreen(): ScreenContext | null;
|
|
57
|
+
getIdentity(): Record<string, string>;
|
|
58
|
+
getRecentEvents(): Array<{
|
|
59
|
+
name: string;
|
|
60
|
+
at: number;
|
|
61
|
+
data?: Record<string, unknown>;
|
|
62
|
+
}>;
|
|
57
63
|
getAvailableActions(): string[];
|
|
58
64
|
registerAction(name: string, handler: ActionHandler): () => void;
|
|
59
65
|
get availableActions(): string[];
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ACTION_DEADLINE_MS, CONTEXT_THROTTLE_MS, } from "./protocol";
|
|
2
|
-
import { maskDeep } from "./mask";
|
|
1
|
+
import { ACTION_DEADLINE_MS, CONTEXT_THROTTLE_MS, } from "./protocol.js";
|
|
2
|
+
import { maskDeep } from "./mask.js";
|
|
3
3
|
export const SUBPROTOCOL = "graine.embed.v1";
|
|
4
4
|
const PING_MS = 25000;
|
|
5
5
|
export class GraineInAppClient {
|
|
@@ -262,6 +262,12 @@ export class GraineInAppClient {
|
|
|
262
262
|
getScreen() {
|
|
263
263
|
return this.screen;
|
|
264
264
|
}
|
|
265
|
+
getIdentity() {
|
|
266
|
+
return { ...this.identity };
|
|
267
|
+
}
|
|
268
|
+
getRecentEvents() {
|
|
269
|
+
return [...this.recentEvents];
|
|
270
|
+
}
|
|
265
271
|
getAvailableActions() {
|
|
266
272
|
return [...this.actions.keys()];
|
|
267
273
|
}
|
|
@@ -299,6 +305,7 @@ export class GraineInAppClient {
|
|
|
299
305
|
this.scheduleScreen();
|
|
300
306
|
}
|
|
301
307
|
scheduleScreen() {
|
|
308
|
+
this.emit("screen", { screen: this.screen, actions: this.getAvailableActions() });
|
|
302
309
|
if (this.contextTimer) {
|
|
303
310
|
this.pendingScreen = true;
|
|
304
311
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol";
|
|
2
|
-
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig } from "./client";
|
|
3
|
-
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice";
|
|
4
|
-
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio";
|
|
5
|
-
export { addMaskRule, maskDeep, maskString } from "./mask";
|
|
6
|
-
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index";
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
1
|
+
export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol.js";
|
|
2
|
+
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig } from "./client.js";
|
|
3
|
+
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
|
|
4
|
+
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
|
|
5
|
+
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
6
|
+
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
7
|
+
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
8
|
+
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
|
9
|
+
export { GraineVoiceLauncher, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
|
10
|
+
export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { GraineInAppClient } from "./client";
|
|
2
|
-
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice";
|
|
3
|
-
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio";
|
|
4
|
-
export { addMaskRule, maskDeep, maskString } from "./mask";
|
|
5
|
-
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, } from "./react-native/index";
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
1
|
+
export { GraineInAppClient } from "./client.js";
|
|
2
|
+
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
|
|
3
|
+
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
|
|
4
|
+
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
5
|
+
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, } from "./react-native/index.js";
|
|
6
|
+
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
7
|
+
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
8
|
+
export { GraineVoiceLauncher, } from "./react-native/voice-launcher.js";
|
|
9
|
+
export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { GraineInAppClient, type GraineInAppOptions } from "../client";
|
|
3
|
-
import { type AudioAdapter, type VoiceSessionOptions } from "../voice";
|
|
4
|
-
import type { ActionHandler, ScreenContext } from "../protocol";
|
|
5
|
-
import { type LauncherInset, type LauncherVisibility } from "./navigation";
|
|
2
|
+
import { GraineInAppClient, type GraineInAppOptions } from "../client.js";
|
|
3
|
+
import { type AudioAdapter, type VoiceSessionOptions } from "../voice.js";
|
|
4
|
+
import type { ActionHandler, ScreenContext } from "../protocol.js";
|
|
5
|
+
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
6
6
|
interface GraineContextValue {
|
|
7
7
|
client: GraineInAppClient;
|
|
8
8
|
connected: boolean;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { AppState } from "react-native";
|
|
4
|
-
import { GraineInAppClient } from "../client";
|
|
5
|
-
import { VoiceSession } from "../voice";
|
|
6
|
-
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation";
|
|
4
|
+
import { GraineInAppClient } from "../client.js";
|
|
5
|
+
import { VoiceSession } from "../voice.js";
|
|
6
|
+
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
7
7
|
const Ctx = createContext(null);
|
|
8
8
|
export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, includeScreens, launcherDelayMs = 0, visibility, ...options }) {
|
|
9
9
|
const clientRef = useRef(null);
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export interface GraineBarTheme {
|
|
3
|
+
surface: string;
|
|
4
|
+
border: string;
|
|
5
|
+
text: string;
|
|
6
|
+
sub: string;
|
|
7
|
+
chip: string;
|
|
8
|
+
onAccent: string;
|
|
9
|
+
danger: string;
|
|
10
|
+
}
|
|
2
11
|
export interface GraineAgentBarProps {
|
|
3
12
|
accent?: string;
|
|
4
13
|
name?: string;
|
|
@@ -6,7 +15,9 @@ export interface GraineAgentBarProps {
|
|
|
6
15
|
bottomInset?: number;
|
|
7
16
|
hidden?: boolean;
|
|
8
17
|
captionsOn?: boolean;
|
|
18
|
+
scheme?: "dark" | "light" | "auto";
|
|
19
|
+
theme?: Partial<GraineBarTheme>;
|
|
9
20
|
}
|
|
10
|
-
export declare function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset, hidden, captionsOn, }: GraineAgentBarProps): React.JSX.Element | null;
|
|
21
|
+
export declare function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset, hidden, captionsOn, scheme, theme: themeProp, }: GraineAgentBarProps): React.JSX.Element | null;
|
|
11
22
|
export declare const GraineLauncher: typeof GraineAgentBar;
|
|
12
23
|
export type GraineLauncherProps = GraineAgentBarProps;
|
package/dist/react-native/ui.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
|
|
4
|
-
import { useGraineAgent } from "./index";
|
|
5
|
-
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
|
|
4
|
+
import { useGraineAgent } from "./index.js";
|
|
5
|
+
const DARK = {
|
|
6
|
+
surface: "#17161A",
|
|
7
|
+
border: "rgba(255,255,255,0.09)",
|
|
8
|
+
text: "#F2F3F5",
|
|
9
|
+
sub: "#9aa0a6",
|
|
10
|
+
chip: "rgba(255,255,255,0.08)",
|
|
11
|
+
onAccent: "#FFFFFF",
|
|
12
|
+
danger: "#E5484D",
|
|
13
|
+
};
|
|
14
|
+
const LIGHT = {
|
|
15
|
+
surface: "#FFFFFF",
|
|
16
|
+
border: "rgba(0,0,0,0.10)",
|
|
17
|
+
text: "#16181C",
|
|
18
|
+
sub: "#61666D",
|
|
19
|
+
chip: "rgba(0,0,0,0.05)",
|
|
20
|
+
onAccent: "#FFFFFF",
|
|
21
|
+
danger: "#D32F2F",
|
|
22
|
+
};
|
|
23
|
+
export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, scheme = "auto", theme: themeProp, }) {
|
|
6
24
|
const { connected, connecting, error, messages, send, muted, setMuted, caption, agentSpeaking, launcherVisible, launcherInset, appearance, } = useGraineAgent();
|
|
7
25
|
const accent = accentProp ?? appearance?.accent ?? "#318CE7";
|
|
8
26
|
const name = nameProp ?? appearance?.title ?? "Assistant";
|
|
9
27
|
const avatarUrl = avatarProp ?? appearance?.logo ?? undefined;
|
|
28
|
+
const deviceScheme = useColorScheme();
|
|
29
|
+
const styles = useMemo(() => {
|
|
30
|
+
const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
|
|
31
|
+
return makeStyles(themeProp ? { ...base, ...themeProp } : base);
|
|
32
|
+
}, [scheme, deviceScheme, themeProp]);
|
|
10
33
|
const [expanded, setExpanded] = useState(false);
|
|
11
34
|
const [captions, setCaptions] = useState(captionsOn);
|
|
12
35
|
const [draft, setDraft] = useState("");
|
|
@@ -18,7 +41,7 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
18
41
|
: muted ? "muted"
|
|
19
42
|
: agentSpeaking || (last?.role === "agent" && last.live) ? "speaking"
|
|
20
43
|
: connected ? "listening"
|
|
21
|
-
: "
|
|
44
|
+
: "offline";
|
|
22
45
|
useEffect(() => {
|
|
23
46
|
if (expanded)
|
|
24
47
|
requestAnimationFrame(() => scrollRef.current?.scrollToEnd({ animated: true }));
|
|
@@ -46,51 +69,55 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
46
69
|
], children: [expanded && (_jsxs(View, { style: styles.panel, children: [_jsxs(ScrollView, { ref: scrollRef, style: styles.transcript, contentContainerStyle: styles.transcriptInner, showsVerticalScrollIndicator: false, children: [messages.length === 0 && (_jsx(Text, { style: styles.empty, children: "I can see what you're working on \u2014 ask me anything, or I'll step in if you get stuck." })), messages.map((m, i) => (_jsx(View, { style: [
|
|
47
70
|
styles.bubble,
|
|
48
71
|
m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
|
|
49
|
-
], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor:
|
|
72
|
+
], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), _jsx(Text, { style: styles.status, children: status })] }), connecting ? _jsx(ActivityIndicator, { size: "small", color: accent }) : null, _jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && styles.glyphOnAccent], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && styles.glyphOnAccent], children: muted ? "🔇" : "🎙" }) }), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "⌄" : "⌃" }) })] })] }));
|
|
50
73
|
}
|
|
51
74
|
export const GraineLauncher = GraineAgentBar;
|
|
52
|
-
const
|
|
75
|
+
const makeStyles = (t) => StyleSheet.create({
|
|
53
76
|
root: { position: "absolute", left: 12, right: 12, zIndex: 900 },
|
|
54
77
|
panel: {
|
|
55
|
-
backgroundColor:
|
|
78
|
+
backgroundColor: t.surface, borderRadius: 22, borderWidth: 1, borderColor: t.border,
|
|
56
79
|
marginBottom: 8, overflow: "hidden",
|
|
57
80
|
},
|
|
58
81
|
transcript: { maxHeight: 240 },
|
|
59
82
|
transcriptInner: { padding: 14, gap: 8 },
|
|
60
|
-
empty: { color:
|
|
83
|
+
empty: { color: t.sub, fontSize: 14, lineHeight: 21 },
|
|
61
84
|
bubble: { maxWidth: "88%", paddingHorizontal: 12, paddingVertical: 9, borderRadius: 16 },
|
|
62
|
-
agentBubble: { alignSelf: "flex-start", backgroundColor:
|
|
85
|
+
agentBubble: { alignSelf: "flex-start", backgroundColor: t.chip, borderBottomLeftRadius: 5 },
|
|
63
86
|
customerBubble: { alignSelf: "flex-end", borderBottomRightRadius: 5 },
|
|
64
|
-
agentText: { color:
|
|
65
|
-
customerText: { color:
|
|
87
|
+
agentText: { color: t.text, fontSize: 14.5, lineHeight: 21 },
|
|
88
|
+
customerText: { color: t.onAccent, fontSize: 14.5, lineHeight: 21 },
|
|
66
89
|
composer: { flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 12, paddingBottom: 12 },
|
|
67
90
|
input: {
|
|
68
|
-
flex: 1, borderWidth: 1, borderColor:
|
|
69
|
-
paddingHorizontal: 12, paddingVertical: Platform.OS === "ios" ? 10 : 7, color:
|
|
91
|
+
flex: 1, borderWidth: 1, borderColor: t.border, borderRadius: 16,
|
|
92
|
+
paddingHorizontal: 12, paddingVertical: Platform.OS === "ios" ? 10 : 7, color: t.text, fontSize: 14.5,
|
|
70
93
|
},
|
|
71
94
|
send: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
|
|
72
|
-
sendLabel: { color:
|
|
95
|
+
sendLabel: { color: t.onAccent, fontSize: 17, lineHeight: 20 },
|
|
73
96
|
bar: {
|
|
74
97
|
flexDirection: "row", alignItems: "center", gap: 11,
|
|
75
|
-
backgroundColor:
|
|
98
|
+
backgroundColor: t.surface, borderRadius: 26, borderWidth: 1, borderColor: t.border,
|
|
76
99
|
paddingHorizontal: 8, paddingVertical: 8,
|
|
77
100
|
shadowColor: "#000", shadowOpacity: 0.3, shadowRadius: 16, shadowOffset: { width: 0, height: 6 }, elevation: 12,
|
|
78
101
|
},
|
|
79
102
|
avatar: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
|
|
80
|
-
avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor:
|
|
103
|
+
avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor: t.chip },
|
|
81
104
|
captionWrap: {
|
|
82
|
-
backgroundColor:
|
|
105
|
+
backgroundColor: t.surface, borderRadius: 18, borderWidth: 1, borderColor: t.border,
|
|
106
|
+
paddingHorizontal: 14, paddingVertical: 11, marginBottom: 8,
|
|
107
|
+
shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 6,
|
|
83
108
|
},
|
|
84
|
-
caption: { color:
|
|
109
|
+
caption: { color: t.text, fontSize: 15, lineHeight: 21, fontWeight: "600" },
|
|
85
110
|
captionLive: { opacity: 0.8 },
|
|
86
|
-
glyph: { color:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
glyph: { color: t.text, fontSize: 13, fontWeight: "800" },
|
|
112
|
+
glyphOnAccent: { color: t.onAccent },
|
|
113
|
+
placeholder: { color: t.sub },
|
|
114
|
+
muted: { backgroundColor: t.danger },
|
|
115
|
+
avatarGlyph: { color: t.onAccent, fontSize: 17, lineHeight: 20 },
|
|
116
|
+
name: { color: t.text, fontSize: 15, fontWeight: "800" },
|
|
117
|
+
status: { color: t.sub, fontSize: 12.5, marginTop: 1 },
|
|
91
118
|
round: {
|
|
92
119
|
width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center",
|
|
93
|
-
backgroundColor:
|
|
120
|
+
backgroundColor: t.chip,
|
|
94
121
|
},
|
|
95
|
-
chevron: { color:
|
|
122
|
+
chevron: { color: t.text, fontSize: 16, lineHeight: 18 },
|
|
96
123
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { AppState, View } from "react-native";
|
|
4
|
-
import { useGraineAgent } from "./index";
|
|
4
|
+
import { useGraineAgent } from "./index.js";
|
|
5
5
|
export function GraineVoiceLauncher({ webView: WebView, autoStart = false, onCaption, onCallState, onMicDenied, onEnded, children, }) {
|
|
6
6
|
const { client, appearance } = useGraineAgent();
|
|
7
7
|
const ref = useRef(null);
|
|
8
|
+
const startedRef = useRef(false);
|
|
8
9
|
const [connected, setConnected] = useState(false);
|
|
9
10
|
const [connecting, setConnecting] = useState(false);
|
|
10
11
|
const [muted, setMutedState] = useState(false);
|
|
@@ -40,12 +41,16 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, onCap
|
|
|
40
41
|
case "graine:ready":
|
|
41
42
|
setReady(true);
|
|
42
43
|
pushContext();
|
|
43
|
-
if (autoStart)
|
|
44
|
+
if (autoStart && !startedRef.current) {
|
|
45
|
+
startedRef.current = true;
|
|
44
46
|
post({ type: "graine:start-call" });
|
|
47
|
+
}
|
|
45
48
|
return;
|
|
46
49
|
case "graine:call":
|
|
47
50
|
setConnected(!!msg.connected);
|
|
48
51
|
setConnecting(!!msg.connecting);
|
|
52
|
+
if (!msg.connected && !msg.connecting)
|
|
53
|
+
startedRef.current = false;
|
|
49
54
|
onCallState?.({ connected: !!msg.connected, connecting: !!msg.connecting });
|
|
50
55
|
return;
|
|
51
56
|
case "graine:muted":
|
|
@@ -93,13 +98,28 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, onCap
|
|
|
93
98
|
useEffect(() => () => { post({ type: "graine:end-call" }); }, [post]);
|
|
94
99
|
useEffect(() => {
|
|
95
100
|
let last = AppState.currentState;
|
|
101
|
+
let pending = null;
|
|
102
|
+
const CONFIRM_MS = 2000;
|
|
96
103
|
const sub = AppState.addEventListener("change", (next) => {
|
|
97
104
|
const wasActive = last === "active";
|
|
98
105
|
last = next;
|
|
99
|
-
if (next === "
|
|
100
|
-
|
|
106
|
+
if (next === "active") {
|
|
107
|
+
if (pending) {
|
|
108
|
+
clearTimeout(pending);
|
|
109
|
+
pending = null;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (next === "background" && wasActive && !pending) {
|
|
114
|
+
pending = setTimeout(() => {
|
|
115
|
+
pending = null;
|
|
116
|
+
if (AppState.currentState !== "active")
|
|
117
|
+
post({ type: "graine:end-call" });
|
|
118
|
+
}, CONFIRM_MS);
|
|
119
|
+
}
|
|
101
120
|
});
|
|
102
|
-
return () =>
|
|
121
|
+
return () => { if (pending)
|
|
122
|
+
clearTimeout(pending); sub.remove(); };
|
|
103
123
|
}, [post]);
|
|
104
124
|
const agentId = client.config?.agentId;
|
|
105
125
|
if (!agentId)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type RtcVoiceState = "idle" | "connecting" | "ringing" | "active" | "ended";
|
|
2
|
+
export type RtcVoiceErrorCode = "mint_failed" | "microphone_denied" | "connect_failed" | "already_active";
|
|
3
|
+
export declare class RtcVoiceError extends Error {
|
|
4
|
+
readonly code: RtcVoiceErrorCode;
|
|
5
|
+
readonly cause?: unknown;
|
|
6
|
+
constructor(code: RtcVoiceErrorCode, message: string, cause?: unknown);
|
|
7
|
+
}
|
|
8
|
+
export interface RtcVoiceSession {
|
|
9
|
+
server: string;
|
|
10
|
+
username: string;
|
|
11
|
+
password: string;
|
|
12
|
+
realm: string;
|
|
13
|
+
application_sid: string;
|
|
14
|
+
agent_id?: string;
|
|
15
|
+
expires_at?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface RtcVoiceOptions {
|
|
18
|
+
publishableKey?: string;
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
getSession?: () => Promise<RtcVoiceSession>;
|
|
21
|
+
onState?: (s: RtcVoiceState) => void;
|
|
22
|
+
onEnded?: (reason: string) => void;
|
|
23
|
+
onLog?: (line: string) => void;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class RtcVoiceSessionController {
|
|
27
|
+
private client;
|
|
28
|
+
private call;
|
|
29
|
+
private state;
|
|
30
|
+
private startToken;
|
|
31
|
+
readonly sessionId: string;
|
|
32
|
+
private opts;
|
|
33
|
+
constructor(opts: RtcVoiceOptions);
|
|
34
|
+
getState(): RtcVoiceState;
|
|
35
|
+
private set;
|
|
36
|
+
private log;
|
|
37
|
+
private mint;
|
|
38
|
+
start(): Promise<void>;
|
|
39
|
+
setMuted(muted: boolean): void;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export class RtcVoiceError extends Error {
|
|
2
|
+
constructor(code, message, cause) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "RtcVoiceError";
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function newSessionId() {
|
|
10
|
+
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
11
|
+
}
|
|
12
|
+
export class RtcVoiceSessionController {
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.client = null;
|
|
15
|
+
this.call = null;
|
|
16
|
+
this.state = "idle";
|
|
17
|
+
this.startToken = null;
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
this.sessionId = opts.sessionId || newSessionId();
|
|
20
|
+
}
|
|
21
|
+
getState() {
|
|
22
|
+
return this.state;
|
|
23
|
+
}
|
|
24
|
+
set(s) {
|
|
25
|
+
this.state = s;
|
|
26
|
+
this.opts.onState?.(s);
|
|
27
|
+
}
|
|
28
|
+
log(l) {
|
|
29
|
+
this.opts.onLog?.(l);
|
|
30
|
+
}
|
|
31
|
+
async mint() {
|
|
32
|
+
if (this.opts.getSession)
|
|
33
|
+
return this.opts.getSession();
|
|
34
|
+
if (!this.opts.publishableKey) {
|
|
35
|
+
throw new RtcVoiceError("mint_failed", "Pass publishableKey (or getSession for a custom backend).");
|
|
36
|
+
}
|
|
37
|
+
const base = (this.opts.baseUrl || "https://www.graine.ai").replace(/\/$/, "");
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await fetch(`${base}/api/embed/rtc-session`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify({ publishableKey: this.opts.publishableKey }),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
throw new RtcVoiceError("mint_failed", "Could not reach the call service.", e);
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
let detail = "";
|
|
51
|
+
try {
|
|
52
|
+
detail = (await res.json())?.error || "";
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
}
|
|
56
|
+
throw new RtcVoiceError("mint_failed", detail || `Could not start a call (${res.status}).`);
|
|
57
|
+
}
|
|
58
|
+
return res.json();
|
|
59
|
+
}
|
|
60
|
+
async start() {
|
|
61
|
+
if (this.state !== "idle" && this.state !== "ended") {
|
|
62
|
+
throw new RtcVoiceError("already_active", "A call is already in progress.");
|
|
63
|
+
}
|
|
64
|
+
const token = (this.startToken = Symbol("start"));
|
|
65
|
+
this.set("connecting");
|
|
66
|
+
try {
|
|
67
|
+
const session = await this.mint();
|
|
68
|
+
if (token !== this.startToken)
|
|
69
|
+
return;
|
|
70
|
+
this.log(`session for ${session.realm}`);
|
|
71
|
+
const { createJambonzClient } = await import("@jambonz/client-sdk-react-native");
|
|
72
|
+
this.client = createJambonzClient({
|
|
73
|
+
server: session.server,
|
|
74
|
+
username: session.username,
|
|
75
|
+
password: session.password,
|
|
76
|
+
realm: session.realm,
|
|
77
|
+
});
|
|
78
|
+
this.client.on("error", (e) => this.log(`client error: ${e?.message ?? e}`));
|
|
79
|
+
await this.client.connect();
|
|
80
|
+
if (token !== this.startToken) {
|
|
81
|
+
await this.stop();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.log("registered");
|
|
85
|
+
this.call = this.client.callApplication(session.application_sid, {
|
|
86
|
+
headers: {
|
|
87
|
+
"X-Graine-Session-Id": this.sessionId,
|
|
88
|
+
...(session.agent_id ? { "X-Graine-Agent-Id": session.agent_id } : {}),
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
this.call.on("stateChanged", (s) => this.log(`call: ${s}`));
|
|
92
|
+
this.call.on("accepted", () => {
|
|
93
|
+
this.set("active");
|
|
94
|
+
this.log("audio flowing");
|
|
95
|
+
});
|
|
96
|
+
this.call.on("ended", (c) => {
|
|
97
|
+
this.set("ended");
|
|
98
|
+
this.opts.onEnded?.(c?.reason || "ended");
|
|
99
|
+
});
|
|
100
|
+
this.call.on("failed", (c) => {
|
|
101
|
+
this.set("ended");
|
|
102
|
+
this.opts.onEnded?.(c?.reason || "failed");
|
|
103
|
+
});
|
|
104
|
+
this.set("ringing");
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (token !== this.startToken)
|
|
108
|
+
return;
|
|
109
|
+
this.set("ended");
|
|
110
|
+
throw err instanceof RtcVoiceError
|
|
111
|
+
? err
|
|
112
|
+
: new RtcVoiceError("connect_failed", err?.message || "Could not connect.", err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
setMuted(muted) {
|
|
116
|
+
try {
|
|
117
|
+
if (muted)
|
|
118
|
+
this.call?.mute?.();
|
|
119
|
+
else
|
|
120
|
+
this.call?.unmute?.();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async stop() {
|
|
126
|
+
if (this.state === "idle" || this.state === "ended")
|
|
127
|
+
return;
|
|
128
|
+
this.startToken = null;
|
|
129
|
+
try {
|
|
130
|
+
this.call?.hangup?.();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
await this.client?.disconnect?.();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
}
|
|
139
|
+
this.call = null;
|
|
140
|
+
this.client = null;
|
|
141
|
+
this.set("ended");
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dist/voice.d.ts
CHANGED
package/dist/voice.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EchoGuard, base64ToBytes, decodeAgentAudio, decodePcm16 } from "./audio";
|
|
1
|
+
import { EchoGuard, base64ToBytes, decodeAgentAudio, decodePcm16 } from "./audio.js";
|
|
2
2
|
export const CAPTURE_SAMPLE_RATE = 16000;
|
|
3
3
|
export const MIN_PLAYOUT_BUFFER_SECONDS = 0.15;
|
|
4
4
|
export function base64ToPcm16(b64) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graineai/inapp-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"private": false,
|
|
@@ -35,7 +36,8 @@
|
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
38
|
"react": ">=17",
|
|
38
|
-
"react-native": ">=0.68"
|
|
39
|
+
"react-native": ">=0.68",
|
|
40
|
+
"@jambonz/client-sdk-react-native": "^0.1.4"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
43
|
"@types/react": "^18.2.0",
|
|
@@ -60,5 +62,10 @@
|
|
|
60
62
|
"homepage": "https://www.graine.ai/docs/in-app",
|
|
61
63
|
"bugs": {
|
|
62
64
|
"url": "https://www.graine.ai/docs/in-app"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@jambonz/client-sdk-react-native": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
}
|