@graineai/inapp-react-native 0.33.6 β†’ 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,6 +6,9 @@ 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";
@@ -18,7 +21,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, type Fri
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,6 +3,8 @@ 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
10
  export { PENDING_WIDGET_MS, AWAITING_REPLY_MS, MIN_LOADER_MS } from "./react-native/index.js";
@@ -11,7 +13,7 @@ export { FrictionDetector, resolveFriction, rulesFor, DEFAULT_FRICTION, } from "
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
@@ -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";
@@ -97,6 +104,8 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
97
104
  children: React.ReactNode;
98
105
  client?: GraineInAppClient;
99
106
  autoConnect?: boolean;
107
+ surface?: AgentSurface;
108
+ onSurfaceChange?: (surface: AgentSurface) => void;
100
109
  onProactive?: (text: string) => void;
101
110
  endOnBackground?: boolean;
102
111
  navigationRef?: {
@@ -112,8 +121,14 @@ export interface GraineProviderProps extends Partial<GraineInAppOptions> {
112
121
  launcherDelayMs?: number;
113
122
  visibility?: LauncherVisibility;
114
123
  }
115
- 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;
116
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
+ };
117
132
  export declare function useGraineEvents(handler: (e: GraineEvent) => void): void;
118
133
  export declare function useGraineTrack(): (name: string, data?: Record<string, unknown>) => void;
119
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,6 +7,8 @@ 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;
@@ -24,7 +26,7 @@ export function useGraineClient() {
24
26
  }
25
27
  return client;
26
28
  }
27
- 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 }) {
28
30
  const clientRef = useRef(null);
29
31
  if (!clientRef.current) {
30
32
  clientRef.current =
@@ -57,6 +59,28 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
57
59
  }, []);
58
60
  const [appearance, setAppearance] = useState(null);
59
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]);
60
84
  const [currentScreen, setCurrentScreen] = useState(null);
61
85
  const [launcherVisible, setLauncherVisible] = useState(false);
62
86
  const [launcherInset, setLauncherInset] = useState({ right: 16, bottom: 20 });
@@ -212,7 +236,7 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
212
236
  client.highlight(name, Number(args?.ttl_ms) || 6000);
213
237
  return { status: "ok", message: `Highlighted ${name} for the customer.` };
214
238
  }));
215
- if (autoConnect) {
239
+ if (autoConnect && !(client.hasVoiceHost && surfaceRef.current !== "chat")) {
216
240
  setConnecting(true);
217
241
  client.connect().catch((err) => {
218
242
  setConnecting(false);
@@ -225,6 +249,27 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
225
249
  client.close();
226
250
  };
227
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]);
228
273
  useEffect(() => {
229
274
  if (!endOnBackground)
230
275
  return;
@@ -279,8 +324,13 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
279
324
  onEvent,
280
325
  track: (name, data) => client.track(name, data),
281
326
  identify: (user) => client.identify(user),
327
+ surface,
328
+ surfaces: surfacePolicy.allowed,
329
+ canSwitch: surfacePolicy.canSwitch,
330
+ setSurface,
282
331
  }), [client, connected, connecting, error, open, messages, widgets, muted, agentSpeaking, caption,
283
- launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady]);
332
+ launcherVisible, launcherInset, currentScreen, onEvent, appearance, sessionReady,
333
+ surface, surfacePolicy, setSurface]);
284
334
  const wantsCapture = captureTaps ?? !!fab;
285
335
  const body = fab || wantsCapture ? (_jsxs(GraineTouchCapture, { enabled: wantsCapture, children: [children, fab ? _jsx(GraineFab, { ...fab }) : null] })) : children;
286
336
  return _jsx(Ctx.Provider, { value: value, children: body });
@@ -294,6 +344,10 @@ function useGraine() {
294
344
  export function useGraineAgent() {
295
345
  return useGraine();
296
346
  }
347
+ export function useGraineSurface() {
348
+ const { surface, surfaces, canSwitch, setSurface } = useGraine();
349
+ return { surface, surfaces, canSwitch, setSurface };
350
+ }
297
351
  export function useGraineEvents(handler) {
298
352
  const { onEvent } = useGraine();
299
353
  const ref = useRef(handler);
@@ -23,7 +23,8 @@ const LIGHT = {
23
23
  danger: "#D32F2F",
24
24
  };
25
25
  export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl: avatarProp, bottomInset = 96, hidden = false, captionsOn = false, scheme = "auto", theme: themeProp, }) {
26
- 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";
27
28
  const dashboard = resolveNativeAppearance(appearance, {
28
29
  accent: "#318CE7",
29
30
  heading: "Assistant",
@@ -36,7 +37,7 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
36
37
  const base = (scheme === "auto" ? deviceScheme === "light" : scheme === "light") ? LIGHT : DARK;
37
38
  return makeStyles(themeProp ? { ...base, ...themeProp } : base);
38
39
  }, [scheme, deviceScheme, themeProp]);
39
- const [expanded, setExpanded] = useState(false);
40
+ const [expanded, setExpanded] = useState(surface === "chat");
40
41
  const [captions, setCaptions] = useState(captionsOn);
41
42
  const [draft, setDraft] = useState("");
42
43
  const scrollRef = useRef(null);
@@ -47,13 +48,15 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
47
48
  ? "unavailable"
48
49
  : connecting ? "connecting…"
49
50
  : muted ? "muted"
50
- : agentSpeaking || (last?.role === "agent" && last.live) ? "speaking"
51
- : connected ? "listening"
51
+ : agentSpeaking || (last?.role === "agent" && last.live) ? (isChat ? "replying" : "speaking")
52
+ : connected ? (isChat ? "ready" : "listening")
52
53
  : "offline";
53
54
  useEffect(() => {
54
55
  if (expanded)
55
56
  requestAnimationFrame(() => scrollRef.current?.scrollToEnd({ animated: true }));
56
57
  }, [messages, expanded]);
58
+ useEffect(() => { if (surface === "chat")
59
+ setExpanded(true); }, [surface]);
57
60
  useEffect(() => {
58
61
  if (!expanded && last?.role === "agent")
59
62
  setExpanded(true);
@@ -74,10 +77,12 @@ export function GraineAgentBar({ accent: accentProp, name: nameProp, avatarUrl:
74
77
  right: launcherInset.right ?? 12,
75
78
  left: launcherInset.left ?? 12,
76
79
  },
77
- ], 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: [
78
83
  styles.bubble,
79
84
  m.role === "agent" ? styles.agentBubble : [styles.customerBubble, { backgroundColor: accent }],
80
- ], 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 }), waiting ? (_jsx(GraineLoader, { moment: waitingMoment, size: 15 })) : (_jsx(Text, { style: styles.status, children: status }))] }), waiting ? 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 ? "βŒ„" : "βŒƒ" }) })] }))] })] }));
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 ? "βŒ„" : "βŒƒ" }) })] }))] })] }));
81
86
  }
82
87
  export const GraineLauncher = GraineAgentBar;
83
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;
@@ -399,7 +408,7 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
399
408
  return () => { if (pending)
400
409
  clearTimeout(pending); sub.remove(); };
401
410
  }, [post]);
402
- if (!voiceAgentId)
411
+ if (!voiceAgentId || !surfaces.includes("voice"))
403
412
  return _jsx(_Fragment, { children: children?.(api) });
404
413
  const effectiveTransport = transport ?? (appearance?.audioTransport === "rtc" ? "rtc" : "ws");
405
414
  const src = `${client.opts.baseUrl.replace(/\/$/, "")}/embed/${voiceAgentId}` +
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@graineai/inapp-react-native",
3
- "version": "0.33.6",
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",