@graineai/inapp-react-native 0.33.5 β 0.33.6
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/protocol.js +1 -1
- package/dist/react-native/index.d.ts +1 -0
- package/dist/react-native/index.js +32 -4
- package/dist/react-native/ui.js +6 -3
- package/dist/react-native/voice-launcher.js +1 -0
- package/dist/react-native/widget.js +9 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type { WidgetSelectAction } from "./protocol.js";
|
|
|
11
11
|
export { GraineWidget } from "./react-native/widget.js";
|
|
12
12
|
export { GraineLoader, useGraineLoader } from "./react-native/loader.js";
|
|
13
13
|
export type { GraineLoaderProps } from "./react-native/loader.js";
|
|
14
|
-
export { PENDING_WIDGET_MS } from "./react-native/index.js";
|
|
14
|
+
export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
|
|
15
15
|
export type { GraineWidgetProps, GraineWidgetTheme } from "./react-native/widget.js";
|
|
16
16
|
export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
|
|
17
17
|
export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type FrictionConfig, type FrictionScreenRules, type FrictionNudge, type FrictionSignal, type FrictionKind, } from "./friction.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { normaliseSelectAction, fillSelectArgs, fillSelectTemplate, selectCall,
|
|
|
5
5
|
export { WIDGET_SELECTS, MAX_SELECT_DEFAULT, manySummary } from "./protocol.js";
|
|
6
6
|
export { GraineWidget } from "./react-native/widget.js";
|
|
7
7
|
export { GraineLoader, useGraineLoader } from "./react-native/loader.js";
|
|
8
|
-
export { PENDING_WIDGET_MS } from "./react-native/index.js";
|
|
8
|
+
export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
|
|
9
9
|
export { GraineInAppClient, } from "./client.js";
|
|
10
10
|
export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "./friction.js";
|
|
11
11
|
export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
|
package/dist/protocol.js
CHANGED
|
@@ -204,7 +204,7 @@ export function resolveLoader(style) {
|
|
|
204
204
|
px: LOADER_PX[s.size ?? "medium"],
|
|
205
205
|
periodMs: LOADER_PERIOD[s.speed ?? "normal"],
|
|
206
206
|
text: { ...DEFAULT_LOADER_TEXT, ...(s.text ?? {}) },
|
|
207
|
-
skeleton: s.skeleton ??
|
|
207
|
+
skeleton: s.skeleton ?? false,
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
const ACTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
@@ -10,6 +10,7 @@ export { widgetSurface } from "../protocol.js";
|
|
|
10
10
|
import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
|
|
11
11
|
export const PENDING_WIDGET_MS = 12000;
|
|
12
12
|
export const AWAITING_REPLY_MS = 12000;
|
|
13
|
+
export const MIN_LOADER_MS = 450;
|
|
13
14
|
export const Ctx = createContext(null);
|
|
14
15
|
let defaultClient = null;
|
|
15
16
|
export function setDefaultClient(client) {
|
|
@@ -128,6 +129,12 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
128
129
|
openRef.current = open;
|
|
129
130
|
useEffect(() => {
|
|
130
131
|
const offs = [
|
|
132
|
+
client.on("call_state", (s) => {
|
|
133
|
+
if (client.isConnected)
|
|
134
|
+
return;
|
|
135
|
+
setConnected(!!s?.connected);
|
|
136
|
+
setConnecting(!!s?.connecting);
|
|
137
|
+
}),
|
|
131
138
|
client.on("open", () => {
|
|
132
139
|
setConnected(true);
|
|
133
140
|
setConnecting(false);
|
|
@@ -406,6 +413,8 @@ export function useGraineWidget() {
|
|
|
406
413
|
const [answered, setAnswered] = useState(null);
|
|
407
414
|
const [loading, setLoading] = useState(null);
|
|
408
415
|
const loadingTimer = useRef(null);
|
|
416
|
+
const shownAt = useRef(null);
|
|
417
|
+
const minTimer = useRef(null);
|
|
409
418
|
const [awaitingReply, setAwaitingReply] = useState(false);
|
|
410
419
|
const [closed, setClosed] = useState(null);
|
|
411
420
|
const awaitingTimer = useRef(null);
|
|
@@ -414,6 +423,8 @@ export function useGraineWidget() {
|
|
|
414
423
|
clearTimeout(loadingTimer.current);
|
|
415
424
|
if (awaitingTimer.current)
|
|
416
425
|
clearTimeout(awaitingTimer.current);
|
|
426
|
+
if (minTimer.current)
|
|
427
|
+
clearTimeout(minTimer.current);
|
|
417
428
|
}, []);
|
|
418
429
|
useEffect(() => {
|
|
419
430
|
const done = () => {
|
|
@@ -439,6 +450,7 @@ export function useGraineWidget() {
|
|
|
439
450
|
if (pending)
|
|
440
451
|
setWidget((cur) => (cur && cur.id === pending.id ? cur : pending));
|
|
441
452
|
const offPending = client.on("widget_pending", (hint) => {
|
|
453
|
+
shownAt.current = Date.now();
|
|
442
454
|
setLoading({ id: String(hint?.widget_id ?? ""), kind: String(hint?.kind ?? "") });
|
|
443
455
|
if (loadingTimer.current)
|
|
444
456
|
clearTimeout(loadingTimer.current);
|
|
@@ -452,10 +464,21 @@ export function useGraineWidget() {
|
|
|
452
464
|
clearTimeout(loadingTimer.current);
|
|
453
465
|
loadingTimer.current = null;
|
|
454
466
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
467
|
+
const show = () => {
|
|
468
|
+
shownAt.current = null;
|
|
469
|
+
setLoading(null);
|
|
470
|
+
setClosed(null);
|
|
471
|
+
setWidget(next);
|
|
472
|
+
setAnswered(null);
|
|
473
|
+
};
|
|
474
|
+
const seen = shownAt.current === null ? MIN_LOADER_MS : Date.now() - shownAt.current;
|
|
475
|
+
if (seen >= MIN_LOADER_MS)
|
|
476
|
+
show();
|
|
477
|
+
else {
|
|
478
|
+
if (minTimer.current)
|
|
479
|
+
clearTimeout(minTimer.current);
|
|
480
|
+
minTimer.current = setTimeout(show, MIN_LOADER_MS - seen);
|
|
481
|
+
}
|
|
459
482
|
});
|
|
460
483
|
return () => { offPending(); offWidget(); };
|
|
461
484
|
}, [client, adopt]);
|
|
@@ -490,6 +513,11 @@ export function useGraineWidget() {
|
|
|
490
513
|
awaitingTimer.current = setTimeout(() => setAwaitingReply(false), AWAITING_REPLY_MS);
|
|
491
514
|
}, [client, widget]);
|
|
492
515
|
const dismiss = useCallback(() => {
|
|
516
|
+
if (minTimer.current) {
|
|
517
|
+
clearTimeout(minTimer.current);
|
|
518
|
+
minTimer.current = null;
|
|
519
|
+
}
|
|
520
|
+
shownAt.current = null;
|
|
493
521
|
if (widget) {
|
|
494
522
|
client.reportEvent({
|
|
495
523
|
name: "component_dismissed",
|
package/dist/react-native/ui.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
|
|
4
4
|
import { useGraineAgent } from "./index.js";
|
|
5
|
+
import { GraineLoader } from "./loader.js";
|
|
5
6
|
import { resolveNativeAppearance } from "./appearance.js";
|
|
6
7
|
const DARK = {
|
|
7
8
|
surface: "#17161A",
|
|
@@ -40,6 +41,8 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
40
41
|
const [draft, setDraft] = useState("");
|
|
41
42
|
const scrollRef = useRef(null);
|
|
42
43
|
const last = messages[messages.length - 1];
|
|
44
|
+
const waiting = connecting;
|
|
45
|
+
const waitingMoment = "connecting";
|
|
43
46
|
const status = error
|
|
44
47
|
? "unavailable"
|
|
45
48
|
: connecting ? "connectingβ¦"
|
|
@@ -74,7 +77,7 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
74
77
|
], children: [expanded && (_jsxs(View, { style: styles.panel, children: [_jsxs(ScrollView, { ref: scrollRef, style: styles.transcript, contentContainerStyle: styles.transcriptInner, showsVerticalScrollIndicator: false, children: [messages.length === 0 && (_jsx(Text, { style: styles.empty, children: "I can see what you're working on \u2014 ask me anything, or I'll step in if you get stuck." })), messages.map((m, i) => (_jsx(View, { style: [
|
|
75
78
|
styles.bubble,
|
|
76
79
|
m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
|
|
77
|
-
], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), _jsx(Text, { style: styles.status, children: status })] }),
|
|
80
|
+
], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), waiting ? (_jsx(GraineLoader, { moment: waitingMoment, size: 15 })) : (_jsx(Text, { style: styles.status, children: status }))] }), waiting ? null : (_jsxs(_Fragment, { children: [_jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && styles.glyphOnAccent], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && styles.glyphOnAccent], children: muted ? "π" : "π" }) }), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "β" : "β" }) })] }))] })] }));
|
|
78
81
|
}
|
|
79
82
|
export const GraineLauncher = GraineAgentBar;
|
|
80
83
|
const makeStyles = (t) => StyleSheet.create({
|
|
@@ -236,6 +236,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
236
236
|
endedNotifiedRef.current = false;
|
|
237
237
|
setConnected(!!msg.connected);
|
|
238
238
|
setConnecting(!!msg.connecting);
|
|
239
|
+
client.emit?.("call_state", { connected: !!msg.connected, connecting: !!msg.connecting });
|
|
239
240
|
if (!msg.connected && !msg.connecting)
|
|
240
241
|
startedRef.current = false;
|
|
241
242
|
onCallState?.({ connected: !!msg.connected, connecting: !!msg.connecting });
|
|
@@ -123,16 +123,19 @@ export function GraineWidget(props) {
|
|
|
123
123
|
const chosen = answered ? (_jsx(AnsweredLine, { answered: answered, theme: theme, accent: accent, radius: radius, awaiting: awaitingReply, onClear: dismiss })) : null;
|
|
124
124
|
if (!widget && loading) {
|
|
125
125
|
const loader = resolveLoader(appearance?.loader);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
126
|
+
const kindSurface = widgetSurface(loading.kind);
|
|
127
|
+
const pendingSurface = kindSurface;
|
|
128
|
+
const big = pendingSurface === "screen" ? 34 : pendingSurface === "sheet" ? 26 : undefined;
|
|
129
|
+
const mark = (_jsx(View, { style: { alignItems: "center", gap: 10 }, children: _jsx(GraineLoader, { moment: "component", size: big, stacked: true }) }));
|
|
130
|
+
const bone = (w, h) => (_jsx(View, { style: { width: w, height: h, borderRadius: 8, backgroundColor: tint(theme.border, 0.6) } }));
|
|
131
|
+
const bones = (rows) => (_jsx(View, { style: { gap: 10, opacity: 0.55 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: Array.from({ length: rows }, (_, i) => (_jsxs(View, { style: { borderRadius: Math.max(10, radius - 6), borderWidth: 1, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))) }));
|
|
129
132
|
if (pendingSurface === "screen") {
|
|
130
|
-
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children:
|
|
133
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface, paddingTop: 56, paddingHorizontal: 18 }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: { width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center", backgroundColor: tint(theme.border, 0.4) }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "\u00D7" }) }), _jsx(View, { style: { flex: 1, justifyContent: "center", paddingBottom: loader.skeleton ? 24 : 72 }, children: mark }), loader.skeleton ? bones(3) : null] }) })] }));
|
|
131
134
|
}
|
|
132
135
|
if (pendingSurface === "sheet") {
|
|
133
|
-
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children:
|
|
136
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: { backgroundColor: theme.surface, borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius), paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 18 }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), _jsx(View, { style: { paddingVertical: loader.skeleton ? 10 : 34 }, children: mark }), loader.skeleton ? bones(2) : null] })] }) })] }));
|
|
134
137
|
}
|
|
135
|
-
return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical:
|
|
138
|
+
return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical: 14, marginBottom: 8, alignItems: "center" }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
|
|
136
139
|
}
|
|
137
140
|
if (!widget)
|
|
138
141
|
return chosen;
|
package/package.json
CHANGED