@graineai/inapp-react-native 0.27.5 → 0.28.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
CHANGED
|
@@ -558,6 +558,53 @@ your funnel.
|
|
|
558
558
|
|
|
559
559
|
---
|
|
560
560
|
|
|
561
|
+
## Captions and hanging up
|
|
562
|
+
|
|
563
|
+
`GraineVoiceLauncher`'s render prop gives you the whole transcript and the CC
|
|
564
|
+
switch, so a caption box is a `map` and nothing else. **0.28.0.**
|
|
565
|
+
|
|
566
|
+
```tsx
|
|
567
|
+
<GraineVoiceLauncher webView={WebView} active={open} autoStart>
|
|
568
|
+
{(api) => (
|
|
569
|
+
<>
|
|
570
|
+
{api.captionsOn && api.captions.map((c, i) => (
|
|
571
|
+
<Text key={i} style={{ opacity: c.live ? 0.7 : 1 }}>
|
|
572
|
+
{c.role === "agent" ? "Ring" : "You"}: {c.text}
|
|
573
|
+
</Text>
|
|
574
|
+
))}
|
|
575
|
+
<Button title="CC" onPress={() => api.setCaptionsOn(!api.captionsOn)} />
|
|
576
|
+
<Button title="End" onPress={api.end} />
|
|
577
|
+
</>
|
|
578
|
+
)}
|
|
579
|
+
</GraineVoiceLauncher>
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
| On `api` | |
|
|
583
|
+
|---|---|
|
|
584
|
+
| `captions` | Every turn so far, oldest first, capped at `CAPTION_HISTORY` (12) |
|
|
585
|
+
| `caption` | The turn being said now, or the last one. `null` before any |
|
|
586
|
+
| `captionsOn` / `setCaptionsOn` | The CC switch. Defaults **on** |
|
|
587
|
+
| `clearCaptions()` | Forget the transcript without ending the call |
|
|
588
|
+
| `end()` | Hang up |
|
|
589
|
+
|
|
590
|
+
A turn arrives many times as its words come in — each with fuller text and
|
|
591
|
+
`live: true` — then once more settled with `live: false`. The launcher folds
|
|
592
|
+
those into one entry, so `captions` holds turns, not keystrokes. Render `live`
|
|
593
|
+
ones as provisional if you want; they are the same line about to be finished.
|
|
594
|
+
|
|
595
|
+
The transcript is **cleared when a call starts and when you set `active={false}`**.
|
|
596
|
+
A call's words outliving its call is a privacy question, not a feature — if you
|
|
597
|
+
need them afterwards, copy them in `onEnded`.
|
|
598
|
+
|
|
599
|
+
`onCaption` still fires for every line and is unchanged; it is now a convenience
|
|
600
|
+
on top of `captions` rather than the only way to see them.
|
|
601
|
+
|
|
602
|
+
**Hanging up** happens on `api.end()` and again on `active={false}` — both post
|
|
603
|
+
the same idempotent stop frame, so a host that does one, the other, or both
|
|
604
|
+
ends exactly one call. Before 0.27.6 the deactivate path was conditional on the
|
|
605
|
+
launcher's own idea of whether a call was running, which could close the UI and
|
|
606
|
+
leave the call up.
|
|
607
|
+
|
|
561
608
|
## Tap capture when your screens sit above the provider
|
|
562
609
|
|
|
563
610
|
`GraineProvider` wraps its children in `GraineTouchCapture`, so an app whose
|
|
@@ -1077,7 +1124,7 @@ told to ignore).
|
|
|
1077
1124
|
| `onMicDenied` | `(reason) => void` | `NotAllowedError` is a refusal; `NotFoundError` is a device with no usable microphone. Different sentences for a customer. |
|
|
1078
1125
|
| `onError` | `(message) => void` | Voice could not be set up at all — key rejected, agent not enabled for apps, no network. Without this the launcher renders nothing and you cannot tell why. |
|
|
1079
1126
|
| `children` | `(api) => ReactNode` | Receives `{ connected, connecting, muted, start, end, setMuted }`. |
|
|
1080
|
-
| `active` | `boolean` | Default `true`. Mount early with `false` so the engine page loads before the customer needs it (it registers nothing while idle); flip to `true` when your UI opens — with `autoStart` that places the call, without it it warms the registration so the tap is instant. Back to `false` ends a live call. (0.27.5) |
|
|
1127
|
+
| `active` | `boolean` | Default `true`. Mount early with `false` so the engine page loads before the customer needs it (it registers nothing while idle); flip to `true` when your UI opens — with `autoStart` that places the call, without it it warms the registration so the tap is instant. Back to `false` ends a live call. (0.27.5; 0.27.6 makes that hangup unconditional) |
|
|
1081
1128
|
|
|
1082
1129
|
**The launcher owns the connection.** It asks for the microphone, resolves the
|
|
1083
1130
|
session, holds the socket and relays your screen, actions and events onto it. So
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@ export { UNNAMED_TAP } from "./protocol.js";
|
|
|
11
11
|
export { resolveNativeAppearance, type NativeAppearance, type NativeAppearanceFallback, } from "./react-native/appearance.js";
|
|
12
12
|
export { RtcVoiceSessionController, RtcVoiceError, type RtcVoiceState, type RtcVoiceErrorCode, type RtcVoiceSession, type RtcVoiceOptions, } from "./react-native/voice-rtc.js";
|
|
13
13
|
export { GraineAgentBar, GraineLauncher, type GraineAgentBarProps, type GraineLauncherProps, type GraineBarTheme, } from "./react-native/ui.js";
|
|
14
|
-
export { GraineVoiceLauncher, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
|
14
|
+
export { GraineVoiceLauncher, CAPTION_HISTORY, type GraineVoiceLauncherProps, type GraineVoiceApi, } from "./react-native/voice-launcher.js";
|
|
15
15
|
export { LauncherVisibilityTracker, activeRouteName, type LauncherInset, type LauncherContinuity, type LauncherDelayPolicy, type LauncherGroup, type LauncherVisibility, type VisibilityDecision, } from "./react-native/navigation.js";
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { UNNAMED_TAP } from "./protocol.js";
|
|
|
10
10
|
export { resolveNativeAppearance, } from "./react-native/appearance.js";
|
|
11
11
|
export { RtcVoiceSessionController, RtcVoiceError, } from "./react-native/voice-rtc.js";
|
|
12
12
|
export { GraineAgentBar, GraineLauncher, } from "./react-native/ui.js";
|
|
13
|
-
export { GraineVoiceLauncher, } from "./react-native/voice-launcher.js";
|
|
13
|
+
export { GraineVoiceLauncher, CAPTION_HISTORY, } from "./react-native/voice-launcher.js";
|
|
14
14
|
export { LauncherVisibilityTracker, activeRouteName, } from "./react-native/navigation.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { type Caption } from "./index.js";
|
|
2
3
|
export interface GraineVoiceLauncherProps {
|
|
3
4
|
transport?: "ws" | "rtc";
|
|
4
5
|
onTransport?: (info: {
|
|
@@ -32,5 +33,11 @@ export interface GraineVoiceApi {
|
|
|
32
33
|
start: () => void;
|
|
33
34
|
end: () => void;
|
|
34
35
|
setMuted: (m: boolean) => void;
|
|
36
|
+
captions: Caption[];
|
|
37
|
+
caption: Caption | null;
|
|
38
|
+
captionsOn: boolean;
|
|
39
|
+
setCaptionsOn: (on: boolean) => void;
|
|
40
|
+
clearCaptions: () => void;
|
|
35
41
|
}
|
|
42
|
+
export declare const CAPTION_HISTORY = 12;
|
|
36
43
|
export declare function GraineVoiceLauncher({ webView: WebView, autoStart, active, transport, onTransport, onCaption, onCallState, onMicDenied, onError, onEnded, children, }: GraineVoiceLauncherProps): React.JSX.Element;
|
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import { AppState, View } from "react-native";
|
|
4
4
|
import { useGraineAgent } from "./index.js";
|
|
5
5
|
import { requestMicrophonePermission } from "./permissions.js";
|
|
6
|
+
export const CAPTION_HISTORY = 12;
|
|
6
7
|
export function GraineVoiceLauncher({ webView: WebView, autoStart = false, active = true, transport, onTransport, onCaption, onCallState, onMicDenied, onError, onEnded, children, }) {
|
|
7
8
|
const { client, appearance } = useGraineAgent();
|
|
8
9
|
const ref = useRef(null);
|
|
@@ -14,6 +15,16 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
14
15
|
const [connecting, setConnecting] = useState(false);
|
|
15
16
|
const [muted, setMutedState] = useState(false);
|
|
16
17
|
const [ready, setReady] = useState(false);
|
|
18
|
+
const [captions, setCaptions] = useState([]);
|
|
19
|
+
const [captionsOn, setCaptionsOn] = useState(true);
|
|
20
|
+
const foldCaption = useCallback((next) => {
|
|
21
|
+
setCaptions((prev) => {
|
|
22
|
+
const tail = prev[prev.length - 1];
|
|
23
|
+
const continues = tail && tail.live && tail.role === next.role;
|
|
24
|
+
const merged = continues ? [...prev.slice(0, -1), next] : [...prev, next];
|
|
25
|
+
return merged.length > CAPTION_HISTORY ? merged.slice(-CAPTION_HISTORY) : merged;
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
17
28
|
const post = useCallback((msg) => {
|
|
18
29
|
ref.current?.injectJavaScript(`window.postMessage(${JSON.stringify(msg)}, "*"); true;`);
|
|
19
30
|
}, []);
|
|
@@ -132,9 +143,16 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
132
143
|
case "graine:muted":
|
|
133
144
|
setMutedState(!!msg.muted);
|
|
134
145
|
return;
|
|
135
|
-
case "graine:caption":
|
|
136
|
-
|
|
146
|
+
case "graine:caption": {
|
|
147
|
+
const line = {
|
|
148
|
+
role: msg.role === "agent" ? "agent" : "customer",
|
|
149
|
+
text: String(msg.text ?? ""),
|
|
150
|
+
live: !!msg.live,
|
|
151
|
+
};
|
|
152
|
+
foldCaption(line);
|
|
153
|
+
onCaption?.(line);
|
|
137
154
|
return;
|
|
155
|
+
}
|
|
138
156
|
case "graine:ended":
|
|
139
157
|
setConnected(false);
|
|
140
158
|
setConnecting(false);
|
|
@@ -161,15 +179,21 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
161
179
|
return;
|
|
162
180
|
}
|
|
163
181
|
}
|
|
164
|
-
}, [autoStart, beginCall, client, onCaption, onCallState, onMicDenied, onTransport, onError, onEnded, post, pushContext]);
|
|
182
|
+
}, [autoStart, beginCall, client, onCaption, onCallState, onMicDenied, onTransport, onError, onEnded, post, pushContext, foldCaption]);
|
|
165
183
|
const pendingStartRef = useRef(false);
|
|
166
184
|
const api = {
|
|
167
185
|
connected,
|
|
168
186
|
connecting,
|
|
169
187
|
muted,
|
|
170
188
|
ready: !!voiceAgentId && ready,
|
|
189
|
+
captions,
|
|
190
|
+
caption: captions.length ? captions[captions.length - 1] : null,
|
|
191
|
+
captionsOn,
|
|
192
|
+
setCaptionsOn,
|
|
193
|
+
clearCaptions: useCallback(() => setCaptions([]), []),
|
|
171
194
|
start: useCallback(() => {
|
|
172
195
|
setConnecting(true);
|
|
196
|
+
setCaptions([]);
|
|
173
197
|
if (!ready) {
|
|
174
198
|
pendingStartRef.current = true;
|
|
175
199
|
return;
|
|
@@ -191,6 +215,8 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
191
215
|
if (autoStart) {
|
|
192
216
|
if (startedRef.current)
|
|
193
217
|
return;
|
|
218
|
+
setConnecting(true);
|
|
219
|
+
setCaptions([]);
|
|
194
220
|
if (ready) {
|
|
195
221
|
startedRef.current = true;
|
|
196
222
|
void beginCall();
|
|
@@ -205,10 +231,13 @@ export function GraineVoiceLauncher({ webView: WebView, autoStart = false, activ
|
|
|
205
231
|
}
|
|
206
232
|
if (first)
|
|
207
233
|
return;
|
|
208
|
-
|
|
209
|
-
post({ type: "graine:end-call" });
|
|
234
|
+
post({ type: "graine:end-call" });
|
|
210
235
|
startedRef.current = false;
|
|
211
236
|
pendingStartRef.current = false;
|
|
237
|
+
setConnected(false);
|
|
238
|
+
setConnecting(false);
|
|
239
|
+
setCaptions([]);
|
|
240
|
+
onCallState?.({ connected: false, connecting: false });
|
|
212
241
|
}, [active]);
|
|
213
242
|
useEffect(() => {
|
|
214
243
|
let last = AppState.currentState;
|
package/package.json
CHANGED