@akhilpulse/samadhaan-session-replay 0.6.6 → 0.6.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akhilpulse/samadhaan-session-replay",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
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.6";
59
+ const SDK_VERSION = "0.6.8";
60
60
 
61
61
  // ---- Module-level session singleton --------------------------------------
62
62
  // Multiple SessionReplayProvider mounts (intentional or via React StrictMode's
@@ -162,7 +162,7 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
162
162
  const [assistantPhase, setAssistantPhase] = useState<AssistantPhase>("idle");
163
163
  const [assistMicStatus, setAssistMicStatus] = useState<MicStatus | null>(null);
164
164
  const [assistantArrow, setAssistantArrow] = useState<
165
- { nx: number; ny: number; label?: string; instruction?: string } | null
165
+ { nx: number; ny: number; nw?: number; nh?: number; label?: string; instruction?: string } | null
166
166
  >(null);
167
167
  const assistantSessionIdRef = useRef<string | null>(null);
168
168
  const voiceRef = useRef<UltravoxVoiceSession | null>(null);
@@ -537,9 +537,15 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
537
537
  setPointer(null);
538
538
  break;
539
539
  case "assistant_arrow": {
540
- // AI voice guide pointing at the next element. Coords are normalized 0..1.
540
+ // AI voice guide highlighting the next element. Coords are normalized 0..1;
541
+ // nw/nh are the target's bounding-box size (optional, defaults applied in UI).
541
542
  if (typeof msg.nx === "number" && typeof msg.ny === "number") {
542
- setAssistantArrow({ nx: msg.nx, ny: msg.ny, label: msg.label, instruction: msg.instruction });
543
+ setAssistantArrow({
544
+ nx: msg.nx, ny: msg.ny,
545
+ nw: typeof msg.nw === "number" ? msg.nw : undefined,
546
+ nh: typeof msg.nh === "number" ? msg.nh : undefined,
547
+ label: msg.label, instruction: msg.instruction,
548
+ });
543
549
  }
544
550
  break;
545
551
  }
@@ -877,20 +883,15 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
877
883
  </Animated.View>
878
884
  )}
879
885
 
880
- {/* Shake-to-Assist: Siri-style bottom overlaya white translucent
881
- gradient rising from the bottom edge (no dark pill bar) with the
882
- multicolor voice wave + status text floating over it, and a small
883
- frosted to end the session. Gradient is built from stacked
884
- strips so we don't need a native linear-gradient dependency. */}
886
+ {/* Shake-to-Assist: iOS glass-style floating voice pill frosted
887
+ translucent white pill with the multicolor voice wave + status
888
+ text, and a small to end the session. (Approximated glass:
889
+ translucency + hairline border + soft shadow; a real native blur
890
+ would require an extra native dependency.) */}
885
891
  {assistantActive && (
886
892
  <View style={styles.assistDock} pointerEvents="box-none">
887
- <View style={StyleSheet.absoluteFill} pointerEvents="none">
888
- {ASSIST_GRADIENT_STOPS.map((o, i) => (
889
- <View key={i} style={{ flex: 1, backgroundColor: "#ffffff", opacity: o }} />
890
- ))}
891
- </View>
892
- <View style={styles.assistStatusRow} pointerEvents="box-none">
893
- {/* Wave + text are display-only: pass touches through to the app. */}
893
+ <View style={styles.assistGlassPill} pointerEvents="box-none">
894
+ {/* Wave + text are display-only: pass touches through. */}
894
895
  <View pointerEvents="none">
895
896
  <VoiceWave phase={assistantPhase} />
896
897
  </View>
@@ -910,15 +911,17 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
910
911
  </View>
911
912
  )}
912
913
 
