@akhilpulse/samadhaan-session-replay 0.6.1 → 0.6.3
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/README.md +10 -2
- package/ios/SessionReplayPcmPlayer.m +12 -0
- package/package.json +1 -1
- package/src/SessionReplayProvider.tsx +70 -31
- package/src/voice.ts +35 -16
package/README.md
CHANGED
|
@@ -140,8 +140,16 @@ cd ios && pod install
|
|
|
140
140
|
the first time the assistant opens. If the user denies it, the voice bar
|
|
141
141
|
shows "Mic blocked — allow microphone access in Settings" instead of
|
|
142
142
|
pretending to listen.
|
|
143
|
-
- **iOS** — add `NSMicrophoneUsageDescription` to `Info.plist`
|
|
144
|
-
|
|
143
|
+
- **iOS** — add `NSMicrophoneUsageDescription` to `Info.plist` (**required** —
|
|
144
|
+
without it iOS terminates the app the moment the mic is touched):
|
|
145
|
+
```xml
|
|
146
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
147
|
+
<string>Needed so the voice assistant can hear you.</string>
|
|
148
|
+
```
|
|
149
|
+
Starting in **0.6.2 the SDK explicitly triggers the system mic prompt** when
|
|
150
|
+
the assistant opens (via the bundled native module — run `pod install` and
|
|
151
|
+
rebuild after upgrading). If the user denies it, the voice bar shows
|
|
152
|
+
"Mic blocked" instead of pretending to listen.
|
|
145
153
|
|
|
146
154
|
#### Agent voice playback
|
|
147
155
|
|
|
@@ -148,6 +148,18 @@ RCT_EXPORT_METHOD(stop)
|
|
|
148
148
|
[self tearDownEngine];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// Explicitly triggers the iOS microphone permission prompt (first call shows
|
|
152
|
+
// the system dialog; later calls resolve with the remembered answer).
|
|
153
|
+
// REQUIRES NSMicrophoneUsageDescription in the host app's Info.plist —
|
|
154
|
+
// without it iOS terminates the app the moment the mic is touched.
|
|
155
|
+
RCT_EXPORT_METHOD(requestMicPermission:(RCTPromiseResolveBlock)resolve
|
|
156
|
+
rejecter:(__unused RCTPromiseRejectBlock)reject)
|
|
157
|
+
{
|
|
158
|
+
[[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
|
|
159
|
+
resolve(@(granted));
|
|
160
|
+
}];
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
- (void)invalidate
|
|
152
164
|
{
|
|
153
165
|
[self tearDownEngine];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akhilpulse/samadhaan-session-replay",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "React Native SDK for Samadhaan Guided Support — session recording, live remote screen-share, take-control, guided-tour overlays, and Shake-to-Assist AI voice guide.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -56,7 +56,7 @@ let pixelCopyDisabled = false; // turned true after repeated runtime failures to
|
|
|
56
56
|
let pixelCopyFailStreak = 0; // consecutive non-busy failures; resets on success
|
|
57
57
|
|
|
58
58
|
const STORAGE_KEY = "@akhilpulse/samadhaan-session-replay/sessionId";
|
|
59
|
-
const SDK_VERSION = "0.6.
|
|
59
|
+
const SDK_VERSION = "0.6.3";
|
|
60
60
|
|
|
61
61
|
// ---- Module-level session singleton --------------------------------------
|
|
62
62
|
// Multiple SessionReplayProvider mounts (intentional or via React StrictMode's
|
|
@@ -857,36 +857,6 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
857
857
|
</>
|
|
858
858
|
)}
|
|
859
859
|
|
|
860
|
-
{/* Shake-to-Assist: on-screen arrow pointing at the next element to tap. */}
|
|
861
|
-
{assistantActive && assistantArrow && (() => {
|
|
862
|
-
const { w, h } = viewSizeRef.current;
|
|
863
|
-
const px = assistantArrow.nx * (w || 0);
|
|
864
|
-
const py = assistantArrow.ny * (h || 0);
|
|
865
|
-
// Keep the label bubble on-screen: flip below the target near the top edge.
|
|
866
|
-
const below = py < 96;
|
|
867
|
-
return (
|
|
868
|
-
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
869
|
-
<View style={[styles.assistHalo, { left: px - 26, top: py - 26 }]} />
|
|
870
|
-
<View style={[styles.assistDot, { left: px - 9, top: py - 9 }]} />
|
|
871
|
-
{/* Arrow shaft pointing down at the target (or up when flipped). */}
|
|
872
|
-
<View
|
|
873
|
-
style={[
|
|
874
|
-
styles.assistArrow,
|
|
875
|
-
below
|
|
876
|
-
? { left: px - 9, top: py + 12 }
|
|
877
|
-
: { left: px - 9, top: py - 30, transform: [{ rotate: "180deg" }] },
|
|
878
|
-
]}
|
|
879
|
-
/>
|
|
880
|
-
{!!(assistantArrow.label || assistantArrow.instruction) && (
|
|
881
|
-
<View style={[styles.assistBubble, { left: 16, right: 16, [below ? "top" : "bottom"]: below ? py + 36 : (h || 0) - py + 36 } as any]}>
|
|
882
|
-
{!!assistantArrow.label && <Text style={styles.assistBubbleLabel}>{assistantArrow.label}</Text>}
|
|
883
|
-
{!!assistantArrow.instruction && <Text style={styles.assistBubbleText}>{assistantArrow.instruction}</Text>}
|
|
884
|
-
</View>
|
|
885
|
-
)}
|
|
886
|
-
</View>
|
|
887
|
-
);
|
|
888
|
-
})()}
|
|
889
|
-
|
|
890
860
|
{/* Floating help button — draggable, tap to open the AI voice guide.
|
|
891
861
|
This is the definitive trigger for Expo apps (no native shake module).
|
|
892
862
|
Hidden during guided tours so it never collides with the End-tour pill. */}
|
|
@@ -925,12 +895,81 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
|
|
|
925
895
|
</View>
|
|
926
896
|
</View>
|
|
927
897
|
)}
|
|
898
|
+
|
|
899
|
+
{/* Shake-to-Assist: on-screen arrow pointing at the next element to tap.
|
|
900
|
+
Rendered LAST so it draws above the bottom voice dock — targets near
|
|
901
|
+
the bottom of the screen used to be hidden underneath it. */}
|
|
902
|
+
{assistantActive && assistantArrow && (
|
|
903
|
+
<AssistArrowOverlay
|
|
904
|
+
nx={assistantArrow.nx}
|
|
905
|
+
ny={assistantArrow.ny}
|
|
906
|
+
label={assistantArrow.label}
|
|
907
|
+
instruction={assistantArrow.instruction}
|
|
908
|
+
w={viewSizeRef.current.w}
|
|
909
|
+
h={viewSizeRef.current.h}
|
|
910
|
+
/>
|
|
911
|
+
)}
|
|
928
912
|
</View>
|
|
929
913
|
</SessionReplayContext.Provider>
|
|
930
914
|
);
|
|
931
915
|
}
|
|
932
916
|
|
|
933
917
|
// ---- Shake-to-Assist UI bits ----
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Pulsing arrow + label bubble pointing at the next element to tap.
|
|
921
|
+
* - Halo pulses continuously so the target is hard to miss.
|
|
922
|
+
* - The label bubble is clamped so it never hides behind the bottom voice dock
|
|
923
|
+
* (which occupies roughly the bottom 110px of the screen).
|
|
924
|
+
*/
|
|
925
|
+
function AssistArrowOverlay({
|
|
926
|
+
nx, ny, label, instruction, w, h,
|
|
927
|
+
}: { nx: number; ny: number; label?: string; instruction?: string; w: number; h: number }) {
|
|
928
|
+
const pulse = useRef(new Animated.Value(0)).current;
|
|
929
|
+
useEffect(() => {
|
|
930
|
+
const loop = Animated.loop(
|
|
931
|
+
Animated.sequence([
|
|
932
|
+
Animated.timing(pulse, { toValue: 1, duration: 650, useNativeDriver: true }),
|
|
933
|
+
Animated.timing(pulse, { toValue: 0, duration: 650, useNativeDriver: true }),
|
|
934
|
+
])
|
|
935
|
+
);
|
|
936
|
+
loop.start();
|
|
937
|
+
return () => loop.stop();
|
|
938
|
+
}, [pulse]);
|
|
939
|
+
|
|
940
|
+
const px = nx * (w || 0);
|
|
941
|
+
const py = ny * (h || 0);
|
|
942
|
+
// Keep the label bubble on-screen: flip below the target near the top edge.
|
|
943
|
+
const below = py < 96;
|
|
944
|
+
// Never let the bubble sink behind the bottom voice dock (~110px tall).
|
|
945
|
+
const bubbleBottom = Math.max((h || 0) - py + 36, 122);
|
|
946
|
+
const scale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.45] });
|
|
947
|
+
const opacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 0.35] });
|
|
948
|
+
|
|
949
|
+
return (
|
|
950
|
+
<View pointerEvents="none" style={StyleSheet.absoluteFill}>
|
|
951
|
+
<Animated.View
|
|
952
|
+
style={[styles.assistHalo, { left: px - 26, top: py - 26, transform: [{ scale }], opacity }]}
|
|
953
|
+
/>
|
|
954
|
+
<View style={[styles.assistDot, { left: px - 9, top: py - 9 }]} />
|
|
955
|
+
{/* Arrow shaft pointing down at the target (or up when flipped). */}
|
|
956
|
+
<View
|
|
957
|
+
style={[
|
|
958
|
+
styles.assistArrow,
|
|
959
|
+
below
|
|
960
|
+
? { left: px - 9, top: py + 12 }
|
|
961
|
+
: { left: px - 9, top: py - 30, transform: [{ rotate: "180deg" }] },
|
|
962
|
+
]}
|
|
963
|
+
/>
|
|
964
|
+
{!!(label || instruction) && (
|
|
965
|
+
<View style={[styles.assistBubble, { left: 16, right: 16, [below ? "top" : "bottom"]: below ? py + 36 : bubbleBottom } as any]}>
|
|
966
|
+
{!!label && <Text style={styles.assistBubbleLabel}>{label}</Text>}
|
|
967
|
+
{!!instruction && <Text style={styles.assistBubbleText}>{instruction}</Text>}
|
|
968
|
+
</View>
|
|
969
|
+
)}
|
|
970
|
+
</View>
|
|
971
|
+
);
|
|
972
|
+
}
|
|
934
973
|
function assistantStatusLabel(phase: AssistantPhase, micStatus?: MicStatus | null): string {
|
|
935
974
|
// Be honest when we can't actually hear the user — a fake "Listening…"
|
|
936
975
|
// with a dead mic is the worst possible UX (bar stuck forever).
|
package/src/voice.ts
CHANGED
|
@@ -37,25 +37,44 @@ function loadOptional<T = any>(name: string): T | null {
|
|
|
37
37
|
/**
|
|
38
38
|
* Android requires a runtime RECORD_AUDIO grant — react-native-live-audio-stream
|
|
39
39
|
* does NOT request it itself; without it the recorder silently captures nothing
|
|
40
|
-
* and the assistant can never hear the user.
|
|
41
|
-
*
|
|
40
|
+
* and the assistant can never hear the user.
|
|
41
|
+
* iOS (SDK 0.6.2+): the bundled native module explicitly triggers the system
|
|
42
|
+
* mic prompt via AVAudioSession requestRecordPermission. The host app MUST have
|
|
43
|
+
* NSMicrophoneUsageDescription in Info.plist or iOS kills the app on first use.
|
|
44
|
+
* On older iOS builds without the native method, we fall through and let the
|
|
45
|
+
* recorder's own first mic access raise the prompt.
|
|
42
46
|
*/
|
|
43
47
|
async function ensureMicPermission(): Promise<boolean> {
|
|
44
|
-
if (Platform.OS
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
if (Platform.OS === "android") {
|
|
49
|
+
try {
|
|
50
|
+
const perm = PermissionsAndroid.PERMISSIONS.RECORD_AUDIO;
|
|
51
|
+
const already = await PermissionsAndroid.check(perm);
|
|
52
|
+
if (already) return true;
|
|
53
|
+
const res = await PermissionsAndroid.request(perm, {
|
|
54
|
+
title: "Microphone access",
|
|
55
|
+
message: "Needed so the voice assistant can hear you.",
|
|
56
|
+
buttonPositive: "Allow",
|
|
57
|
+
buttonNegative: "Not now",
|
|
58
|
+
});
|
|
59
|
+
return res === PermissionsAndroid.RESULTS.GRANTED;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Platform.OS === "ios") {
|
|
65
|
+
const native: any = (NativeModules as any).SessionReplayPcmPlayer;
|
|
66
|
+
if (native && typeof native.requestMicPermission === "function") {
|
|
67
|
+
try {
|
|
68
|
+
return !!(await native.requestMicPermission());
|
|
69
|
+
} catch {
|
|
70
|
+
// Native call failed — don't block; recorder start may still prompt.
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Old build without the native method — recorder start triggers the prompt.
|
|
75
|
+
return true;
|
|
58
76
|
}
|
|
77
|
+
return true;
|
|
59
78
|
}
|
|
60
79
|
|
|
61
80
|
/** Why voice input isn't working (surfaced so the UI can tell the user). */
|