@graineai/inapp-react-native 0.33.5 → 0.34.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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext, type ScreenField } from "./protocol.js";
1
+ import { type ActionHandler, type AppActionResult, type AppEvent, type ScreenContext, type ScreenField, type AgentSurface } from "./protocol.js";
2
2
  import { type FrictionConfig } from "./friction.js";
3
3
  export declare const SUBPROTOCOL = "graine.embed.v1";
4
4
  export interface GraineInAppOptions {
@@ -66,6 +66,12 @@ export declare class GraineInAppClient {
66
66
  private lastWidget;
67
67
  get pendingWidget(): any;
68
68
  clearPendingWidget(): void;
69
+ private voiceHost;
70
+ get hasVoiceHost(): boolean;
71
+ setVoiceHost(on: boolean): void;
72
+ private currentSurface;
73
+ get surface(): AgentSurface;
74
+ setSurface(next: AgentSurface): void;
69
75
  emit(event: string, payload?: any): void;
70
76
  private get fetch();
71
77
  session(): Promise<SessionConfig>;
@@ -73,6 +79,7 @@ export declare class GraineInAppClient {
73
79
  private reportActionMismatch;
74
80
  private ticket;
75
81
  connect(): Promise<void>;
82
+ private get socketIsVoice();
76
83
  private openSocket;
77
84
  private send;
78
85
  private onFrame;
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ACTION_DEADLINE_MS, CONTEXT_THROTTLE_MS, } from "./protocol.js";
1
+ import { contextToken, ACTION_DEADLINE_MS, CONTEXT_THROTTLE_MS, } from "./protocol.js";
2
2
  import { maskDeep } from "./mask.js";
3
3
  import { FrictionDetector, resolveFriction } from "./friction.js";
4
4
  export const SUBPROTOCOL = "graine.embed.v1";
@@ -29,6 +29,8 @@ export class GraineInAppClient {
29
29
  this.appVersion = null;
30
30
  this.recentEvents = [];
31
31
  this.lastWidget = null;
32
+ this.voiceHost = false;
33
+ this.currentSurface = "voice";
32
34
  if (!opts?.publishableKey)
33
35
  throw new Error("[Graine] publishableKey is required.");
34
36
  if (!opts?.baseUrl)
@@ -64,6 +66,26 @@ export class GraineInAppClient {
64
66
  clearPendingWidget() {
65
67
  this.lastWidget = null;
66
68
  }
69
+ get hasVoiceHost() {
70
+ return this.voiceHost;
71
+ }
72
+ setVoiceHost(on) {
73
+ if (this.voiceHost === on)
74
+ return;
75
+ this.voiceHost = on;
76
+ this.emit("voice_host", { present: on });
77
+ }
78
+ get surface() {
79
+ return this.currentSurface;
80
+ }
81
+ setSurface(next) {
82
+ if (next !== "voice" && next !== "chat")
83
+ return;
84
+ if (this.currentSurface === next)
85
+ return;
86
+ this.currentSurface = next;
87
+ this.emit("surface", { surface: next });
88
+ }
67
89
  emit(event, payload) {
68
90
  if (event === "widget")
69
91
  this.lastWidget = payload ?? null;
@@ -138,7 +160,7 @@ export class GraineInAppClient {
138
160
  headers: { "Content-Type": "application/json" },
139
161
  body: JSON.stringify({
140
162
  publishableKey: this.opts.publishableKey,
141
- mode: this.opts.voice ? "voice" : "text",
163
+ mode: this.socketIsVoice ? "voice" : "text",
142
164
  }),
143
165
  });
144
166
  if (!res.ok)
@@ -157,6 +179,9 @@ export class GraineInAppClient {
157
179
  this.connecting = this.openSocket().finally(() => { this.connecting = null; });
158
180
  return this.connecting;
159
181
  }
182
+ get socketIsVoice() {
183
+ return !!this.opts.voice && this.currentSurface !== "chat";
184
+ }
160
185
  async openSocket() {
161
186
  if (!this.config)
162
187
  await this.session();
@@ -171,10 +196,13 @@ export class GraineInAppClient {
171
196
  }
172
197
  else {
173
198
  const host = (this.config.wsHost || "wss://ws.graine.ai").replace(/\/$/, "");
174
- const mode = this.opts.voice ? "webcall=1" : "text_chat=1";
199
+ const mode = this.socketIsVoice ? "webcall=1" : "text_chat=1";
175
200
  url = `${host}/chat/v1/${encodeURIComponent(agentId)}?${mode}`;
176
201
  this.transport = "direct";
177
202
  }
203
+ const token = contextToken({ ...(this.config.variables ?? {}), ...(this.opts.variables ?? {}), ...this.identity }, this.socketIsVoice ? "webcall" : "text_chat");
204
+ if (token)
205
+ url += `${url.includes("?") ? "&" : "?"}context_token=${encodeURIComponent(token)}`;
178
206
  const WS = this.opts.WebSocketImpl ?? globalThis.WebSocket;
179
207
  const ws = new WS(url, protocols);
180
208
  this.ws = ws;
package/dist/index.d.ts CHANGED
@@ -6,19 +6,22 @@ export { LOADER_KINDS, LOADER_SIZES, LOADER_SPEEDS, LOADER_MOMENTS, DEFAULT_LOAD
6
6
  export type { LoaderKind, LoaderSize, LoaderSpeed, LoaderMoment, LoaderText, LoaderStyle, LoaderLook } from "./protocol.js";
7
7
  export { normaliseSelectAction, fillSelectArgs, fillSelectTemplate, selectCall, NAVIGATE_ACTION, SELECT_ARG_MAX, SELECT_VALUE_MAX } from "./protocol.js";
8
8
  export { WIDGET_SELECTS, MAX_SELECT_DEFAULT, manySummary } from "./protocol.js";
9
+ export { contextToken } from "./protocol.js";
10
+ export { resolveSurface } from "./protocol.js";
11
+ export type { AgentSurface, SurfacePolicy } from "./protocol.js";
9
12
  export type { WidgetSelect } from "./protocol.js";
10
13
  export type { WidgetSelectAction } from "./protocol.js";
11
14
  export { GraineWidget } from "./react-native/widget.js";
12
15
  export { GraineLoader, useGraineLoader } from "./react-native/loader.js";
13
16
  export type { GraineLoaderProps } from "./react-native/loader.js";
14
- export { PENDING_WIDGET_MS } from "./react-native/index.js";
17
+ export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
15
18
  export type { GraineWidgetProps, GraineWidgetTheme } from "./react-native/widget.js";
16
19
  export { GraineInAppClient, type GraineInAppOptions, type SessionConfig, type ActionDiagnosis, } from "./client.js";
17
20
  export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type FrictionConfig, type FrictionScreenRules, type FrictionNudge, type FrictionSignal, type FrictionKind, } from "./friction.js";
18
21
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, type AudioAdapter, type VoiceSessionOptions, } from "./voice.js";
19
22
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, type AudioFormat, type DecodedAudio, } from "./audio.js";
20
23
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
21
- export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
24
+ export { GraineProvider, useGraineAgent, useGraineSurface, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, type GraineEvent, type GraineProviderProps, type Turn, type Caption, } from "./react-native/index.js";
22
25
  export { requestMicrophonePermission } from "./react-native/permissions.js";
23
26
  export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, type GraineFabProps, type GraineTouchCaptureProps, } from "./react-native/fab.js";
24
27
  export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
package/dist/index.js CHANGED
@@ -3,15 +3,17 @@ export { WIDGET_PRESETS, WIDGET_IMAGE_SIZES, WIDGET_DENSITIES, WIDGET_BUTTONS, W
3
3
  export { LOADER_KINDS, LOADER_SIZES, LOADER_SPEEDS, LOADER_MOMENTS, DEFAULT_LOADER_KIND, DEFAULT_LOADER_TEXT, LOADER_TEXT_MAX, normaliseLoaderStyle, resolveLoader, } from "./protocol.js";
4
4
  export { normaliseSelectAction, fillSelectArgs, fillSelectTemplate, selectCall, NAVIGATE_ACTION, SELECT_ARG_MAX, SELECT_VALUE_MAX } from "./protocol.js";
5
5
  export { WIDGET_SELECTS, MAX_SELECT_DEFAULT, manySummary } from "./protocol.js";
6
+ export { contextToken } from "./protocol.js";
7
+ export { resolveSurface } from "./protocol.js";
6
8
  export { GraineWidget } from "./react-native/widget.js";
7
9
  export { GraineLoader, useGraineLoader } from "./react-native/loader.js";
8
- export { PENDING_WIDGET_MS } from "./react-native/index.js";
10
+ export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
9
11
  export { GraineInAppClient, } from "./client.js";
10
12
  export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "./friction.js";
11
13
  export { VoiceSession, liveAudioStreamAdapter, base64ToPcm16, CAPTURE_SAMPLE_RATE, } from "./voice.js";
12
14
  export { EchoGuard, decodeAgentAudio, decodeMuLaw, decodePcm16, base64ToBytes, } from "./audio.js";
13
15
  export { addMaskRule, maskDeep, maskString } from "./mask.js";
14
- export { GraineProvider, useGraineAgent, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, } from "./react-native/index.js";
16
+ export { GraineProvider, useGraineAgent, useGraineSurface, useGraineScreen, useGraineAction, useGraineVoice, useGraineIdentify, useGraineEvents, useGraineField, useGraineClient, setDefaultClient, useGraineTrack, useGraineTap, useGraineHighlight, useGraineWidget, useGraineReady, useGraineInit, } from "./react-native/index.js";
15
17
  export { requestMicrophonePermission } from "./react-native/permissions.js";
16
18
  export { GraineFab, GraineTouchCapture, TAP_MAX_MOVE_PX, TAP_MAX_MS, } from "./react-native/fab.js";
17
19
  export { UNNAMED_TAP, resolveWidgetImages } from "./protocol.js";
@@ -179,3 +179,11 @@ export declare function selectCall(onSelect: WidgetSelectAction | undefined | nu
179
179
  args: Record<string, string>;
180
180
  } | null;
181
181
  export declare function fillSelectArgs(args: Record<string, string> | undefined, item: Record<string, unknown>): Record<string, string>;
182
+ export declare function contextToken(variables: Record<string, unknown> | null | undefined, callType?: string): string | null;
183
+ export type AgentSurface = "voice" | "chat";
184
+ export interface SurfacePolicy {
185
+ allowed: AgentSurface[];
186
+ start: AgentSurface;
187
+ canSwitch: boolean;
188
+ }
189
+ export declare function resolveSurface(appearance?: Record<string, any> | null, prefer?: AgentSurface | null): SurfacePolicy;
package/dist/protocol.js CHANGED
@@ -204,7 +204,7 @@ export function resolveLoader(style) {
204
204
  px: LOADER_PX[s.size ?? "medium"],
205
205
  periodMs: LOADER_PERIOD[s.speed ?? "normal"],
206
206
  text: { ...DEFAULT_LOADER_TEXT, ...(s.text ?? {}) },
207
- skeleton: s.skeleton ?? true,
207
+ skeleton: s.skeleton ?? false,
208
208
  };
209
209
  }
210
210
  const ACTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
@@ -261,3 +261,62 @@ export function fillSelectArgs(args, item) {
261
261
  out[k] = fillSelectTemplate(v, item);
262
262
  return out;
263
263
  }
264
+ export function contextToken(variables, callType = "webcall") {
265
+ const vals = {};
266
+ for (const [k, v] of Object.entries(variables ?? {})) {
267
+ if (v === null || v === undefined || typeof v === "object")
268
+ continue;
269
+ const key = String(k).trim();
270
+ if (key)
271
+ vals[key] = typeof v === "string" ? v : String(v);
272
+ }
273
+ if (!Object.keys(vals).length)
274
+ return null;
275
+ const payload = { ...vals, call_type: callType, recipient_data: { ...vals, call_type: callType } };
276
+ try {
277
+ return base64(JSON.stringify(payload));
278
+ }
279
+ catch {
280
+ return null;
281
+ }
282
+ }
283
+ const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
284
+ function base64(text) {
285
+ const bytes = [];
286
+ for (let i = 0; i < text.length; i++) {
287
+ let c = text.charCodeAt(i);
288
+ if (c < 0x80)
289
+ bytes.push(c);
290
+ else if (c < 0x800)
291
+ bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
292
+ else if (c >= 0xd800 && c <= 0xdbff && i + 1 < text.length) {
293
+ const lo = text.charCodeAt(++i);
294
+ c = 0x10000 + ((c - 0xd800) << 10) + (lo - 0xdc00);
295
+ bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
296
+ }
297
+ else
298
+ bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
299
+ }
300
+ let out = "";
301
+ for (let i = 0; i < bytes.length; i += 3) {
302
+ const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2];
303
+ out += B64[b0 >> 2];
304
+ out += B64[((b0 & 3) << 4) | ((b1 ?? 0) >> 4)];
305
+ out += b1 === undefined ? "=" : B64[((b1 & 15) << 2) | ((b2 ?? 0) >> 6)];
306
+ out += b2 === undefined ? "=" : B64[b2 & 63];
307
+ }
308
+ return out;
309
+ }
310
+ export function resolveSurface(appearance, prefer) {
311
+ const mode = String(appearance?.mode ?? "both").toLowerCase();
312
+ const allowed = mode === "voice" ? ["voice"] : mode === "chat" ? ["chat"] : ["voice", "chat"];
313
+ const configured = String(appearance?.start ?? "").toLowerCase();
314
+ const start = prefer && allowed.includes(prefer)
315
+ ? prefer
316
+ : configured === "chat" || configured === "voice"
317
+ ? allowed.includes(configured)
318
+ ? configured
319
+ : allowed[0]
320
+ : allowed[0];
321
+ return { allowed, start, canSwitch: allowed.length > 1 };
322
+ }
@@ -7,6 +7,9 @@ export { GraineFab, GraineTouchCapture } from "./fab.js";
7
7
  export type { GraineFabProps, GraineTouchCaptureProps } from "./fab.js";
8
8
  export { widgetSurface } from "../protocol.js";
9
9
  export type { WidgetSurface } from "../protocol.js";
10
+ export { resolveSurface } from "../protocol.js";
11
+ export type { AgentSurface, SurfacePolicy } from "../protocol.js";
12
+ import { type AgentSurface } from "../protocol.js";
10
13
  import type { FrictionConfig } from "../friction.js";
11
14
  import { type LauncherInset, type LauncherVisibility } from "./navigation.js";
12
15
  interface GraineContextValue {
@@ -31,6 +34,10 @@ interface GraineContextValue {
31
34
  onEvent: (fn: (e: GraineEvent) => void) => () => void;
32
35
  track: (name: string, data?: Record<string, unknown>) => void;
33
36
  identify: (user: Record<string, unknown> | null) => void;
37
+ surface: AgentSurface;
38
+ surfaces: AgentSurface[];
39
+ canSwitch: boolean;
40
+ setSurface: (surface: AgentSurface) => void;
34
41
  }
35
42
  export type GraineEvent = {
36
43
  type: "conversation_started";
@@ -71,6 +78,7 @@ export type GraineEvent = {
71
78
  };
72
79
  export declare const PENDING_WIDGET_MS = 12000;
73
80
  export declare const AWAITING_REPLY_MS = 12000;
81
+ export declare const MIN_LOADER_MS = 450;
74
82
  export interface GraineWidget {
75
83
  id: string;
76
84
  kind: string;
@@ -96,6 +104,8 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
96
104
  children: React.ReactNode;
97
105
  client?: GraineInAppClient;
98
106
  autoConnect?: boolean;
107
+ surface?: AgentSurface;
108
+ onSurfaceChange?: (surface: AgentSurface) => void;
99
109
  onProactive?: (text: string) => void;
100
110
  endOnBackground?: boolean;
101
111
  navigationRef?: {
@@ -111,8 +121,14 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
111
121
  launcherDelayMs?: number;
112
122
  visibility?: LauncherVisibility;
113
123
  }
114
- export declare function GraineProvider({ children, autoConnect, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
124
+ export declare function GraineProvider({ children, autoConnect, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs, visibility, client: providedClient, ...options }: GraineProviderProps): React.JSX.Element;
115
125
  export declare function useGraineAgent(): GraineContextValue;
126
+ export declare function useGraineSurface(): {
127
+ surface: AgentSurface;
128
+ surfaces: AgentSurface[];
129
+ canSwitch: boolean;
130
+ setSurface: (surface: AgentSurface) => void;
131
+ };
116
132
  export declare function useGraineEvents(handler: (e: GraineEvent) => void): void;
117
133
  export declare function useGraineTrack(): (name: string, data?: Record<string, unknown>) => void;
118
134
  export declare function useGraineTap<T extends (...args: any[]) => any>(name: string, handler?: T, data?: Record<string, unknown>): (...args: Parameters<T>) => ReturnType<T> | undefined;
@@ -7,9 +7,12 @@ import { resolveWidgetImages, normaliseWidgetStyle, normaliseSelectAction, selec
7
7
  import { GraineFab, GraineTouchCapture } from "./fab.js";
8
8
  export { GraineFab, GraineTouchCapture } from "./fab.js";
9
9
  export { widgetSurface } from "../protocol.js";
10
+ export { resolveSurface } from "../protocol.js";
11
+ import { resolveSurface } from "../protocol.js";
10
12
  import { LauncherVisibilityTracker, activeRouteName, } from "./navigation.js";
11
13
  export const PENDING_WIDGET_MS = 12000;
12
14
  export const AWAITING_REPLY_MS = 12000;
15
+ export const MIN_LOADER_MS = 450;
13
16
  export const Ctx = createContext(null);
14
17
  let defaultClient = null;
15
18
  export function setDefaultClient(client) {
@@ -23,7 +26,7 @@ export function useGraineClient() {
23
26
  }
24
27
  return client;
25
28
  }
26
- export function GraineProvider({ children, autoConnect = true, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
29
+ export function GraineProvider({ children, autoConnect = true, surface: surfaceProp, onSurfaceChange, onProactive, endOnBackground = true, navigationRef, fab, captureTaps, appVersion, friction, includeScreens, launcherDelayMs = 0, visibility, client: providedClient, ...options }) {
27
30
  const clientRef = useRef(null);
28
31
  if (!clientRef.current) {
29
32
  clientRef.current =
@@ -56,6 +59,28 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
56
59
  }, []);
57
60
  const [appearance, setAppearance] = useState(null);
58
61
  const [sessionReady, setSessionReady] = useState(false);
62
+ const [chosenSurface, setChosenSurface] = useState(null);
63
+ const surfacePolicy = useMemo(() => resolveSurface(appearance, chosenSurface ?? surfaceProp ?? null), [appearance, chosenSurface, surfaceProp]);
64
+ const surface = surfacePolicy.start;
65
+ const surfaceRef = useRef(surface);
66
+ surfaceRef.current = surface;
67
+ const surfaceChangeRef = useRef(onSurfaceChange);
68
+ surfaceChangeRef.current = onSurfaceChange;
69
+ const setSurface = useCallback((next) => {
70
+ if (next !== "voice" && next !== "chat")
71
+ return;
72
+ setChosenSurface((cur) => (cur === next ? cur : next));
73
+ }, []);
74
+ const announcedSurfaceRef = useRef(null);
75
+ useEffect(() => { client.setSurface(surface); }, [client, surface]);
76
+ useEffect(() => {
77
+ if (announcedSurfaceRef.current === surface)
78
+ return;
79
+ const first = announcedSurfaceRef.current === null;
80
+ announcedSurfaceRef.current = surface;
81
+ if (!first)
82
+ surfaceChangeRef.current?.(surface);
83
+ }, [surface]);
59
84
  const [currentScreen, setCurrentScreen] = useState(null);
60
85
  const [launcherVisible, setLauncherVisible] = useState(false);
61
86
  const [launcherInset, setLauncherInset] = useState({ right: 16, bottom: 20 });
@@ -128,6 +153,12 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
128
153
  openRef.current = open;
129
154
  useEffect(() => {
130
155
  const offs = [
156
+ client.on("call_state", (s) => {
157
+ if (client.isConnected)
158
+ return;
159
+ setConnected(!!s?.connected);
160
+ setConnecting(!!s?.connecting);
161
+ }),
131
162
  client.on("open", () => {
132
163
  setConnected(true);
133
164
  setConnecting(false);
@@ -205,7 +236,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
205
236
  client.highlight(name, Number(args?.ttl_ms) || 6000);
206
237
  return { status: "ok", message: `Highlighted ${name} for the customer.` };
207
238
  }));
208
- if (autoConnect) {
239
+ if (autoConnect && !(client.hasVoiceHost && surfaceRef.current !== "chat")) {
209
240
  setConnecting(true);
210
241
  client.connect().catch((err) => {
211
242
  setConnecting(false);
@@ -218,6 +249,27 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
218
249
  client.close();
219
250
  };
220
251
  }, [client, autoConnect]);
252
+ useEffect(() => {
253
+ const apply = () => {
254
+ if (!client.hasVoiceHost)
255
+ return;
256
+ if (surface === "chat") {
257
+ if (client.isConnected)
258
+ return;
259
+ setConnecting(true);
260
+ client.connect().catch((err) => {
261
+ setConnecting(false);
262
+ console.error("[Graine]", err?.message ?? err);
263
+ setError(err?.message ?? "Could not start the chat.");
264
+ });
265
+ return;
266
+ }
267
+ client.close();
268
+ setConnecting(false);
269
+ };
270
+ apply();
271
+ return client.on("voice_host", apply);
272
+ }, [client, surface]);
221
273
  useEffect(() => {
222
274
  if (!endOnBackground)
223
275
  return;
@@ -272,8 +324,13 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
272
324
  onEvent,
273
325
  track: (name, data) => client.track(name, data),
274
326
  identify: (user) => client.identify(user),
327
+ surface,
328
+ surfaces: surfacePolicy.allowed,
329
+ canSwitch: surfacePolicy.canSwitch,
330
+ setSurface,
275
331
  }), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
276
- launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady]);
332
+ launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady,
333
+ surface, surfacePolicy, setSurface]);
277
334
  const wantsCapture = captureTaps ?? !!fab;
278
335
  const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
279
336
  return _jsx(Ctx.Provider, { value: value, children: body });
@@ -287,6 +344,10 @@ function useGraine() {
287
344
  export function useGraineAgent() {
288
345
  return useGraine();
289
346
  }
347
+ export function useGraineSurface() {
348
+ const { surface, surfaces, canSwitch, setSurface } = useGraine();
349
+ return { surface, surfaces, canSwitch, setSurface };
350
+ }
290
351
  export function useGraineEvents(handler) {
291
352
  const { onEvent } = useGraine();
292
353
  const ref = useRef(handler);
@@ -406,6 +467,8 @@ export function useGraineWidget() {
406
467
  const [answered, setAnswered] = useState(null);
407
468
  const [loading, setLoading] = useState(null);
408
469
  const loadingTimer = useRef(null);
470
+ const shownAt = useRef(null);
471
+ const minTimer = useRef(null);
409
472
  const [awaitingReply, setAwaitingReply] = useState(false);
410
473
  const [closed, setClosed] = useState(null);
411
474
  const awaitingTimer = useRef(null);
@@ -414,6 +477,8 @@ export function useGraineWidget() {
414
477
  clearTimeout(loadingTimer.current);
415
478
  if (awaitingTimer.current)
416
479
  clearTimeout(awaitingTimer.current);
480
+ if (minTimer.current)
481
+ clearTimeout(minTimer.current);
417
482
  }, []);
418
483
  useEffect(() => {
419
484
  const done = () => {
@@ -439,6 +504,7 @@ export function useGraineWidget() {
439
504
  if (pending)
440
505
  setWidget((cur) => (cur && cur.id === pending.id ? cur : pending));
441
506
  const offPending = client.on("widget_pending", (hint) => {
507
+ shownAt.current = Date.now();
442
508
  setLoading({ id: String(hint?.widget_id ?? ""), kind: String(hint?.kind ?? "") });
443
509
  if (loadingTimer.current)
444
510
  clearTimeout(loadingTimer.current);
@@ -452,10 +518,21 @@ export function useGraineWidget() {
452
518
  clearTimeout(loadingTimer.current);
453
519
  loadingTimer.current = null;
454
520
  }
455
- setLoading(null);
456
- setClosed(null);
457
- setWidget(next);
458
- setAnswered(null);
521
+ const show = () => {
522
+ shownAt.current = null;
523
+ setLoading(null);
524
+ setClosed(null);
525
+ setWidget(next);
526
+ setAnswered(null);
527
+ };
528
+ const seen = shownAt.current === null ? MIN_LOADER_MS : Date.now() - shownAt.current;
529
+ if (seen >= MIN_LOADER_MS)
530
+ show();
531
+ else {
532
+ if (minTimer.current)
533
+ clearTimeout(minTimer.current);
534
+ minTimer.current = setTimeout(show, MIN_LOADER_MS - seen);
535
+ }
459
536
  });
460
537
  return () => { offPending(); offWidget(); };
461
538
  }, [client, adopt]);
@@ -490,6 +567,11 @@ export function useGraineWidget() {
490
567
  awaitingTimer.current = setTimeout(() => setAwaitingReply(false), AWAITING_REPLY_MS);
491
568
  }, [client, widget]);
492
569
  const dismiss = useCallback(() => {
570
+ if (minTimer.current) {
571
+ clearTimeout(minTimer.current);
572
+ minTimer.current = null;
573
+ }
574
+ shownAt.current = null;
493
575
  if (widget) {
494
576
  client.reportEvent({
495
577
  name: "component_dismissed",
@@ -1,7 +1,8 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { ActivityIndicator, Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
3
+ import { Image, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, useColorScheme, View, } from "react-native";
4
4
  import { useGraineAgent } from "./index.js";
5
+ import { GraineLoader } from "./loader.js";
5
6
  import { resolveNativeAppearance } from "./appearance.js";
6
7
  const DARK = {
7
8
  surface: "#17161A",
@@ -22,7 +23,8 @@ const LIGHT = {
22
23
  danger: "#D32F2F",
23
24
  };
24
25
  export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, scheme = "auto", theme: themeProp, }) {
25
- const { connected, connecting, error, messages, send, muted, setMuted, caption, agentSpeaking, launcherVisible, launcherInset, appearance, } = useGraineAgent();
26
+ const { connected, connecting, error, messages, send, muted, setMuted, caption, agentSpeaking, launcherVisible, launcherInset, appearance, surface, canSwitch, setSurface, } = useGraineAgent();
27
+ const isChat = surface === "chat";
26
28
  const dashboard = resolveNativeAppearance(appearance, {
27
29
  accent: "#318CE7",
28
30
  heading: "Assistant",
@@ -35,22 +37,26 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
35
37
  const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
36
38
  return makeStyles(themeProp ? { ...base, ...themeProp } : base);
37
39
  }, [scheme, deviceScheme, themeProp]);
38
- const [expanded, setExpanded] = useState(false);
40
+ const [expanded, setExpanded] = useState(surface === "chat");
39
41
  const [captions, setCaptions] = useState(captionsOn);
40
42
  const [draft, setDraft] = useState("");
41
43
  const scrollRef = useRef(null);
42
44
  const last = messages[messages.length - 1];
45
+ const waiting = connecting;
46
+ const waitingMoment = "connecting";
43
47
  const status = error
44
48
  ? "unavailable"
45
49
  : connecting ? "connecting…"
46
50
  : muted ? "muted"
47
- : agentSpeaking || (last?.role === "agent" && last.live) ? "speaking"
48
- : connected ? "listening"
51
+ : agentSpeaking || (last?.role === "agent" && last.live) ? (isChat ? "replying" : "speaking")
52
+ : connected ? (isChat ? "ready" : "listening")
49
53
  : "offline";
50
54
  useEffect(() => {
51
55
  if (expanded)
52
56
  requestAnimationFrame(() => scrollRef.current?.scrollToEnd({ animated: true }));
53
57
  }, [messages, expanded]);
58
+ useEffect(() => { if (surface === "chat")
59
+ setExpanded(true); }, [surface]);
54
60
  useEffect(() => {
55
61
  if (!expanded && last?.role === "agent")
56
62
  setExpanded(true);
@@ -71,10 +77,12 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
71
77
  right: launcherInset.right ?? 12,
72
78
  left: launcherInset.left ?? 12,
73
79
  },
74
- ], 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: [
80
+ ], 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: isChat
81
+ ? "I can see what you\u2019re working on \u2014 type a question, or I\u2019ll step in if you get stuck."
82
+ : "I can see what you\u2019re working on \u2014 ask me anything, or I\u2019ll step in if you get stuck." })), messages.map((m, i) => (_jsx(View, { style: [
75
83
  styles.bubble,
76
84
  m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
77
- ], children: _jsx(Text, { style: m.role === "agent" ? styles.agentText : styles.customerText, children: m.text }) }, i)))] }), _jsx(KeyboardAvoidingView, { behavior: Platform.OS === "ios" ? "padding" : undefined, children: _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { style: styles.input, value: draft, onChangeText: setDraft, placeholder: "Type a message", placeholderTextColor: styles.placeholder.color, onSubmitEditing: submit, returnKeyType: "send", blurOnSubmit: false }), _jsx(Pressable, { onPress: submit, disabled: !draft.trim(), accessibilityLabel: "Send", style: [styles.send, { backgroundColor: accent, opacity: draft.trim() ? 1 : 0.4 }], children: _jsx(Text, { style: styles.sendLabel, children: "\u2191" }) })] }) })] })), captions && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), _jsx(Text, { style: styles.status, children: status })] }), 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 ? "⌄" : "⌃" }) })] })] }));
85
+ ], 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 && !isChat && caption?.text ? (_jsx(View, { style: styles.captionWrap, children: _jsx(Text, { numberOfLines: 3, style: [styles.caption, caption.live && styles.captionLive], children: caption.text }) })) : null, _jsxs(View, { style: styles.bar, children: [avatarUrl ? (_jsx(Image, { source: { uri: avatarUrl }, style: styles.avatarImage, accessibilityIgnoresInvertColors: true })) : (_jsx(View, { style: [styles.avatar, { backgroundColor: accent }], children: _jsx(Text, { style: styles.avatarGlyph, children: "\u2726" }) })), _jsxs(Pressable, { onPress: () => setExpanded((v) => !v), style: { flex: 1 }, accessibilityLabel: expanded ? "Collapse" : "Expand", children: [_jsx(Text, { style: styles.name, children: name }), waiting ? (_jsx(GraineLoader, { moment: waitingMoment, size: 15 })) : (_jsx(Text, { style: styles.status, children: status }))] }), canSwitch ? (_jsx(Pressable, { onPress: () => setSurface(isChat ? "voice" : "chat"), hitSlop: 8, accessibilityRole: "button", accessibilityLabel: isChat ? "Switch to a call" : "Switch to chat", style: styles.round, children: _jsx(Text, { style: styles.glyph, children: isChat ? "📞" : "💬" }) })) : null, waiting ? null : (_jsxs(_Fragment, { children: [isChat ? null : (_jsxs(_Fragment, { children: [_jsx(Pressable, { onPress: () => setCaptions((v) => !v), hitSlop: 8, accessibilityLabel: captions ? "Hide captions" : "Show captions", style: [styles.round, captions && { backgroundColor: accent }], children: _jsx(Text, { style: [styles.glyph, captions && styles.glyphOnAccent], children: "CC" }) }), _jsx(Pressable, { onPress: () => setMuted(!muted), disabled: !connected, hitSlop: 8, accessibilityLabel: muted ? "Unmute" : "Mute", style: [styles.round, muted && styles.muted, !connected && { opacity: 0.4 }], children: _jsx(Text, { style: [styles.glyph, muted && styles.glyphOnAccent], children: muted ? "🔇" : "🎙" }) })] })), _jsx(Pressable, { onPress: () => setExpanded((v) => !v), hitSlop: 8, accessibilityLabel: expanded ? "Collapse" : "Expand", style: styles.round, children: _jsx(Text, { style: styles.chevron, children: expanded ? "⌄" : "⌃" }) })] }))] })] }));
78
86
  }
