@graineai/inapp-react-native 0.39.0 → 0.40.1
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 +113 -113
- package/README.md +13 -5
- package/dist/client.d.ts +6 -0
- package/dist/client.js +52 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.js +11 -0
- package/dist/react-native/appearance.d.ts +2 -0
- package/dist/react-native/appearance.js +2 -1
- package/dist/react-native/fab.js +14 -9
- package/dist/react-native/index.d.ts +1 -0
- package/dist/react-native/index.js +2 -1
- package/dist/react-native/voice-bar.js +31 -11
- package/dist/react-native/voice-launcher.d.ts +5 -0
- package/dist/react-native/voice-launcher.js +44 -3
- package/dist/theme.d.ts +12 -0
- package/dist/theme.js +743 -2
- package/dist/voice.d.ts +2 -0
- package/dist/voice.js +6 -2
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normaliseLogoPlate } from "../theme.js";
|
|
1
2
|
const HEX = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
2
3
|
const RADIUS_CEILING = 48;
|
|
3
4
|
const DEFAULT_ORIGIN = "https://www.graine.ai";
|
|
@@ -25,7 +26,7 @@ export function resolveNativeAppearance(appearance, fallback) {
|
|
|
25
26
|
const origin = String(fallback.origin || DEFAULT_ORIGIN).replace(/\/+$/, "");
|
|
26
27
|
const logoUrl = logoKey ? `${origin}/api/embed/logo?k=${encodeURIComponent(logoKey)}` : null;
|
|
27
28
|
const radius = clamp(numeric(a.radius) ?? numeric(fallback.radius) ?? 18, max);
|
|
28
|
-
return { accent, heading, logoUrl, radius };
|
|
29
|
+
return { accent, heading, logoUrl, logoPlate: normaliseLogoPlate(a.logoPlate), radius };
|
|
29
30
|
}
|
|
30
31
|
const LIGHT = { surface: "#FFFFFF", border: "#E5E4E0", text: "#101318", muted: "#5F6672" };
|
|
31
32
|
const DARK = { surface: "#17181B", border: "#2C2E33", text: "#F5F5F4", muted: "#A0A5AE" };
|
package/dist/react-native/fab.js
CHANGED
|
@@ -7,7 +7,7 @@ import { GraineAgentBar } from "./ui.js";
|
|
|
7
7
|
import { resolveNativeAppearance } from "./appearance.js";
|
|
8
8
|
import { useDesign } from "./use-design.js";
|
|
9
9
|
import { glyphPixels, launcherRadius, onAccentFor, shadowStyle } from "./design-math.js";
|
|
10
|
-
import { glyphUrl } from "../theme.js";
|
|
10
|
+
import { glyphUrl, LOGO_PLATE_WHITE, logoBox, logoPlateLook, readableOn } from "../theme.js";
|
|
11
11
|
import { UNNAMED_TAP } from "../protocol.js";
|
|
12
12
|
function AboveNativeScreens({ overlay: Overlay, children, }) {
|
|
13
13
|
if (!Overlay)
|
|
@@ -69,15 +69,18 @@ export function GraineFab({ webView, position = "bottom-right", size = 56, accen
|
|
|
69
69
|
return null;
|
|
70
70
|
const side = position === "bottom-left" ? { left: launcherInset.right } : { right: launcherInset.right };
|
|
71
71
|
if (!look.classic) {
|
|
72
|
-
return (_jsx(DesignedLauncher, { look: look, size: size, accent: accentProp ?? look.tokens.colors.accent, name: name, label: label ?? `Talk to ${name}`, logoUrl: dashboard.logoUrl, origin: client.opts.baseUrl, place: { position: "absolute", bottom: launcherInset.bottom, ...side }, onPress: () => { setOpen(true); api.start(); onOpen?.(); } }));
|
|
72
|
+
return (_jsx(DesignedLauncher, { look: look, size: size, accent: accentProp ?? look.tokens.colors.accent, name: name, label: label ?? `Talk to ${name}`, logoUrl: dashboard.logoUrl, logoPlate: dashboard.logoPlate, origin: client.opts.baseUrl, place: { position: "absolute", bottom: launcherInset.bottom, ...side }, onPress: () => { setOpen(true); api.start(); onOpen?.(); } }));
|
|
73
73
|
}
|
|
74
74
|
return (_jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: label ?? `Talk to ${name}`, onPress: () => { setOpen(true); api.start(); onOpen?.(); }, style: ({ pressed }) => ({
|
|
75
75
|
position: "absolute", bottom: launcherInset.bottom, ...side,
|
|
76
|
-
width: size, height: size, borderRadius: size / 2,
|
|
76
|
+
width: size, height: size, borderRadius: size / 2,
|
|
77
|
+
...(dashboard.logoUrl && dashboard.logoPlate === "white"
|
|
78
|
+
? { backgroundColor: LOGO_PLATE_WHITE, borderWidth: 1, borderColor: "#E5E7EB" }
|
|
79
|
+
: { backgroundColor: dashboardAccent }),
|
|
77
80
|
alignItems: "center", justifyContent: "center",
|
|
78
81
|
opacity: pressed ? 0.85 : 1,
|
|
79
82
|
elevation: 6, shadowColor: "#000", shadowOpacity: 0.25, shadowRadius: 8, shadowOffset: { width: 0, height: 4 },
|
|
80
|
-
}), children: dashboard.logoUrl ? (_jsx(Image, { source: { uri: dashboard.logoUrl }, style: { width: size
|
|
83
|
+
}), children: dashboard.logoUrl ? (_jsx(Image, { source: { uri: dashboard.logoUrl }, resizeMode: "contain", style: { width: logoBox("circle", size), height: logoBox("circle", size) }, accessibilityIgnoresInvertColors: true })) : (_jsx(Text, { style: { color: "#fff", fontSize: size * 0.4, fontWeight: "700" }, children: name.trim().charAt(0).toUpperCase() || "•" })) }));
|
|
81
84
|
};
|
|
82
85
|
if (!prewarm && !open)
|
|
83
86
|
return _jsx(AboveNativeScreens, { overlay: overlay, children: button({ start: () => { } }) });
|
|
@@ -87,9 +90,9 @@ export function GraineLauncherButton({ onPress, style, label, size = 56 }) {
|
|
|
87
90
|
const { appearance, client } = useGraineAgent();
|
|
88
91
|
const look = useDesign();
|
|
89
92
|
const dashboard = resolveNativeAppearance(appearance, { accent: look.tokens.colors.accent, heading: "Assistant" });
|
|
90
|
-
return (_jsx(DesignedLauncher, { look: look, size: size, accent: look.tokens.colors.accent, name: dashboard.heading, label: label ?? `Talk to ${dashboard.heading}`, logoUrl: dashboard.logoUrl, origin: client.opts.baseUrl, place: style ?? {}, onPress: onPress }));
|
|
93
|
+
return (_jsx(DesignedLauncher, { look: look, size: size, accent: look.tokens.colors.accent, name: dashboard.heading, label: label ?? `Talk to ${dashboard.heading}`, logoUrl: dashboard.logoUrl, logoPlate: dashboard.logoPlate, origin: client.opts.baseUrl, place: style ?? {}, onPress: onPress }));
|
|
91
94
|
}
|
|
92
|
-
function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, place, onPress, }) {
|
|
95
|
+
function DesignedLauncher({ look, size, accent, name, label, logoUrl, logoPlate = "accent", origin, place, onPress, }) {
|
|
93
96
|
const t = look.tokens;
|
|
94
97
|
const [failed, setFailed] = useState([]);
|
|
95
98
|
const fail = (uri) => setFailed((cur) => (cur.includes(uri) ? cur : [...cur, uri]));
|
|
@@ -113,11 +116,12 @@ function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, pl
|
|
|
113
116
|
const glyph = glyphUri && !failed.includes(glyphUri) ? glyphUri : null;
|
|
114
117
|
const logo = logoUrl && !failed.includes(logoUrl) ? logoUrl : null;
|
|
115
118
|
const hard = t.elevation.hardOffset;
|
|
119
|
+
const plate = logoPlateLook(!glyph && logo ? logoPlate : "accent", { ...t.colors, accent }, t.shape.borderWidth);
|
|
116
120
|
const corners = {
|
|
117
121
|
borderRadius: radius,
|
|
118
122
|
...(t.shape.launcher === "squircle" && Platform.OS === "ios" ? { borderCurve: "continuous" } : null),
|
|
119
123
|
};
|
|
120
|
-
const mark = glyph ? (_jsx(Image, { source: { uri: glyph }, style: { width: g, height: g }, onError: () => fail(glyph), accessibilityIgnoresInvertColors: true }, glyph)) : logo ? (_jsx(Image, { source: { uri: logo }, resizeMode: "contain", style: { width:
|
|
124
|
+
const mark = glyph ? (_jsx(Image, { source: { uri: glyph }, style: { width: g, height: g }, onError: () => fail(glyph), accessibilityIgnoresInvertColors: true }, glyph)) : logo ? (_jsx(Image, { source: { uri: logo }, resizeMode: "contain", style: { width: logoBox(pill ? "circle" : t.shape.launcher, size), height: logoBox(pill ? "circle" : t.shape.launcher, size) }, onError: () => fail(logo), accessibilityIgnoresInvertColors: true }, logo)) : (_jsx(Text, { style: {
|
|
121
125
|
color: onAccent, fontSize: Math.round(size * 0.46), fontWeight: String(t.font.weights.title),
|
|
122
126
|
fontFamily: look.font(t.font.heading),
|
|
123
127
|
}, children: name.trim().charAt(0).toUpperCase() || "\u2022" }));
|
|
@@ -126,11 +130,12 @@ function DesignedLauncher({ look, size, accent, name, label, logoUrl, origin, pl
|
|
|
126
130
|
height: size, minWidth: size, width: pill ? undefined : size,
|
|
127
131
|
flexDirection: "row", alignItems: "center", justifyContent: "center",
|
|
128
132
|
gap: pill ? 8 : 0, paddingHorizontal: pill ? Math.round(size * 0.32) : 0,
|
|
129
|
-
backgroundColor:
|
|
133
|
+
backgroundColor: plate.fill,
|
|
134
|
+
...(plate.outline && !hard ? { borderWidth: plate.outline.width, borderColor: plate.outline.color } : null),
|
|
130
135
|
opacity: pressed ? 0.85 : 1,
|
|
131
136
|
...(hard ? { borderWidth: t.shape.borderWidth, borderColor: t.colors.text } : shadowStyle(t.elevation.floating)),
|
|
132
137
|
}), children: [mark, pill ? (_jsx(Text, { numberOfLines: 1, style: {
|
|
133
|
-
color: onAccent, fontSize: t.font.scale.body, fontWeight: String(t.font.weights.strong),
|
|
138
|
+
color: plate.outline ? readableOn(LOGO_PLATE_WHITE) : onAccent, fontSize: t.font.scale.body, fontWeight: String(t.font.weights.strong),
|
|
134
139
|
fontFamily: look.font(t.font.body),
|
|
135
140
|
}, children: name })) : null] })] }));
|
|
136
141
|
}
|
|
@@ -43,6 +43,7 @@ interface GraineContextValue {
|
|
|
43
43
|
fonts: Record<string, string> | null;
|
|
44
44
|
faces: LoadedFaces;
|
|
45
45
|
svg: SvgKit | null;
|
|
46
|
+
endOnBackground: boolean;
|
|
46
47
|
sessionReady: boolean;
|
|
47
48
|
onEvent: (fn: (e: GraineEvent) => void) => () => void;
|
|
48
49
|
track: (name: string, data?: Record<string, unknown>) => void;
|
|
@@ -390,9 +390,10 @@ export function GraineProvider({ children, autoConnect = true, surface: surfaceP
|
|
|
390
390
|
connectionQuality,
|
|
391
391
|
network,
|
|
392
392
|
setVariables: (vars) => client.setVariables(vars),
|
|
393
|
+
endOnBackground,
|
|
393
394
|
}), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
|
|
394
395
|
launcherVisible, launcherInset, currentScreen, onEvent, appearance, design, fonts, faces, svgKit, sessionReady,
|
|
395
|
-
surface, surfacePolicy, setSurface, connectionQuality, network]);
|
|
396
|
+
surface, surfacePolicy, setSurface, connectionQuality, network, endOnBackground]);
|
|
396
397
|
const wantsCapture = captureTaps ?? !!fab;
|
|
397
398
|
const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
|
|
398
399
|
return _jsx(Ctx.Provider, { value: value, children: body });
|
|
@@ -2,19 +2,29 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Animated, Easing, Image, PixelRatio, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
|
4
4
|
import { Ctx, useGraineAgent } from "./index.js";
|
|
5
|
-
import { GraineLoader } from "./loader.js";
|
|
5
|
+
import { GraineLoader, useGraineLoader } from "./loader.js";
|
|
6
6
|
import { GraineWidget } from "./widget.js";
|
|
7
7
|
import { resolveNativeAppearance } from "./appearance.js";
|
|
8
8
|
import { useDesign } from "./use-design.js";
|
|
9
9
|
import { glyphPixels, launcherRadius, letterSpacing, lineHeight, shadowStyle, surfaceFill } from "./design-math.js";
|
|
10
10
|
import { useKeyboardLift } from "./keyboard.js";
|
|
11
11
|
import { useDraggablePlacement } from "./placement.js";
|
|
12
|
-
import { glyphUrl, SDK_ICONS } from "../theme.js";
|
|
12
|
+
import { glyphUrl, logoBox, logoPlateLook, SDK_ICONS } from "../theme.js";
|
|
13
|
+
import { DEFAULT_LOADER_TEXT } from "../protocol.js";
|
|
13
14
|
import { TitleBar, windowed } from "./window-chrome.js";
|
|
14
15
|
const BAR_HEIGHT = 60;
|
|
15
16
|
const BUTTON = 38;
|
|
16
17
|
const METER = [0.55, 1, 0.7, 0.4];
|
|
17
|
-
function
|
|
18
|
+
function connectingLook(api, connectingText) {
|
|
19
|
+
if (api.connectStage === "retrying" || api.connectStage === "switching") {
|
|
20
|
+
return { text: "Reconnecting…", waiting: "reconnecting" };
|
|
21
|
+
}
|
|
22
|
+
if (api.slowConnect && connectingText === DEFAULT_LOADER_TEXT.connecting) {
|
|
23
|
+
return { text: "Still connecting…", waiting: "connecting", label: "Still connecting…" };
|
|
24
|
+
}
|
|
25
|
+
return { text: connectingText, waiting: "connecting" };
|
|
26
|
+
}
|
|
27
|
+
function statusOf(api, isChat, chatConnected, connectingText = DEFAULT_LOADER_TEXT.connecting) {
|
|
18
28
|
const conn = api.connectivity?.state;
|
|
19
29
|
const say = (text) => ({ text, waiting: null, meter: false });
|
|
20
30
|
if (api.error)
|
|
@@ -29,8 +39,10 @@ function statusOf(api, isChat, chatConnected) {
|
|
|
29
39
|
return say("Needs microphone");
|
|
30
40
|
if (conn === "failed")
|
|
31
41
|
return say("Couldn’t load · tap to retry");
|
|
42
|
+
if (api.callFailed)
|
|
43
|
+
return say("Couldn’t connect · tap to retry");
|
|
32
44
|
if (!api.ready || api.connecting)
|
|
33
|
-
return {
|
|
45
|
+
return { ...connectingLook(api, connectingText), meter: false };
|
|
34
46
|
if (api.muted)
|
|
35
47
|
return say("Muted");
|
|
36
48
|
if (api.connected && api.phase === "thinking")
|
|
@@ -63,7 +75,8 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
|
|
|
63
75
|
initialOffset: placement?.bottomOffset,
|
|
64
76
|
onChange: (bottomOffset) => onPlacementChange?.({ bottomOffset }),
|
|
65
77
|
});
|
|
66
|
-
const
|
|
78
|
+
const loader = useGraineLoader();
|
|
79
|
+
const status = statusOf(api, isChat, chatConnected, loader.text.connecting);
|
|
67
80
|
const conn = api.connectivity?.state;
|
|
68
81
|
const micBlocked = !!api.error || api.micDenied || api.connecting || (!api.ready && conn !== "failed");
|
|
69
82
|
const caption = !isChat && api.captionsOn && api.caption?.text ? api.caption : null;
|
|
@@ -79,11 +92,11 @@ export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, pl
|
|
|
79
92
|
return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
|
|
80
93
|
position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
|
|
81
94
|
bottom: baseBottom + drag.offset + keyboardLift,
|
|
82
|
-
}, children: [_jsx(WidgetGuard, { children: _jsx(GraineWidget, {}) }), isChat ? (_jsx(ChatWindow, { look: look, kit: kit, origin: origin, heading: heading, logoUrl: dash.logoUrl ?? undefined, messages: messages, send: send, online: chatConnected, lifted: keyboardLift > 0, onCollapse: () => (canSwitch ? setSurface("voice") : endCall()) })) : caption ? (_jsx(CaptionCard, { look: look, role: caption.role, text: caption.text, live: !!caption.live })) : null, _jsx(Raised, { t: t, level: "floating", radius: t.shape.radius.xl, children: _jsxs(View, { style: {
|
|
95
|
+
}, children: [_jsx(WidgetGuard, { children: _jsx(GraineWidget, {}) }), isChat ? (_jsx(ChatWindow, { look: look, kit: kit, origin: origin, heading: heading, logoUrl: dash.logoUrl ?? undefined, logoPlate: dash.logoPlate, messages: messages, send: send, online: chatConnected, lifted: keyboardLift > 0, onCollapse: () => (canSwitch ? setSurface("voice") : endCall()) })) : caption ? (_jsx(CaptionCard, { look: look, role: caption.role, text: caption.text, live: !!caption.live })) : null, _jsx(Raised, { t: t, level: "floating", radius: t.shape.radius.xl, children: _jsxs(View, { style: {
|
|
83
96
|
height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
|
|
84
97
|
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
85
98
|
borderRadius: t.shape.radius.xl, ...softShadow(t, "floating"),
|
|
86
|
-
}, children: [_jsx(View, { style: { width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2,
|
|
99
|
+
}, children: [_jsx(View, { style: { width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, ...markFill(t, dash.logoPlate, !t.icons.launcher && !!dash.logoUrl), alignItems: "center", justifyContent: "center", overflow: "hidden" }, children: _jsx(Mark, { t: t, size: BUTTON, glyph: 18, kit: kit, origin: origin, logoUrl: dash.logoUrl ?? undefined, initial: heading.charAt(0).toUpperCase() || "A" }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { ...body(14, t.font.weights.strong), color: c.text, lineHeight: 17.5 }, children: heading }), _jsx(View, { style: { flexDirection: "row", alignItems: "center", gap: 6, marginTop: 1 }, children: status.waiting ? (_jsx(GraineLoader, { moment: status.waiting, label: status.label, size: 14 })) : (_jsxs(_Fragment, { children: [status.meter ? _jsx(Meter, { look: look, color: c.accent }) : null, _jsx(Text, { numberOfLines: 1, style: { ...body(t.font.scale.caption, t.font.weights.body), color: c.textMuted, lineHeight: lineHeight(t.font.scale.caption, 1.3) }, children: status.text })] })) })] }), canSwitch ? (_jsx(RoundButton, { label: isChat ? "Switch to a call" : "Switch to chat", fill: c.accentSoft, onPress: () => setSurface(isChat ? "voice" : "chat"), children: _jsx(Icon, { name: isChat ? "call" : "chat", t: t, color: c.text, kit: kit }) })) : null, isChat ? null : (_jsx(RoundButton, { label: !api.connected ? `Start talking to ${spoken}` : api.muted ? "Unmute" : "Mute", fill: api.muted ? c.danger : c.accent, disabled: micBlocked, onPress: () => {
|
|
87
100
|
if (conn === "failed") {
|
|
88
101
|
api.retry();
|
|
89
102
|
return;
|
|
@@ -125,7 +138,13 @@ function Icon({ name, t, color, size = 18, kit, rotate }) {
|
|
|
125
138
|
const common = { stroke: color, strokeLinecap: sharp ? "square" : "round", strokeLinejoin: sharp ? "miter" : "round" };
|
|
126
139
|
return (_jsx(View, { style: rotate ? { transform: [{ rotate: "180deg" }] } : undefined, children: _jsxs(Svg, { width: size, height: size, viewBox: "0 0 24 24", children: [icon.body ? (_jsx(Path, { d: icon.body, ...common, fill: filled || style === "duotone" ? color : "none", fillOpacity: style === "duotone" ? 0.28 : 1, strokeWidth: filled ? Math.max(1, stroke * 0.75) : stroke })) : null, icon.lines ? _jsx(Path, { d: icon.lines, ...common, fill: "none", strokeWidth: stroke }) : null] }) }));
|
|
127
140
|
}
|
|
128
|
-
function
|
|
141
|
+
function markFill(t, plate, showsLogo) {
|
|
142
|
+
const look = logoPlateLook(showsLogo ? plate : "accent", t.colors, t.shape.borderWidth);
|
|
143
|
+
return look.outline
|
|
144
|
+
? { backgroundColor: look.fill, borderWidth: look.outline.width, borderColor: look.outline.color }
|
|
145
|
+
: { backgroundColor: look.fill };
|
|
146
|
+
}
|
|
147
|
+
function Mark({ t, size, glyph, kit, origin, logoUrl, initial, shape = "circle", }) {
|
|
129
148
|
const on = t.colors.onAccent;
|
|
130
149
|
const g = t.icons.launcher;
|
|
131
150
|
if (g) {
|
|
@@ -136,7 +155,8 @@ function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, })
|
|
|
136
155
|
return (_jsx(Image, { source: { uri: glyphUrl(origin, g.id, on, glyphPixels(size, PixelRatio.get()), t.icons.stroke) }, style: { width: glyph, height: glyph }, accessibilityIgnoresInvertColors: true }));
|
|
137
156
|
}
|
|
138
157
|
if (logoUrl) {
|
|
139
|
-
|
|
158
|
+
const box = logoBox(shape, size);
|
|
159
|
+
return _jsx(Image, { source: { uri: logoUrl }, style: { width: box, height: box }, resizeMode: "contain", accessibilityIgnoresInvertColors: true });
|
|
140
160
|
}
|
|
141
161
|
return _jsx(Text, { style: { color: on, fontSize: Math.round(size * 0.4), fontWeight: "700" }, children: initial });
|
|
142
162
|
}
|
|
@@ -191,7 +211,7 @@ function CaptionCard({ look, role, text, live }) {
|
|
|
191
211
|
borderRadius: t.shape.radius.lg, paddingHorizontal: 12, paddingVertical: 8, ...softShadow(t, "card"),
|
|
192
212
|
}, children: [win ? (_jsx(View, { style: { marginBottom: 6 }, children: _jsx(TitleBar, { t: t, title: "captions", size: "caption", radius: t.shape.radius.lg, bleed: { horizontal: 12, top: 8 } }) })) : null, role === "customer" ? (_jsx(Text, { style: { ...look.face(t.font.body, t.font.weights.strong), color: c.accent, fontSize: 10.5, letterSpacing: 0.6, textTransform: "uppercase", marginBottom: 2 }, children: "You" })) : null, _jsx(Text, { numberOfLines: 2, style: { ...look.face(t.font.body, t.font.weights.strong), color: c.text, fontSize: 13, lineHeight: 17.5, opacity: live ? 0.75 : 1 }, children: text })] }) }) }));
|
|
193
213
|
}
|
|
194
|
-
function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, online, lifted, onCollapse, }) {
|
|
214
|
+
function ChatWindow({ look, kit, origin, heading, logoUrl, logoPlate = "accent", messages, send, online, lifted, onCollapse, }) {
|
|
195
215
|
const t = look.tokens;
|
|
196
216
|
const c = t.colors;
|
|
197
217
|
const r = t.shape.radius;
|
|
@@ -217,7 +237,7 @@ function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, onlin
|
|
|
217
237
|
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
218
238
|
});
|
|
219
239
|
const titleSize = Math.min(t.font.scale.title, 16);
|
|
220
|
-
return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "floating", radius: r.xl, children: _jsx(View, { style: { backgroundColor: c.background, borderWidth: bw, borderColor: c.border, borderRadius: r.xl, ...softShadow(t, "floating") }, children: _jsxs(View, { style: { borderRadius: Math.max(0, r.xl - bw), overflow: "hidden" }, children: [windowed(t) ? (_jsx(TitleBar, { t: t, title: heading, online: online, radius: r.xl, right: _jsx(Pressable, { onPress: onCollapse, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Collapse", children: _jsx(Icon, { name: "chevron", t: t, color: c.textMuted, kit: kit, size: 16, rotate: true }) }) })) : (_jsxs(View, { style: { height: 50, flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 12, backgroundColor: surfaceFill(c.surface, t), borderBottomWidth: bw, borderBottomColor: c.border }, children: [_jsx(View, { style: { width: 30, height: 30, borderRadius: launcherRadius(markShape, 30, t),
|
|
240
|
+
return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "floating", radius: r.xl, children: _jsx(View, { style: { backgroundColor: c.background, borderWidth: bw, borderColor: c.border, borderRadius: r.xl, ...softShadow(t, "floating") }, children: _jsxs(View, { style: { borderRadius: Math.max(0, r.xl - bw), overflow: "hidden" }, children: [windowed(t) ? (_jsx(TitleBar, { t: t, title: heading, online: online, radius: r.xl, right: _jsx(Pressable, { onPress: onCollapse, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Collapse", children: _jsx(Icon, { name: "chevron", t: t, color: c.textMuted, kit: kit, size: 16, rotate: true }) }) })) : (_jsxs(View, { style: { height: 50, flexDirection: "row", alignItems: "center", gap: 10, paddingHorizontal: 12, backgroundColor: surfaceFill(c.surface, t), borderBottomWidth: bw, borderBottomColor: c.border }, children: [_jsx(View, { style: { width: 30, height: 30, borderRadius: launcherRadius(markShape, 30, t), ...markFill(t, logoPlate, !t.icons.launcher && !!logoUrl), alignItems: "center", justifyContent: "center", overflow: "hidden", ...(Platform.OS === "ios" && markShape === "squircle" ? { borderCurve: "continuous" } : null) }, children: _jsx(Mark, { t: t, size: 30, glyph: 14, kit: kit, origin: origin, logoUrl: logoUrl, initial: heading.charAt(0).toUpperCase() || "A", shape: markShape }) }), _jsxs(View, { style: { flex: 1, minWidth: 0 }, children: [_jsx(Text, { numberOfLines: 1, style: { ...look.face(t.font.heading, t.font.weights.title), color: c.text, fontSize: titleSize, lineHeight: lineHeight(titleSize, t.font.lineHeight.title), letterSpacing: letterSpacing(titleSize, t.font.letterSpacing.title) }, children: heading }), _jsxs(View, { style: { flexDirection: "row", alignItems: "center", gap: 4 }, children: [online ? _jsx(View, { style: { width: 6, height: 6, borderRadius: 3, backgroundColor: c.success } }) : null, _jsx(Text, { style: { ...text(t.font.scale.caption, t.font.weights.body), color: c.textMuted, lineHeight: lineHeight(t.font.scale.caption, 1.2) }, children: online ? "Online" : "Connecting…" })] })] }), _jsx(Pressable, { onPress: onCollapse, hitSlop: 10, accessibilityRole: "button", accessibilityLabel: "Collapse", children: _jsx(Icon, { name: "chevron", t: t, color: c.textMuted, kit: kit, rotate: true }) })] })), _jsxs(ScrollView, { ref: scroll, style: { maxHeight: lifted ? 150 : 260 }, contentContainerStyle: { padding: 12, gap: t.spacing.gap }, keyboardShouldPersistTaps: "handled", children: [messages.length === 0 ? (_jsx(Text, { style: { ...text(t.font.scale.body, t.font.weights.body), color: c.textMuted, lineHeight: lineHeight(t.font.scale.body, t.font.lineHeight.body) }, children: online ? `${heading} can see what you’re looking at — ask it anything.` : "Connecting…" })) : null, messages.map((m, i) => {
|
|
221
241
|
const b = bubble(t, m.role !== "agent");
|
|
222
242
|
return (_jsx(View, { style: [{ maxWidth: "84%" }, b.box], children: _jsx(Text, { style: { ...text(t.font.scale.body, t.font.weights.body), color: b.color, lineHeight: lineHeight(t.font.scale.body, t.font.lineHeight.body), opacity: m.live ? 0.75 : 1 }, children: m.text }) }, i));
|
|
223
243
|
})] }), _jsxs(View, { style: { height: 58, flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 10, backgroundColor: c.background }, children: [_jsx(TextInput, { value: draft, onChangeText: setDraft, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false, placeholder: "Message\u2026", placeholderTextColor: c.textMuted, style: {
|
|
@@ -31,6 +31,7 @@ export interface GraineVoiceLauncherProps {
|
|
|
31
31
|
bootMs?: number;
|
|
32
32
|
hardCapMs?: number;
|
|
33
33
|
endWatchdogMs?: number;
|
|
34
|
+
slowConnectMs?: number;
|
|
34
35
|
};
|
|
35
36
|
children?: (api: GraineVoiceApi) => React.ReactNode;
|
|
36
37
|
}
|
|
@@ -61,6 +62,10 @@ export interface GraineVoiceApi {
|
|
|
61
62
|
reconnecting: boolean;
|
|
62
63
|
error: string | null;
|
|
63
64
|
micDenied: boolean;
|
|
65
|
+
connectStage: "registering" | "dialling" | "retrying" | "switching" | null;
|
|
66
|
+
slowConnect: boolean;
|
|
67
|
+
callFailed: boolean;
|
|
64
68
|
}
|
|
65
69
|
export declare const CAPTION_HISTORY = 12;
|
|
70
|
+
export declare const SLOW_CONNECT_MS = 4000;
|
|
66
71
|
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active: activeProp, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { AppState, View } from "react-native";
|
|
4
|
-
import { useGraineAgent } from "./index.js";
|
|
4
|
+
import { Ctx, useGraineAgent } from "./index.js";
|
|
5
5
|
import { requestMicrophonePermission } from "./permissions.js";
|
|
6
6
|
export const CAPTION_HISTORY = 12;
|
|
7
|
+
export const SLOW_CONNECT_MS = 4000;
|
|
7
8
|
export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active: activeProp = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }) {
|
|
8
9
|
const { client, appearance, surface, surfaces } = useGraineAgent();
|
|
10
|
+
const endOnBackground = useContext(Ctx)?.endOnBackground ?? true;
|
|
11
|
+
const endOnBackgroundRef = useRef(endOnBackground);
|
|
12
|
+
endOnBackgroundRef.current = endOnBackground;
|
|
9
13
|
const active = activeProp && surface !== "chat";
|
|
10
14
|
const ref = useRef(null);
|
|
11
15
|
useEffect(() => {
|
|
@@ -22,6 +26,9 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
22
26
|
const [muted, setMutedState] = useState(false);
|
|
23
27
|
const [fault, setFault] = useState(null);
|
|
24
28
|
const [micDenied, setMicDenied] = useState(false);
|
|
29
|
+
const [connectStage, setConnectStage] = useState(null);
|
|
30
|
+
const [slowConnect, setSlowConnect] = useState(false);
|
|
31
|
+
const [callFailed, setCallFailed] = useState(false);
|
|
25
32
|
const [ready, setReady] = useState(false);
|
|
26
33
|
const readyRef = useRef(false);
|
|
27
34
|
readyRef.current = ready;
|
|
@@ -291,6 +298,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
291
298
|
if (msg.connected) {
|
|
292
299
|
endedNotifiedRef.current = false;
|
|
293
300
|
setReconnecting(false);
|
|
301
|
+
setConnectStage(null);
|
|
302
|
+
setCallFailed(false);
|
|
294
303
|
}
|
|
295
304
|
setConnected(!!msg.connected);
|
|
296
305
|
setConnecting(!!msg.connecting);
|
|
@@ -299,6 +308,22 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
299
308
|
startedRef.current = false;
|
|
300
309
|
onCallState?.({ connected: !!msg.connected, connecting: !!msg.connecting });
|
|
301
310
|
return;
|
|
311
|
+
case "graine:progress": {
|
|
312
|
+
const stage = String(msg.stage || "");
|
|
313
|
+
if (stage === "failed") {
|
|
314
|
+
setConnectStage(null);
|
|
315
|
+
setConnecting(false);
|
|
316
|
+
setCallFailed(true);
|
|
317
|
+
startedRef.current = false;
|
|
318
|
+
pendingStartRef.current = false;
|
|
319
|
+
onCallState?.({ connected: false, connecting: false });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (stage === "registering" || stage === "dialling" || stage === "retrying" || stage === "switching") {
|
|
323
|
+
setConnectStage(stage);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
302
327
|
case "graine:muted":
|
|
303
328
|
setMutedState(!!msg.muted);
|
|
304
329
|
return;
|
|
@@ -372,6 +397,15 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
372
397
|
}
|
|
373
398
|
}, [autoStart, beginCall, client, onCaption, onCallState, onMicDenied, onTransport, onError, onEnded, post, pushContext, foldCaption]);
|
|
374
399
|
const pendingStartRef = useRef(false);
|
|
400
|
+
const waitingOnConnect = active && !connected && !callFailed && (connecting || !ready);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (!waitingOnConnect) {
|
|
403
|
+
setSlowConnect(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const t = setTimeout(() => setSlowConnect(true), timing?.slowConnectMs ?? SLOW_CONNECT_MS);
|
|
407
|
+
return () => clearTimeout(t);
|
|
408
|
+
}, [waitingOnConnect]);
|
|
375
409
|
const lastCaption = captions.length ? captions[captions.length - 1] : null;
|
|
376
410
|
const phase = reconnecting || connectivity.state === "failed" || connectivity.state === "retrying" ? "reconnecting"
|
|
377
411
|
: connecting || (!ready && !!voiceAgentId) ? "connecting"
|
|
@@ -394,6 +428,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
394
428
|
setConnecting(true);
|
|
395
429
|
setMicDenied(false);
|
|
396
430
|
setFault(null);
|
|
431
|
+
setCallFailed(false);
|
|
432
|
+
setConnectStage(null);
|
|
397
433
|
setCaptions([]);
|
|
398
434
|
setAwaitingAgent(false);
|
|
399
435
|
client.clearPendingWidget();
|
|
@@ -411,6 +447,9 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
411
447
|
reconnecting,
|
|
412
448
|
error: fault,
|
|
413
449
|
micDenied,
|
|
450
|
+
connectStage,
|
|
451
|
+
slowConnect,
|
|
452
|
+
callFailed,
|
|
414
453
|
setMuted: useCallback((m) => {
|
|
415
454
|
setMutedState(m);
|
|
416
455
|
post({ type: "graine:set-muted", muted: m });
|
|
@@ -460,6 +499,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
460
499
|
setCaptions([]);
|
|
461
500
|
setMicDenied(false);
|
|
462
501
|
setFault(null);
|
|
502
|
+
setCallFailed(false);
|
|
503
|
+
setConnectStage(null);
|
|
463
504
|
client.clearPendingWidget();
|
|
464
505
|
onCallState?.({ connected: false, connecting: false });
|
|
465
506
|
}, [active]);
|
|
@@ -480,7 +521,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
480
521
|
post({ type: "graine:warm" });
|
|
481
522
|
return;
|
|
482
523
|
}
|
|
483
|
-
if (next === "background" && wasActive && !pending) {
|
|
524
|
+
if (next === "background" && wasActive && !pending && endOnBackgroundRef.current) {
|
|
484
525
|
pending = setTimeout(() => {
|
|
485
526
|
pending = null;
|
|
486
527
|
if (AppState.currentState !== "active")
|
package/dist/theme.d.ts
CHANGED
|
@@ -163,6 +163,18 @@ export declare const DESIGN_BUBBLES: ReadonlyArray<DesignBubbles>;
|
|
|
163
163
|
export declare const LAUNCHER_SHAPES: ReadonlyArray<LauncherShape>;
|
|
164
164
|
export declare const ICON_STYLES: ReadonlyArray<IconStyle>;
|
|
165
165
|
export declare const DESIGN_CHROMES: ReadonlyArray<DesignChrome>;
|
|
166
|
+
export type LogoPlate = "accent" | "white";
|
|
167
|
+
export declare const LOGO_PLATES: ReadonlyArray<LogoPlate>;
|
|
168
|
+
export declare const LOGO_PLATE_WHITE = "#FFFFFF";
|
|
169
|
+
export declare function normaliseLogoPlate(value: unknown): LogoPlate;
|
|
170
|
+
export declare function logoBox(shape: string, size: number): number;
|
|
171
|
+
export declare function logoPlateLook(plate: LogoPlate, colors: Pick<ThemeColors, "accent" | "border">, borderWidth?: number): {
|
|
172
|
+
fill: string;
|
|
173
|
+
outline: {
|
|
174
|
+
width: number;
|
|
175
|
+
color: string;
|
|
176
|
+
} | null;
|
|
177
|
+
};
|
|
166
178
|
export declare function normaliseHex(value: unknown): string | null;
|
|
167
179
|
export declare function mixHex(a: string, b: string, t: number): string;
|
|
168
180
|
export declare function luminanceOf(hex: string): number;
|