@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 +8 -1
- package/dist/client.js +31 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.js +59 -0
- package/dist/react-native/index.d.ts +16 -1
- package/dist/react-native/index.js +57 -3
- package/dist/react-native/ui.js +11 -6
- package/dist/react-native/voice-launcher.d.ts +1 -1
- package/dist/react-native/voice-launcher.js +12 -3
- 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,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";
|
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
|
@@ -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);
|
package/dist/react-native/ui.js
CHANGED
|
@@ -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(
|
|
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:
|
|
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.
|
|
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",
|