79
87
  export const GraineLauncher = GraineAgentBar;
80
88
  const makeStyles = (t) => StyleSheet.create({
@@ -54,4 +54,4 @@ export interface GraineVoiceApi {
54
54
  clearCaptions: () => void;
55
55
  }
56
56
  export declare const CAPTION_HISTORY = 12;
57
- export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
57
+ export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active: activeProp, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
@@ -4,9 +4,14 @@ import { AppState, View } from "react-native";
4
4
  import { useGraineAgent } from "./index.js";
5
5
  import { requestMicrophonePermission } from "./permissions.js";
6
6
  export const CAPTION_HISTORY = 12;
7
- export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }) {
8
- const { client, appearance } = useGraineAgent();
7
+ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active: activeProp = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, timing, onEnded, children, }) {
8
+ const { client, appearance, surface, surfaces } = useGraineAgent();
9
+ const active = activeProp && surface !== "chat";
9
10
  const ref = useRef(null);
11
+ useEffect(() => {
12
+ client.setVoiceHost(true);
13
+ return () => client.setVoiceHost(false);
14
+ }, [client]);
10
15
  const startedRef = useRef(false);
11
16
  const activeRef = useRef(active);
12
17
  activeRef.current = active;
@@ -216,6 +221,10 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
216
221
  }
