@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.
@@ -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]);