@graineai/inapp-react-native 0.26.1 → 0.27.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 +9 -4
- package/README.md +3 -0
- package/dist/client.d.ts +4 -1
- package/dist/client.js +19 -1
- package/dist/friction.js +4 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +1 -0
- package/dist/react-native/fab.d.ts +20 -0
- package/dist/react-native/fab.js +85 -0
- package/dist/react-native/index.d.ts +10 -1
- package/dist/react-native/index.js +16 -5
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -819,9 +819,12 @@ fourth. The guarantees, so you do not have to think about them:
|
|
|
819
819
|
applies only to the SDK's own text socket, never to a host's channel.
|
|
820
820
|
- **Ordered.** Every context frame carries a monotonic `seq`; the runtime keeps
|
|
821
821
|
the newest and drops anything that arrives late. The last screen always wins.
|
|
822
|
-
- **Cleanup-safe.**
|
|
823
|
-
|
|
824
|
-
|
|
822
|
+
- **Cleanup-safe.** The description is re-sent whenever its state changes and
|
|
823
|
+
withdrawn only on unmount — never on a change (0.26.2; earlier versions sent
|
|
824
|
+
a screen-less frame before every navigation). The withdrawal clears the
|
|
825
|
+
screen only if it is still the one being described (`client.clearScreen(id)`),
|
|
826
|
+
so a navigation library unmounting the old screen after the new one mounted
|
|
827
|
+
cannot erase it.
|
|
825
828
|
- **Reconnect-safe.** The newest context is replayed whenever the media or the
|
|
826
829
|
screen channel (re)connects.
|
|
827
830
|
- **Self-healing.** The newest screen is re-asserted every few seconds during
|
|
@@ -882,7 +885,9 @@ whole translation:
|
|
|
882
885
|
| `continuous` / `perScreen` | same |
|
|
883
886
|
| `perScreen` / `oncePerGroupEntry` / `oncePerAppSession` | same |
|
|
884
887
|
| `EmbedButtonInset` | `LauncherInset` — same shape |
|
|
885
|
-
| `EmbedButton`
|
|
888
|
+
| `EmbedButton` / the provider mounts the FAB | `fab={{ webView }}` on `GraineProvider`, or `GraineFab` yourself |
|
|
889
|
+
| Best-effort click tracking on touchables | `captureTaps` (on by default with `fab`) — `tap:screen` events |
|
|
890
|
+
| `useInitialize` → `{ isInitialized, error }` | `useGraineInit()` |
|
|
886
891
|
| `Embed.Event(USER_DATA, { app_user_id, data })` | `useGraineIdentify()` / `client.identify({ id, name, … })` |
|
|
887
892
|
| `Embed.Event(SCREEN_STATE, { screen, data })` | `useGraineScreen({ screen, fields })` |
|
|
888
893
|
| `Embed.Event(CUSTOM_EVENT, data)` / `ANALYTICS_DATA` | `useGraineTrack()` / `client.track(name, data)` |
|
package/README.md
CHANGED
|
@@ -225,6 +225,9 @@ requests a permission on your behalf.
|
|
|
225
225
|
| `useGraineIdentify()` | Who the customer is — it reaches the greeting and the call record. |
|
|
226
226
|
| `useGraineHighlight()` / `useGraineWidget()` | Let the agent point at things and draw components. |
|
|
227
227
|
| `GraineVoiceLauncher` | The voice engine: WebView, microphone, WebRTC or WebSocket. |
|
|
228
|
+
| `fab={{ webView }}` on the provider / `GraineFab` | The floating button, mounted for you; opens the conversation, folds back on end. |
|
|
229
|
+
| `captureTaps` / `GraineTouchCapture` | Every tap tracked automatically — no per-button wrapping. |
|
|
230
|
+
| `useGraineInit()` | `{ isInitialized, error }`. |
|
|
228
231
|
| `GraineAgentBar` | Optional default UI. Mount at the root. Replace it with your own. |
|
|
229
232
|
| `addMaskRule(pattern, token)` | Mask an app-specific identifier format. |
|
|
230
233
|
|
package/dist/client.d.ts
CHANGED
|
@@ -97,7 +97,10 @@ export declare class GraineInAppClient {
|
|
|
97
97
|
setScreen(context: ScreenContext | null): void;
|
|
98
98
|
private dispatchFriction;
|
|
99
99
|
reportEvent(event: AppEvent): void;
|
|
100
|
-
track(name: string, data?: Record<string, unknown
|
|
100
|
+
track(name: string, data?: Record<string, unknown>, opts?: {
|
|
101
|
+
coalesce?: boolean;
|
|
102
|
+
}): void;
|
|
103
|
+
private scheduleScreenTrailing;
|
|
101
104
|
private scheduleScreen;
|
|
102
105
|
setAppVersion(version: string | null | undefined): void;
|
|
103
106
|
buildContextFrame(): {
|
package/dist/client.js
CHANGED
|
@@ -415,16 +415,34 @@ export class GraineInAppClient {
|
|
|
415
415
|
this.emit("app_event", event);
|
|
416
416
|
this.send({ type: "app_event", event });
|
|
417
417
|
}
|
|
418
|
-
track(name, data) {
|
|
418
|
+
track(name, data, opts) {
|
|
419
419
|
if (!name)
|
|
420
420
|
return;
|
|
421
421
|
this.recentEvents.push({ name, at: Date.now(), ...(data ? { data } : {}) });
|
|
422
422
|
if (this.recentEvents.length > 10)
|
|
423
423
|
this.recentEvents.shift();
|
|
424
|
+
if (opts?.coalesce) {
|
|
425
|
+
this.dispatchFriction(this.detector.onTrack(name, Date.now()));
|
|
426
|
+
if (this.screen)
|
|
427
|
+
this.scheduleScreenTrailing();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
424
430
|
this.dispatchFriction(this.detector.onTrack(name, Date.now()));
|
|
425
431
|
if (this.screen)
|
|
426
432
|
this.scheduleScreen();
|
|
427
433
|
}
|
|
434
|
+
scheduleScreenTrailing() {
|
|
435
|
+
this.pendingScreen = true;
|
|
436
|
+
if (this.contextTimer)
|
|
437
|
+
return;
|
|
438
|
+
this.contextTimer = setTimeout(() => {
|
|
439
|
+
this.contextTimer = null;
|
|
440
|
+
if (this.pendingScreen) {
|
|
441
|
+
this.pendingScreen = false;
|
|
442
|
+
this.scheduleScreen();
|
|
443
|
+
}
|
|
444
|
+
}, CONTEXT_THROTTLE_MS);
|
|
445
|
+
}
|
|
428
446
|
scheduleScreen() {
|
|
429
447
|
this.emit("screen", { screen: this.screen, actions: this.getAvailableActions() });
|
|
430
448
|
if (this.contextTimer) {
|
package/dist/friction.js
CHANGED
|
@@ -225,7 +225,10 @@ export class FrictionDetector {
|
|
|
225
225
|
if (times.length < taps)
|
|
226
226
|
return [];
|
|
227
227
|
this.taps.set(key, []);
|
|
228
|
-
const
|
|
228
|
+
const label = name.slice(4);
|
|
229
|
+
const s = this.signal("rage_taps", { tap: label, count: times.length, seconds: (windowMs / 1000).toFixed(1) }, label === "screen"
|
|
230
|
+
? `The customer tapped the "{title}" screen {count} times in {seconds}s — something on it may not be responding or not doing what they expect.`
|
|
231
|
+
: `The customer tapped "{tap}" {count} times in {seconds}s on the "{title}" screen — it may not be responding or not doing what they expect.`, now);
|
|
229
232
|
return s ? [s] : [];
|
|
230
233
|
}
|
|
231
234
|
idleDueIn(now) {
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ 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, useGraineField, useGraineClient, setDefaultClient, 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, useGraineInit, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
|
|
8
8
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
9
|
+
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, type GraineFabProps, type GraineTouchCaptureProps, } from "./react-native/fab.js";
|
|
10
|
+
export { UNNAMED_TAP } from "./protocol.js";
|
|
9
11
|
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
|
|
10
12
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
11
13
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
package/dist/index.js
CHANGED
|
@@ -3,8 +3,10 @@ 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, useGraineField, useGraineClient, setDefaultClient, 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, useGraineInit, } from "./react-native/index.js";
|
|
7
7
|
export { requestMicrophonePermission } from "./react-native/permissions.js";
|
|
8
|
+
export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, } from "./react-native/fab.js";
|
|
9
|
+
export { UNNAMED_TAP } from "./protocol.js";
|
|
8
10
|
export { resolveNativeAppearance, } from "./react-native/appearance.js";
|
|
9
11
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
10
12
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -58,3 +58,4 @@ export interface AppActionResultFrame extends AppActionResult {
|
|
|
58
58
|
export type ActionHandler = (args: Record<string, any>) => AppActionResult | void | Promise<AppActionResult | void>;
|
|
59
59
|
export declare const ACTION_DEADLINE_MS = 6000;
|
|
60
60
|
export declare const CONTEXT_THROTTLE_MS = 400;
|
|
61
|
+
export declare const UNNAMED_TAP = "tap:screen";
|
package/dist/protocol.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React, { type ComponentType } from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
export declare const TAP_MAX_MOVE_PX = 12;
|
|
4
|
+
export declare const TAP_MAX_MS = 400;
|
|
5
|
+
export interface GraineTouchCaptureProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
}
|
|
10
|
+
export declare function GraineTouchCapture({ children, enabled, style }: GraineTouchCaptureProps): React.JSX.Element;
|
|
11
|
+
export interface GraineFabProps {
|
|
12
|
+
webView: ComponentType<any>;
|
|
13
|
+
position?: "bottom-right" | "bottom-left";
|
|
14
|
+
size?: number;
|
|
15
|
+
accent?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
onOpen?: () => void;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function GraineFab({ webView, position, size, accent: accentProp, label, onOpen, onClose }: GraineFabProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useContext, useRef, useState } from "react";
|
|
3
|
+
import { Image, Platform, Pressable, Text, View } from "react-native";
|
|
4
|
+
let FullWindowOverlay = null;
|
|
5
|
+
try {
|
|
6
|
+
if (Platform.OS === "ios" && typeof require === "function") {
|
|
7
|
+
FullWindowOverlay = require("react-native-screens")?.FullWindowOverlay ?? null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
FullWindowOverlay = null;
|
|
12
|
+
}
|
|
13
|
+
function AboveNativeScreens({ children }) {
|
|
14
|
+
if (!FullWindowOverlay)
|
|
15
|
+
return _jsx(_Fragment, { children: children });
|
|
16
|
+
const Overlay = FullWindowOverlay;
|
|
17
|
+
return _jsx(Overlay, { children: _jsx(View, { style: { flex: 1 }, pointerEvents: "box-none", children: children }) });
|
|
18
|
+
}
|
|
19
|
+
import { Ctx, useGraineAgent, useGraineClient } from "./index.js";
|
|
20
|
+
import { GraineVoiceLauncher } from "./voice-launcher.js";
|
|
21
|
+
import { GraineAgentBar } from "./ui.js";
|
|
22
|
+
import { resolveNativeAppearance } from "./appearance.js";
|
|
23
|
+
import { UNNAMED_TAP } from "../protocol.js";
|
|
24
|
+
export const TAP_MAX_MOVE_PX = 12;
|
|
25
|
+
export const TAP_MAX_MS = 400;
|
|
26
|
+
export function GraineTouchCapture({ children, enabled = true, style }) {
|
|
27
|
+
const client = useGraineClient();
|
|
28
|
+
const ctx = useContext(Ctx);
|
|
29
|
+
const start = useRef(null);
|
|
30
|
+
const onTouchStart = useCallback((e) => {
|
|
31
|
+
try {
|
|
32
|
+
const t = e?.nativeEvent;
|
|
33
|
+
if (!t || (t.touches?.length ?? 1) > 1) {
|
|
34
|
+
start.current = null;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
start.current = { x: t.pageX, y: t.pageY, at: Date.now() };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
start.current = null;
|
|
41
|
+
}
|
|
42
|
+
}, []);
|
|
43
|
+
const onTouchEnd = useCallback((e) => {
|
|
44
|
+
try {
|
|
45
|
+
const s = start.current;
|
|
46
|
+
start.current = null;
|
|
47
|
+
if (!enabled || !s)
|
|
48
|
+
return;
|
|
49
|
+
const t = e?.nativeEvent;
|
|
50
|
+
if (!t)
|
|
51
|
+
return;
|
|
52
|
+
if (Math.hypot(t.pageX - s.x, t.pageY - s.y) > TAP_MAX_MOVE_PX)
|
|
53
|
+
return;
|
|
54
|
+
if (Date.now() - s.at > TAP_MAX_MS)
|
|
55
|
+
return;
|
|
56
|
+
if (!client.getScreen())
|
|
57
|
+
return;
|
|
58
|
+
if (ctx && !ctx.launcherVisible)
|
|
59
|
+
return;
|
|
60
|
+
client.track(UNNAMED_TAP, { x: Math.round(t.pageX), y: Math.round(t.pageY) }, { coalesce: true });
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
}, [client, ctx, enabled]);
|
|
64
|
+
return (_jsx(View, { style: [{ flex: 1 }, style], onTouchStart: onTouchStart, onTouchEnd: onTouchEnd, children: children }));
|
|
65
|
+
}
|
|
66
|
+
export function GraineFab({ webView, position = "bottom-right", size = 56, accent: accentProp, label, onOpen, onClose }) {
|
|
67
|
+
const { launcherVisible, launcherInset, appearance, error } = useGraineAgent();
|
|
68
|
+
const [open, setOpen] = useState(false);
|
|
69
|
+
const dashboard = resolveNativeAppearance(appearance, { accent: "#318CE7", heading: "Assistant", radius: 18 });
|
|
70
|
+
const accent = accentProp ?? dashboard.accent;
|
|
71
|
+
const name = dashboard.heading;
|
|
72
|
+
if (error || !launcherVisible)
|
|
73
|
+
return null;
|
|
74
|
+
if (open) {
|
|
75
|
+
return (_jsx(AboveNativeScreens, { children: _jsx(GraineVoiceLauncher, { webView: webView, autoStart: true, onEnded: () => { setOpen(false); onClose?.(); }, children: () => _jsx(GraineAgentBar, { accent: accent, name: name }) }) }));
|
|
76
|
+
}
|
|
77
|
+
const side = position === "bottom-left" ? { left: launcherInset.right } : { right: launcherInset.right };
|
|
78
|
+
return (_jsx(AboveNativeScreens, { children: _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label ?? `Talk to ${name}`, onPress: () => { setOpen(true); onOpen?.(); }, style: ({ pressed }) => ({
|
|
79
|
+
position: "absolute", bottom: launcherInset.bottom, ...side,
|
|
80
|
+
width: size, height: size, borderRadius: size / 2, backgroundColor: accent,
|
|
81
|
+
alignItems: "center", justifyContent: "center",
|
|
82
|
+
opacity: pressed ? 0.85 : 1,
|
|
83
|
+
elevation: 6, shadowColor: "#000", shadowOpacity: 0.25, shadowRadius: 8, shadowOffset: { width: 0, height: 4 },
|
|
84
|
+
}), children: dashboard.logoUrl ? (_jsx(Image, { source: { uri: dashboard.logoUrl }, style: { width: size * 0.6, height: size * 0.6, borderRadius: size * 0.3 } })) : (_jsx(Text, { style: { color: "#fff", fontSize: size * 0.4, fontWeight: "700" }, children: name.trim().charAt(0).toUpperCase() || "•" })) }) }));
|
|
85
|
+
}
|
|
@@ -80,6 +80,7 @@ export interface Turn {
|
|
|
80
80
|
text: string;
|
|
81
81
|
live?: boolean;
|
|
82
82
|
}
|
|
83
|
+
export declare const Ctx: React.Context<GraineContextValue | null>;
|
|
83
84
|
export declare function setDefaultClient(client: GraineInAppClient | null): void;
|
|
84
85
|
export declare function useGraineClient(): GraineInAppClient;
|
|
85
86
|
export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
@@ -93,11 +94,15 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
|
|
|
93
94
|
} | null;
|
|
94
95
|
appVersion?: string;
|
|
95
96
|
friction?: FrictionConfig;
|
|
97
|
+
fab?: Omit<import("./fab.js").GraineFabProps, "webView"> & {
|
|
98
|
+
webView: React.ComponentType<any>;
|
|
99
|
+
};
|
|
100
|
+
captureTaps?: boolean;
|
|
96
101
|
includeScreens?: string[];
|
|
97
102
|
launcherDelayMs?: number;
|
|
98
103
|
visibility?: LauncherVisibility;
|
|
99
104
|
}
|
|
100
|
-
export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, appVersion, friction, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
105
|
+
export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
|
|
101
106
|
export declare function useGraineAgent(): GraineContextValue;
|
|
102
107
|
export declare function useGraineEvents(handler: (e: GraineEvent) => void): void;
|
|
103
108
|
export declare function useGraineTrack(): (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -120,6 +125,10 @@ export declare function useGraineWidget(): {
|
|
|
120
125
|
dismiss: () => void;
|
|
121
126
|
};
|
|
122
127
|
export declare function useGraineAction(name: string, handler: ActionHandler): void;
|
|
128
|
+
export declare function useGraineInit(): {
|
|
129
|
+
isInitialized: boolean;
|
|
130
|
+
error: string | null;
|
|
131
|
+
};
|
|
123
132
|
export declare function useGraineReady(): {
|
|
124
133
|
ready: boolean;
|
|
125
134
|
error: string | null;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { AppState } from "react-native";
|
|
4
4
|
import { GraineInAppClient } from "../client.js";
|
|
5
5
|
import { VoiceSession } from "../voice.js";
|
|
6
|
+
import { GraineFab, GraineTouchCapture } from "./fab.js";
|
|
6
7
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
7
|
-
const Ctx = createContext(null);
|
|
8
|
+
export const Ctx = createContext(null);
|
|
8
9
|
let defaultClient = null;
|
|
9
10
|
export function setDefaultClient(client) {
|
|
10
11
|
defaultClient = client;
|
|
@@ -17,7 +18,7 @@ export function useGraineClient() {
|
|
|
17
18
|
}
|
|
18
19
|
return client;
|
|
19
20
|
}
|
|
20
|
-
export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
|
|
21
|
+
export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
|
|
21
22
|
const clientRef = useRef(null);
|
|
22
23
|
if (!clientRef.current) {
|
|
23
24
|
clientRef.current =
|
|
@@ -268,7 +269,9 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
268
269
|
identify: (user) => client.identify(user),
|
|
269
270
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
270
271
|
launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady]);
|
|
271
|
-
|
|
272
|
+
const wantsCapture = captureTaps ?? !!fab;
|
|
273
|
+
const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
|
|
274
|
+
return _jsx(Ctx.Provider, { value: value, children: body });
|
|
272
275
|
}
|
|
273
276
|
function useGraine() {
|
|
274
277
|
const ctx = useContext(Ctx);
|
|
@@ -346,8 +349,12 @@ export function useGraineScreen(context) {
|
|
|
346
349
|
const serialised = JSON.stringify(context ?? null);
|
|
347
350
|
useEffect(() => {
|
|
348
351
|
client.setScreen(context);
|
|
349
|
-
return () => client.clearScreen(context?.screen);
|
|
350
352
|
}, [client, serialised]);
|
|
353
|
+
const reported = useRef(context?.screen);
|
|
354
|
+
reported.current = context?.screen;
|
|
355
|
+
useEffect(() => () => {
|
|
356
|
+
client.clearScreen(reported.current);
|
|
357
|
+
}, [client]);
|
|
351
358
|
}
|
|
352
359
|
export function useGraineField(name, field) {
|
|
353
360
|
const client = useGraineClient();
|
|
@@ -406,6 +413,10 @@ export function useGraineAction(name, handler) {
|
|
|
406
413
|
handlerRef.current = handler;
|
|
407
414
|
useEffect(() => client.registerAction(name, (args) => handlerRef.current(args)), [client, name]);
|
|
408
415
|
}
|
|
416
|
+
export function useGraineInit() {
|
|
417
|
+
const { sessionReady, error } = useGraineAgent();
|
|
418
|
+
return { isInitialized: sessionReady, error };
|
|
419
|
+
}
|
|
409
420
|
export function useGraineReady() {
|
|
410
421
|
const { sessionReady, error } = useGraineAgent();
|
|
411
422
|
return { ready: sessionReady, error };
|
package/package.json
CHANGED