217
222
  return;
218
223
  case "graine:ready":
224
+ post({
225
+ type: "graine:variables",
226
+ variables: { ...(client.opts.variables ?? {}), ...client.getIdentity() },
227
+ });
219
228
  clearTimer(readyTimerRef);
220
229
  clearTimer(retryTimerRef);
221
230
  attemptRef.current = 0;
@@ -236,6 +245,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
236
245
  endedNotifiedRef.current = false;
237
246
  setConnected(!!msg.connected);
238
247
  setConnecting(!!msg.connecting);
248
+ client.emit?.("call_state", { connected: !!msg.connected, connecting: !!msg.connecting });
239
249
  if (!msg.connected && !msg.connecting)
240
250
  startedRef.current = false;
241
251
  onCallState?.({ connected: !!msg.connected, connecting: !!msg.connecting });
@@ -398,7 +408,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
398
408
  return () => { if (pending)
399
409
  clearTimeout(pending); sub.remove(); };
400
410
  }, [post]);
401
- if (!voiceAgentId)
411
+ if (!voiceAgentId || !surfaces.includes("voice"))
402
412
  return _jsx(_Fragment, { children: children?.(api) });
403
413
  const effectiveTransport = transport ?? (appearance?.audioTransport === "rtc" ? "rtc" : "ws");
