@graineai/inapp-react-native 0.22.0 → 0.23.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 +33 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.js +6 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/react-native/index.d.ts +13 -1
- package/dist/react-native/index.js +13 -2
- package/dist/react-native/permissions.d.ts +5 -0
- package/dist/react-native/permissions.js +21 -0
- package/dist/react-native/voice-launcher.js +5 -21
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -765,6 +765,39 @@ and resamples in its output stage, which is where the artefacts came from.
|
|
|
765
765
|
|
|
766
766
|
---
|
|
767
767
|
|
|
768
|
+
## Coming from RevRag
|
|
769
|
+
|
|
770
|
+
Same shape, different names. If you have a RevRag integration, this is the
|
|
771
|
+
whole translation:
|
|
772
|
+
|
|
773
|
+
| RevRag | Graine |
|
|
774
|
+
| --- | --- |
|
|
775
|
+
| `useInitialize({ apiKey })` → `{ isInitialized, error }` | `useGraineReady()` → `{ ready, error }` (the key goes on `GraineProvider`) |
|
|
776
|
+
| `EmbedProvider navigationRef appVersion` | `GraineProvider navigationRef appVersion` |
|
|
777
|
+
| `includeScreens` | `includeScreens` |
|
|
778
|
+
| `embedButtonDelayMs` | `launcherDelayMs` |
|
|
779
|
+
| `embedButtonVisibilityConfig` `{ defaultDelayMs, defaultInset, groups }` | `visibility` `{ defaultDelayMs, defaultInset, groups }` |
|
|
780
|
+
| `EmbedButtonGroupConfig` `{ id, screens, continuity, delayMs, delayPolicy, inset }` | `LauncherGroup` — identical fields |
|
|
781
|
+
| `continuous` / `perScreen` | same |
|
|
782
|
+
| `perScreen` / `oncePerGroupEntry` / `oncePerAppSession` | same |
|
|
783
|
+
| `EmbedButtonInset` | `LauncherInset` — same shape |
|
|
784
|
+
| `EmbedButton` (mount it yourself) | `GraineLauncher` |
|
|
785
|
+
| `Embed.Event(USER_DATA, { app_user_id, data })` | `useGraineIdentify()` / `client.identify({ id, name, … })` |
|
|
786
|
+
| `Embed.Event(SCREEN_STATE, { screen, data })` | `useGraineScreen({ screen, fields })` |
|
|
787
|
+
| `Embed.Event(CUSTOM_EVENT, data)` / `ANALYTICS_DATA` | `useGraineTrack()` / `client.track(name, data)` |
|
|
788
|
+
| `embedOnAgent(AGENT_CONVERSATION_STARTED / ENDED)` | `useGraineEvents(e => e.type === "conversation_started" / "conversation_ended")` |
|
|
789
|
+
| `AgentEvent.MICROPHONE_PERMISSION_DENIED` | `{ type: "mic_denied", reason }` on the same stream |
|
|
790
|
+
| `AgentEvent.POPUP_MESSAGE_VISIBLE` | `{ type: "launcher_shown", screen }` |
|
|
791
|
+
| `checkPermissions()` | `requestMicrophonePermission()` |
|
|
792
|
+
| server `widget_config` | `appearance` from the session, via `resolveNativeAppearance` |
|
|
793
|
+
| LiveKit native setup | none — see *Why there is no LiveKit step* |
|
|
794
|
+
|
|
795
|
+
Two things RevRag has that are deliberately absent: a LiveKit install step
|
|
796
|
+
(audio rides a WebView on a hosted page, so audio fixes ship without an app
|
|
797
|
+
release), and click tracking on every touchable by default (`useGraineTap` is
|
|
798
|
+
opt-in per element, because a stream of every tap is noise the agent has to be
|
|
799
|
+
told to ignore).
|
|
800
|
+
|
|
768
801
|
## Reference
|
|
769
802
|
|
|
770
803
|
| Hook | For |
|
package/dist/client.d.ts
CHANGED
|
@@ -50,10 +50,11 @@ export declare class GraineInAppClient {
|
|
|
50
50
|
private muted;
|
|
51
51
|
private playoutClock;
|
|
52
52
|
private identity;
|
|
53
|
+
private appVersion;
|
|
53
54
|
private recentEvents;
|
|
54
55
|
constructor(opts: GraineInAppOptions);
|
|
55
56
|
on(event: string, fn: Listener): () => void;
|
|
56
|
-
|
|
57
|
+
emit(event: string, payload?: any): void;
|
|
57
58
|
private get fetch();
|
|
58
59
|
session(): Promise<SessionConfig>;
|
|
59
60
|
checkActions(): ActionDiagnosis;
|
|
@@ -84,6 +85,7 @@ export declare class GraineInAppClient {
|
|
|
84
85
|
reportEvent(event: AppEvent): void;
|
|
85
86
|
track(name: string, data?: Record<string, unknown>): void;
|
|
86
87
|
private scheduleScreen;
|
|
88
|
+
setAppVersion(version: string | null | undefined): void;
|
|
87
89
|
buildContextFrame(): {
|
|
88
90
|
type: "app_context";
|
|
89
91
|
context: Record<string, unknown>;
|
package/dist/client.js
CHANGED
|
@@ -24,6 +24,7 @@ export class GraineInAppClient {
|
|
|
24
24
|
this.muted = false;
|
|
25
25
|
this.playoutClock = null;
|
|
26
26
|
this.identity = {};
|
|
27
|
+
this.appVersion = null;
|
|
27
28
|
this.recentEvents = [];
|
|
28
29
|
if (!opts?.publishableKey)
|
|
29
30
|
throw new Error("[Graine] publishableKey is required.");
|
|
@@ -370,6 +371,10 @@ export class GraineInAppClient {
|
|
|
370
371
|
}
|
|
371
372
|
}, CONTEXT_THROTTLE_MS);
|
|
372
373
|
}
|
|
374
|
+
setAppVersion(version) {
|
|
375
|
+
const v = version == null ? "" : String(version).trim();
|
|
376
|
+
this.appVersion = v || null;
|
|
377
|
+
}
|
|
373
378
|
buildContextFrame() {
|
|
374
379
|
const hasIdentity = Object.keys(this.identity).length > 0;
|
|
375
380
|
if (!this.screen && !hasIdentity && this.recentEvents.length === 0)
|
|
@@ -381,6 +386,7 @@ export class GraineInAppClient {
|
|
|
381
386
|
...rest,
|
|
382
387
|
...(this.screen ? { idle_ms: Date.now() - this.screenSince } : {}),
|
|
383
388
|
...(Object.keys(this.identity).length ? { user: this.identity } : {}),
|
|
389
|
+
...(this.appVersion ? { app_version: this.appVersion } : {}),
|
|
384
390
|
...(this.recentEvents.length ? { recent_events: this.recentEvents } : {}),
|
|
385
391
|
...(this.highlightables.size ? { highlightable: [...this.highlightables] } : {}),
|
|
386
392
|
}),
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type Ac
|
|
|
3
3
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
|
|
4
4
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
|
|
5
5
|
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
6
|
-
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
6
|
+
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
7
|
+
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
7
8
|
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
|
|
8
9
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
9
10
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ export { GraineInAppClient, } from "./client.js";
|
|
|
2
2
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
|
|
3
3
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
|
|
4
4
|
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
5
|
-
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, } from "./react-native/index.js";
|
|
5
|
+
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, } from "./react-native/index.js";
|
|
6
|
+
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
6
7
|
export { resolveNativeAppearance, } from "./react-native/appearance.js";
|
|
7
8
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
8
9
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
@@ -21,6 +21,7 @@ interface GraineContextValue {
|
|
|
21
21
|
launcherInset: LauncherInset;
|
|
22
22
|
currentScreen: string | null;
|
|
23
23
|
appearance: Record<string, any> | null;
|
|
24
|
+
sessionReady: boolean;
|
|
24
25
|
onEvent: (fn: (e: GraineEvent) => void) => () => void;
|
|
25
26
|
track: (name: string, data?: Record<string, unknown>) => void;
|
|
26
27
|
identify: (user: Record<string, unknown> | null) => void;
|
|
@@ -49,6 +50,12 @@ export type GraineEvent = {
|
|
|
49
50
|
} | {
|
|
50
51
|
type: "error";
|
|
51
52
|
message: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: "mic_denied";
|
|
55
|
+
reason: string;
|
|
56
|
+
} | {
|
|
57
|
+
type: "launcher_shown";
|
|
58
|
+
screen: string | null;
|
|
52
59
|
};
|
|
53
60
|
export interface GraineWidget {
|
|
54
61
|
id: string;
|
|
@@ -75,11 +82,12 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
|
75
82
|
navigationRef?: {
|
|
76
83
|
current: any;
|
|
77
84
|
} | null;
|
|
85
|
+
appVersion?: string;
|
|
78
86
|
includeScreens?: string[];
|
|
79
87
|
launcherDelayMs?: number;
|
|
80
88
|
visibility?: LauncherVisibility;
|
|
81
89
|
}
|
|
82
|
-
export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
90
|
+
export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, appVersion, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
83
91
|
export declare function useGraineAgent(): GraineContextValue;
|
|
84
92
|
export declare function useGraineEvents(handler: (e: GraineEvent) => void): void;
|
|
85
93
|
export declare function useGraineTrack(): (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -101,4 +109,8 @@ export declare function useGraineWidget(): {
|
|
|
101
109
|
dismiss: () => void;
|
|
102
110
|
};
|
|
103
111
|
export declare function useGraineAction(name: string, handler: ActionHandler): void;
|
|
112
|
+
export declare function useGraineReady(): {
|
|
113
|
+
ready: boolean;
|
|
114
|
+
error: string | null;
|
|
115
|
+
};
|
|
104
116
|
export {};
|
|
@@ -5,7 +5,7 @@ import { GraineInAppClient } from "../client.js";
|
|
|
5
5
|
import { VoiceSession } from "../voice.js";
|
|
6
6
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
7
7
|
const Ctx = createContext(null);
|
|
8
|
-
export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
|
|
8
|
+
export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, appVersion, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
|
|
9
9
|
const clientRef = useRef(null);
|
|
10
10
|
if (!clientRef.current) {
|
|
11
11
|
clientRef.current =
|
|
@@ -37,6 +37,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
37
37
|
return () => { listenersRef.current.delete(fn); };
|
|
38
38
|
}, []);
|
|
39
39
|
const [appearance, setAppearance] = useState(null);
|
|
40
|
+
const [sessionReady, setSessionReady] = useState(false);
|
|
40
41
|
const [currentScreen, setCurrentScreen] = useState(null);
|
|
41
42
|
const [launcherVisible, setLauncherVisible] = useState(false);
|
|
42
43
|
const [launcherInset, setLauncherInset] = useState({ right: 16, bottom: 20 });
|
|
@@ -76,12 +77,14 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
76
77
|
}
|
|
77
78
|
if (decision.delayMs <= 0) {
|
|
78
79
|
setLauncherVisible(true);
|
|
80
|
+
emitEvent({ type: "launcher_shown", screen: route });
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
setLauncherVisible(false);
|
|
82
84
|
showTimerRef.current = setTimeout(() => {
|
|
83
85
|
showTimerRef.current = null;
|
|
84
86
|
setLauncherVisible(true);
|
|
87
|
+
emitEvent({ type: "launcher_shown", screen: route });
|
|
85
88
|
}, decision.delayMs);
|
|
86
89
|
};
|
|
87
90
|
apply();
|
|
@@ -95,6 +98,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
95
98
|
nav.removeListener("state", apply);
|
|
96
99
|
};
|
|
97
100
|
}, [navigationRef, client]);
|
|
101
|
+
useEffect(() => { client.setAppVersion(appVersion); }, [client, appVersion]);
|
|
98
102
|
useEffect(() => {
|
|
99
103
|
if (!navigationRef?.current)
|
|
100
104
|
setLauncherVisible(true);
|
|
@@ -129,6 +133,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
129
133
|
return [...prev, { role: "agent", text, live: true }];
|
|
130
134
|
})),
|
|
131
135
|
client.on("widget", (widget) => setWidgets((prev) => [...prev, widget])),
|
|
136
|
+
client.on("mic_denied", (p) => emitEvent({ type: "mic_denied", reason: String(p?.reason || "NotAllowedError") })),
|
|
132
137
|
client.on("muted", (m) => {
|
|
133
138
|
setMutedState(m);
|
|
134
139
|
emitEvent({ type: "muted", muted: m });
|
|
@@ -153,6 +158,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
153
158
|
const cfg = client.config ?? (await client.session());
|
|
154
159
|
if (cfg?.appearance)
|
|
155
160
|
setAppearance(cfg.appearance);
|
|
161
|
+
setSessionReady(true);
|
|
156
162
|
}
|
|
157
163
|
catch {
|
|
158
164
|
}
|
|
@@ -234,11 +240,12 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
234
240
|
launcherInset,
|
|
235
241
|
currentScreen,
|
|
236
242
|
appearance,
|
|
243
|
+
sessionReady,
|
|
237
244
|
onEvent,
|
|
238
245
|
track: (name, data) => client.track(name, data),
|
|
239
246
|
identify: (user) => client.identify(user),
|
|
240
247
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
241
|
-
launcherVisible, launcherInset, currentScreen, onEvent, appearance]);
|
|
248
|
+
launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady]);
|
|
242
249
|
return _jsx(Ctx.Provider, { value: value, children: children });
|
|
243
250
|
}
|
|
244
251
|
function useGraine() {
|
|
@@ -368,3 +375,7 @@ export function useGraineAction(name, handler) {
|
|
|
368
375
|
handlerRef.current = handler;
|
|
369
376
|
useEffect(() => client.registerAction(name, (args) => handlerRef.current(args)), [client, name]);
|
|
370
377
|
}
|
|
378
|
+
export function useGraineReady() {
|
|
379
|
+
const { sessionReady, error } = useGraineAgent();
|
|
380
|
+
return { ready: sessionReady, error };
|
|
381
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PermissionsAndroid, Platform } from "react-native";
|
|
2
|
+
export async function requestMicrophonePermission(rationale = {}) {
|
|
3
|
+
if (Platform.OS !== "android")
|
|
4
|
+
return true;
|
|
5
|
+
try {
|
|
6
|
+
const perm = PermissionsAndroid?.PERMISSIONS?.RECORD_AUDIO;
|
|
7
|
+
if (!perm)
|
|
8
|
+
return true;
|
|
9
|
+
if (await PermissionsAndroid.check(perm))
|
|
10
|
+
return true;
|
|
11
|
+
const res = await PermissionsAndroid.request(perm, {
|
|
12
|
+
title: rationale.title ?? "Microphone",
|
|
13
|
+
message: rationale.message ?? "Allow the microphone so you can talk to the assistant.",
|
|
14
|
+
buttonPositive: rationale.buttonPositive ?? "Allow",
|
|
15
|
+
});
|
|
16
|
+
return res === PermissionsAndroid.RESULTS.GRANTED;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
-
import { AppState,
|
|
3
|
+
import { AppState, View } from "react-native";
|
|
4
4
|
import { useGraineAgent } from "./index.js";
|
|
5
|
+
import { requestMicrophonePermission } from "./permissions.js";
|
|
5
6
|
export function GraineVoiceLauncher({ webView: WebView, autoStart = false, transport, onTransport, onCaption, onCallState, onMicDenied, onError, onEnded, children, }) {
|
|
6
7
|
const { client, appearance } = useGraineAgent();
|
|
7
8
|
const ref = useRef(null);
|
|
@@ -34,26 +35,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
|
|
|
34
35
|
})();
|
|
35
36
|
return () => { cancelled = true; };
|
|
36
37
|
}, [client, voiceAgentId]);
|
|
37
|
-
const ensureMic = useCallback(
|
|
38
|
-
if (Platform.OS !== "android")
|
|
39
|
-
return true;
|
|
40
|
-
try {
|
|
41
|
-
const perm = PermissionsAndroid?.PERMISSIONS?.RECORD_AUDIO;
|
|
42
|
-
if (!perm)
|
|
43
|
-
return true;
|
|
44
|
-
if (await PermissionsAndroid.check(perm))
|
|
45
|
-
return true;
|
|
46
|
-
const res = await PermissionsAndroid.request(perm, {
|
|
47
|
-
title: "Microphone",
|
|
48
|
-
message: "Allow the microphone so you can talk to the assistant.",
|
|
49
|
-
buttonPositive: "Allow",
|
|
50
|
-
});
|
|
51
|
-
return res === PermissionsAndroid.RESULTS.GRANTED;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}, []);
|
|
38
|
+
const ensureMic = useCallback(() => requestMicrophonePermission(), []);
|
|
57
39
|
const pushContext = useCallback(() => {
|
|
58
40
|
const frame = client.buildContextFrame();
|
|
59
41
|
if (!frame)
|
|
@@ -80,6 +62,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
|
|
|
80
62
|
if (!(await ensureMic())) {
|
|
81
63
|
setConnecting(false);
|
|
82
64
|
onMicDenied?.("NotAllowedError");
|
|
65
|
+
client.emit("mic_denied", { reason: "NotAllowedError" });
|
|
83
66
|
return;
|
|
84
67
|
}
|
|
85
68
|
post({ type: "graine:start-call" });
|
|
@@ -155,6 +138,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
|
|
|
155
138
|
onEnded?.();
|
|
156
139
|
return;
|
|
157
140
|
case "graine:mic-denied":
|
|
141
|
+
client.emit("mic_denied", { reason: String(msg.reason || "NotAllowedError") });
|
|
158
142
|
onMicDenied?.(String(msg.reason || "denied"));
|
|
159
143
|
return;
|
|
160
144
|
case "graine:widget": {
|
package/package.json
CHANGED