@graineai/inapp-react-native 0.36.0 → 0.38.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 +26 -12
- package/README.md +7 -2
- package/dist/client.d.ts +2 -0
- package/dist/design.d.ts +5 -0
- package/dist/design.js +50 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/react-native/design-math.d.ts +13 -0
- package/dist/react-native/design-math.js +85 -0
- package/dist/react-native/fab.d.ts +7 -0
- package/dist/react-native/fab.js +63 -5
- package/dist/react-native/font-weights.d.ts +1 -0
- package/dist/react-native/font-weights.js +26 -0
- package/dist/react-native/fonts.d.ts +20 -0
- package/dist/react-native/fonts.js +69 -0
- package/dist/react-native/index.d.ts +15 -1
- package/dist/react-native/index.js +47 -2
- package/dist/react-native/loader.js +23 -7
- package/dist/react-native/ui.js +139 -6
- package/dist/react-native/use-design.d.ts +18 -0
- package/dist/react-native/use-design.js +84 -0
- package/dist/react-native/voice-bar.d.ts +16 -0
- package/dist/react-native/voice-bar.js +255 -0
- package/dist/react-native/voice-launcher.d.ts +2 -0
- package/dist/react-native/voice-launcher.js +11 -0
- package/dist/react-native/widget.js +171 -61
- package/dist/theme.d.ts +211 -0
- package/dist/theme.js +2009 -0
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useContext, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Animated, Easing, Image, PixelRatio, Platform, Pressable, ScrollView, Text, TextInput, View } from "react-native";
|
|
4
|
+
import { Ctx, useGraineAgent } from "./index.js";
|
|
5
|
+
import { GraineLoader } from "./loader.js";
|
|
6
|
+
import { GraineWidget } from "./widget.js";
|
|
7
|
+
import { resolveNativeAppearance } from "./appearance.js";
|
|
8
|
+
import { useDesign } from "./use-design.js";
|
|
9
|
+
import { glyphPixels, launcherRadius, letterSpacing, lineHeight, shadowStyle, surfaceFill } from "./design-math.js";
|
|
10
|
+
import { useKeyboardLift } from "./keyboard.js";
|
|
11
|
+
import { useDraggablePlacement } from "./placement.js";
|
|
12
|
+
import { glyphUrl, SDK_ICONS } from "../theme.js";
|
|
13
|
+
const BAR_HEIGHT = 60;
|
|
14
|
+
const BUTTON = 38;
|
|
15
|
+
const METER = [0.55, 1, 0.7, 0.4];
|
|
16
|
+
function statusOf(api, isChat, chatConnected) {
|
|
17
|
+
const conn = api.connectivity?.state;
|
|
18
|
+
const say = (text) => ({ text, waiting: null, meter: false });
|
|
19
|
+
if (api.error)
|
|
20
|
+
return say("Unavailable");
|
|
21
|
+
if (isChat)
|
|
22
|
+
return chatConnected ? say("Online") : { text: "Connecting…", waiting: "connecting", meter: false };
|
|
23
|
+
if (api.network === "offline")
|
|
24
|
+
return say("No connection");
|
|
25
|
+
if (api.reconnecting || conn === "retrying")
|
|
26
|
+
return { text: "Reconnecting…", waiting: "reconnecting", meter: false };
|
|
27
|
+
if (api.micDenied)
|
|
28
|
+
return say("Needs microphone");
|
|
29
|
+
if (conn === "failed")
|
|
30
|
+
return say("Couldn’t load · tap to retry");
|
|
31
|
+
if (!api.ready || api.connecting)
|
|
32
|
+
return { text: "Connecting…", waiting: "connecting", meter: false };
|
|
33
|
+
if (api.muted)
|
|
34
|
+
return say("Muted");
|
|
35
|
+
if (api.connected && api.phase === "thinking")
|
|
36
|
+
return { text: "Thinking…", waiting: "thinking", meter: false };
|
|
37
|
+
if (api.connected && api.caption?.role === "agent" && api.caption.live)
|
|
38
|
+
return { text: "Speaking", waiting: null, meter: true };
|
|
39
|
+
if (api.connected && api.quality === "poor")
|
|
40
|
+
return say("Weak connection");
|
|
41
|
+
if (api.connected)
|
|
42
|
+
return { text: "Listening", waiting: null, meter: true };
|
|
43
|
+
return say("Tap to talk");
|
|
44
|
+
}
|
|
45
|
+
export function GraineVoiceBar({ api, onClose, bottomInset, draggable = true, placement, onPlacementChange, name: nameProp, }) {
|
|
46
|
+
const ctx = useContext(Ctx);
|
|
47
|
+
const { messages, send, connected: chatConnected, appearance, surface, canSwitch, setSurface, launcherInset } = useGraineAgent();
|
|
48
|
+
const look = useDesign();
|
|
49
|
+
const t = look.tokens;
|
|
50
|
+
const c = t.colors;
|
|
51
|
+
const kit = ctx?.svg ?? null;
|
|
52
|
+
const origin = ctx?.client?.opts?.baseUrl || "https://www.graine.ai";
|
|
53
|
+
const dash = resolveNativeAppearance(appearance, { accent: c.accent, heading: "Assistant" });
|
|
54
|
+
const heading = dash.heading;
|
|
55
|
+
const spoken = nameProp || heading;
|
|
56
|
+
const isChat = surface === "chat";
|
|
57
|
+
const keyboardLift = useKeyboardLift();
|
|
58
|
+
const baseBottom = bottomInset ?? launcherInset?.bottom ?? 96;
|
|
59
|
+
const drag = useDraggablePlacement({
|
|
60
|
+
baseBottom,
|
|
61
|
+
enabled: draggable,
|
|
62
|
+
initialOffset: placement?.bottomOffset,
|
|
63
|
+
onChange: (bottomOffset) => onPlacementChange?.({ bottomOffset }),
|
|
64
|
+
});
|
|
65
|
+
const status = statusOf(api, isChat, chatConnected);
|
|
66
|
+
const conn = api.connectivity?.state;
|
|
67
|
+
const micBlocked = !!api.error || api.micDenied || api.connecting || (!api.ready && conn !== "failed");
|
|
68
|
+
const caption = !isChat && api.captionsOn && api.caption?.text ? api.caption : null;
|
|
69
|
+
const body = (size, weight) => ({
|
|
70
|
+
...look.face(t.font.body, weight),
|
|
71
|
+
fontSize: size,
|
|
72
|
+
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
73
|
+
});
|
|
74
|
+
const endCall = () => {
|
|
75
|
+
api.end();
|
|
76
|
+
setTimeout(() => onClose?.(), 400);
|
|
77
|
+
};
|
|
78
|
+
return (_jsxs(View, { pointerEvents: "box-none", ...drag.panHandlers, onLayout: drag.onLayout, style: {
|
|
79
|
+
position: "absolute", zIndex: 900, gap: 8, left: 12, right: 12,
|
|
80
|
+
bottom: baseBottom + drag.offset + keyboardLift,
|
|
81
|
+
}, 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: {
|
|
82
|
+
height: BAR_HEIGHT, paddingHorizontal: 9, flexDirection: "row", alignItems: "center", gap: 10,
|
|
83
|
+
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
84
|
+
borderRadius: t.shape.radius.xl, ...softShadow(t, "floating"),
|
|
85
|
+
}, children: [_jsx(View, { style: { width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, backgroundColor: c.accent, 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", cover: true }) }), _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, 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: () => {
|
|
86
|
+
if (conn === "failed") {
|
|
87
|
+
api.retry();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (api.connected)
|
|
91
|
+
api.setMuted(!api.muted);
|
|
92
|
+
else
|
|
93
|
+
api.start();
|
|
94
|
+
}, children: _jsx(Icon, { name: api.muted ? "micOff" : "mic", t: t, color: api.muted ? "#FFFFFF" : c.onAccent, kit: kit }) })), _jsx(RoundButton, { label: api.connected ? "End call" : "Close", fill: api.connected ? c.danger : c.accentSoft, onPress: endCall, children: _jsx(Icon, { name: api.connected ? "end" : "close", t: t, color: api.connected ? "#FFFFFF" : c.textMuted, kit: kit }) })] }) })] }));
|
|
95
|
+
}
|
|
96
|
+
function softShadow(t, level) {
|
|
97
|
+
return t.elevation.hardOffset ? {} : shadowStyle(t.elevation[level]);
|
|
98
|
+
}
|
|
99
|
+
function Raised({ t, radius, children }) {
|
|
100
|
+
const hard = t.elevation.hardOffset;
|
|
101
|
+
if (!hard)
|
|
102
|
+
return _jsx(_Fragment, { children: children });
|
|
103
|
+
return (_jsxs(View, { children: [_jsx(View, { pointerEvents: "none", style: { position: "absolute", left: hard.x, top: hard.y, right: -hard.x, bottom: -hard.y, borderRadius: radius, backgroundColor: t.colors.text } }), children] }));
|
|
104
|
+
}
|
|
105
|
+
function RoundButton({ label, fill, disabled = false, onPress, children, }) {
|
|
106
|
+
return (_jsx(Pressable, { onPress: onPress, disabled: disabled, hitSlop: 8, accessibilityRole: "button", accessibilityLabel: label, style: {
|
|
107
|
+
width: BUTTON, height: BUTTON, borderRadius: BUTTON / 2, alignItems: "center", justifyContent: "center",
|
|
108
|
+
backgroundColor: fill, opacity: disabled ? 0.4 : 1,
|
|
109
|
+
}, children: children }));
|
|
110
|
+
}
|
|
111
|
+
const FALLBACK = {
|
|
112
|
+
mic: "🎙", micOff: "🔇", end: "✕", send: "↑", chat: "💬", call: "📞", chevron: "⌄", close: "✕",
|
|
113
|
+
};
|
|
114
|
+
function Icon({ name, t, color, size = 18, kit, rotate }) {
|
|
115
|
+
if (!kit) {
|
|
116
|
+
return _jsx(Text, { style: { color, fontSize: Math.round(size * 0.8), lineHeight: size, textAlign: "center" }, children: FALLBACK[name] });
|
|
117
|
+
}
|
|
118
|
+
const { Svg, Path } = kit;
|
|
119
|
+
const icon = SDK_ICONS[name];
|
|
120
|
+
const style = t.icons.style;
|
|
121
|
+
const sharp = style === "sharp";
|
|
122
|
+
const filled = style === "filled";
|
|
123
|
+
const stroke = t.icons.stroke;
|
|
124
|
+
const common = { stroke: color, strokeLinecap: sharp ? "square" : "round", strokeLinejoin: sharp ? "miter" : "round" };
|
|
125
|
+
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] }) }));
|
|
126
|
+
}
|
|
127
|
+
function Mark({ t, size, glyph, kit, origin, logoUrl, initial, cover = false, }) {
|
|
128
|
+
const on = t.colors.onAccent;
|
|
129
|
+
const g = t.icons.launcher;
|
|
130
|
+
if (g) {
|
|
131
|
+
if (kit) {
|
|
132
|
+
const { Svg, Path } = kit;
|
|
133
|
+
return (_jsx(Svg, { width: glyph, height: glyph, viewBox: "0 0 24 24", children: _jsx(Path, { d: g.path, fill: g.stroke ? "none" : on, stroke: g.stroke ? on : "none", strokeWidth: g.stroke ? t.icons.stroke : 0, strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
134
|
+
}
|
|
135
|
+
return (_jsx(Image, { source: { uri: glyphUrl(origin, g.id, on, glyphPixels(size, PixelRatio.get()), t.icons.stroke) }, style: { width: glyph, height: glyph }, accessibilityIgnoresInvertColors: true }));
|
|
136
|
+
}
|
|
137
|
+
if (logoUrl) {
|
|
138
|
+
return _jsx(Image, { source: { uri: logoUrl }, style: cover ? { width: size, height: size } : { width: glyph, height: glyph }, resizeMode: cover ? "cover" : "contain", accessibilityIgnoresInvertColors: true });
|
|
139
|
+
}
|
|
140
|
+
return _jsx(Text, { style: { color: on, fontSize: Math.round(size * 0.4), fontWeight: "700" }, children: initial });
|
|
141
|
+
}
|
|
142
|
+
function Meter({ look, color }) {
|
|
143
|
+
const still = look.durationMs <= 0;
|
|
144
|
+
const bars = useRef(METER.map(() => new Animated.Value(1))).current;
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (still) {
|
|
147
|
+
bars.forEach((b) => b.setValue(1));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const period = Math.max(420, look.durationMs * 3.2);
|
|
151
|
+
const ease = Easing.inOut(Easing.ease);
|
|
152
|
+
const runs = bars.map((b, i) => {
|
|
153
|
+
b.setValue(0.35);
|
|
154
|
+
return Animated.sequence([
|
|
155
|
+
Animated.delay(i * 90),
|
|
156
|
+
Animated.loop(Animated.sequence([
|
|
157
|
+
Animated.timing(b, { toValue: 1, duration: period, easing: ease, useNativeDriver: true }),
|
|
158
|
+
Animated.timing(b, { toValue: 0.35, duration: period, easing: ease, useNativeDriver: true }),
|
|
159
|
+
])),
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
runs.forEach((r) => r.start());
|
|
163
|
+
return () => runs.forEach((r) => r.stop());
|
|
164
|
+
}, [still, look.durationMs, bars]);
|
|
165
|
+
return (_jsx(View, { style: { flexDirection: "row", alignItems: "flex-end", height: 12, gap: 2 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: METER.map((h, i) => (_jsx(Animated.View, { style: {
|
|
166
|
+
width: 2.5, height: Math.round(12 * h * 10) / 10, borderRadius: 1.25, backgroundColor: color,
|
|
167
|
+
transform: [{ scaleY: bars[i] }], transformOrigin: "bottom",
|
|
168
|
+
} }, i))) }));
|
|
169
|
+
}
|
|
170
|
+
function useRise(look) {
|
|
171
|
+
const v = useRef(new Animated.Value(look.durationMs > 0 ? 0 : 1)).current;
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (look.durationMs <= 0) {
|
|
174
|
+
v.setValue(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const a = Animated.timing(v, { toValue: 1, duration: look.durationMs, easing: look.easing, useNativeDriver: true });
|
|
178
|
+
a.start();
|
|
179
|
+
return () => a.stop();
|
|
180
|
+
}, []);
|
|
181
|
+
return { opacity: v, transform: [{ translateY: v.interpolate({ inputRange: [0, 1], outputRange: [8, 0] }) }] };
|
|
182
|
+
}
|
|
183
|
+
function CaptionCard({ look, role, text, live }) {
|
|
184
|
+
const t = look.tokens;
|
|
185
|
+
const c = t.colors;
|
|
186
|
+
const rise = useRise(look);
|
|
187
|
+
return (_jsx(Animated.View, { style: rise, children: _jsx(Raised, { t: t, level: "card", radius: t.shape.radius.lg, children: _jsxs(View, { style: {
|
|
188
|
+
backgroundColor: surfaceFill(c.surface, t), borderWidth: t.shape.borderWidth, borderColor: c.border,
|
|
189
|
+
borderRadius: t.shape.radius.lg, paddingHorizontal: 12, paddingVertical: 8, ...softShadow(t, "card"),
|
|
190
|
+
}, children: [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 })] }) }) }));
|
|
191
|
+
}
|
|
192
|
+
function ChatWindow({ look, kit, origin, heading, logoUrl, messages, send, online, lifted, onCollapse, }) {
|
|
193
|
+
const t = look.tokens;
|
|
194
|
+
const c = t.colors;
|
|
195
|
+
const r = t.shape.radius;
|
|
196
|
+
const bw = t.shape.borderWidth;
|
|
197
|
+
const pad = t.spacing.cardPadding;
|
|
198
|
+
const rise = useRise(look);
|
|
199
|
+
const [draft, setDraft] = useState("");
|
|
200
|
+
const scroll = useRef(null);
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const id = setTimeout(() => scroll.current?.scrollToEnd?.({ animated: true }), 40);
|
|
203
|
+
return () => clearTimeout(id);
|
|
204
|
+
}, [messages.length]);
|
|
205
|
+
const submit = () => {
|
|
206
|
+
const text = draft.trim();
|
|
207
|
+
if (!text)
|
|
208
|
+
return;
|
|
209
|
+
setDraft("");
|
|
210
|
+
send(text);
|
|
211
|
+
};
|
|
212
|
+
const markShape = t.shape.launcher === "pill" ? "circle" : t.shape.launcher;
|
|
213
|
+
const text = (size, weight) => ({
|
|
214
|
+
...look.face(t.font.body, weight), fontSize: size,
|
|
215
|
+
letterSpacing: letterSpacing(size, t.font.letterSpacing.body),
|
|
216
|
+
});
|
|
217
|
+
const titleSize = Math.min(t.font.scale.title, 16);
|
|
218
|
+
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: [_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), backgroundColor: c.accent, 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" }) }), _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) => {
|
|
219
|
+
const b = bubble(t, m.role !== "agent");
|
|
220
|
+
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));
|
|
221
|
+
})] }), _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: {
|
|
222
|
+
flex: 1, height: 40, paddingHorizontal: 14, backgroundColor: c.surfaceRaised, borderWidth: bw, borderColor: c.border,
|
|
223
|
+
borderRadius: r.xl, color: c.text, ...look.face(t.font.body, t.font.weights.body), fontSize: t.font.scale.body - 1,
|
|
224
|
+
} }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityRole: "button", accessibilityLabel: "Send", style: { width: 40, height: 40, borderRadius: 20, alignItems: "center", justifyContent: "center", backgroundColor: c.accent, opacity: draft.trim() ? 1 : 0.4 }, children: _jsx(Icon, { name: "send", t: t, color: c.onAccent, kit: kit }) })] })] }) }) }) }));
|
|
225
|
+
}
|
|
226
|
+
function bubble(t, user) {
|
|
227
|
+
const c = t.colors;
|
|
228
|
+
const r = t.shape.radius;
|
|
229
|
+
const pad = t.spacing.cardPadding;
|
|
230
|
+
const shape = {
|
|
231
|
+
alignSelf: user ? "flex-end" : "flex-start",
|
|
232
|
+
paddingHorizontal: pad, paddingVertical: Math.round(pad * 0.75), borderRadius: r.lg,
|
|
233
|
+
...(user ? { borderBottomRightRadius: r.sm } : { borderBottomLeftRadius: r.sm }),
|
|
234
|
+
};
|
|
235
|
+
if (t.bubbles === "outline") {
|
|
236
|
+
return { box: { ...shape, backgroundColor: "transparent", borderWidth: t.shape.borderWidth, borderColor: user ? c.accent : c.border }, color: c.text };
|
|
237
|
+
}
|
|
238
|
+
if (t.bubbles === "minimal") {
|
|
239
|
+
return user
|
|
240
|
+
? { box: { ...shape, backgroundColor: c.accentSoft }, color: c.text }
|
|
241
|
+
: { box: { alignSelf: "flex-start", paddingHorizontal: 2, paddingVertical: 2, backgroundColor: "transparent" }, color: c.text };
|
|
242
|
+
}
|
|
243
|
+
return user
|
|
244
|
+
? { box: { ...shape, backgroundColor: c.userBubble }, color: c.userBubbleText }
|
|
245
|
+
: { box: { ...shape, backgroundColor: c.agentBubble }, color: c.agentBubbleText };
|
|
246
|
+
}
|
|
247
|
+
class WidgetGuard extends React.Component {
|
|
248
|
+
constructor() {
|
|
249
|
+
super(...arguments);
|
|
250
|
+
this.state = { failed: false };
|
|
251
|
+
}
|
|
252
|
+
static getDerivedStateFromError() { return { failed: true }; }
|
|
253
|
+
componentDidCatch(error) { console.error("[graine] the agent's component failed to render", error); }
|
|
254
|
+
render() { return this.state.failed ? null : this.props.children; }
|
|
255
|
+
}
|
|
@@ -59,6 +59,8 @@ export interface GraineVoiceApi {
|
|
|
59
59
|
quality: ConnectionQuality;
|
|
60
60
|
network: "online" | "offline" | "unknown";
|
|
61
61
|
reconnecting: boolean;
|
|
62
|
+
error: string | null;
|
|
63
|
+
micDenied: boolean;
|
|
62
64
|
}
|
|
63
65
|
export declare const CAPTION_HISTORY = 12;
|
|
64
66
|
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active: activeProp, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
@@ -20,6 +20,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
20
20
|
const [connected, setConnected] = useState(false);
|
|
21
21
|
const [connecting, setConnecting] = useState(false);
|
|
22
22
|
const [muted, setMutedState] = useState(false);
|
|
23
|
+
const [fault, setFault] = useState(null);
|
|
24
|
+
const [micDenied, setMicDenied] = useState(false);
|
|
23
25
|
const [ready, setReady] = useState(false);
|
|
24
26
|
const readyRef = useRef(false);
|
|
25
27
|
readyRef.current = ready;
|
|
@@ -214,6 +216,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
214
216
|
}
|
|
215
217
|
if (!(await ensureMic())) {
|
|
216
218
|
setConnecting(false);
|
|
219
|
+
setMicDenied(true);
|
|
217
220
|
onMicDenied?.("NotAllowedError");
|
|
218
221
|
client.emit("mic_denied", { reason: "NotAllowedError" });
|
|
219
222
|
return;
|
|
@@ -260,6 +263,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
260
263
|
message: msg.message,
|
|
261
264
|
});
|
|
262
265
|
if (msg.reason === "not_configured" && msg.message) {
|
|
266
|
+
setFault(String(msg.message));
|
|
263
267
|
onError?.(String(msg.message));
|
|
264
268
|
}
|
|
265
269
|
return;
|
|
@@ -346,6 +350,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
346
350
|
return;
|
|
347
351
|
case "graine:mic-denied":
|
|
348
352
|
client.emit("mic_denied", { reason: String(msg.reason || "NotAllowedError") });
|
|
353
|
+
setMicDenied(true);
|
|
349
354
|
onMicDenied?.(String(msg.reason || "denied"));
|
|
350
355
|
return;
|
|
351
356
|
case "graine:widget": {
|
|
@@ -387,6 +392,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
387
392
|
clearCaptions: useCallback(() => setCaptions([]), []),
|
|
388
393
|
start: useCallback(() => {
|
|
389
394
|
setConnecting(true);
|
|
395
|
+
setMicDenied(false);
|
|
396
|
+
setFault(null);
|
|
390
397
|
setCaptions([]);
|
|
391
398
|
setAwaitingAgent(false);
|
|
392
399
|
client.clearPendingWidget();
|
|
@@ -402,6 +409,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
402
409
|
quality,
|
|
403
410
|
network,
|
|
404
411
|
reconnecting,
|
|
412
|
+
error: fault,
|
|
413
|
+
micDenied,
|
|
405
414
|
setMuted: useCallback((m) => {
|
|
406
415
|
setMutedState(m);
|
|
407
416
|
post({ type: "graine:set-muted", muted: m });
|
|
@@ -449,6 +458,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
449
458
|
setConnected(false);
|
|
450
459
|
setConnecting(false);
|
|
451
460
|
setCaptions([]);
|
|
461
|
+
setMicDenied(false);
|
|
462
|
+
setFault(null);
|
|
452
463
|
client.clearPendingWidget();
|
|
453
464
|
onCallState?.({ connected: false, connecting: false });
|
|
454
465
|
}, [active]);
|