404
414
  const src = `${client.opts.baseUrl.replace(/\/$/, "")}/embed/${voiceAgentId}` +
@@ -123,16 +123,19 @@ export function GraineWidget(props) {
123
123
  const chosen = answered ? (_jsx(AnsweredLine, { answered: answered, theme: theme, accent: accent, radius: radius, awaiting: awaitingReply, onClear: dismiss })) : null;
124
124
  if (!widget && loading) {
125
125
  const loader = resolveLoader(appearance?.loader);
126
- const pendingSurface = loader.skeleton ? widgetSurface(loading.kind) : "card";
127
- const bone = (w, h, extra = {}) => (_jsx(View, { style: { width: w, height: h, borderRadius: 8, backgroundColor: tint(theme.border, 0.6), ...extra } }));
128
- const bones = (_jsxs(View, { style: { gap: 10 }, accessibilityLabel: loader.text.component, children: [[0, 1, 2].map((i) => (_jsxs(View, { style: { borderRadius: Math.max(10, radius - 6), borderWidth: 1, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))), _jsx(View, { style: { alignItems: "center", paddingTop: 6 }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
126
+ const kindSurface = widgetSurface(loading.kind);
127
+ const pendingSurface = kindSurface;
128
+ const big = pendingSurface === "screen" ? 34 : pendingSurface === "sheet" ? 26 : undefined;
129
+ const mark = (_jsx(View, { style: { alignItems: "center", gap: 10 }, children: _jsx(GraineLoader, { moment: "component", size: big, stacked: true }) }));
130
+ const bone = (w, h) => (_jsx(View, { style: { width: w, height: h, borderRadius: 8, backgroundColor: tint(theme.border, 0.6) } }));
131
+ const bones = (rows) => (_jsx(View, { style: { gap: 10, opacity: 0.55 }, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: Array.from({ length: rows }, (_, i) => (_jsxs(View, { style: { borderRadius: Math.max(10, radius - 6), borderWidth: 1, borderColor: theme.border, padding: 10, flexDirection: "row", gap: 10, alignItems: "center" }, children: [bone(72, 72), _jsxs(View, { style: { flex: 1, gap: 8 }, children: [bone("70%", 12), bone("40%", 12), bone("55%", 10)] })] }, i))) }));
129
132
  if (pendingSurface === "screen") {
130
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsx(View, { style: { flex: 1, backgroundColor: theme.surface, paddingTop: 64, paddingHorizontal: 18 }, children: bones }) })] }));
133
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", onRequestClose: dismiss, transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, children: _jsxs(View, { style: { flex: 1, backgroundColor: theme.surface, paddingTop: 56, paddingHorizontal: 18 }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close and keep talking", hitSlop: 10, style: { width: 44, height: 44, borderRadius: 999, alignItems: "center", justifyContent: "center", backgroundColor: tint(theme.border, 0.4) }, children: _jsx(Text, { style: { color: theme.text, fontSize: 20, fontWeight: "600" }, children: "\u00D7" }) }), _jsx(View, { style: { flex: 1, justifyContent: "center", paddingBottom: loader.skeleton ? 24 : 72 }, children: mark }), loader.skeleton ? bones(3) : null] }) })] }));
131
134
  }