913
- {/* Shake-to-Assist: on-screen arrow pointing at the next element to tap.
914
- Rendered LAST so it draws above the bottom voice dock — targets near
915
- the bottom of the screen used to be hidden underneath it. */}
914
+ {/* Shake-to-Assist: glowing highlight border around the element to tap.
915
+ No text bubble the voice says the instruction; the border shows
916
+ where. Rendered LAST so it draws above the voice pill. The overlay
917
+ stays mounted between steps so the border GLIDES from the previous
918
+ target to the next instead of popping. */}
916
919
  {assistantActive && assistantArrow && (
917
- <AssistArrowOverlay
920
+ <AssistHighlightOverlay
918
921
  nx={assistantArrow.nx}
919
922
  ny={assistantArrow.ny}
920
- label={assistantArrow.label}
921
- instruction={assistantArrow.instruction}
923
+ nw={assistantArrow.nw}
924
+ nh={assistantArrow.nh}
922
925
  w={viewSizeRef.current.w}
923
926
  h={viewSizeRef.current.h}
924
927
  />
@@ -930,75 +933,86 @@ export function SessionReplayProvider({ config, children }: { config: Config; ch
930
933
 
931
934
  // ---- Shake-to-Assist UI bits ----
932
935
 
933
- // Opacity stops for the Siri-style white gradient (top → bottom). Many thin
934
- // strips fake a smooth linear gradient without a native gradient dependency.
935
- const ASSIST_GRADIENT_STOPS = [
936
- 0, 0.03, 0.07, 0.13, 0.2, 0.29, 0.39, 0.5, 0.61, 0.71, 0.8, 0.87, 0.92, 0.95,
937
- ];
938
-
939
936
  // Apple-ish multicolor voice wave bars.
940
937
  const SIRI_WAVE_COLORS = ["#0A84FF", "#64D2FF", "#BF5AF2", "#FF375F", "#5E5CE6"];
941
938
 
942
939
  /**
943
- * Pulsing arrow + label bubble pointing at the next element to tap.
944
- * - Halo pulses continuously so the target is hard to miss.
945
- * - The label bubble is clamped so it never hides behind the bottom voice dock
946
- * (which occupies roughly the bottom 110px of the screen).
940
+ * Glowing rounded-rectangle highlight border around the element to tap.
941
+ * - No text: the voice speaks the instruction, the border shows where.
942
+ * - Position + size are spring-animated, so when the next step arrives the
943
+ * border GLIDES from the old target to the new one instead of popping.
944
+ * - On first appearance it fades in while settling down from a slightly
945
+ * larger scale (a soft "landing" effect).
946
+ * - The border glow pulses gently so the target stays easy to spot.
947
+ * - When the vision model doesn't return a box size (nw/nh), a sensible
948
+ * button-sized default is used around the center point.
947
949
  */
948
- function AssistArrowOverlay({
949
- nx, ny, label, instruction, w, h,
950
- }: { nx: number; ny: number; label?: string; instruction?: string; w: number; h: number }) {
951
- const pulse = useRef(new Animated.Value(0)).current;
950
+ function AssistHighlightOverlay({
951
+ nx, ny, nw, nh, w, h,
952
+ }: { nx: number; ny: number; nw?: number; nh?: number; w: number; h: number }) {
953
+ const W = w || 0;
954
+ const H = h || 0;
955
+ // Default box ~ a typical button when no size is provided; clamp to screen.
956
+ const boxW = Math.min(Math.max((nw || 0.42) * W, 56), Math.max(W - 16, 56));
957
+ const boxH = Math.min(Math.max((nh || 0.065) * H, 44), Math.max(H * 0.5, 44));
958
+ const left = Math.min(Math.max(nx * W - boxW / 2, 8), Math.max(W - boxW - 8, 8));
959
+ const top = Math.min(Math.max(ny * H - boxH / 2, 8), Math.max(H - boxH - 8, 8));
960
+
961
+ // Layout values animate with the JS driver (native driver can't animate
962
+ // left/top/width/height).
963
+ const anim = useRef({
964
+ left: new Animated.Value(left),
965
+ top: new Animated.Value(top),
966
+ width: new Animated.Value(boxW),
967
+ height: new Animated.Value(boxH),
968
+ appear: new Animated.Value(0),
969
+ pulse: new Animated.Value(0),
970
+ }).current;
971
+
952
972
  useEffect(() => {
973
+ // Glide to the (new) target with a soft spring.
974
+ const springCfg = { useNativeDriver: false, friction: 9, tension: 50 };
975
+ Animated.parallel([
976
+ Animated.spring(anim.left, { toValue: left, ...springCfg }),
977
+ Animated.spring(anim.top, { toValue: top, ...springCfg }),
978
+ Animated.spring(anim.width, { toValue: boxW, ...springCfg }),
979
+ Animated.spring(anim.height, { toValue: boxH, ...springCfg }),
980
+ ]).start();
981
+ }, [left, top, boxW, boxH, anim]);
982
+
983
+ useEffect(() => {
984
+ Animated.timing(anim.appear, { toValue: 1, duration: 320, useNativeDriver: false }).start();
953
985
  const loop = Animated.loop(
954
986
  Animated.sequence([
955
- Animated.timing(pulse, { toValue: 1, duration: 650, useNativeDriver: true }),
956
- Animated.timing(pulse, { toValue: 0, duration: 650, useNativeDriver: true }),
987
+ Animated.timing(anim.pulse, { toValue: 1, duration: 750, useNativeDriver: false }),
988
+ Animated.timing(anim.pulse, { toValue: 0, duration: 750, useNativeDriver: false }),
957
989
  ])
958
990
  );
959
991
  loop.start();
960
992
  return () => loop.stop();
961
- }, [pulse]);
962
-
963
- const px = nx * (w || 0);
964
- const py = ny * (h || 0);
965
- // Bubble placement:
966
- // - target near the TOP edge -> bubble just below the target
967
- // - target near the BOTTOM -> bubble pinned to the top of the screen, so it
968
- // never covers the target or the app's bottom UI
969
- // (bottom sheets / nav bars live there)
970
- // - otherwise -> bubble just above the target
971
- const nearTop = py < 96;
972
- const nearBottom = py > (h || 0) - 260;
973
- const bubblePos: any = nearTop
974
- ? { top: py + 36 }
975
- : nearBottom
976
- ? { top: 70 }
977
- : { bottom: (h || 0) - py + 36 };
978
- const scale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.45] });
979
- const opacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 0.35] });
993
+ }, [anim]);
994
+
995
+ const glowOpacity = anim.pulse.interpolate({ inputRange: [0, 1], outputRange: [0.25, 0.75] });
996
+ const entryScale = anim.appear.interpolate({ inputRange: [0, 1], outputRange: [1.18, 1] });
980
997
 
