@graineai/inapp-react-native 0.24.0 → 0.26.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 +45 -1
- package/README.md +18 -1
- package/dist/client.d.ts +4 -1
- package/dist/client.js +26 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react-native/index.d.ts +4 -1
- package/dist/react-native/index.js +31 -9
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -809,6 +809,33 @@ useGraineEvents((e) => {
|
|
|
809
809
|
});
|
|
810
810
|
```
|
|
811
811
|
|
|
812
|
+
## Fast navigation: the agent always has the latest screen
|
|
813
|
+
|
|
814
|
+
A customer who taps through four screens and then asks "where am I?" gets the
|
|
815
|
+
fourth. The guarantees, so you do not have to think about them:
|
|
816
|
+
|
|
817
|
+
- **Immediate.** `useGraineScreen` reports a screen the moment it mounts, and
|
|
818
|
+
the voice launcher relays it to the runtime at once — the 400ms coalescing
|
|
819
|
+
applies only to the SDK's own text socket, never to a host's channel.
|
|
820
|
+
- **Ordered.** Every context frame carries a monotonic `seq`; the runtime keeps
|
|
821
|
+
the newest and drops anything that arrives late. The last screen always wins.
|
|
822
|
+
- **Cleanup-safe.** A screen's unmount clears the screen only if it is still
|
|
823
|
+
the one being described (`client.clearScreen(id)`), so a navigation library
|
|
824
|
+
unmounting the old screen after the new one mounted cannot erase it.
|
|
825
|
+
- **Reconnect-safe.** The newest context is replayed whenever the media or the
|
|
826
|
+
screen channel (re)connects.
|
|
827
|
+
- **Applied every turn.** The runtime puts the last-reported screen on the
|
|
828
|
+
prompt at every generation, so nothing that rewrites the prompt mid-call — a
|
|
829
|
+
language switch, a summary — can make the agent forget where the customer is.
|
|
830
|
+
|
|
831
|
+
Every reporting hook works **outside a provider** too — against the client the
|
|
832
|
+
provider registered, or one you pass to `setDefaultClient(client)` once — so an
|
|
833
|
+
app whose provider wraps only its bar has no reason to re-implement them.
|
|
834
|
+
|
|
835
|
+
If you drive the client yourself instead of using `useGraineScreen`, call
|
|
836
|
+
`client.setScreen(ctx)` on mount and `client.clearScreen(ctx.screen)` on
|
|
837
|
+
unmount — never `setScreen(null)` from a cleanup.
|
|
838
|
+
|
|
812
839
|
## What Call History shows for an in-app conversation
|
|
813
840
|
|
|
814
841
|
Every conversation lands in the dashboard's Call History with, beside the
|
|
@@ -876,11 +903,13 @@ told to ignore).
|
|
|
876
903
|
| `useGraineAgent()` | connection state, transcript, mute, send a turn |
|
|
877
904
|
| `useGraineIdentify()` | who the customer is |
|
|
878
905
|
| `useGraineScreen()` | what is on the screen |
|
|
906
|
+
| `useGraineField()` | one input as it changes — value, error, status; friction reads it |
|
|
879
907
|
| `useGraineAction()` | one handler per declared action |
|
|
880
908
|
| `useGraineHighlight()` | let the agent point at an element on this screen |
|
|
881
909
|
| `useGraineWidget()` | the component the agent drew, and how to answer it |
|
|
882
910
|
| `useGraineTrack()` | product events that inform but do not interrupt |
|
|
883
|
-
| `useGraineEvents()` | conversation lifecycle |
|
|
911
|
+
| `useGraineEvents()` | conversation lifecycle, action outcomes, friction signals |
|
|
912
|
+
| `useGraineTap()` | wrap an `onPress` so the tap is tracked (and rage taps detected) |
|
|
884
913
|
| `useGraineVoice()` | only if you bring your own native audio instead of the WebView |
|
|
885
914
|
|
|
886
915
|
### `GraineProvider`
|
|
@@ -897,6 +926,21 @@ told to ignore).
|
|
|
897
926
|
| `autoConnect` | `boolean` | Default `true`. Set `false` when `GraineVoiceLauncher` owns the connection — see below. |
|
|
898
927
|
| `onProactive` | `(text) => void` | The agent spoke first; draw your own nudge. |
|
|
899
928
|
| `endOnBackground` | `boolean` | Default `true`. You want it on. |
|
|
929
|
+
| `appVersion` | `string` | Which build the conversation happened in. Lands on the call record. |
|
|
930
|
+
| `friction` | `FrictionConfig` | This app's friction rules, layered over the dashboard's. See *Friction detection*. |
|
|
931
|
+
|
|
932
|
+
### `GraineInAppClient`
|
|
933
|
+
|
|
934
|
+
| Method | For |
|
|
935
|
+
|---|---|
|
|
936
|
+
| `setScreen(ctx)` / `clearScreen(id)` | Report a screen; withdraw it only if it is still the current one. |
|
|
937
|
+
| `updateField(field)` / `removeField(name)` | One input changed; merged into the current screen by name. |
|
|
938
|
+
| `identify(user)` | Who the customer is. Masked on the way out. |
|
|
939
|
+
| `track(name, data)` / `reportEvent(event)` | Inform the agent / ask it to speak up. |
|
|
940
|
+
| `registerAction(name, handler)` / `executeAction(name, args)` | Implement and run in-app actions. |
|
|
941
|
+
| `configureFriction(rules)` / `friction` | Replace the app's friction layer / read the resolved rules. |
|
|
942
|
+
| `session()` | The dashboard config for this key: appearance, actions, friction. |
|
|
943
|
+
| `on(event, fn)` | `screen`, `action`, `action_result`, `friction`, `app_event`, `mic_denied`, … |
|
|
900
944
|
|
|
901
945
|
### `GraineVoiceLauncher`
|
|
902
946
|
|
package/README.md
CHANGED
|
@@ -217,13 +217,30 @@ requests a permission on your behalf.
|
|
|
217
217
|
| `GraineProvider` | Connects the agent. Set `voice` for spoken conversations. |
|
|
218
218
|
| `useGraineAgent()` | Connection state, transcript, and `send()`. |
|
|
219
219
|
| `useGraineScreen(context)` | Report what this screen shows. |
|
|
220
|
+
| `useGraineField(name, field)` | One input as it changes — value, error, status. |
|
|
220
221
|
| `useGraineAction(name, handler)` | Implement one action the agent may call. |
|
|
221
222
|
| `useGraineVoice(adapter, opts)` | Microphone, playback and barge-in. |
|
|
223
|
+
| `useGraineEvents(fn)` | Conversation lifecycle, `action_completed`, `friction_detected`, `mic_denied`. |
|
|
224
|
+
| `useGraineTrack()` / `useGraineTap()` | Product events and taps the agent knows about but does not interrupt for. |
|
|
225
|
+
| `useGraineIdentify()` | Who the customer is — it reaches the greeting and the call record. |
|
|
226
|
+
| `useGraineHighlight()` / `useGraineWidget()` | Let the agent point at things and draw components. |
|
|
227
|
+
| `GraineVoiceLauncher` | The voice engine: WebView, microphone, WebRTC or WebSocket. |
|
|
222
228
|
| `GraineAgentBar` | Optional default UI. Mount at the root. Replace it with your own. |
|
|
223
229
|
| `addMaskRule(pattern, token)` | Mask an app-specific identifier format. |
|
|
224
230
|
|
|
231
|
+
**Friction detection** — the agent notices a stuck customer (idle, repeated
|
|
232
|
+
field errors, back-and-forth, rage taps, error banners) and speaks first. Rules
|
|
233
|
+
are JSON, per screen, configured in the dashboard; pass `friction` on the
|
|
234
|
+
provider only for the screens your app knows better. `DEFAULT_FRICTION`,
|
|
235
|
+
`resolveFriction` and `FrictionDetector` are exported for hosts that want the
|
|
236
|
+
engine on their own timers.
|
|
237
|
+
|
|
238
|
+
Every reporting hook works outside a provider too — call `setDefaultClient(client)`
|
|
239
|
+
once if your provider wraps only your bar.
|
|
240
|
+
|
|
225
241
|
`GraineInAppClient` is exported for apps that want the transport without the
|
|
226
|
-
React layer
|
|
242
|
+
React layer: `setScreen`, `clearScreen`, `updateField`, `removeField`, `identify`, `track`, `reportEvent`,
|
|
243
|
+
`registerAction`, `executeAction`, `configureFriction`, `friction`, `session`.
|
|
227
244
|
|
|
228
245
|
## Support
|
|
229
246
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext } from "./protocol.js";
|
|
1
|
+
import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext, type ScreenField } from "./protocol.js";
|
|
2
2
|
import { type FrictionConfig } from "./friction.js";
|
|
3
3
|
export declare const SUBPROTOCOL = "graine.embed.v1";
|
|
4
4
|
export interface GraineInAppOptions {
|
|
@@ -91,6 +91,9 @@ export declare class GraineInAppClient {
|
|
|
91
91
|
highlight(name: string | null, ttlMs?: number): void;
|
|
92
92
|
registerAction(name: string, handler: ActionHandler): () => void;
|
|
93
93
|
get availableActions(): string[];
|
|
94
|
+
updateField(field: ScreenField): void;
|
|
95
|
+
removeField(name: string): void;
|
|
96
|
+
clearScreen(screenId: string | null | undefined): void;
|
|
94
97
|
setScreen(context: ScreenContext | null): void;
|
|
95
98
|
private dispatchFriction;
|
|
96
99
|
reportEvent(event: AppEvent): void;
|
package/dist/client.js
CHANGED
|
@@ -363,6 +363,32 @@ export class GraineInAppClient {
|
|
|
363
363
|
get availableActions() {
|
|
364
364
|
return [...this.actions.keys()];
|
|
365
365
|
}
|
|
366
|
+
updateField(field) {
|
|
367
|
+
if (!this.screen || !field || !field.name)
|
|
368
|
+
return;
|
|
369
|
+
const fields = [...(this.screen.fields ?? [])];
|
|
370
|
+
const i = fields.findIndex((f) => f.name === field.name);
|
|
371
|
+
if (i >= 0)
|
|
372
|
+
fields[i] = { ...fields[i], ...field };
|
|
373
|
+
else
|
|
374
|
+
fields.push(field);
|
|
375
|
+
this.setScreen({ ...this.screen, fields });
|
|
376
|
+
}
|
|
377
|
+
removeField(name) {
|
|
378
|
+
if (!this.screen || !name)
|
|
379
|
+
return;
|
|
380
|
+
const fields = this.screen.fields ?? [];
|
|
381
|
+
if (!fields.some((f) => f.name === name))
|
|
382
|
+
return;
|
|
383
|
+
this.setScreen({ ...this.screen, fields: fields.filter((f) => f.name !== name) });
|
|
384
|
+
}
|
|
385
|
+
clearScreen(screenId) {
|
|
386
|
+
if (!this.screen)
|
|
387
|
+
return;
|
|
388
|
+
if (screenId && this.screen.screen !== screenId)
|
|
389
|
+
return;
|
|
390
|
+
this.setScreen(null);
|
|
391
|
+
}
|
|
366
392
|
setScreen(context) {
|
|
367
393
|
const changedScreen = context?.screen !== this.screen?.screen;
|
|
368
394
|
this.screen = context;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type Fri
|
|
|
4
4
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
|
|
5
5
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
|
|
6
6
|
export { addMaskRule, maskDeep, maskString } from "./mask.js";
|
|
7
|
-
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 { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
8
8
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
9
9
|
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
|
|
10
10
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "
|
|
|
3
3
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
|
|
4
4
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } 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, useGraineReady, } from "./react-native/index.js";
|
|
6
|
+
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, } from "./react-native/index.js";
|
|
7
7
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
8
8
|
export { resolveNativeAppearance, } from "./react-native/appearance.js";
|
|
9
9
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { GraineInAppClient, type GraineInAppOptions } from "../client.js";
|
|
3
3
|
import { type AudioAdapter, type VoiceSessionOptions } from "../voice.js";
|
|
4
|
-
import type { ActionHandler, ScreenContext } from "../protocol.js";
|
|
4
|
+
import type { ActionHandler, ScreenContext, ScreenField } from "../protocol.js";
|
|
5
5
|
import type { FrictionConfig } from "../friction.js";
|
|
6
6
|
import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
|
|
7
7
|
interface GraineContextValue {
|
|
@@ -80,6 +80,8 @@ export interface Turn {
|
|
|
80
80
|
text: string;
|
|
81
81
|
live?: boolean;
|
|
82
82
|
}
|
|
83
|
+
export declare function setDefaultClient(client: GraineInAppClient | null): void;
|
|
84
|
+
export declare function useGraineClient(): GraineInAppClient;
|
|
83
85
|
export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
84
86
|
children: React.ReactNode;
|
|
85
87
|
client?: GraineInAppClient;
|
|
@@ -108,6 +110,7 @@ export declare function useGraineVoice(adapter: AudioAdapter | null, options?: V
|
|
|
108
110
|
stop: () => Promise<void>;
|
|
109
111
|
};
|
|
110
112
|
export declare function useGraineScreen(context: ScreenContext | null): void;
|
|
113
|
+
export declare function useGraineField(name: string, field: Omit<ScreenField, "name">): void;
|
|
111
114
|
export declare function useGraineHighlight(name: string): {
|
|
112
115
|
highlighted: boolean;
|
|
113
116
|
};
|
|
@@ -5,6 +5,18 @@ 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
|
+
let defaultClient = null;
|
|
9
|
+
export function setDefaultClient(client) {
|
|
10
|
+
defaultClient = client;
|
|
11
|
+
}
|
|
12
|
+
export function useGraineClient() {
|
|
13
|
+
const ctx = useContext(Ctx);
|
|
14
|
+
const client = ctx?.client ?? defaultClient;
|
|
15
|
+
if (!client) {
|
|
16
|
+
throw new Error("[Graine] No client. Either render inside <GraineProvider>, or call setDefaultClient(client) once at startup.");
|
|
17
|
+
}
|
|
18
|
+
return client;
|
|
19
|
+
}
|
|
8
20
|
export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
|
|
9
21
|
const clientRef = useRef(null);
|
|
10
22
|
if (!clientRef.current) {
|
|
@@ -99,6 +111,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
99
111
|
};
|
|
100
112
|
}, [navigationRef, client]);
|
|
101
113
|
useEffect(() => { client.setAppVersion(appVersion); }, [client, appVersion]);
|
|
114
|
+
useEffect(() => { setDefaultClient(client); }, [client]);
|
|
102
115
|
useEffect(() => { if (friction !== undefined)
|
|
103
116
|
client.configureFriction(friction); }, [client, friction]);
|
|
104
117
|
useEffect(() => {
|
|
@@ -273,11 +286,12 @@ export function useGraineEvents(handler) {
|
|
|
273
286
|
useEffect(() => onEvent((e) => ref.current(e)), [onEvent]);
|
|
274
287
|
}
|
|
275
288
|
export function useGraineTrack() {
|
|
276
|
-
const
|
|
277
|
-
|
|
289
|
+
const ctx = useContext(Ctx);
|
|
290
|
+
const client = useGraineClient();
|
|
291
|
+
return useCallback((name, data) => (ctx ? ctx.track(name, data) : client.track(name, data)), [ctx, client]);
|
|
278
292
|
}
|
|
279
293
|
export function useGraineTap(name, handler, data) {
|
|
280
|
-
const
|
|
294
|
+
const track = useGraineTrack();
|
|
281
295
|
const handlerRef = useRef(handler);
|
|
282
296
|
handlerRef.current = handler;
|
|
283
297
|
const dataRef = useRef(data);
|
|
@@ -292,7 +306,7 @@ export function useGraineTap(name, handler, data) {
|
|
|
292
306
|
}, [name, track]);
|
|
293
307
|
}
|
|
294
308
|
export function useGraineIdentify() {
|
|
295
|
-
const
|
|
309
|
+
const client = useGraineClient();
|
|
296
310
|
return useCallback((user) => client.identify(user), [client]);
|
|
297
311
|
}
|
|
298
312
|
export function useGraineVoice(adapter, options = {}) {
|
|
@@ -328,15 +342,23 @@ export function useGraineVoice(adapter, options = {}) {
|
|
|
328
342
|
};
|
|
329
343
|
}
|
|
330
344
|
export function useGraineScreen(context) {
|
|
331
|
-
const
|
|
345
|
+
const client = useGraineClient();
|
|
332
346
|
const serialised = JSON.stringify(context ?? null);
|
|
333
347
|
useEffect(() => {
|
|
334
348
|
client.setScreen(context);
|
|
335
|
-
return () => client.
|
|
349
|
+
return () => client.clearScreen(context?.screen);
|
|
336
350
|
}, [client, serialised]);
|
|
337
351
|
}
|
|
352
|
+
export function useGraineField(name, field) {
|
|
353
|
+
const client = useGraineClient();
|
|
354
|
+
const serialised = JSON.stringify(field ?? null);
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
client.updateField({ name, ...(field ?? {}) });
|
|
357
|
+
}, [client, name, serialised]);
|
|
358
|
+
useEffect(() => () => client.removeField(name), [client, name]);
|
|
359
|
+
}
|
|
338
360
|
export function useGraineHighlight(name) {
|
|
339
|
-
const
|
|
361
|
+
const client = useGraineClient();
|
|
340
362
|
const [highlighted, setHighlighted] = useState(false);
|
|
341
363
|
useEffect(() => client.registerHighlightable(name), [client, name]);
|
|
342
364
|
useEffect(() => {
|
|
@@ -357,7 +379,7 @@ export function useGraineHighlight(name) {
|
|
|
357
379
|
return { highlighted };
|
|
358
380
|
}
|
|
359
381
|
export function useGraineWidget() {
|
|
360
|
-
const
|
|
382
|
+
const client = useGraineClient();
|
|
361
383
|
const [widget, setWidget] = useState(null);
|
|
362
384
|
useEffect(() => client.on("widget", (w) => {
|
|
363
385
|
if (!w?.widget_id)
|
|
@@ -379,7 +401,7 @@ export function useGraineWidget() {
|
|
|
379
401
|
return { widget, submit, dismiss };
|
|
380
402
|
}
|
|
381
403
|
export function useGraineAction(name, handler) {
|
|
382
|
-
const
|
|
404
|
+
const client = useGraineClient();
|
|
383
405
|
const handlerRef = useRef(handler);
|
|
384
406
|
handlerRef.current = handler;
|
|
385
407
|
useEffect(() => client.registerAction(name, (args) => handlerRef.current(args)), [client, name]);
|
package/package.json
CHANGED