@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 +8 -1
- package/dist/client.js +31 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.js +60 -1
- package/dist/react-native/index.d.ts +17 -1
- package/dist/react-native/index.js +89 -7
- package/dist/react-native/ui.js +16 -8
- package/dist/react-native/voice-launcher.d.ts +1 -1
- package/dist/react-native/voice-launcher.js +13 -3
- package/dist/react-native/widget.js +9 -6
- package/package.json +2 -2
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.
|
|
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.
|
|
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";
|
package/dist/protocol.d.ts
CHANGED
|
@@ -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 ??
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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",
|
package/dist/react-native/ui.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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:
|
|
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 })] }),
|
|
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
|
|
127
|
-
const
|
|
128
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Graine in-app agent for React Native
|
|
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",
|