@mobileai/react-native 0.9.27 → 0.9.29

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.
Files changed (65) hide show
  1. package/README.md +28 -16
  2. package/android/build.gradle +17 -0
  3. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
  4. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
  5. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
  6. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
  7. package/bin/generate-map.cjs +45 -6
  8. package/ios/MobileAIFloatingOverlayComponentView.h +8 -0
  9. package/ios/MobileAIFloatingOverlayComponentView.mm +12 -41
  10. package/ios/Podfile +63 -0
  11. package/ios/Podfile.lock +2290 -0
  12. package/ios/Podfile.properties.json +4 -0
  13. package/ios/mobileaireactnative/AppDelegate.swift +69 -0
  14. package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  15. package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
  16. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
  17. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
  18. package/ios/mobileaireactnative/Info.plist +55 -0
  19. package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
  20. package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
  21. package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
  22. package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
  23. package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
  24. package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
  25. package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
  26. package/lib/module/components/AIAgent.js +501 -191
  27. package/lib/module/components/AgentChatBar.js +250 -59
  28. package/lib/module/components/FloatingOverlayWrapper.js +68 -32
  29. package/lib/module/config/endpoints.js +22 -1
  30. package/lib/module/core/AgentRuntime.js +110 -8
  31. package/lib/module/core/FiberTreeWalker.js +211 -10
  32. package/lib/module/core/OutcomeVerifier.js +149 -0
  33. package/lib/module/core/systemPrompt.js +96 -25
  34. package/lib/module/providers/GeminiProvider.js +9 -3
  35. package/lib/module/services/telemetry/TelemetryService.js +21 -2
  36. package/lib/module/services/telemetry/TouchAutoCapture.js +235 -38
  37. package/lib/module/services/telemetry/analyticsLabeling.js +187 -0
  38. package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
  39. package/lib/module/support/supportPrompt.js +22 -7
  40. package/lib/module/support/supportStyle.js +55 -0
  41. package/lib/module/support/types.js +2 -0
  42. package/lib/module/tools/typeTool.js +20 -0
  43. package/lib/module/utils/humanizeScreenName.js +49 -0
  44. package/lib/typescript/src/components/AIAgent.d.ts +6 -2
  45. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
  46. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
  47. package/lib/typescript/src/config/endpoints.d.ts +4 -0
  48. package/lib/typescript/src/core/AgentRuntime.d.ts +12 -3
  49. package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
  50. package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
  51. package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
  52. package/lib/typescript/src/core/types.d.ts +63 -0
  53. package/lib/typescript/src/index.d.ts +1 -0
  54. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
  55. package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +6 -1
  56. package/lib/typescript/src/services/telemetry/analyticsLabeling.d.ts +20 -0
  57. package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
  58. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
  59. package/lib/typescript/src/support/index.d.ts +1 -0
  60. package/lib/typescript/src/support/supportStyle.d.ts +9 -0
  61. package/lib/typescript/src/support/types.d.ts +3 -0
  62. package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
  63. package/package.json +10 -10
  64. package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
  65. package/ios/MobileAIPilotIntents.swift +0 -51
package/README.md CHANGED
@@ -307,14 +307,13 @@ npm install react-native-agentic-ai
307
307
 
308
308
  No native modules required by default. Works with **Expo managed workflow** out of the box — no eject needed.
309
309
 
310
- ### Optional Dependencies
310
+ ### Screenshot Capture
311
311
 
312
312
  <details>
313
313
  <summary><b>📸 Screenshots</b> — for image/video content understanding</summary>
314
314
 
315
- ```bash
316
- npx expo install react-native-view-shot
317
- ```
315
+ `react-native-view-shot` is a required dependency for screenshot capture and is included with
316
+ `@mobileai/react-native`, so you do **not** need to add it separately.
318
317
 
319
318
  </details>
320
319
 
@@ -391,6 +390,8 @@ npx @mobileai/react-native generate-map
391
390
 
392
391
  ### 2. Wrap Your App
393
392
 