132
135
  if (pendingSurface === "sheet") {
133
- return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsx(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: _jsx(View, { style: { backgroundColor: theme.surface, borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius), padding: 18, paddingBottom: 28 }, children: bones }) }) })] }));
136
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(Modal, { visible: true, animationType: "fade", transparent: true, presentationStyle: "overFullScreen", statusBarTranslucent: true, onRequestClose: dismiss, children: _jsxs(View, { style: { flex: 1, backgroundColor: "rgba(0,0,0,0.45)", justifyContent: "flex-end" }, children: [_jsx(Pressable, { onPress: dismiss, accessibilityRole: "button", accessibilityLabel: "Close", style: { flex: 1 } }), _jsxs(View, { style: { backgroundColor: theme.surface, borderTopLeftRadius: Math.max(20, radius), borderTopRightRadius: Math.max(20, radius), paddingHorizontal: 18, paddingTop: 10, paddingBottom: 28, gap: 18 }, children: [_jsx(View, { style: { alignItems: "center" }, children: _jsx(View, { style: { width: grammar.grabber.width, height: grammar.grabber.height, borderRadius: 999, backgroundColor: theme.border } }) }), _jsx(View, { style: { paddingVertical: loader.skeleton ? 10 : 34 }, children: mark }), loader.skeleton ? bones(2) : null] })] }) })] }));
134
137
  }
135
- return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical: 12, marginBottom: 8 }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
138
+ return (_jsxs(_Fragment, { children: [chosen, _jsx(View, { style: { backgroundColor: theme.surface, borderRadius: radius, borderWidth: 1, borderColor: theme.border, paddingHorizontal: 14, paddingVertical: 14, marginBottom: 8, alignItems: "center" }, children: _jsx(GraineLoader, { moment: "component" }) })] }));
136
139
  }
137
140
  if (!widget)
138
141
  return chosen;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.33.5",
3
+ "version": "0.34.0",
4
4
  "type": "module",
5
- "description": "Graine in-app agent for React Native \u2014 an agent that sees the screen your customer is on and can act on it.",
5
+ "description": "Graine in-app agent for React Native — an agent that sees the screen your customer is on and can act on it.",
6
6
  "license": "MIT",
7
7
  "private": false,
8
8
  "main": "dist/index.js",