@graineai/inapp-react-native 0.21.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 +15 -1
- package/dist/client.js +35 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- 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 +2 -2
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
|
@@ -17,6 +17,16 @@ export interface SessionConfig {
|
|
|
17
17
|
appearance?: Record<string, any>;
|
|
18
18
|
variables?: Record<string, string>;
|
|
19
19
|
components?: any[];
|
|
20
|
+
appActions?: Array<{
|
|
21
|
+
tool_name?: string;
|
|
22
|
+
toolName?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export interface ActionDiagnosis {
|
|
27
|
+
usable: string[];
|
|
28
|
+
undeclared: string[];
|
|
29
|
+
unhandled: string[];
|
|
20
30
|
}
|
|
21
31
|
export declare class GraineInAppClient {
|
|
22
32
|
readonly opts: GraineInAppOptions;
|
|
@@ -40,12 +50,15 @@ export declare class GraineInAppClient {
|
|
|
40
50
|
private muted;
|
|
41
51
|
private playoutClock;
|
|
42
52
|
private identity;
|
|
53
|
+
private appVersion;
|
|
43
54
|
private recentEvents;
|
|
44
55
|
constructor(opts: GraineInAppOptions);
|
|
45
56
|
on(event: string, fn: Listener): () => void;
|
|
46
|
-
|
|
57
|
+
emit(event: string, payload?: any): void;
|
|
47
58
|
private get fetch();
|
|
48
59
|
session(): Promise<SessionConfig>;
|
|
60
|
+
checkActions(): ActionDiagnosis;
|
|
61
|
+
private reportActionMismatch;
|
|
49
62
|
private ticket;
|
|
50
63
|
connect(): Promise<void>;
|
|
51
64
|
private openSocket;
|
|
@@ -72,6 +85,7 @@ export declare class GraineInAppClient {
|
|
|
72
85
|
reportEvent(event: AppEvent): void;
|
|
73
86
|
track(name: string, data?: Record<string, unknown>): void;
|
|
74
87
|
private scheduleScreen;
|
|
88
|
+
setAppVersion(version: string | null | undefined): void;
|
|
75
89
|
buildContextFrame(): {
|
|
76
90
|
type: "app_context";
|
|
77
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.");
|
|
@@ -68,8 +69,37 @@ export class GraineInAppClient {
|
|
|
68
69
|
throw new Error(data?.error || `Embed rejected (${res.status})`);
|
|
69
70
|
}
|
|
70
71
|
this.config = data;
|
|
72
|
+
this.reportActionMismatch();
|
|
71
73
|
return this.config;
|
|
72
74
|
}
|
|
75
|
+
checkActions() {
|
|
76
|
+
const registered = [...this.actions.keys()];
|
|
77
|
+
const declared = new Set((this.config?.appActions ?? [])
|
|
78
|
+
.map((a) => a?.tool_name || a?.toolName || a?.name)
|
|
79
|
+
.filter((n) => Boolean(n)));
|
|
80
|
+
return {
|
|
81
|
+
usable: registered.filter((n) => declared.has(n)),
|
|
82
|
+
undeclared: registered.filter((n) => !declared.has(n)),
|
|
83
|
+
unhandled: [...declared].filter((n) => !this.actions.has(n)),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
reportActionMismatch() {
|
|
87
|
+
if (this.actions.size === 0)
|
|
88
|
+
return;
|
|
89
|
+
const { undeclared, unhandled } = this.checkActions();
|
|
90
|
+
if (!undeclared.length && !unhandled.length)
|
|
91
|
+
return;
|
|
92
|
+
this.emit("diagnostic", { type: "actions", ...this.checkActions() });
|
|
93
|
+
if (undeclared.length) {
|
|
94
|
+
console.warn(`[Graine] This app registered ${undeclared.length} action(s) the agent is not ` +
|
|
95
|
+
`configured to call, so the agent cannot use them: ${undeclared.join(", ")}. ` +
|
|
96
|
+
`Add them to the agent's app_actions in Embed & Widgets.`);
|
|
97
|
+
}
|
|
98
|
+
if (unhandled.length) {
|
|
99
|
+
console.warn(`[Graine] The agent is configured to call ${unhandled.length} action(s) this ` +
|
|
100
|
+
`build does not implement: ${unhandled.join(", ")}. It will be refused mid-sentence.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
73
103
|
async ticket() {
|
|
74
104
|
try {
|
|
75
105
|
const res = await this.fetch(`${this.opts.baseUrl}/api/embed/ticket`, {
|
|
@@ -341,6 +371,10 @@ export class GraineInAppClient {
|
|
|
341
371
|
}
|
|
342
372
|
}, CONTEXT_THROTTLE_MS);
|
|
343
373
|
}
|
|
374
|
+
setAppVersion(version) {
|
|
375
|
+
const v = version == null ? "" : String(version).trim();
|
|
376
|
+
this.appVersion = v || null;
|
|
377
|
+
}
|
|
344
378
|
buildContextFrame() {
|
|
345
379
|
const hasIdentity = Object.keys(this.identity).length > 0;
|
|
346
380
|
if (!this.screen && !hasIdentity && this.recentEvents.length === 0)
|
|
@@ -352,6 +386,7 @@ export class GraineInAppClient {
|
|
|
352
386
|
...rest,
|
|
353
387
|
...(this.screen ? { idle_ms: Date.now() - this.screenSince } : {}),
|
|
354
388
|
...(Object.keys(this.identity).length ? { user: this.identity } : {}),
|
|
389
|
+
...(this.appVersion ? { app_version: this.appVersion } : {}),
|
|
355
390
|
...(this.recentEvents.length ? { recent_events: this.recentEvents } : {}),
|
|
356
391
|
...(this.highlightables.size ? { highlightable: [...this.highlightables] } : {}),
|
|
357
392
|
}),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export type { ActionHandler, AppActionRequest, AppActionResult, AppEvent, ScreenContext, ScreenField, } from "./protocol.js";
|
|
2
|
-
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig } from "./client.js";
|
|
2
|
+
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
|
|
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
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export { GraineInAppClient } from "./client.js";
|
|
1
|
+
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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graineai/inapp-react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Graine in-app agent for React Native
|
|
5
|
+
"description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
8
8
|
"main": "dist/index.js",
|