393
+ If you use a MobileAI publishable key, the SDK now defaults to the hosted MobileAI text and voice proxies automatically. You only need to pass `proxyUrl` and `voiceProxyUrl` when you want to override them with your own backend.
394
+
394
395
  #### React Navigation
395
396
 
396
397
  ```tsx
@@ -403,13 +404,10 @@ export default function App() {
403
404
 
404
405
  return (
405
406
  <AIAgent
406
- // Your MobileAI Dashboard ID — instantly enables cloud intelligence
407
+ // Your MobileAI Dashboard ID
408
+ // This now auto-configures the hosted MobileAI text + voice proxies too.
407
409
  analyticsKey="mobileai_pub_xxxxxxxx"
408
410
 
409
- // Route all traffic through the secure MobileAI Cloud proxies
410
- proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
411
- voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
412
-
413
411
  navRef={navRef}
414
412
  screenMap={screenMap} // optional but recommended
415
413
  >
@@ -435,9 +433,8 @@ export default function RootLayout() {
435
433
 
436
434
  return (
437
435
  <AIAgent
436
+ // Hosted MobileAI proxies are inferred automatically from analyticsKey
438
437
  analyticsKey="mobileai_pub_xxxxxxxx"
439
- proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
440
- voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
441
438
  navRef={navRef}
442
439
  screenMap={screenMap}
443
440
  >
@@ -464,6 +461,19 @@ The examples above use **Gemini** (default). To use **OpenAI** for text mode, ad
464
461
 
465
462
  A floating chat bar appears automatically. Ask the AI to navigate, tap buttons, fill forms, answer questions.
466
463
 
464
+ ### Hosted MobileAI Defaults
465
+
466
+ For the standard MobileAI Cloud setup, this is enough:
467
+
468
+ ```tsx
469
+ <AIAgent analyticsKey="mobileai_pub_xxxxxxxx" navRef={navRef} />
470
+ ```
471
+
472
+ Only pass explicit proxy props when:
473
+ - you want to use your own backend proxy
474
+ - you want a dedicated voice proxy
475
+ - you are self-hosting the MobileAI backend
476
+
467
477
  ### Knowledge-Only Mode — AI Assistant Without UI Automation
468
478
 
469
479
  Set `enableUIControl={false}` for a lightweight FAQ / support assistant. Single LLM call, ~70% fewer tokens:
@@ -871,9 +881,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
871
881
  |------|------|---------|-------------|
872
882
  | `apiKey` | `string` | — | API key for your provider (prototyping only — use `proxyUrl` in production). |
873
883
  | `provider` | `'gemini' \| 'openai'` | `'gemini'` | LLM provider for text mode. |
874
- | `proxyUrl` | `string` | | Backend proxy URL (production). Routes all LLM traffic through your server. |
884
+ | `proxyUrl` | `string` | Hosted MobileAI text proxy when `analyticsKey` is set | Backend proxy URL (production). Routes all LLM traffic through your server. |
875
885
  | `proxyHeaders` | `Record<string, string>` | — | Auth headers for proxy (e.g., `Authorization: Bearer ${token}`). |
876
- | `voiceProxyUrl` | `string` | | Dedicated proxy for Voice Mode WebSockets. Falls back to `proxyUrl`. |
886
+ | `voiceProxyUrl` | `string` | Hosted MobileAI voice proxy when `analyticsKey` is set; otherwise falls back to `proxyUrl` | Dedicated proxy for Voice Mode WebSockets. |
877
887
  | `voiceProxyHeaders` | `Record<string, string>` | — | Auth headers for voice proxy. |
878
888
  | `model` | `string` | Provider default | Model name (e.g. `gemini-2.5-flash`, `gpt-4.1-mini`). |
879
889
  | `navRef` | `NavigationContainerRef` | — | Navigation ref for auto-navigation. |
@@ -953,7 +963,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
953
963
 
954
964
  | Prop | Type | Default | Description |
955
965
  |------|------|---------|-------------|
956
- | `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics. |
966
+ | `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics and, by default, the hosted MobileAI text/voice proxies. |
957
967
  | `analyticsProxyUrl` | `string` | — | Enterprise: route events through your backend. |
958
968
  | `analyticsProxyHeaders` | `Record<string, string>` | — | Auth headers for analytics proxy. |
959
969
 
@@ -1065,7 +1075,7 @@ const { send } = useAI({
1065
1075
 
1066
1076
  ## 📊 Zero-Config Analytics — Auto-Capture Every Tap
1067
1077
 
1068
- Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. **Zero code changes** to your app components.
1078
+ Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. It also enables the default hosted MobileAI AI proxies unless you override them. **Zero code changes** to your app components.
1069
1079
 
1070
1080
  ```tsx
