@graineai/inapp-react-native 0.26.2 → 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 +3 -1
- 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 +11 -4
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -885,7 +885,9 @@ whole translation:
|
|
|
885
885
|
| `continuous` / `perScreen` | same |
|
|
886
886
|
| `perScreen` / `oncePerGroupEntry` / `oncePerAppSession` | same |
|
|
887
887
|
| `EmbedButtonInset` | `LauncherInset` — same shape |
|
|
888
|
-
| `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()` |
|
|
889
891
|
| `Embed.Event(USER_DATA, { app_user_id, data })` | `useGraineIdentify()` / `client.identify({ id, name, … })` |
|
|
890
892
|
| `Embed.Event(SCREEN_STATE, { screen, data })` | `useGraineScreen({ screen, fields })` |
|
|
891
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);
|
|
@@ -410,6 +413,10 @@ export function useGraineAction(name, handler) {
|
|
|
410
413
|
handlerRef.current = handler;
|
|
411
414
|
useEffect(() => client.registerAction(name, (args) => handlerRef.current(args)), [client, name]);
|
|
412
415
|
}
|
|
416
|
+
export function useGraineInit() {
|
|
417
|
+
const { sessionReady, error } = useGraineAgent();
|
|
418
|
+
return { isInitialized: sessionReady, error };
|
|
419
|
+
}
|
|
413
420
|
export function useGraineReady() {
|
|
414
421
|
const { sessionReady, error } = useGraineAgent();
|
|
415
422
|
return { ready: sessionReady, error };
|
package/package.json
CHANGED