981
998
  return (
982
999
  <View pointerEvents="none" style={StyleSheet.absoluteFill}>
983
1000
  <Animated.View
984
- style={[styles.assistHalo, { left: px - 26, top: py - 26, transform: [{ scale }], opacity }]}
985
- />
986
- <View style={[styles.assistDot, { left: px - 9, top: py - 9 }]} />
987
- {/* Arrow shaft pointing down at the target (or up when flipped). */}
988
- <View
989
- style={[
990
- styles.assistArrow,
991
- nearTop
992
- ? { left: px - 9, top: py + 12 }
993
- : { left: px - 9, top: py - 30, transform: [{ rotate: "180deg" }] },
994
- ]}
995
- />
996
- {!!(label || instruction) && (
997
- <View style={[styles.assistBubble, { left: 16, right: 16, ...bubblePos }]}>
998
- {!!label && <Text style={styles.assistBubbleLabel}>{label}</Text>}
999
- {!!instruction && <Text style={styles.assistBubbleText}>{instruction}</Text>}
1000
- </View>
1001
- )}
1001
+ style={{
1002
+ position: "absolute",
1003
+ left: anim.left,
1004
+ top: anim.top,
1005
+ width: anim.width,
1006
+ height: anim.height,
1007
+ opacity: anim.appear,
1008
+ transform: [{ scale: entryScale }],
1009
+ }}
1010
+ >
1011
+ {/* Outer soft pulsing glow ring */}
1012
+ <Animated.View style={[styles.assistGlowRing, { opacity: glowOpacity }]} />
1013
+ {/* Crisp inner border */}
1014
+ <View style={styles.assistHighlightBorder} />
1015
+ </Animated.View>
1002
1016
  </View>
1003
1017
  );
1004
1018
  }