1071
1081
  <AIAgent
@@ -1116,6 +1126,8 @@ MobileAI.identify('user_123', { plan: 'pro' });
1116
1126
 
1117
1127
  ### Backend Proxy — Keep API Keys Secure
1118
1128
 
1129
+ Use this only when you want to override the default hosted MobileAI proxy behavior or route traffic through your own backend.
1130
+
1119
1131
  ```tsx
1120
1132
  <AIAgent
1121
1133
  proxyUrl="https://myapp.vercel.app/api/gemini"
@@ -1262,7 +1274,7 @@ Tag any element with `aiPriority` to control AI visibility:
1262
1274
  | `date_picker(index, date)` | Set a date on a date picker |
1263
1275
  | `navigate(screen)` | Navigate to any screen |
1264
1276
  | `wait(seconds)` | Wait for loading states before acting |
1265
- | `capture_screenshot(reason)` | Capture the screen as an image (requires `react-native-view-shot`) |
1277
+ | `capture_screenshot(reason)` | Capture the SDK root component as an image (requires `react-native-view-shot`) |
1266
1278
  | `done(text)` | Finish the task with a response |
1267
1279
  | `ask_user(question)` | Ask the user for clarification |
1268
1280
  | `query_knowledge(question)` | Search the knowledge base |
@@ -10,6 +10,15 @@ buildscript {
10
10
  }
11
11
  }
12
12
 
13
+ def isNewArchitectureEnabled() {
14
+ // Libraries opt into Fabric/TurboModule codegen only when the consuming app enables it.
15
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
16
+ }
17
+
18
+ if (isNewArchitectureEnabled()) {
19
+ apply plugin: "com.facebook.react"
20
+ }
21
+
13
22
  apply plugin: "com.android.library"
14
23
  apply plugin: "kotlin-android"
15
24
 
@@ -59,3 +68,11 @@ repositories {
59
68
  dependencies {
60
69
  implementation "com.facebook.react:react-android"
61
70
  }
71
+
72
+ if (isNewArchitectureEnabled()) {
73
+ react {
74
+ jsRootDir = file("../")
75
+ libraryName = "RNMobileAIOverlaySpec"
76
+ codegenJavaPackageName = "com.mobileai.overlay"
77
+ }
78
+ }
@@ -0,0 +1,243 @@
1
+ package com.mobileai.overlay
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.view.MotionEvent
6
+ import android.view.View
7
+ import android.view.ViewConfiguration
8
+ import com.facebook.react.bridge.GuardedRunnable
9
+ import com.facebook.react.bridge.WritableMap
10
+ import com.facebook.react.bridge.WritableNativeMap
11
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
12
+ import com.facebook.react.common.build.ReactBuildConfig
13
+ import com.facebook.react.config.ReactFeatureFlags
14
+ import com.facebook.react.uimanager.JSPointerDispatcher
15
+ import com.facebook.react.uimanager.JSTouchDispatcher
16
+ import com.facebook.react.uimanager.PixelUtil.pxToDp
17
+ import com.facebook.react.uimanager.RootView
18
+ import com.facebook.react.uimanager.StateWrapper
19
+ import com.facebook.react.uimanager.ThemedReactContext
20
+ import com.facebook.react.uimanager.UIManagerModule
21
+ import com.facebook.react.uimanager.events.EventDispatcher
22
+ import com.facebook.react.views.view.ReactViewGroup
23
+ import kotlin.math.abs
24
+ import kotlin.math.roundToInt
25
+
26
+ /**
27
+ * Root view rendered inside the Android floating panel dialog.
28
+ *
29
+ * This mirrors the touch and layout behavior of React Native's modal dialog
30
+ * host so JS children remain fully interactive even though they render inside
31
+ * a separate native window.
32
+ */
33
+ internal class FloatingOverlayDialogRootViewGroup(context: Context) :
34
+ ReactViewGroup(context),
35
+ RootView {
36
+
37
+ var stateWrapper: StateWrapper? = null
38
+ var eventDispatcher: EventDispatcher? = null
39
+ var overlayHost: FloatingOverlayView? = null
40
+
41
+ private var viewWidth: Int = 0
42
+ private var viewHeight: Int = 0
43
+ private val jsTouchDispatcher = JSTouchDispatcher(this)
44
+ private var jsPointerDispatcher: JSPointerDispatcher? = null
45
+ private val touchSlop: Float = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
46
+ private var dragCandidate: Boolean = false
47
+ private var isDraggingWindow: Boolean = false
48
+ private var dragStartRawX: Float = 0f
49
+ private var dragStartRawY: Float = 0f
50
+ private var dragStartWindowX: Int = 0
51
+ private var dragStartWindowY: Int = 0
52
+
53
+ private val reactContext: ThemedReactContext
54
+ get() = context as ThemedReactContext
55
+
56
+ init {
57
+ if (ReactFeatureFlags.dispatchPointerEvents) {
58
+ jsPointerDispatcher = JSPointerDispatcher(this)
59
+ }
60
+ }
61
+
62
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
63
+ super.onSizeChanged(w, h, oldw, oldh)
64
+ viewWidth = w
65
+ viewHeight = h
66
+ updateState(viewWidth, viewHeight)
67
+ }
68
+
69
+ fun updateState(width: Int, height: Int) {
70
+ val realWidth = width.toFloat().pxToDp()
71
+ val realHeight = height.toFloat().pxToDp()
72
+
73
+ val wrapper = stateWrapper
74
+ if (wrapper != null) {
75
+ val newStateData: WritableMap = WritableNativeMap()
76
+ newStateData.putDouble("screenWidth", realWidth.toDouble())
77
+ newStateData.putDouble("screenHeight", realHeight.toDouble())
78
+ wrapper.updateState(newStateData)
79
+ } else if (
80
+ !ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE &&
81
+ !reactContext.isBridgeless
82
+ ) {
83
+ @Suppress("DEPRECATION")
84
+ reactContext.runOnNativeModulesQueueThread(
85
+ object : GuardedRunnable(reactContext) {
86
+ override fun runGuarded() {
87
+ reactContext.reactApplicationContext
88
+ .getNativeModule(UIManagerModule::class.java)
89
+ ?.updateNodeSize(id, viewWidth, viewHeight)
90
+ }
91
+ }
92
+ )
93
+ }
94
+ }
95
+
96
+ override fun handleException(t: Throwable) {
97
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
98
+ }
99
+
100
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
101
+ when (event.actionMasked) {
102
+ MotionEvent.ACTION_DOWN -> {
103
+ dragCandidate = isInNativeDragRegion(event.x, event.y)
104
+ isDraggingWindow = false
105
+ if (dragCandidate) {
106
+ dragStartRawX = event.rawX
107
+ dragStartRawY = event.rawY
108
+ dragStartWindowX = overlayHost?.getWindowXPx() ?: 0
109
+ dragStartWindowY = overlayHost?.getWindowYPx() ?: 0
110
+ }
111
+ }
112
+
113
+ MotionEvent.ACTION_MOVE -> {
114
+ if (dragCandidate && !isDraggingWindow && hasExceededTouchSlop(event)) {
115
+ isDraggingWindow = true
116
+ parent?.requestDisallowInterceptTouchEvent(true)
117
+ updateNativeWindowDrag(event)
118
+ return true
119
+ }
120
+ }
121
+
122
+ MotionEvent.ACTION_UP,
123
+ MotionEvent.ACTION_CANCEL -> resetNativeDrag()
124
+ }
125
+
126
+ if (isDraggingWindow) return true
127
+
128
+ eventDispatcher?.let { dispatcher ->
129
+ jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
130
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
131
+ }
132
+ return super.onInterceptTouchEvent(event)
133
+ }
134
+
135
+ @SuppressLint("ClickableViewAccessibility")
136
+ override fun onTouchEvent(event: MotionEvent): Boolean {
137
+ if (isDraggingWindow || dragCandidate) {
138
+ when (event.actionMasked) {
139
+ MotionEvent.ACTION_MOVE -> {
140
+ if (!isDraggingWindow && hasExceededTouchSlop(event)) {
141
+ isDraggingWindow = true
142
+ parent?.requestDisallowInterceptTouchEvent(true)
143
+ }
144
+
145
+ if (isDraggingWindow) {
146
+ updateNativeWindowDrag(event)
147
+ return true
148
+ }
149
+ }
150
+
151
+ MotionEvent.ACTION_UP -> {
152
+ if (isDraggingWindow) {
153
+ updateNativeWindowDrag(event)
154
+ overlayHost?.emitWindowDragEnd()
155
+ resetNativeDrag()
156
+ return true
157
+ }
158
+ resetNativeDrag()
159
+ }
160
+
161
+ MotionEvent.ACTION_CANCEL -> {
162
+ if (isDraggingWindow) {
163
+ overlayHost?.emitWindowDragEnd()
164
+ resetNativeDrag()
165
+ return true
166
+ }
167
+ resetNativeDrag()
168
+ }
169
+ }
170
+ }
171
+
172
+ eventDispatcher?.let { dispatcher ->
173
+ jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
174
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
175
+ }
176
+ super.onTouchEvent(event)
177
+ return true
178
+ }
179
+
180
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
181
+ eventDispatcher?.let { dispatcher ->
182
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
183
+ }
184
+ return super.onInterceptHoverEvent(event)
185
+ }
186
+
187
+ override fun onHoverEvent(event: MotionEvent): Boolean {
188
+ eventDispatcher?.let { dispatcher ->
189
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
190
+ }
191
+ return super.onHoverEvent(event)
192
+ }
193
+
194
+ @OptIn(UnstableReactNativeAPI::class)
195
+ override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
196
+ eventDispatcher?.let { dispatcher ->
197
+ jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher, reactContext)
198
+ jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
199
+ }
200
+ }
201
+
202
+ override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
203
+ eventDispatcher?.let { dispatcher ->
204
+ jsTouchDispatcher.onChildEndedNativeGesture(ev, dispatcher)
205
+ }
206
+ jsPointerDispatcher?.onChildEndedNativeGesture()
207
+ }
208
+
209
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
210
+ // No-op so the root can continue receiving events for JS dispatch.
211
+ }
212
+
213
+ private fun hasExceededTouchSlop(event: MotionEvent): Boolean {
214
+ return abs(event.rawX - dragStartRawX) > touchSlop || abs(event.rawY - dragStartRawY) > touchSlop
215
+ }
216
+
217
+ private fun updateNativeWindowDrag(event: MotionEvent) {
218
+ val targetX = dragStartWindowX + (event.rawX - dragStartRawX).roundToInt()
219
+ val targetY = dragStartWindowY + (event.rawY - dragStartRawY).roundToInt()
220
+ overlayHost?.updateWindowPositionFromNative(targetX, targetY)
221
+ }
222
+
223
+ private fun resetNativeDrag() {
224
+ dragCandidate = false
225
+ isDraggingWindow = false
226
+ }
227
+
228
+ private fun isInNativeDragRegion(x: Float, y: Float): Boolean {
229
+ if (viewWidth <= dpToPx(80) && viewHeight <= dpToPx(80)) {
230
+ return true
231
+ }
232
+
233
+ val handleTop = dpToPx(44)
234
+ val handleHalfWidth = dpToPx(72)
235
+ val centerX = viewWidth / 2f
236
+
237
+ return y <= handleTop && x in (centerX - handleHalfWidth)..(centerX + handleHalfWidth)
238
+ }
239
+
240
+ private fun dpToPx(value: Int): Float {
241
+ return value * resources.displayMetrics.density
242
+ }
243
+ }