@graineai/inapp-react-native 0.11.1 → 0.12.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 +56 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react-native/ui.d.ts +12 -1
- package/dist/react-native/ui.js +52 -25
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -318,6 +318,62 @@ part. Everything else is unchanged.
|
|
|
318
318
|
|
|
319
319
|
---
|
|
320
320
|
|
|
321
|
+
## Making the bar yours
|
|
322
|
+
|
|
323
|
+
Three levels, and most apps stop at the first.
|
|
324
|
+
|
|
325
|
+
**1. The dashboard.** Accent, title and logo come from Appearance on the agent
|
|
326
|
+
and reach every surface without an app release. Set the colour in Graine and it
|
|
327
|
+
changes here. This is the level a brand team should be working at.
|
|
328
|
+
|
|
329
|
+
**2. Props, when the dashboard cannot express it.** Any prop passed wins over
|
|
330
|
+
the dashboard, so an app that needs something per-build still can.
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
<GraineAgentBar
|
|
334
|
+
scheme="auto" // "dark" | "light" | "auto" (default) — follows the device
|
|
335
|
+
accent="#318CE7" // overrides the dashboard
|
|
336
|
+
name="Ring"
|
|
337
|
+
bottomInset={110} // clear of your tab bar
|
|
338
|
+
captionsOn
|
|
339
|
+
/>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`scheme` defaults to `auto` rather than dark: an agent that ignores the system
|
|
343
|
+
setting is the one piece of the screen that looks like it came from somewhere
|
|
344
|
+
else. Pin it only if your app pins its own.
|
|
345
|
+
|
|
346
|
+
**3. The palette, when your brand is not one of the two.** `theme` is merged
|
|
347
|
+
over the scheme's colours, so pass the one you care about:
|
|
348
|
+
|
|
349
|
+
```tsx
|
|
350
|
+
import type { GraineBarTheme } from "@graineai/inapp-react-native";
|
|
351
|
+
|
|
352
|
+
<GraineAgentBar theme={{ surface: "#0E1116", border: "#232A33" }} />
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
| Key | Where it lands |
|
|
356
|
+
|---|---|
|
|
357
|
+
| `surface` | The bar, the expanded panel, the caption bubble |
|
|
358
|
+
| `border` | Hairline around all three |
|
|
359
|
+
| `text` | The agent's name, its replies, what the customer types |
|
|
360
|
+
| `sub` | Status line, placeholder, empty state |
|
|
361
|
+
| `chip` | Quiet round buttons, and the agent's message bubbles |
|
|
362
|
+
| `onAccent` | Text and glyphs sitting on the accent, and on `danger` |
|
|
363
|
+
| `danger` | Muted — the one state worth its own colour |
|
|
364
|
+
|
|
365
|
+
**4. Or draw your own.** Everything the bar does is built on the public hooks,
|
|
366
|
+
so an app with a design system should ignore the component entirely:
|
|
367
|
+
|
|
368
|
+
```tsx
|
|
369
|
+
const { connected, muted, setMuted, messages, send, caption } = useGraineAgent();
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
That is the expected path for anyone with a brand team, and nothing else in the
|
|
373
|
+
SDK depends on this component.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
321
377
|
## Session lifecycle
|
|
322
378
|
|
|
323
379
|
One session per client, and it follows the app.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,6 @@ export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RAT
|
|
|
4
4
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio";
|
|
5
5
|
export { addMaskRule, maskDeep, maskString } from "./mask";
|
|
6
6
|
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index";
|
|
7
|
-
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps } from "./react-native/ui";
|
|
7
|
+
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui";
|
|
8
8
|
export { GraineVoiceLauncher, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher";
|
|
9
9
|
export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,6 @@ export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RAT
|
|
|
3
3
|
export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio";
|
|
4
4
|
export { addMaskRule, maskDeep, maskString } from "./mask";
|
|
5
5
|
export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineTrack, useGraineTap, } from "./react-native/index";
|
|
6
|
-
export { GraineAgentBar, GraineLauncher } from "./react-native/ui";
|
|
6
|
+
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui";
|
|
7
7
|
export { GraineVoiceLauncher, } from "./react-native/voice-launcher";
|
|
8
8
|
export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation";
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
export interface GraineBarTheme {
|
|
3
|
+
surface: string;
|
|
4
|
+
border: string;
|
|
5
|
+
text: string;
|
|
6
|
+
sub: string;
|
|
7
|
+
chip: string;
|
|
8
|
+
onAccent: string;
|
|
9
|
+
danger: string;
|
|
10
|
+
}
|
|
2
11
|
export interface GraineAgentBarProps {
|
|
3
12
|
accent?: string;
|
|
4
13
|
name?: string;
|
|
@@ -6,7 +15,9 @@ export interface GraineAgentBarProps {
|
|
|
6
15
|
bottomInset?: number;
|
|
7
16
|
hidden?: boolean;
|
|
8
17
|
captionsOn?: boolean;
|
|
18
|
+
scheme?: "dark" | "light" | "auto";
|
|
19
|
+
theme?: Partial<GraineBarTheme>;
|
|
9
20
|
}
|
|
10
|
-
export declare function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset, hidden, captionsOn, }: GraineAgentBarProps): React.JSX.Element | null;
|
|
21
|
+
export declare function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset, hidden, captionsOn, scheme, theme: themeProp, }: GraineAgentBarProps): React.JSX.Element | null;
|
|
11
22
|
export declare const GraineLauncher: typeof GraineAgentBar;
|
|
12
23
|
export type GraineLauncherProps = GraineAgentBarProps;
|
package/dist/react-native/ui.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
|
|
4
4
|
import { useGraineAgent } from "./index";
|
|
5
|
-
|
|
5
|
+
const DARK = {
|
|
6
|
+
surface: "#17161A",
|
|
7
|
+
border: "rgba(255,255,255,0.09)",
|
|
8
|
+
text: "#F2F3F5",
|
|
9
|
+
sub: "#9aa0a6",
|
|
10
|
+
chip: "rgba(255,255,255,0.08)",
|
|
11
|
+
onAccent: "#FFFFFF",
|
|
12
|
+
danger: "#E5484D",
|
|
13
|
+
};
|
|
14
|
+
const LIGHT = {
|
|
15
|
+
surface: "#FFFFFF",
|
|
16
|
+
border: "rgba(0,0,0,0.10)",
|
|
17
|
+
text: "#16181C",
|
|
18
|
+
sub: "#61666D",
|
|
19
|
+
chip: "rgba(0,0,0,0.05)",
|
|
20
|
+
onAccent: "#FFFFFF",
|
|
21
|
+
danger: "#D32F2F",
|
|
22
|
+
};
|
|
23
|
+
export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, scheme = "auto", theme: themeProp, }) {
|
|
6
24
|
const { connected, connecting, error, messages, send, muted, setMuted, caption, agentSpeaking, launcherVisible, launcherInset, appearance, } = useGraineAgent();
|
|
7
25
|
const accent = accentProp ?? appearance?.accent ?? "#318CE7";
|
|
8
26
|
const name = nameProp ?? appearance?.title ?? "Assistant";
|
|
9
27
|
const avatarUrl = avatarProp ?? appearance?.logo ?? undefined;
|
|
28
|
+
const deviceScheme = useColorScheme();
|
|
29
|
+
const styles = useMemo(() => {
|
|
30
|
+
const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
|
|
31
|
+
return makeStyles(themeProp ? { ...base, ...themeProp } : base);
|
|
32
|
+
}, [scheme, deviceScheme, themeProp]);
|
|
10
33
|
const [expanded, setExpanded] = useState(false);
|
|
11
34
|
const [captions, setCaptions] = useState(captionsOn);
|
|
12
35
|
const [draft, setDraft] = useState("");
|
|
@@ -18,7 +41,7 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
18
41
|
: muted ? "muted"
|
|
19
42
|
: agentSpeaking || (last?.role === "agent" && last.live) ? "speaking"
|
|
20
43
|
: connected ? "listening"
|
|
21
|
-
: "
|
|
44
|
+
: "offline";
|
|
22
45
|
useEffect(() => {
|
|
23
46
|
if (expanded)
|
|
24
47
|
requestAnimationFrame(() => scrollRef.current?.scrollToEnd({ animated: true }));
|
|
@@ -46,51 +69,55 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
|
|
|
46
69
|
], 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: [
|
|
47
70
|
styles.bubble,
|
|
48
71
|
m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
|
|
49
|
-
], 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:
|
|
72
|
+
], 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 })] }), connecting ? _jsx(ActivityIndicator, { size: "small", color: accent }) : null, _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 ? "⌄" : "⌃" }) })] })] }));
|
|
50
73
|
}
|
|
51
74
|
export const GraineLauncher = GraineAgentBar;
|
|
52
|
-
const
|
|
75
|
+
const makeStyles = (t) => StyleSheet.create({
|
|
53
76
|
root: { position: "absolute", left: 12, right: 12, zIndex: 900 },
|
|
54
77
|
panel: {
|
|
55
|
-
backgroundColor:
|
|
78
|
+
backgroundColor: t.surface, borderRadius: 22, borderWidth: 1, borderColor: t.border,
|
|
56
79
|
marginBottom: 8, overflow: "hidden",
|
|
57
80
|
},
|
|
58
81
|
transcript: { maxHeight: 240 },
|
|
59
82
|
transcriptInner: { padding: 14, gap: 8 },
|
|
60
|
-
empty: { color:
|
|
83
|
+
empty: { color: t.sub, fontSize: 14, lineHeight: 21 },
|
|
61
84
|
bubble: { maxWidth: "88%", paddingHorizontal: 12, paddingVertical: 9, borderRadius: 16 },
|
|
62
|
-
agentBubble: { alignSelf: "flex-start", backgroundColor:
|
|
85
|
+
agentBubble: { alignSelf: "flex-start", backgroundColor: t.chip, borderBottomLeftRadius: 5 },
|
|
63
86
|
customerBubble: { alignSelf: "flex-end", borderBottomRightRadius: 5 },
|
|
64
|
-
agentText: { color:
|
|
65
|
-
customerText: { color:
|
|
87
|
+
agentText: { color: t.text, fontSize: 14.5, lineHeight: 21 },
|
|
88
|
+
customerText: { color: t.onAccent, fontSize: 14.5, lineHeight: 21 },
|
|
66
89
|
composer: { flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 12, paddingBottom: 12 },
|
|
67
90
|
input: {
|
|
68
|
-
flex: 1, borderWidth: 1, borderColor:
|
|
69
|
-
paddingHorizontal: 12, paddingVertical: Platform.OS === "ios" ? 10 : 7, color:
|
|
91
|
+
flex: 1, borderWidth: 1, borderColor: t.border, borderRadius: 16,
|
|
92
|
+
paddingHorizontal: 12, paddingVertical: Platform.OS === "ios" ? 10 : 7, color: t.text, fontSize: 14.5,
|
|
70
93
|
},
|
|
71
94
|
send: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
|
|
72
|
-
sendLabel: { color:
|
|
95
|
+
sendLabel: { color: t.onAccent, fontSize: 17, lineHeight: 20 },
|
|
73
96
|
bar: {
|
|
74
97
|
flexDirection: "row", alignItems: "center", gap: 11,
|
|
75
|
-
backgroundColor:
|
|
98
|
+
backgroundColor: t.surface, borderRadius: 26, borderWidth: 1, borderColor: t.border,
|
|
76
99
|
paddingHorizontal: 8, paddingVertical: 8,
|
|
77
100
|
shadowColor: "#000", shadowOpacity: 0.3, shadowRadius: 16, shadowOffset: { width: 0, height: 6 }, elevation: 12,
|
|
78
101
|
},
|
|
79
102
|
avatar: { width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center" },
|
|
80
|
-
avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor:
|
|
103
|
+
avatarImage: { width: 38, height: 38, borderRadius: 19, backgroundColor: t.chip },
|
|
81
104
|
captionWrap: {
|
|
82
|
-
backgroundColor:
|
|
105
|
+
backgroundColor: t.surface, borderRadius: 18, borderWidth: 1, borderColor: t.border,
|
|
106
|
+
paddingHorizontal: 14, paddingVertical: 11, marginBottom: 8,
|
|
107
|
+
shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 12, shadowOffset: { width: 0, height: 4 }, elevation: 6,
|
|
83
108
|
},
|
|
84
|
-
caption: { color:
|
|
109
|
+
caption: { color: t.text, fontSize: 15, lineHeight: 21, fontWeight: "600" },
|
|
85
110
|
captionLive: { opacity: 0.8 },
|
|
86
|
-
glyph: { color:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
glyph: { color: t.text, fontSize: 13, fontWeight: "800" },
|
|
112
|
+
glyphOnAccent: { color: t.onAccent },
|
|
113
|
+
placeholder: { color: t.sub },
|
|
114
|
+
muted: { backgroundColor: t.danger },
|
|
115
|
+
avatarGlyph: { color: t.onAccent, fontSize: 17, lineHeight: 20 },
|
|
116
|
+
name: { color: t.text, fontSize: 15, fontWeight: "800" },
|
|
117
|
+
status: { color: t.sub, fontSize: 12.5, marginTop: 1 },
|
|
91
118
|
round: {
|
|
92
119
|
width: 38, height: 38, borderRadius: 19, alignItems: "center", justifyContent: "center",
|
|
93
|
-
backgroundColor:
|
|
120
|
+
backgroundColor: t.chip,
|
|
94
121
|
},
|
|
95
|
-
chevron: { color:
|
|
122
|
+
chevron: { color: t.text, fontSize: 16, lineHeight: 18 },
|
|
96
123
|
});
|
package/package.json
CHANGED