@@ -1170,37 +1184,35 @@ const styles = StyleSheet.create({
1170
1184
  position: "absolute", width: 48, height: 48, borderRadius: 24,
1171
1185
  backgroundColor: "rgba(239,68,68,0.25)",
1172
1186
  },
1173
- // ---- Shake-to-Assist arrow + bubble ----
1174
- assistHalo: {
1175
- position: "absolute", width: 52, height: 52, borderRadius: 26,
1176
- backgroundColor: "rgba(59,130,246,0.22)",
1187
+ // ---- Shake-to-Assist glowing highlight border ----
1188
+ // Outer soft glow ring: slightly larger than the target box, thick
1189
+ // translucent border whose opacity pulses.
1190
+ assistGlowRing: {
1191
+ position: "absolute", left: -7, top: -7, right: -7, bottom: -7,
1192
+ borderRadius: 20, borderWidth: 6, borderColor: "rgba(59,130,246,0.45)",
1177
1193
  },
1178
- assistDot: {
1179
- position: "absolute", width: 18, height: 18, borderRadius: 9,
1180
- backgroundColor: "#3b82f6", borderWidth: 3, borderColor: "#fff",
1181
- shadowColor: "#3b82f6", shadowOpacity: 0.9, shadowRadius: 8,
1194
+ // Crisp inner border hugging the target element.
1195
+ assistHighlightBorder: {
1196
+ position: "absolute", left: 0, top: 0, right: 0, bottom: 0,
1197
+ borderRadius: 14, borderWidth: 3, borderColor: "#3b82f6",
1198
+ shadowColor: "#3b82f6", shadowOpacity: 0.7, shadowRadius: 10,
1199
+ shadowOffset: { width: 0, height: 0 },
1182
1200
  },
1183
- // A simple chevron-like arrow built from a rotated bordered box.
1184
- assistArrow: {
1185
- position: "absolute", width: 18, height: 18,
1186
- borderRightWidth: 4, borderBottomWidth: 4, borderColor: "#3b82f6",
1187
- transform: [{ rotate: "45deg" }],
1188
- },
1189
- assistBubble: {
1190
- position: "absolute", backgroundColor: "rgba(17,24,39,0.94)",
1191
- borderRadius: 12, paddingVertical: 10, paddingHorizontal: 14,
1192
- shadowColor: "#000", shadowOpacity: 0.3, shadowRadius: 10, shadowOffset: { width: 0, height: 4 },
1193
- },
1194
- assistBubbleLabel: { color: "#93c5fd", fontSize: 12, fontWeight: "700", marginBottom: 2 },
1195
- assistBubbleText: { color: "#fff", fontSize: 14, lineHeight: 19 },
1196
- // ---- Shake-to-Assist Siri-style bottom overlay ----
1201
+ // ---- Shake-to-Assist iOS glass-style floating voice pill ----
1197
1202
  assistDock: {
1198
- position: "absolute", left: 0, right: 0, bottom: 0,
1199
- height: 140, justifyContent: "flex-end",
1203
+ position: "absolute", left: 0, right: 0, bottom: 24,
1204
+ alignItems: "center", paddingHorizontal: 16,
1200
1205
  },
1201
- assistStatusRow: {
1202
- flexDirection: "row", alignItems: "center", justifyContent: "center",
1203
- paddingBottom: 30, paddingHorizontal: 20,
1206
+ assistGlassPill: {
1207
+ flexDirection: "row", alignItems: "center",
1208
+ maxWidth: "100%",
1209
+ backgroundColor: "rgba(255,255,255,0.78)",
1210
+ borderRadius: 999,
1211
+ borderWidth: 1, borderColor: "rgba(255,255,255,0.95)",
1212
+ paddingVertical: 12, paddingHorizontal: 18,
1213
+ shadowColor: "#000", shadowOpacity: 0.18, shadowRadius: 18,
1214
+ shadowOffset: { width: 0, height: 8 },
1215
+ elevation: 10,
1204
1216
  },
1205
1217
  assistStatusText: {
1206
1218
  color: "#111827", fontSize: 15, fontWeight: "600",
package/src/voice.ts CHANGED
@@ -23,11 +23,26 @@
23
23
 
24
24
  import { NativeModules, PermissionsAndroid, Platform } from "react-native";
25
25
 
26
+ type OptionalModuleName = "react-native-shake" | "react-native-live-audio-stream";
27
+
26
28
  // Loads an optional native module without crashing Metro when it's absent.
27
- function loadOptional<T = any>(name: string): T | null {
29
+ // NOTE: `require()` must be called with a static string literal Metro's
30
+ // bundler cannot resolve `require(someVariable)` (it throws "Dynamic require
31
+ // ... not supported by Metro" at runtime, every time, regardless of whether
32
+ // the package is actually installed). A switch over literal require() calls
33
+ // keeps each one statically analyzable.
34
+ function loadOptional<T = any>(name: OptionalModuleName): T | null {
28
35
  try {
29
- // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
30
- return require(name) as T;
36
+ switch (name) {
37
+ case "react-native-shake":
38
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
39
+ return require("react-native-shake") as T;
40
+ case "react-native-live-audio-stream":
41
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
42
+ return require("react-native-live-audio-stream") as T;
43
+ default:
44
+ return null;
45
+ }
31
46
  } catch {
32
47
  return null;
33
48
  }