@graineai/inapp-react-native 0.18.0 → 0.20.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/INTEGRATION.md +70 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +19 -0
- package/dist/react-native/index.d.ts +14 -0
- package/dist/react-native/index.js +57 -0
- package/dist/react-native/voice-launcher.js +21 -3
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -389,6 +389,74 @@ nowhere. Save is what pushes the catalogue to the agent.
|
|
|
389
389
|
|
|
390
390
|
---
|
|
391
391
|
|
|
392
|
+
## Step 4b — Show them where, and draw them things
|
|
393
|
+
|
|
394
|
+
### Pointing at something on screen
|
|
395
|
+
|
|
396
|
+
"Tap Continue" is useless if the customer cannot find Continue.
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
const { highlighted } = useGraineHighlight("continue_button");
|
|
400
|
+
|
|
401
|
+
<Pressable style={[styles.btn, highlighted && styles.glow]}>
|
|
402
|
+
<Text>Continue</Text>
|
|
403
|
+
</Pressable>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
That is the whole integration. Registering the name tells the agent the element
|
|
407
|
+
exists and can be pointed at; the boolean tells you when it is being pointed at.
|
|
408
|
+
|
|
409
|
+
**The SDK does not draw the highlight**, deliberately — it does not own your
|
|
410
|
+
design language, and a ring that looks right in one app looks broken in the
|
|
411
|
+
next. You get a boolean and style it the way the rest of your app would.
|
|
412
|
+
|
|
413
|
+
Highlights expire after six seconds. One that stays lit because the
|
|
414
|
+
conversation moved on is worse than none: the customer keeps looking at the
|
|
415
|
+
wrong thing.
|
|
416
|
+
|
|
417
|
+
Declare `highlight_element` once on the agent, in the dashboard, alongside your
|
|
418
|
+
other actions. **You do not implement it** — the SDK answers it, checks the name
|
|
419
|
+
against what is actually registered on this screen, and refuses with the list of
|
|
420
|
+
real targets if the agent invents one.
|
|
421
|
+
|
|
422
|
+
### Components, not just sentences
|
|
423
|
+
|
|
424
|
+
Sometimes a card, a set of choices or a three-field form serves the customer
|
|
425
|
+
better than another sentence. The agent decides, and sends one:
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
const { widget, submit, dismiss } = useGraineWidget();
|
|
429
|
+
|
|
430
|
+
if (widget?.kind === "choices") {
|
|
431
|
+
return (
|
|
432
|
+
<Choices
|
|
433
|
+
options={widget.data.options}
|
|
434
|
+
onPick={(o) => submit({ chosen: o.id }, `I'll go with ${o.label}.`)}
|
|
435
|
+
/>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
`submit` takes **two** things and both matter: the structured answer your
|
|
441
|
+
backend wants, and a sentence for the conversation. Send only the first and the
|
|
442
|
+
customer taps a card, sees a tick, and the agent carries on as though nothing
|
|
443
|
+
happened.
|
|
444
|
+
|
|
445
|
+
`widget.fallbackText` is what the agent would have said instead. Render it if
|
|
446
|
+
you get a `kind` you do not handle — a component you cannot draw should still
|
|
447
|
+
move the conversation forward.
|
|
448
|
+
|
|
449
|
+
`dismiss()` is local only. A customer closing a card has not answered the
|
|
450
|
+
question, so the agent is not told; it will ask again in words, which is right.
|
|
451
|
+
|
|
452
|
+
<Callout>
|
|
453
|
+
Works on both transports. On `rtc` these travel the control channel beside
|
|
454
|
+
screen context and actions, for the same reason: they must arrive whole and in
|
|
455
|
+
order.
|
|
456
|
+
</Callout>
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
392
460
|
## Step 5 — Product events, without interrupting
|
|
393
461
|
|
|
394
462
|
```tsx
|
|
@@ -705,6 +773,8 @@ and resamples in its output stage, which is where the artefacts came from.
|
|
|
705
773
|
| `useGraineIdentify()` | who the customer is |
|
|
706
774
|
| `useGraineScreen()` | what is on the screen |
|
|
707
775
|
| `useGraineAction()` | one handler per declared action |
|
|
776
|
+
| `useGraineHighlight()` | let the agent point at an element on this screen |
|
|
777
|
+
| `useGraineWidget()` | the component the agent drew, and how to answer it |
|
|
708
778
|
| `useGraineTrack()` | product events that inform but do not interrupt |
|
|
709
779
|
| `useGraineEvents()` | conversation lifecycle |
|
|
710
780
|
| `useGraineVoice()` | only if you bring your own native audio instead of the WebView |
|
package/dist/client.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class GraineInAppClient {
|
|
|
26
26
|
private connecting;
|
|
27
27
|
private listeners;
|
|
28
28
|
private actions;
|
|
29
|
+
private highlightables;
|
|
29
30
|
private screen;
|
|
30
31
|
private screenSeq;
|
|
31
32
|
private screenSince;
|
|
@@ -62,6 +63,9 @@ export declare class GraineInAppClient {
|
|
|
62
63
|
data?: Record<string, unknown>;
|
|
63
64
|
}>;
|
|
64
65
|
getAvailableActions(): string[];
|
|
66
|
+
registerHighlightable(name: string): () => void;
|
|
67
|
+
getHighlightables(): string[];
|
|
68
|
+
highlight(name: string | null, ttlMs?: number): void;
|
|
65
69
|
registerAction(name: string, handler: ActionHandler): () => void;
|
|
66
70
|
get availableActions(): string[];
|
|
67
71
|
setScreen(context: ScreenContext | null): void;
|
package/dist/client.js
CHANGED
|
@@ -10,6 +10,7 @@ export class GraineInAppClient {
|
|
|
10
10
|
this.connecting = null;
|
|
11
11
|
this.listeners = new Map();
|
|
12
12
|
this.actions = new Map();
|
|
13
|
+
this.highlightables = new Set();
|
|
13
14
|
this.screen = null;
|
|
14
15
|
this.screenSeq = 0;
|
|
15
16
|
this.screenSince = 0;
|
|
@@ -274,6 +275,22 @@ export class GraineInAppClient {
|
|
|
274
275
|
getAvailableActions() {
|
|
275
276
|
return [...this.actions.keys()];
|
|
276
277
|
}
|
|
278
|
+
registerHighlightable(name) {
|
|
279
|
+
if (!name)
|
|
280
|
+
return () => { };
|
|
281
|
+
this.highlightables.add(name);
|
|
282
|
+
this.scheduleScreen();
|
|
283
|
+
return () => {
|
|
284
|
+
this.highlightables.delete(name);
|
|
285
|
+
this.scheduleScreen();
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
getHighlightables() {
|
|
289
|
+
return [...this.highlightables];
|
|
290
|
+
}
|
|
291
|
+
highlight(name, ttlMs = 6000) {
|
|
292
|
+
this.emit("highlight", { name, ttlMs });
|
|
293
|
+
}
|
|
277
294
|
registerAction(name, handler) {
|
|
278
295
|
this.actions.set(name, handler);
|
|
279
296
|
this.scheduleScreen();
|
|
@@ -336,6 +353,7 @@ export class GraineInAppClient {
|
|
|
336
353
|
...(this.screen ? { idle_ms: Date.now() - this.screenSince } : {}),
|
|
337
354
|
...(Object.keys(this.identity).length ? { user: this.identity } : {}),
|
|
338
355
|
...(this.recentEvents.length ? { recent_events: this.recentEvents } : {}),
|
|
356
|
+
...(this.highlightables.size ? { highlightable: [...this.highlightables] } : {}),
|
|
339
357
|
}),
|
|
340
358
|
available_actions: (availableActions ?? this.availableActions).filter((a) => this.actions.has(a)),
|
|
341
359
|
seq: ++this.screenSeq,
|
|
@@ -424,6 +442,7 @@ export class GraineInAppClient {
|
|
|
424
442
|
return this.agentSpeaking;
|
|
425
443
|
}
|
|
426
444
|
submitWidget(widgetId, data, summary) {
|
|
445
|
+
this.emit("widget_result", { widget_id: widgetId, data, summary });
|
|
427
446
|
this.send({ type: "widget_result", widget_id: widgetId, data });
|
|
428
447
|
if (summary)
|
|
429
448
|
this.say(summary);
|
|
@@ -50,6 +50,12 @@ export type GraineEvent = {
|
|
|
50
50
|
type: "error";
|
|
51
51
|
message: string;
|
|
52
52
|
};
|
|
53
|
+
export interface GraineWidget {
|
|
54
|
+
id: string;
|
|
55
|
+
kind: string;
|
|
56
|
+
data: Record<string, unknown>;
|
|
57
|
+
fallbackText: string;
|
|
58
|
+
}
|
|
53
59
|
export interface Caption {
|
|
54
60
|
role: "agent" | "customer";
|
|
55
61
|
text: string;
|
|
@@ -86,5 +92,13 @@ export declare function useGraineVoice(adapter: AudioAdapter | null, options?: V
|
|
|
86
92
|
stop: () => Promise<void>;
|
|
87
93
|
};
|
|
88
94
|
export declare function useGraineScreen(context: ScreenContext | null): void;
|
|
95
|
+
export declare function useGraineHighlight(name: string): {
|
|
96
|
+
highlighted: boolean;
|
|
97
|
+
};
|
|
98
|
+
export declare function useGraineWidget(): {
|
|
99
|
+
widget: GraineWidget | null;
|
|
100
|
+
submit: (data: Record<string, unknown>, summary: string) => void;
|
|
101
|
+
dismiss: () => void;
|
|
102
|
+
};
|
|
89
103
|
export declare function useGraineAction(name: string, handler: ActionHandler): void;
|
|
90
104
|
export {};
|
|
@@ -148,6 +148,20 @@ export function GraineProvider({ children, autoConnect = true, onProactive, endO
|
|
|
148
148
|
? { ...prev, text: prev.text + text }
|
|
149
149
|
: { role: "agent", text, live: true })),
|
|
150
150
|
];
|
|
151
|
+
offs.push(client.registerAction("highlight_element", async (args) => {
|
|
152
|
+
const name = String(args?.name || args?.element || args?.target || "");
|
|
153
|
+
const known = client.getHighlightables();
|
|
154
|
+
if (!name || !known.includes(name)) {
|
|
155
|
+
return {
|
|
156
|
+
status: "refused",
|
|
157
|
+
message: known.length
|
|
158
|
+
? `There is nothing called "${name}" on this screen. I can point at: ${known.join(", ")}.`
|
|
159
|
+
: "There is nothing on this screen I can point at.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
client.highlight(name, Number(args?.ttl_ms) || 6000);
|
|
163
|
+
return { status: "ok", message: `Highlighted ${name} for the customer.` };
|
|
164
|
+
}));
|
|
151
165
|
if (autoConnect) {
|
|
152
166
|
setConnecting(true);
|
|
153
167
|
client.connect().catch((err) => {
|
|
@@ -296,6 +310,49 @@ export function useGraineScreen(context) {
|
|
|
296
310
|
return () => client.setScreen(null);
|
|
297
311
|
}, [client, serialised]);
|
|
298
312
|
}
|
|
313
|
+
export function useGraineHighlight(name) {
|
|
314
|
+
const { client } = useGraine();
|
|
315
|
+
const [highlighted, setHighlighted] = useState(false);
|
|
316
|
+
useEffect(() => client.registerHighlightable(name), [client, name]);
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
let timer = null;
|
|
319
|
+
const off = client.on("highlight", (e) => {
|
|
320
|
+
const on = e?.name === name;
|
|
321
|
+
setHighlighted(on);
|
|
322
|
+
if (timer) {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
timer = null;
|
|
325
|
+
}
|
|
326
|
+
if (on)
|
|
327
|
+
timer = setTimeout(() => setHighlighted(false), Number(e?.ttlMs) || 6000);
|
|
328
|
+
});
|
|
329
|
+
return () => { if (timer)
|
|
330
|
+
clearTimeout(timer); off(); };
|
|
331
|
+
}, [client, name]);
|
|
332
|
+
return { highlighted };
|
|
333
|
+
}
|
|
334
|
+
export function useGraineWidget() {
|
|
335
|
+
const { client } = useGraine();
|
|
336
|
+
const [widget, setWidget] = useState(null);
|
|
337
|
+
useEffect(() => client.on("widget", (w) => {
|
|
338
|
+
if (!w?.widget_id)
|
|
339
|
+
return;
|
|
340
|
+
setWidget({
|
|
341
|
+
id: String(w.widget_id),
|
|
342
|
+
kind: String(w.kind || ""),
|
|
343
|
+
data: (w.data || {}),
|
|
344
|
+
fallbackText: String(w.fallback_text || ""),
|
|
345
|
+
});
|
|
346
|
+
}), [client]);
|
|
347
|
+
const submit = useCallback((data, summary) => {
|
|
348
|
+
if (!widget)
|
|
349
|
+
return;
|
|
350
|
+
client.submitWidget(widget.id, data, summary);
|
|
351
|
+
setWidget(null);
|
|
352
|
+
}, [client, widget]);
|
|
353
|
+
const dismiss = useCallback(() => setWidget(null), []);
|
|
354
|
+
return { widget, submit, dismiss };
|
|
355
|
+
}
|
|
299
356
|
export function useGraineAction(name, handler) {
|
|
300
357
|
const { client } = useGraine();
|
|
301
358
|
const handlerRef = useRef(handler);
|
|
@@ -66,9 +66,11 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
|
|
|
66
66
|
});
|
|
67
67
|
}, [client, post]);
|
|
68
68
|
useEffect(() => {
|
|
69
|
-
if (ready)
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
if (!ready)
|
|
70
|
+
return;
|
|
71
|
+
pushContext();
|
|
72
|
+
return client.on("screen", () => pushContext());
|
|
73
|
+
}, [client, ready, pushContext]);
|
|
72
74
|
const beginCall = useCallback(async () => {
|
|
73
75
|
if (client.isConnected) {
|
|
74
76
|
console.warn("[graine] The provider has its own connection open while GraineVoiceLauncher " +
|
|
@@ -93,6 +95,18 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
|
|
|
93
95
|
post({ type: "graine:app-event", name, data: rest });
|
|
94
96
|
});
|
|
95
97
|
}, [client, connected, post, pushContext]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!connected)
|
|
100
|
+
return;
|
|
101
|
+
return client.on("widget_result", (r) => {
|
|
102
|
+
post({
|
|
103
|
+
type: "graine:widget-result",
|
|
104
|
+
widgetId: r?.widget_id,
|
|
105
|
+
data: r?.data ?? {},
|
|
106
|
+
summary: r?.summary ?? "",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}, [client, connected, post]);
|
|
96
110
|
const onMessage = useCallback(async (e) => {
|
|
97
111
|
let msg;
|
|
98
112
|
try {
|
|
@@ -143,6 +157,10 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, trans
|
|
|
143
157
|
case "graine:mic-denied":
|
|
144
158
|
onMicDenied?.(String(msg.reason || "denied"));
|
|
145
159
|
return;
|
|
160
|
+
case "graine:widget": {
|
|
161
|
+
client.emit?.("widget", msg.widget || {});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
146
164
|
case "graine:app-action": {
|
|
147
165
|
const action = msg.action || {};
|
|
148
166
|
const result = await client.executeAction(action.name, action.arguments || {});
|
package/package.json
CHANGED