@mobileai/react-native 0.9.28 → 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.
- package/README.md +4 -5
- package/ios/MobileAIFloatingOverlayComponentView.h +8 -0
- package/ios/MobileAIFloatingOverlayComponentView.mm +44 -0
- package/lib/module/components/AIAgent.js +110 -37
- package/lib/module/core/AgentRuntime.js +7 -7
- package/lib/module/core/FiberTreeWalker.js +114 -11
- package/lib/module/services/telemetry/TouchAutoCapture.js +231 -44
- package/lib/module/services/telemetry/analyticsLabeling.js +187 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +3 -3
- package/lib/typescript/src/core/types.d.ts +28 -0
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +6 -1
- package/lib/typescript/src/services/telemetry/analyticsLabeling.d.ts +20 -0
- package/package.json +6 -9
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
|
-
###
|
|
310
|
+
### Screenshot Capture
|
|
311
311
|
|
|
312
312
|
<details>
|
|
313
313
|
<summary><b>📸 Screenshots</b> — for image/video content understanding</summary>
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
|
@@ -1275,7 +1274,7 @@ Tag any element with `aiPriority` to control AI visibility:
|
|
|
1275
1274
|
| `date_picker(index, date)` | Set a date on a date picker |
|
|
1276
1275
|
| `navigate(screen)` | Navigate to any screen |
|
|
1277
1276
|
| `wait(seconds)` | Wait for loading states before acting |
|
|
1278
|
-
| `capture_screenshot(reason)` | Capture the
|
|
1277
|
+
| `capture_screenshot(reason)` | Capture the SDK root component as an image (requires `react-native-view-shot`) |
|
|
1279
1278
|
| `done(text)` | Finish the task with a response |
|
|
1280
1279
|
| `ask_user(question)` | Ask the user for clarification |
|
|
1281
1280
|
| `query_knowledge(question)` | Search the knowledge base |
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
2
|
+
|
|
3
|
+
#import "MobileAIFloatingOverlayComponentView.h"
|
|
4
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
5
|
+
#import <react/renderer/components/RNMobileAIOverlaySpec/ComponentDescriptors.h>
|
|
6
|
+
#import <react/renderer/components/RNMobileAIOverlaySpec/Props.h>
|
|
7
|
+
#import <react/renderer/components/RNMobileAIOverlaySpec/RCTComponentViewHelpers.h>
|
|
8
|
+
|
|
9
|
+
using namespace facebook::react;
|
|
10
|
+
|
|
11
|
+
@implementation MobileAIFloatingOverlayComponentView
|
|
12
|
+
|
|
13
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
14
|
+
{
|
|
15
|
+
return concreteComponentDescriptorProvider<MobileAIFloatingOverlayComponentDescriptor>();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
+ (void)load
|
|
19
|
+
{
|
|
20
|
+
[super load];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
24
|
+
{
|
|
25
|
+
if (self = [super initWithFrame:frame]) {
|
|
26
|
+
static const auto defaultProps = std::make_shared<const MobileAIFloatingOverlayProps>();
|
|
27
|
+
_props = defaultProps;
|
|
28
|
+
}
|
|
29
|
+
return self;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (void)updateProps:(Props::Shared const&)props oldProps:(Props::Shared const&)oldProps
|
|
33
|
+
{
|
|
34
|
+
[super updateProps:props oldProps:oldProps];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@end
|
|
38
|
+
|
|
39
|
+
Class<RCTComponentViewProtocol> MobileAIFloatingOverlayCls(void)
|
|
40
|
+
{
|
|
41
|
+
return MobileAIFloatingOverlayComponentView.class;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#endif
|
|
@@ -28,7 +28,7 @@ import { VoiceService } from "../services/VoiceService.js";
|
|
|
28
28
|
import { AudioInputService } from "../services/AudioInputService.js";
|
|
29
29
|
import { AudioOutputService } from "../services/AudioOutputService.js";
|
|
30
30
|
import { TelemetryService, bindTelemetryService } from "../services/telemetry/index.js";
|
|
31
|
-
import {
|
|
31
|
+
import { extractTouchTargetMetadata, checkRageClick } from "../services/telemetry/TouchAutoCapture.js";
|
|
32
32
|
import { initDeviceId, getDeviceId } from "../services/telemetry/device.js";
|
|
33
33
|
import { AgentErrorBoundary } from "./AgentErrorBoundary.js";
|
|
34
34
|
import { HighlightOverlay } from "./HighlightOverlay.js";
|
|
@@ -69,6 +69,29 @@ function getTooltipStorage() {
|
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
function sanitizeWireframeScreenshot(value) {
|
|
73
|
+
if (!value || typeof value !== 'string') return null;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (!trimmed) return null;
|
|
76
|
+
const base64 = trimmed.startsWith('data:') ? trimmed.split(',')[1] ?? '' : trimmed;
|
|
77
|
+
return base64.length > 0 ? base64 : null;
|
|
78
|
+
}
|
|
79
|
+
async function captureHeatmapScreenshot(rootView) {
|
|
80
|
+
try {
|
|
81
|
+
if (!rootView) return null;
|
|
82
|
+
const viewShot = require('react-native-view-shot');
|
|
83
|
+
const captureRef = viewShot.captureRef || viewShot.default?.captureRef;
|
|
84
|
+
if (!captureRef || typeof captureRef !== 'function') return null;
|
|
85
|
+
const raw = await captureRef(rootView, {
|
|
86
|
+
format: 'jpg',
|
|
87
|
+
quality: 0.28,
|
|
88
|
+
result: 'base64'
|
|
89
|
+
});
|
|
90
|
+
return sanitizeWireframeScreenshot(raw);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
72
95
|
|
|
73
96
|
// ─── Props ─────────────────────────────────────────────────────
|
|
74
97
|
|
|
@@ -240,7 +263,9 @@ export function AIAgent({
|
|
|
240
263
|
try {
|
|
241
264
|
const AS = getTooltipStorage();
|
|
242
265
|
await AS?.setItem('@mobileai_onboarding_completed', 'true');
|
|
243
|
-
} catch {
|
|
266
|
+
} catch {
|
|
267
|
+
/* graceful */
|
|
268
|
+
}
|
|
244
269
|
})();
|
|
245
270
|
} else {
|
|
246
271
|
setCurrentOnboardingIndex(prev => prev + 1);
|
|
@@ -272,7 +297,9 @@ export function AIAgent({
|
|
|
272
297
|
try {
|
|
273
298
|
const AS = getTooltipStorage();
|
|
274
299
|
await AS?.setItem('@mobileai_tooltip_seen', 'true');
|
|
275
|
-
} catch {
|
|
300
|
+
} catch {
|
|
301
|
+
/* graceful */
|
|
302
|
+
}
|
|
276
303
|
})();
|
|
277
304
|
}, []);
|
|
278
305
|
useEffect(() => {
|
|
@@ -434,17 +461,50 @@ export function AIAgent({
|
|
|
434
461
|
});
|
|
435
462
|
});
|
|
436
463
|
}, []);
|
|
437
|
-
const
|
|
464
|
+
const getDeepestRouteName = useCallback(state => {
|
|
465
|
+
if (!state || !Array.isArray(state.routes) || state.index == null) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const route = state.routes[state.index];
|
|
469
|
+
if (!route) return null;
|
|
470
|
+
const nested = route.state;
|
|
471
|
+
if (nested) {
|
|
472
|
+
const nestedName = getDeepestRouteName(nested);
|
|
473
|
+
if (nestedName) return nestedName;
|
|
474
|
+
}
|
|
475
|
+
return typeof route.name === 'string' ? route.name : null;
|
|
476
|
+
}, []);
|
|
477
|
+
const resolveScreenName = useCallback(() => {
|
|
478
|
+
if (pathname) {
|
|
479
|
+
const humanizedPath = humanizeScreenName(pathname === '/' ? 'index' : pathname);
|
|
480
|
+
if (humanizedPath) return humanizedPath;
|
|
481
|
+
}
|
|
482
|
+
const navState = navRef?.getRootState?.() ?? navRef?.getState?.();
|
|
483
|
+
const deepestFromState = getDeepestRouteName(navState);
|
|
484
|
+
if (deepestFromState) {
|
|
485
|
+
const humanized = humanizeScreenName(deepestFromState);
|
|
486
|
+
if (humanized) return humanized;
|
|
487
|
+
}
|
|
438
488
|
const routeName = navRef?.getCurrentRoute?.()?.name;
|
|
439
489
|
if (typeof routeName === 'string' && routeName.trim().length > 0) {
|
|
440
|
-
|
|
490
|
+
const humanized = humanizeScreenName(routeName);
|
|
491
|
+
if (humanized) return humanized;
|
|
441
492
|
}
|
|
442
493
|
const telemetryScreen = telemetryRef.current?.screen;
|
|
443
|
-
if (typeof telemetryScreen === 'string' && telemetryScreen !== 'Unknown') {
|
|
494
|
+
if (typeof telemetryScreen === 'string' && telemetryScreen !== 'Unknown' && telemetryScreen.trim().length > 0) {
|
|
444
495
|
return telemetryScreen;
|
|
445
496
|
}
|
|
446
497
|
return 'unknown';
|
|
447
|
-
}, [navRef]);
|
|
498
|
+
}, [pathname, navRef, getDeepestRouteName]);
|
|
499
|
+
const syncTelemetryScreen = useCallback(() => {
|
|
500
|
+
const screen = resolveScreenName();
|
|
501
|
+
if (screen === 'unknown') {
|
|
502
|
+
return screen;
|
|
503
|
+
}
|
|
504
|
+
telemetryRef.current?.setScreen(screen);
|
|
505
|
+
return screen;
|
|
506
|
+
}, [resolveScreenName]);
|
|
507
|
+
const getResolvedScreenName = useCallback(() => resolveScreenName(), [resolveScreenName]);
|
|
448
508
|
const resolvedKnowledgeBase = useMemo(() => {
|
|
449
509
|
if (knowledgeBase) return knowledgeBase;
|
|
450
510
|
if (!analyticsKey) return undefined;
|
|
@@ -1289,18 +1349,14 @@ export function AIAgent({
|
|
|
1289
1349
|
bindTelemetryService(telemetry);
|
|
1290
1350
|
telemetry.start();
|
|
1291
1351
|
// NavigationContainer is a child of AIAgent, so navRef may not be
|
|
1292
|
-
// ready yet when this effect runs. Poll briefly until it
|
|
1352
|
+
// ready yet when this effect runs. Poll briefly until it is.
|
|
1293
1353
|
const resolveInitialScreen = () => {
|
|
1294
1354
|
let attempts = 0;
|
|
1295
1355
|
const maxAttempts = 15; // 15 × 200ms = 3s max wait
|
|
1296
1356
|
const timer = setInterval(() => {
|
|
1297
1357
|
attempts++;
|
|
1298
|
-
const
|
|
1299
|
-
if (
|
|
1300
|
-
const cleanName = humanizeScreenName(route.name);
|
|
1301
|
-
if (cleanName) {
|
|
1302
|
-
telemetry.setScreen(cleanName);
|
|
1303
|
-
}
|
|
1358
|
+
const cleanName = syncTelemetryScreen();
|
|
1359
|
+
if (cleanName !== 'unknown') {
|
|
1304
1360
|
clearInterval(timer);
|
|
1305
1361
|
} else if (attempts >= maxAttempts) {
|
|
1306
1362
|
clearInterval(timer);
|
|
@@ -1309,7 +1365,7 @@ export function AIAgent({
|
|
|
1309
1365
|
};
|
|
1310
1366
|
resolveInitialScreen();
|
|
1311
1367
|
}); // initDeviceId
|
|
1312
|
-
}, [analyticsKey, analyticsProxyUrl, analyticsProxyHeaders, bindTelemetryService, debug, navRef]);
|
|
1368
|
+
}, [analyticsKey, analyticsProxyUrl, analyticsProxyHeaders, bindTelemetryService, debug, navRef, syncTelemetryScreen]);
|
|
1313
1369
|
|
|
1314
1370
|
// ─── Security warnings ──────────────────────────────────────
|
|
1315
1371
|
|
|
@@ -1332,11 +1388,14 @@ export function AIAgent({
|
|
|
1332
1388
|
if (rootViewRef.current) {
|
|
1333
1389
|
const handle = InteractionManager.runAfterInteractions(() => {
|
|
1334
1390
|
requestAnimationFrame(() => {
|
|
1335
|
-
captureWireframe(rootViewRef, {
|
|
1391
|
+
Promise.all([captureWireframe(rootViewRef, {
|
|
1336
1392
|
screenName
|
|
1337
|
-
}).then(wireframe => {
|
|
1393
|
+
}), captureHeatmapScreenshot(rootViewRef.current)]).then(([wireframe, screenshot]) => {
|
|
1338
1394
|
if (wireframe && telemetryRef.current) {
|
|
1339
|
-
telemetryRef.current.trackWireframe(
|
|
1395
|
+
telemetryRef.current.trackWireframe({
|
|
1396
|
+
...wireframe,
|
|
1397
|
+
screenshot: screenshot || undefined
|
|
1398
|
+
});
|
|
1340
1399
|
}
|
|
1341
1400
|
}).catch(err => {
|
|
1342
1401
|
if (debug) logger.debug('AIAgent', 'Wireframe capture failed:', err);
|
|
@@ -1378,16 +1437,13 @@ export function AIAgent({
|
|
|
1378
1437
|
}
|
|
1379
1438
|
};
|
|
1380
1439
|
const unsubscribe = navRef.addListener('state', () => {
|
|
1381
|
-
const
|
|
1382
|
-
if (
|
|
1383
|
-
|
|
1384
|
-
if (cleanName) {
|
|
1385
|
-
checkScreenMilestone(cleanName);
|
|
1386
|
-
}
|
|
1440
|
+
const cleanName = resolveScreenName();
|
|
1441
|
+
if (cleanName) {
|
|
1442
|
+
checkScreenMilestone(cleanName);
|
|
1387
1443
|
}
|
|
1388
1444
|
});
|
|
1389
1445
|
return () => unsubscribe?.();
|
|
1390
|
-
}, [navRef, customerSuccess, isOnboardingActive, onboarding, currentOnboardingIndex, advanceOnboarding]);
|
|
1446
|
+
}, [navRef, resolveScreenName, customerSuccess, isOnboardingActive, onboarding, currentOnboardingIndex, advanceOnboarding]);
|
|
1391
1447
|
|
|
1392
1448
|
// ─── MCP Bridge ──────────────────────────────────────────────
|
|
1393
1449
|
|
|
@@ -1424,7 +1480,7 @@ export function AIAgent({
|
|
|
1424
1480
|
setProactiveStage('badge');
|
|
1425
1481
|
},
|
|
1426
1482
|
onReset: () => setProactiveStage('hidden'),
|
|
1427
|
-
generateSuggestion: () => proactiveHelp?.generateSuggestion?.(telemetryRef.current?.screen || 'Home') || proactiveHelp?.badgeText ||
|
|
1483
|
+
generateSuggestion: () => proactiveHelp?.generateSuggestion?.(telemetryRef.current?.screen || 'Home') || proactiveHelp?.badgeText || 'Need help with this screen?',
|
|
1428
1484
|
behaviorTriggers: proactiveHelp?.behaviorTriggers
|
|
1429
1485
|
});
|
|
1430
1486
|
return () => {
|
|
@@ -2187,29 +2243,46 @@ export function AIAgent({
|
|
|
2187
2243
|
// Skip if the AI agent is currently executing a tool — those are
|
|
2188
2244
|
// already tracked as `agent_step` events with full context.
|
|
2189
2245
|
if (telemetryRef.current && !telemetryRef.current.isAgentActing) {
|
|
2190
|
-
|
|
2191
|
-
|
|
2246
|
+
syncTelemetryScreen();
|
|
2247
|
+
const target = extractTouchTargetMetadata(event);
|
|
2248
|
+
const pageX = Math.round(event.nativeEvent.pageX);
|
|
2249
|
+
const pageY = Math.round(event.nativeEvent.pageY);
|
|
2250
|
+
if (target.label) {
|
|
2192
2251
|
telemetryRef.current.track('user_action', {
|
|
2193
2252
|
canonical_type: 'button_tapped',
|
|
2194
2253
|
type: 'tap',
|
|
2195
|
-
action: label,
|
|
2196
|
-
label,
|
|
2197
|
-
element_label: label,
|
|
2254
|
+
action: target.label,
|
|
2255
|
+
label: target.label,
|
|
2256
|
+
element_label: target.label,
|
|
2257
|
+
element_kind: target.elementKind,
|
|
2258
|
+
label_confidence: target.labelConfidence,
|
|
2259
|
+
zone_id: target.zoneId,
|
|
2260
|
+
ancestor_path: target.ancestorPath,
|
|
2261
|
+
sibling_labels: target.siblingLabels,
|
|
2262
|
+
component_name: target.componentName,
|
|
2198
2263
|
actor: 'user',
|
|
2199
|
-
x:
|
|
2200
|
-
y:
|
|
2264
|
+
x: pageX,
|
|
2265
|
+
y: pageY
|
|
2201
2266
|
});
|
|
2202
2267
|
|
|
2203
2268
|
// Track if user is rage-tapping this specific element
|
|
2204
|
-
checkRageClick(
|
|
2269
|
+
checkRageClick({
|
|
2270
|
+
...target,
|
|
2271
|
+
x: pageX,
|
|
2272
|
+
y: pageY
|
|
2273
|
+
}, telemetryRef.current);
|
|
2205
2274
|
} else {
|
|
2206
2275
|
// Tapped an unlabelled/empty area
|
|
2207
2276
|
telemetryRef.current.track('dead_click', {
|
|
2208
2277
|
canonical_type: 'dead_click_detected',
|
|
2209
|
-
x:
|
|
2210
|
-
y:
|
|
2278
|
+
x: pageX,
|
|
2279
|
+
y: pageY,
|
|
2211
2280
|
screen: telemetryRef.current.screen,
|
|
2212
|
-
screen_area: 'unknown'
|
|
2281
|
+
screen_area: 'unknown',
|
|
2282
|
+
zone_id: target.zoneId,
|
|
2283
|
+
ancestor_path: target.ancestorPath,
|
|
2284
|
+
sibling_labels: target.siblingLabels,
|
|
2285
|
+
component_name: target.componentName
|
|
2213
2286
|
});
|
|
2214
2287
|
}
|
|
2215
2288
|
}
|
|
@@ -442,14 +442,14 @@ export class AgentRuntime {
|
|
|
442
442
|
// capture_screenshot — on-demand visual capture (for image/video content questions)
|
|
443
443
|
this.tools.set('capture_screenshot', {
|
|
444
444
|
name: 'capture_screenshot',
|
|
445
|
-
description: 'Capture
|
|
445
|
+
description: 'Capture the SDK root component as an image. Use when the user asks about visual content (images, videos, colors, layout appearance) that cannot be determined from the element tree alone.',
|
|
446
446
|
parameters: {},
|
|
447
447
|
execute: async () => {
|
|
448
448
|
const screenshot = await this.captureScreenshot();
|
|
449
449
|
if (screenshot) {
|
|
450
450
|
return `✅ Screenshot captured (${Math.round(screenshot.length / 1024)}KB). Visual content is now available for analysis.`;
|
|
451
451
|
}
|
|
452
|
-
return '❌ Screenshot capture failed. react-native-view-shot
|
|
452
|
+
return '❌ Screenshot capture failed. react-native-view-shot is required and must be installed in your app.';
|
|
453
453
|
}
|
|
454
454
|
});
|
|
455
455
|
|
|
@@ -676,12 +676,12 @@ export class AgentRuntime {
|
|
|
676
676
|
}
|
|
677
677
|
}
|
|
678
678
|
|
|
679
|
-
// ─── Screenshot Capture (
|
|
679
|
+
// ─── Screenshot Capture (react-native-view-shot) ─────
|
|
680
680
|
|
|
681
681
|
/**
|
|
682
|
-
* Captures the
|
|
683
|
-
* Uses react-native-view-shot as
|
|
684
|
-
* Returns null
|
|
682
|
+
* Captures the root component as a base64 JPEG for vision tools.
|
|
683
|
+
* Uses react-native-view-shot as a required peer dependency.
|
|
684
|
+
* Returns null only when capture is temporarily unavailable.
|
|
685
685
|
*/
|
|
686
686
|
async captureScreenshot() {
|
|
687
687
|
try {
|
|
@@ -699,7 +699,7 @@ export class AgentRuntime {
|
|
|
699
699
|
return uri || undefined;
|
|
700
700
|
} catch (error) {
|
|
701
701
|
if (error.message?.includes('Cannot find module') || error.code === 'MODULE_NOT_FOUND' || error.message?.includes('unknown module')) {
|
|
702
|
-
logger.warn('AgentRuntime', 'Screenshot requires react-native-view-shot.
|
|
702
|
+
logger.warn('AgentRuntime', 'Screenshot requires react-native-view-shot. It is a peer dependency; install it in your host app with: npx expo install react-native-view-shot');
|
|
703
703
|
} else {
|
|
704
704
|
logger.debug('AgentRuntime', `Screenshot skipped: ${error.message}`);
|
|
705
705
|
}
|
|
@@ -13,6 +13,7 @@ import { Dimensions } from 'react-native';
|
|
|
13
13
|
import { logger } from "../utils/logger.js";
|
|
14
14
|
import { getChild, getSibling, getParent, getProps, getStateNode, getType, getDisplayName } from "./FiberAdapter.js";
|
|
15
15
|
import { getActiveAlert } from "./NativeAlertInterceptor.js";
|
|
16
|
+
import { chooseBestAnalyticsTarget, getAnalyticsElementKind } from "../services/telemetry/analyticsLabeling.js";
|
|
16
17
|
|
|
17
18
|
// ─── Walk Configuration ─────────
|
|
18
19
|
|
|
@@ -130,6 +131,69 @@ function getComponentName(fiber) {
|
|
|
130
131
|
if (type.render?.name) return type.render.name;
|
|
131
132
|
return null;
|
|
132
133
|
}
|
|
134
|
+
function getCustomAncestorPath(fiber, maxDepth = 6) {
|
|
135
|
+
const path = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
let current = getParent(fiber);
|
|
138
|
+
let depth = 0;
|
|
139
|
+
while (current && depth < maxDepth) {
|
|
140
|
+
const name = getComponentName(current);
|
|
141
|
+
const props = getProps(current);
|
|
142
|
+
const candidate = name === 'AIZone' && typeof props.id === 'string' && props.id.trim() ? props.id.trim() : name;
|
|
143
|
+
if (candidate && !RN_INTERNAL_NAMES.has(candidate) && !PRESSABLE_TYPES.has(candidate) && !seen.has(candidate)) {
|
|
144
|
+
path.push(candidate);
|
|
145
|
+
seen.add(candidate);
|
|
146
|
+
}
|
|
147
|
+
current = getParent(current);
|
|
148
|
+
depth++;
|
|
149
|
+
}
|
|
150
|
+
return path;
|
|
151
|
+
}
|
|
152
|
+
function getAnalyticsTargetForNode(fiber, resolvedType) {
|
|
153
|
+
const props = getProps(fiber);
|
|
154
|
+
const siblingTextLabel = resolvedType && EXTERNALLY_LABELED_TYPES.has(resolvedType) ? extractSiblingTextLabel(fiber) : null;
|
|
155
|
+
return chooseBestAnalyticsTarget([{
|
|
156
|
+
text: props.accessibilityLabel,
|
|
157
|
+
source: 'accessibility'
|
|
158
|
+
}, {
|
|
159
|
+
text: extractDeepTextContent(fiber),
|
|
160
|
+
source: 'deep-text'
|
|
161
|
+
}, {
|
|
162
|
+
text: siblingTextLabel,
|
|
163
|
+
source: 'sibling-text'
|
|
164
|
+
}, {
|
|
165
|
+
text: props.title,
|
|
166
|
+
source: 'title'
|
|
167
|
+
}, {
|
|
168
|
+
text: resolvedType === 'text-input' ? props.placeholder : null,
|
|
169
|
+
source: 'placeholder'
|
|
170
|
+
}, {
|
|
171
|
+
text: props.testID || props.nativeID,
|
|
172
|
+
source: 'test-id'
|
|
173
|
+
}], getAnalyticsElementKind(resolvedType));
|
|
174
|
+
}
|
|
175
|
+
function getSiblingAnalyticsLabels(fiber, maxLabels = 6) {
|
|
176
|
+
const parent = getParent(fiber);
|
|
177
|
+
if (!parent) return [];
|
|
178
|
+
const labels = [];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
let sibling = getChild(parent);
|
|
181
|
+
while (sibling) {
|
|
182
|
+
if (sibling !== fiber) {
|
|
183
|
+
const siblingType = getElementType(sibling);
|
|
184
|
+
if (siblingType && !isDisabled(sibling)) {
|
|
185
|
+
const label = getAnalyticsTargetForNode(sibling, siblingType).label;
|
|
186
|
+
if (label && !seen.has(label.toLowerCase())) {
|
|
187
|
+
labels.push(label);
|
|
188
|
+
seen.add(label.toLowerCase());
|
|
189
|
+
if (labels.length >= maxLabels) break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
sibling = getSibling(sibling);
|
|
194
|
+
}
|
|
195
|
+
return labels;
|
|
196
|
+
}
|
|
133
197
|
function hasSliderLikeSemantics(props) {
|
|
134
198
|
if (!props || typeof props !== 'object') return false;
|
|
135
199
|
if (typeof props.onSlidingComplete === 'function') return true;
|
|
@@ -421,7 +485,7 @@ function isDisabled(fiber) {
|
|
|
421
485
|
* Recursively extract ALL text content from a fiber's children.
|
|
422
486
|
* Pierces through nested interactive elements — unlike typical tree walkers
|
|
423
487
|
* that stop at inner Pressable/TouchableOpacity boundaries.
|
|
424
|
-
*
|
|
488
|
+
*
|
|
425
489
|
* This is critical for wrapper components (e.g. ZButton → internal
|
|
426
490
|
* TouchableOpacity → Text) where stopping at nested interactives
|
|
427
491
|
* would lose the text label entirely.
|
|
@@ -464,7 +528,7 @@ function isIconGlyph(text) {
|
|
|
464
528
|
const trimmed = text.trim();
|
|
465
529
|
if (trimmed.length === 0 || trimmed.length > 2) return false; // Glyphs are 1-2 chars (surrogate pairs)
|
|
466
530
|
const code = trimmed.codePointAt(0) || 0;
|
|
467
|
-
return code >=
|
|
531
|
+
return code >= 0xe000 && code <= 0xf8ff || code >= 0xf0000 && code <= 0xfffff || code >= 0x100000 && code <= 0x10ffff;
|
|
468
532
|
}
|
|
469
533
|
function normalizeRuntimeLabel(text) {
|
|
470
534
|
if (!text) return '';
|
|
@@ -541,11 +605,11 @@ function extractRawText(children) {
|
|
|
541
605
|
/**
|
|
542
606
|
* Recursively search a fiber subtree for icon/symbol components and
|
|
543
607
|
* return their `name` prop as a semantic label.
|
|
544
|
-
*
|
|
608
|
+
*
|
|
545
609
|
* Works generically: any non-RN-internal child component with a string
|
|
546
610
|
* `name` prop is treated as an icon (covers Ionicons, MaterialIcons,
|
|
547
611
|
* FontAwesome, custom SVG wrappers, etc. — no hardcoded list needed).
|
|
548
|
-
*
|
|
612
|
+
*
|
|
549
613
|
* e.g. a TouchableOpacity wrapping <Ionicons name="add-circle" /> → "icon:add-circle"
|
|
550
614
|
*/
|
|
551
615
|
function extractIconName(fiber, maxDepth = 5) {
|
|
@@ -870,6 +934,28 @@ export function walkFiberTree(rootRef, config) {
|
|
|
870
934
|
text: parentContext && !new Set(['ScrollViewContext', 'VirtualizedListContext', 'ViewabilityHelper', 'ScrollResponder', 'AnimatedComponent', 'TouchableOpacity']).has(parentContext) ? parentContext : null,
|
|
871
935
|
source: 'context'
|
|
872
936
|
}]);
|
|
937
|
+
const analyticsTarget = chooseBestAnalyticsTarget([{
|
|
938
|
+
text: derivedProps.accessibilityLabel,
|
|
939
|
+
source: 'accessibility'
|
|
940
|
+
}, {
|
|
941
|
+
text: extractDeepTextContent(node),
|
|
942
|
+
source: 'deep-text'
|
|
943
|
+
}, {
|
|
944
|
+
text: siblingTextLabel,
|
|
945
|
+
source: 'sibling-text'
|
|
946
|
+
}, {
|
|
947
|
+
text: derivedProps.title,
|
|
948
|
+
source: 'title'
|
|
949
|
+
}, {
|
|
950
|
+
text: resolvedType === 'text-input' ? derivedProps.placeholder : null,
|
|
951
|
+
source: 'placeholder'
|
|
952
|
+
}, {
|
|
953
|
+
text: derivedProps.testID || derivedProps.nativeID,
|
|
954
|
+
source: 'test-id'
|
|
955
|
+
}], getAnalyticsElementKind(resolvedType));
|
|
956
|
+
const ancestorPath = getCustomAncestorPath(node);
|
|
957
|
+
const siblingLabels = getSiblingAnalyticsLabels(node);
|
|
958
|
+
const componentName = getComponentName(node);
|
|
873
959
|
interactives.push({
|
|
874
960
|
index: currentIndex,
|
|
875
961
|
type: resolvedType,
|
|
@@ -878,7 +964,14 @@ export function walkFiberTree(rootRef, config) {
|
|
|
878
964
|
zoneId: currentZoneId,
|
|
879
965
|
fiberNode: node,
|
|
880
966
|
props: derivedProps,
|
|
881
|
-
requiresConfirmation: derivedProps.aiConfirm === true
|
|
967
|
+
requiresConfirmation: derivedProps.aiConfirm === true,
|
|
968
|
+
analyticsLabel: analyticsTarget.label,
|
|
969
|
+
analyticsElementKind: analyticsTarget.elementKind,
|
|
970
|
+
analyticsLabelConfidence: analyticsTarget.labelConfidence,
|
|
971
|
+
analyticsZoneId: currentZoneId,
|
|
972
|
+
analyticsAncestorPath: ancestorPath,
|
|
973
|
+
analyticsSiblingLabels: siblingLabels,
|
|
974
|
+
analyticsComponentName: componentName
|
|
882
975
|
});
|
|
883
976
|
|
|
884
977
|
// Build output tag with state attributes
|
|
@@ -1080,7 +1173,7 @@ export function findScrollableContainers(rootRef, screenName) {
|
|
|
1080
1173
|
const contextLabel = getNearestCustomComponentName(node) || name || 'Unknown';
|
|
1081
1174
|
|
|
1082
1175
|
// For scrollable containers, we need the native scroll ref.
|
|
1083
|
-
// FlatList Fiber stateNode may be the component instance —
|
|
1176
|
+
// FlatList Fiber stateNode may be the component instance —
|
|
1084
1177
|
// we need to find the underlying native ScrollView.
|
|
1085
1178
|
let scrollRef = isPagerLike ? getStateNode(node) : resolveNativeScrollRef(node);
|
|
1086
1179
|
if (scrollRef) {
|
|
@@ -1109,13 +1202,13 @@ export function findScrollableContainers(rootRef, screenName) {
|
|
|
1109
1202
|
|
|
1110
1203
|
/**
|
|
1111
1204
|
* Resolve the native scroll view reference from a Fiber node.
|
|
1112
|
-
*
|
|
1205
|
+
*
|
|
1113
1206
|
* Handles multiple React Native internals:
|
|
1114
1207
|
* - RCTScrollView: stateNode IS the native scroll view
|
|
1115
1208
|
* - FlatList/VirtualizedList: stateNode is a component instance,
|
|
1116
1209
|
* need to find the inner ScrollView via getNativeScrollRef() or
|
|
1117
1210
|
* by walking down the Fiber tree to find the RCTScrollView child
|
|
1118
|
-
*/
|
|
1211
|
+
*/
|
|
1119
1212
|
function resolveNativeScrollRef(fiberNode) {
|
|
1120
1213
|
const stateNode = getStateNode(fiberNode);
|
|
1121
1214
|
|
|
@@ -1129,7 +1222,9 @@ function resolveNativeScrollRef(fiberNode) {
|
|
|
1129
1222
|
try {
|
|
1130
1223
|
const ref = stateNode.getNativeScrollRef();
|
|
1131
1224
|
if (ref && typeof ref.scrollTo === 'function') return ref;
|
|
1132
|
-
} catch {
|
|
1225
|
+
} catch {
|
|
1226
|
+
/* fall through */
|
|
1227
|
+
}
|
|
1133
1228
|
}
|
|
1134
1229
|
|
|
1135
1230
|
// Case 3: stateNode has getScrollRef (another VirtualizedList pattern)
|
|
@@ -1142,7 +1237,9 @@ function resolveNativeScrollRef(fiberNode) {
|
|
|
1142
1237
|
const nativeRef = ref.getNativeScrollRef();
|
|
1143
1238
|
if (nativeRef && typeof nativeRef.scrollTo === 'function') return nativeRef;
|
|
1144
1239
|
}
|
|
1145
|
-
} catch {
|
|
1240
|
+
} catch {
|
|
1241
|
+
/* fall through */
|
|
1242
|
+
}
|
|
1146
1243
|
}
|
|
1147
1244
|
|
|
1148
1245
|
// Case 4: stateNode has scrollToOffset directly (VirtualizedList instance)
|
|
@@ -1196,7 +1293,13 @@ function measureElement(el) {
|
|
|
1196
1293
|
if (width > 0 && height > 0) {
|
|
1197
1294
|
resolve({
|
|
1198
1295
|
type: el.type,
|
|
1199
|
-
label: el.
|
|
1296
|
+
label: el.analyticsLabel || '',
|
|
1297
|
+
elementKind: el.analyticsElementKind,
|
|
1298
|
+
labelConfidence: el.analyticsLabelConfidence,
|
|
1299
|
+
zoneId: el.analyticsZoneId,
|
|
1300
|
+
ancestorPath: el.analyticsAncestorPath,
|
|
1301
|
+
siblingLabels: el.analyticsSiblingLabels,
|
|
1302
|
+
componentName: el.analyticsComponentName,
|
|
1200
1303
|
x: pageX,
|
|
1201
1304
|
y: pageY,
|
|
1202
1305
|
width,
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
// React Native imports not needed — we use Fiber internals directly
|
|
18
18
|
|
|
19
|
+
import { chooseBestAnalyticsTarget, getAnalyticsElementKind } from "./analyticsLabeling.js";
|
|
20
|
+
import { getChild, getDisplayName, getParent, getProps, getSibling, getType } from "../../core/FiberAdapter.js";
|
|
21
|
+
|
|
19
22
|
// ─── Rage Click Detection ──────────────────────────────────────────
|
|
20
23
|
//
|
|
21
24
|
// Industry-standard approach (FullStory, PostHog, LogRocket):
|
|
@@ -31,6 +34,158 @@ const MAX_TAP_BUFFER = 8;
|
|
|
31
34
|
|
|
32
35
|
// Labels that are naturally tapped multiple times in sequence (wizards, onboarding, etc.)
|
|
33
36
|
const NAVIGATION_LABELS = new Set(['next', 'continue', 'skip', 'back', 'done', 'ok', 'cancel', 'previous', 'dismiss', 'close', 'got it', 'confirm', 'proceed', 'التالي', 'متابعة', 'تخطي', 'رجوع', 'تم', 'إلغاء', 'إغلاق', 'حسناً']);
|
|
37
|
+
const INTERACTIVE_PROP_KEYS = new Set(['onPress', 'onPressIn', 'onPressOut', 'onLongPress', 'onValueChange', 'onChangeText', 'onChange', 'onBlur', 'onFocus', 'onSubmitEditing', 'onScrollToTop', 'onDateChange', 'onValueChangeComplete', 'onSlidingComplete', 'onRefresh', 'onEndEditing', 'onSelect', 'onCheckedChange']);
|
|
38
|
+
const INTERACTIVE_ROLES = new Set(['button', 'link', 'menuitem', 'tab', 'checkbox', 'switch', 'radio', 'slider', 'search', 'text', 'textbox']);
|
|
39
|
+
const RN_INTERNAL_NAMES = new Set(['View', 'RCTView', 'Pressable', 'TouchableOpacity', 'TouchableHighlight', 'ScrollView', 'RCTScrollView', 'FlatList', 'SectionList', 'SafeAreaView', 'RNCSafeAreaView', 'KeyboardAvoidingView', 'Modal', 'StatusBar', 'Text', 'RCTText', 'AnimatedComponent', 'AnimatedComponentWrapper', 'Animated']);
|
|
40
|
+
function isInteractiveNode(props, typeName) {
|
|
41
|
+
if (!props || typeof props !== 'object') return false;
|
|
42
|
+
for (const key of Object.keys(props)) {
|
|
43
|
+
if (INTERACTIVE_PROP_KEYS.has(key) && typeof props[key] === 'function') {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const role = props.accessibilityRole;
|
|
48
|
+
if (typeof role === 'string' && INTERACTIVE_ROLES.has(role.toLowerCase())) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (!typeName) return false;
|
|
52
|
+
const normalizedType = typeName.toLowerCase();
|
|
53
|
+
return normalizedType.includes('pressable') || normalizedType.includes('touchable') || normalizedType.includes('button') || normalizedType.includes('textfield') || normalizedType.includes('textinput') || normalizedType.includes('switch') || normalizedType.includes('checkbox') || normalizedType.includes('slider') || normalizedType.includes('picker') || normalizedType.includes('datepicker');
|
|
54
|
+
}
|
|
55
|
+
function getComponentName(fiber) {
|
|
56
|
+
const type = getType(fiber);
|
|
57
|
+
if (!type) return null;
|
|
58
|
+
if (typeof type === 'string') return type;
|
|
59
|
+
const displayName = getDisplayName(fiber);
|
|
60
|
+
if (displayName) return displayName;
|
|
61
|
+
if (type.name) return type.name;
|
|
62
|
+
if (type.render?.displayName) return type.render.displayName;
|
|
63
|
+
if (type.render?.name) return type.render.name;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function getZoneId(fiber, maxDepth = 8) {
|
|
67
|
+
let current = fiber;
|
|
68
|
+
let depth = 0;
|
|
69
|
+
while (current && depth < maxDepth) {
|
|
70
|
+
const name = getComponentName(current);
|
|
71
|
+
const props = getProps(current);
|
|
72
|
+
if (name === 'AIZone' && typeof props.id === 'string' && props.id.trim().length > 0) {
|
|
73
|
+
return props.id.trim();
|
|
74
|
+
}
|
|
75
|
+
current = getParent(current);
|
|
76
|
+
depth++;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function getAncestorPath(fiber, maxDepth = 6) {
|
|
81
|
+
const labels = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
let current = getParent(fiber);
|
|
84
|
+
let depth = 0;
|
|
85
|
+
while (current && depth < maxDepth) {
|
|
86
|
+
const name = getComponentName(current);
|
|
87
|
+
const props = getProps(current);
|
|
88
|
+
const candidate = name === 'AIZone' && typeof props.id === 'string' && props.id.trim() ? props.id.trim() : name;
|
|
89
|
+
if (candidate && !RN_INTERNAL_NAMES.has(candidate) && !seen.has(candidate)) {
|
|
90
|
+
labels.push(candidate);
|
|
91
|
+
seen.add(candidate);
|
|
92
|
+
}
|
|
93
|
+
current = getParent(current);
|
|
94
|
+
depth++;
|
|
95
|
+
}
|
|
96
|
+
return labels;
|
|
97
|
+
}
|
|
98
|
+
function getLabelForFiberNode(fiber) {
|
|
99
|
+
const props = getProps(fiber);
|
|
100
|
+
return chooseBestAnalyticsTarget([{
|
|
101
|
+
text: props.accessibilityLabel,
|
|
102
|
+
source: 'accessibility'
|
|
103
|
+
}, {
|
|
104
|
+
text: props.title,
|
|
105
|
+
source: 'title'
|
|
106
|
+
}, {
|
|
107
|
+
text: props.placeholder,
|
|
108
|
+
source: 'placeholder'
|
|
109
|
+
}, {
|
|
110
|
+
text: props.testID,
|
|
111
|
+
source: 'test-id'
|
|
112
|
+
}, {
|
|
113
|
+
text: typeof props.children === 'string' ? props.children : Array.isArray(props.children) ? findTextInChildren(props.children) : props.children && typeof props.children === 'object' ? findTextInChildren([props.children]) : null,
|
|
114
|
+
source: 'deep-text'
|
|
115
|
+
}], getAnalyticsElementKind(props.accessibilityRole || getComponentName(fiber))).label;
|
|
116
|
+
}
|
|
117
|
+
function getSiblingLabels(fiber, maxLabels = 6) {
|
|
118
|
+
const parent = getParent(fiber);
|
|
119
|
+
if (!parent) return [];
|
|
120
|
+
const labels = [];
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
let sibling = getChild(parent);
|
|
123
|
+
while (sibling) {
|
|
124
|
+
if (sibling !== fiber) {
|
|
125
|
+
const siblingProps = getProps(sibling);
|
|
126
|
+
const siblingName = getComponentName(sibling) || undefined;
|
|
127
|
+
if (isInteractiveNode(siblingProps, siblingName)) {
|
|
128
|
+
const label = getLabelForFiberNode(sibling);
|
|
129
|
+
if (label && !seen.has(label.toLowerCase())) {
|
|
130
|
+
labels.push(label);
|
|
131
|
+
seen.add(label.toLowerCase());
|
|
132
|
+
if (labels.length >= maxLabels) break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
sibling = getSibling(sibling);
|
|
137
|
+
}
|
|
138
|
+
return labels;
|
|
139
|
+
}
|
|
140
|
+
function addCandidatesFromProps(candidates, props, isInteractiveContext = false) {
|
|
141
|
+
if (!props) return;
|
|
142
|
+
const candidateSources = [{
|
|
143
|
+
source: 'accessibility',
|
|
144
|
+
text: props.accessibilityLabel
|
|
145
|
+
}, {
|
|
146
|
+
source: 'title',
|
|
147
|
+
text: props.title
|
|
148
|
+
}, {
|
|
149
|
+
source: 'placeholder',
|
|
150
|
+
text: props.placeholder
|
|
151
|
+
}, {
|
|
152
|
+
source: 'test-id',
|
|
153
|
+
text: props.testID
|
|
154
|
+
}];
|
|
155
|
+
for (const item of candidateSources) {
|
|
156
|
+
if (!item.text) continue;
|
|
157
|
+
candidates.push({
|
|
158
|
+
text: item.text,
|
|
159
|
+
source: item.source,
|
|
160
|
+
isInteractiveContext
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (typeof props.children === 'string' && props.children.trim()) {
|
|
164
|
+
candidates.push({
|
|
165
|
+
text: props.children.trim(),
|
|
166
|
+
source: 'deep-text',
|
|
167
|
+
isInteractiveContext
|
|
168
|
+
});
|
|
169
|
+
} else if (Array.isArray(props.children)) {
|
|
170
|
+
const text = findTextInChildren(props.children);
|
|
171
|
+
if (text) {
|
|
172
|
+
candidates.push({
|
|
173
|
+
text,
|
|
174
|
+
source: 'deep-text',
|
|
175
|
+
isInteractiveContext
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} else if (props.children && typeof props.children === 'object') {
|
|
179
|
+
const text = findTextInChildren([props.children]);
|
|
180
|
+
if (text) {
|
|
181
|
+
candidates.push({
|
|
182
|
+
text,
|
|
183
|
+
source: 'deep-text',
|
|
184
|
+
isInteractiveContext
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
34
189
|
function isNavigationLabel(label) {
|
|
35
190
|
return NAVIGATION_LABELS.has(label.toLowerCase().trim());
|
|
36
191
|
}
|
|
@@ -43,7 +198,10 @@ function isNavigationLabel(label) {
|
|
|
43
198
|
* 2. Taps must be on the SAME screen (screen change = not rage, it's navigation)
|
|
44
199
|
* 3. Navigation labels ("Next", "Skip", etc.) are excluded
|
|
45
200
|
*/
|
|
46
|
-
export function checkRageClick(
|
|
201
|
+
export function checkRageClick(target, telemetry) {
|
|
202
|
+
const label = target.label;
|
|
203
|
+
if (!label) return;
|
|
204
|
+
|
|
47
205
|
// Skip navigation-style labels — sequential tapping is by design
|
|
48
206
|
if (isNavigationLabel(label)) return;
|
|
49
207
|
const now = Date.now();
|
|
@@ -64,8 +222,16 @@ export function checkRageClick(label, telemetry) {
|
|
|
64
222
|
canonical_type: 'rage_click_detected',
|
|
65
223
|
label,
|
|
66
224
|
element_label: label,
|
|
225
|
+
element_kind: target.elementKind,
|
|
226
|
+
label_confidence: target.labelConfidence,
|
|
227
|
+
zone_id: target.zoneId,
|
|
228
|
+
ancestor_path: target.ancestorPath,
|
|
229
|
+
sibling_labels: target.siblingLabels,
|
|
230
|
+
component_name: target.componentName,
|
|
67
231
|
count: matching.length,
|
|
68
|
-
screen: currentScreen
|
|
232
|
+
screen: currentScreen,
|
|
233
|
+
x: target.x,
|
|
234
|
+
y: target.y
|
|
69
235
|
});
|
|
70
236
|
// Reset buffer after emitting to avoid duplicate rage events
|
|
71
237
|
recentTaps.length = 0;
|
|
@@ -78,69 +244,90 @@ export function checkRageClick(label, telemetry) {
|
|
|
78
244
|
* @param event - The GestureResponderEvent from onStartShouldSetResponderCapture
|
|
79
245
|
* @returns A descriptive label string for the tapped element
|
|
80
246
|
*/
|
|
81
|
-
export function
|
|
247
|
+
export function extractTouchTargetMetadata(event) {
|
|
82
248
|
const target = event?.nativeEvent?.target;
|
|
83
|
-
if (!target)
|
|
249
|
+
if (!target) {
|
|
250
|
+
return {
|
|
251
|
+
label: null,
|
|
252
|
+
elementKind: 'unknown',
|
|
253
|
+
labelConfidence: 'low'
|
|
254
|
+
};
|
|
255
|
+
}
|
|
84
256
|
try {
|
|
85
257
|
let fiber = event?._targetInst || getFiberFromNativeTag(target);
|
|
86
258
|
if (fiber) {
|
|
87
259
|
let current = fiber;
|
|
88
260
|
let depth = 0;
|
|
89
261
|
const MAX_DEPTH = 12;
|
|
90
|
-
let
|
|
91
|
-
|
|
262
|
+
let foundInteractive = false;
|
|
263
|
+
const candidates = [];
|
|
264
|
+
let detectedKind = getAnalyticsElementKind(null);
|
|
265
|
+
let interactiveFiber = null;
|
|
92
266
|
while (current && depth < MAX_DEPTH) {
|
|
267
|
+
const props = current.memoizedProps;
|
|
268
|
+
const typeName = current.type?.name || current.type?.displayName;
|
|
269
|
+
const nodeInteractive = isInteractiveNode(props, typeName);
|
|
270
|
+
if (nodeInteractive) {
|
|
271
|
+
foundInteractive = true;
|
|
272
|
+
interactiveFiber = current;
|
|
273
|
+
}
|
|
93
274
|
// 1. Detect Component Type Context
|
|
94
|
-
if (
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
} else if (
|
|
98
|
-
|
|
99
|
-
} else if (
|
|
100
|
-
|
|
101
|
-
} else if (
|
|
102
|
-
|
|
103
|
-
} else if (
|
|
104
|
-
|
|
105
|
-
if (typeof name === 'string' && name.length > 2 && !name.toLowerCase().includes('wrapper') && !name.startsWith('RCT')) {
|
|
106
|
-
detectedRole = name;
|
|
107
|
-
}
|
|
275
|
+
if (detectedKind === 'unknown') {
|
|
276
|
+
if (props?.accessibilityRole) {
|
|
277
|
+
detectedKind = getAnalyticsElementKind(props.accessibilityRole);
|
|
278
|
+
} else if (props?.onValueChange && typeof props?.value === 'boolean') {
|
|
279
|
+
detectedKind = 'toggle';
|
|
280
|
+
} else if (props?.onChangeText) {
|
|
281
|
+
detectedKind = 'text_input';
|
|
282
|
+
} else if (props?.onPress) {
|
|
283
|
+
detectedKind = 'button';
|
|
284
|
+
} else if (typeName) {
|
|
285
|
+
detectedKind = getAnalyticsElementKind(typeName);
|
|
108
286
|
}
|
|
109
287
|
}
|
|
288
|
+
if (!props) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
addCandidatesFromProps(candidates, props, nodeInteractive);
|
|
110
292
|
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
} else if (current.memoizedProps?.testID) {
|
|
116
|
-
bestLabel = current.memoizedProps.testID;
|
|
117
|
-
} else if (current.memoizedProps?.title) {
|
|
118
|
-
bestLabel = current.memoizedProps.title;
|
|
119
|
-
} else if (current.memoizedProps?.placeholder) {
|
|
120
|
-
bestLabel = current.memoizedProps.placeholder;
|
|
121
|
-
} else if (typeof current.memoizedProps?.children === 'string' && current.memoizedProps.children.trim()) {
|
|
122
|
-
bestLabel = current.memoizedProps.children.trim();
|
|
123
|
-
} else if (Array.isArray(current.memoizedProps?.children)) {
|
|
124
|
-
bestLabel = findTextInChildren(current.memoizedProps.children);
|
|
125
|
-
} else if (current.memoizedProps?.children && typeof current.memoizedProps.children === 'object') {
|
|
126
|
-
bestLabel = findTextInChildren([current.memoizedProps.children]);
|
|
127
|
-
}
|
|
293
|
+
// Stop at the nearest interactive node. If this node does not provide a
|
|
294
|
+
// usable label, still allow a child text fallback from descendants.
|
|
295
|
+
if (foundInteractive) {
|
|
296
|
+
break;
|
|
128
297
|
}
|
|
129
298
|
current = current.return;
|
|
130
299
|
depth++;
|
|
131
300
|
}
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
301
|
+
if (!foundInteractive) {
|
|
302
|
+
return {
|
|
303
|
+
label: null,
|
|
304
|
+
elementKind: detectedKind,
|
|
305
|
+
labelConfidence: 'low'
|
|
306
|
+
};
|
|
138
307
|
}
|
|
308
|
+
|
|
309
|
+
// Prioritize nearest interactive context when available.
|
|
310
|
+
const resolved = chooseBestAnalyticsTarget(candidates, detectedKind);
|
|
311
|
+
const sourceFiber = interactiveFiber || fiber;
|
|
312
|
+
return {
|
|
313
|
+
...resolved,
|
|
314
|
+
zoneId: getZoneId(sourceFiber),
|
|
315
|
+
ancestorPath: getAncestorPath(sourceFiber),
|
|
316
|
+
siblingLabels: getSiblingLabels(sourceFiber),
|
|
317
|
+
componentName: getComponentName(sourceFiber)
|
|
318
|
+
};
|
|
139
319
|
}
|
|
140
320
|
} catch {
|
|
141
321
|
// Fiber access failed — fall back gracefully
|
|
142
322
|
}
|
|
143
|
-
return
|
|
323
|
+
return {
|
|
324
|
+
label: null,
|
|
325
|
+
elementKind: 'unknown',
|
|
326
|
+
labelConfidence: 'low'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
export function extractTouchLabel(event) {
|
|
330
|
+
return extractTouchTargetMetadata(event).label ?? 'Unknown Element';
|
|
144
331
|
}
|
|
145
332
|
|
|
146
333
|
/**
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const GENERIC_LABELS = new Set(['button', 'buttons', 'component', 'components', 'container', 'containers', 'content', 'cta', 'item', 'items', 'label', 'labels', 'root', 'row', 'rows', 'screen', 'screens', 'text', 'texts', 'title', 'titles', 'unknown', 'value', 'values', 'view', 'views', 'wrapper', 'wrappers']);
|
|
4
|
+
const GENERIC_IDENTIFIER_TOKENS = new Set(['btn', 'button', 'card', 'cell', 'component', 'container', 'content', 'cta', 'icon', 'input', 'item', 'label', 'node', 'pressable', 'root', 'row', 'screen', 'target', 'text', 'tile', 'toggle', 'view', 'wrapper']);
|
|
5
|
+
const INTERNAL_NAME_PATTERNS = [/^RCT[A-Z]/, /^React/, /^TextImpl/i, /^Android/i, /^UI[A-Z]/, /^RN[A-Z]/, /^Virtualized/i, /^ScrollResponder$/i, /^Animated(Component|.*Wrapper)?$/i, /^Touchable[A-Z]/, /^Pressable$/i, /^View$/i, /^Text$/i, /^Modal$/i, /Legacy/i, /Wrapper/i, /Context$/i, /Provider$/i];
|
|
6
|
+
function normalizeWhitespace(value) {
|
|
7
|
+
if (!value) return '';
|
|
8
|
+
return String(value).replace(/\s+/g, ' ').trim();
|
|
9
|
+
}
|
|
10
|
+
function toTitleCase(value) {
|
|
11
|
+
return value.split(/\s+/).filter(Boolean).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
|
|
12
|
+
}
|
|
13
|
+
function humanizeIdentifier(value) {
|
|
14
|
+
const humanized = value.replace(/^icon:/i, '').replace(/[_./-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\b\d+\b/g, ' ').replace(/\s+/g, ' ').trim();
|
|
15
|
+
return toTitleCase(humanized);
|
|
16
|
+
}
|
|
17
|
+
function stripDecorators(value) {
|
|
18
|
+
return value.replace(/^\[[^\]]+\]\s*/g, '').replace(/^["'`]+|["'`]+$/g, '').trim();
|
|
19
|
+
}
|
|
20
|
+
function looksInternal(value) {
|
|
21
|
+
return INTERNAL_NAME_PATTERNS.some(pattern => pattern.test(value));
|
|
22
|
+
}
|
|
23
|
+
function isLowSignalValue(value) {
|
|
24
|
+
const lowered = value.toLowerCase();
|
|
25
|
+
return GENERIC_LABELS.has(lowered);
|
|
26
|
+
}
|
|
27
|
+
function sanitizeLabelValue(rawValue, source) {
|
|
28
|
+
let normalized = normalizeWhitespace(rawValue);
|
|
29
|
+
if (!normalized) return null;
|
|
30
|
+
normalized = stripDecorators(normalized);
|
|
31
|
+
if (!normalized) return null;
|
|
32
|
+
if (source === 'test-id' || source === 'icon' || source === 'context') {
|
|
33
|
+
normalized = humanizeIdentifier(normalized);
|
|
34
|
+
}
|
|
35
|
+
if (!normalized) return null;
|
|
36
|
+
if (normalized.length > 80) return null;
|
|
37
|
+
if (looksInternal(normalized)) return null;
|
|
38
|
+
const words = normalized.split(/\s+/).filter(Boolean);
|
|
39
|
+
if (words.length === 0) return null;
|
|
40
|
+
if (words.length === 1) {
|
|
41
|
+
const token = words[0].toLowerCase();
|
|
42
|
+
if (GENERIC_IDENTIFIER_TOKENS.has(token) || GENERIC_LABELS.has(token)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (isLowSignalValue(normalized)) return null;
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
function scoreAnalyticsLabel(label, source, isInteractiveContext = false) {
|
|
50
|
+
let score = 0;
|
|
51
|
+
const words = label.split(/\s+/).filter(Boolean);
|
|
52
|
+
if (source === 'accessibility') score += 95;
|
|
53
|
+
if (source === 'deep-text') score += 78;
|
|
54
|
+
if (source === 'sibling-text') score += 64;
|
|
55
|
+
if (source === 'title') score += 56;
|
|
56
|
+
if (source === 'placeholder') score += 42;
|
|
57
|
+
if (source === 'context') score += 12;
|
|
58
|
+
if (source === 'icon') score -= 8;
|
|
59
|
+
if (source === 'test-id') score -= 14;
|
|
60
|
+
if (isInteractiveContext) score += 18;
|
|
61
|
+
if (words.length >= 2 && words.length <= 6) score += 20;else if (words.length === 1) score += 4;
|
|
62
|
+
if (label.length >= 4 && label.length <= 36) score += 18;else if (label.length > 56) score -= 20;
|
|
63
|
+
if (/^[A-Z]/.test(label)) score += 8;
|
|
64
|
+
if (/[A-Za-z]/.test(label) && !/[_./]/.test(label)) score += 10;
|
|
65
|
+
return score;
|
|
66
|
+
}
|
|
67
|
+
export function getFallbackAnalyticsLabel(elementKind) {
|
|
68
|
+
switch (elementKind) {
|
|
69
|
+
case 'button':
|
|
70
|
+
return 'Primary action';
|
|
71
|
+
case 'text_input':
|
|
72
|
+
return 'Text input';
|
|
73
|
+
case 'toggle':
|
|
74
|
+
return 'Toggle';
|
|
75
|
+
case 'picker':
|
|
76
|
+
return 'Picker';
|
|
77
|
+
case 'slider':
|
|
78
|
+
return 'Slider';
|
|
79
|
+
case 'link':
|
|
80
|
+
return 'Link';
|
|
81
|
+
case 'tab':
|
|
82
|
+
return 'Tab';
|
|
83
|
+
case 'list_item':
|
|
84
|
+
return 'List item';
|
|
85
|
+
case 'image':
|
|
86
|
+
return 'Image';
|
|
87
|
+
case 'icon':
|
|
88
|
+
return 'Icon';
|
|
89
|
+
case 'text':
|
|
90
|
+
return 'Text';
|
|
91
|
+
case 'card':
|
|
92
|
+
return 'Card';
|
|
93
|
+
case 'modal':
|
|
94
|
+
return 'Modal';
|
|
95
|
+
case 'sheet':
|
|
96
|
+
return 'Bottom sheet';
|
|
97
|
+
case 'scroll_area':
|
|
98
|
+
return 'Scrollable area';
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function getAnalyticsElementKind(elementType) {
|
|
104
|
+
switch (elementType) {
|
|
105
|
+
case 'pressable':
|
|
106
|
+
case 'radio':
|
|
107
|
+
case 'button':
|
|
108
|
+
case 'checkbox':
|
|
109
|
+
return 'button';
|
|
110
|
+
case 'link':
|
|
111
|
+
return 'link';
|
|
112
|
+
case 'tab':
|
|
113
|
+
case 'tabbar':
|
|
114
|
+
return 'tab';
|
|
115
|
+
case 'listitem':
|
|
116
|
+
case 'list-item':
|
|
117
|
+
case 'menuitem':
|
|
118
|
+
return 'list_item';
|
|
119
|
+
case 'text-input':
|
|
120
|
+
case 'text_input':
|
|
121
|
+
case 'textinput':
|
|
122
|
+
return 'text_input';
|
|
123
|
+
case 'switch':
|
|
124
|
+
case 'toggle':
|
|
125
|
+
return 'toggle';
|
|
126
|
+
case 'slider':
|
|
127
|
+
return 'slider';
|
|
128
|
+
case 'picker':
|
|
129
|
+
return 'picker';
|
|
130
|
+
case 'date-picker':
|
|
131
|
+
case 'select':
|
|
132
|
+
case 'dropdown':
|
|
133
|
+
return 'picker';
|
|
134
|
+
case 'image':
|
|
135
|
+
case 'imagebutton':
|
|
136
|
+
return 'image';
|
|
137
|
+
case 'icon':
|
|
138
|
+
return 'icon';
|
|
139
|
+
case 'text':
|
|
140
|
+
case 'label':
|
|
141
|
+
case 'header':
|
|
142
|
+
return 'text';
|
|
143
|
+
case 'card':
|
|
144
|
+
return 'card';
|
|
145
|
+
case 'modal':
|
|
146
|
+
return 'modal';
|
|
147
|
+
case 'sheet':
|
|
148
|
+
case 'bottomsheet':
|
|
149
|
+
return 'sheet';
|
|
150
|
+
case 'scrollable':
|
|
151
|
+
case 'scrollview':
|
|
152
|
+
case 'flatlist':
|
|
153
|
+
case 'sectionlist':
|
|
154
|
+
case 'adjustable':
|
|
155
|
+
return 'scroll_area';
|
|
156
|
+
default:
|
|
157
|
+
return 'unknown';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function chooseBestAnalyticsTarget(candidates, elementKind) {
|
|
161
|
+
let best;
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
const label = sanitizeLabelValue(candidate.text, candidate.source);
|
|
164
|
+
if (!label) continue;
|
|
165
|
+
const score = scoreAnalyticsLabel(label, candidate.source, candidate.isInteractiveContext === true);
|
|
166
|
+
if (!best || score > best.score) {
|
|
167
|
+
best = {
|
|
168
|
+
label,
|
|
169
|
+
score
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (best) {
|
|
174
|
+
const labelConfidence = best.score >= 100 ? 'high' : 'low';
|
|
175
|
+
return {
|
|
176
|
+
label: best.label,
|
|
177
|
+
elementKind,
|
|
178
|
+
labelConfidence
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
label: getFallbackAnalyticsLabel(elementKind),
|
|
183
|
+
elementKind,
|
|
184
|
+
labelConfidence: 'low'
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=analyticsLabeling.js.map
|
|
@@ -82,9 +82,9 @@ export declare class AgentRuntime {
|
|
|
82
82
|
/** Maps a tool call to a user-friendly status label for the loading overlay. */
|
|
83
83
|
private getToolStatusLabel;
|
|
84
84
|
/**
|
|
85
|
-
* Captures the
|
|
86
|
-
* Uses react-native-view-shot as
|
|
87
|
-
* Returns null
|
|
85
|
+
* Captures the root component as a base64 JPEG for vision tools.
|
|
86
|
+
* Uses react-native-view-shot as a required peer dependency.
|
|
87
|
+
* Returns null only when capture is temporarily unavailable.
|
|
88
88
|
*/
|
|
89
89
|
private captureScreenshot;
|
|
90
90
|
/**
|
|
@@ -22,6 +22,8 @@ export interface VerifierConfig {
|
|
|
22
22
|
maxFollowupSteps?: number;
|
|
23
23
|
}
|
|
24
24
|
export type ElementType = 'pressable' | 'text-input' | 'switch' | 'radio' | 'scrollable' | 'slider' | 'picker' | 'date-picker';
|
|
25
|
+
export type AnalyticsElementKind = 'button' | 'text_input' | 'toggle' | 'slider' | 'picker' | 'link' | 'tab' | 'list_item' | 'image' | 'icon' | 'text' | 'card' | 'modal' | 'sheet' | 'scroll_area' | 'unknown';
|
|
26
|
+
export type AnalyticsLabelConfidence = 'high' | 'low';
|
|
25
27
|
export interface InteractiveElement {
|
|
26
28
|
/** Unique index assigned during tree walk */
|
|
27
29
|
index: number;
|
|
@@ -60,10 +62,30 @@ export interface InteractiveElement {
|
|
|
60
62
|
/** 0-based index of the button in the active alert */
|
|
61
63
|
alertButtonIndex: number;
|
|
62
64
|
};
|
|
65
|
+
/** Sanitized analytics label used for telemetry and wireframes. */
|
|
66
|
+
analyticsLabel?: string | null;
|
|
67
|
+
/** Generic analytics-facing element kind. */
|
|
68
|
+
analyticsElementKind?: AnalyticsElementKind;
|
|
69
|
+
/** Confidence that the analytics label is user-facing. */
|
|
70
|
+
analyticsLabelConfidence?: AnalyticsLabelConfidence;
|
|
71
|
+
/** Nearest enclosing AI zone id, preserved for analytics/debugging. */
|
|
72
|
+
analyticsZoneId?: string | null;
|
|
73
|
+
/** Nearest custom component ancestry above the interactive node. */
|
|
74
|
+
analyticsAncestorPath?: string[];
|
|
75
|
+
/** Nearby sibling interactive labels from the same parent group. */
|
|
76
|
+
analyticsSiblingLabels?: string[];
|
|
77
|
+
/** Concrete component name for the matched interactive host. */
|
|
78
|
+
analyticsComponentName?: string | null;
|
|
63
79
|
}
|
|
64
80
|
export interface WireframeComponent {
|
|
65
81
|
type: ElementType;
|
|
66
82
|
label: string;
|
|
83
|
+
elementKind?: AnalyticsElementKind;
|
|
84
|
+
labelConfidence?: AnalyticsLabelConfidence;
|
|
85
|
+
zoneId?: string | null;
|
|
86
|
+
ancestorPath?: string[];
|
|
87
|
+
siblingLabels?: string[];
|
|
88
|
+
componentName?: string | null;
|
|
67
89
|
x: number;
|
|
68
90
|
y: number;
|
|
69
91
|
width: number;
|
|
@@ -75,6 +97,12 @@ export interface WireframeSnapshot {
|
|
|
75
97
|
deviceWidth: number;
|
|
76
98
|
deviceHeight: number;
|
|
77
99
|
capturedAt: string;
|
|
100
|
+
/**
|
|
101
|
+
* Optional JPEG screenshot captured at the same moment as this wireframe.
|
|
102
|
+
* Base64 payload (without data URI prefix) to avoid embedding UI-dependent
|
|
103
|
+
* image schemes in analytics payloads.
|
|
104
|
+
*/
|
|
105
|
+
screenshot?: string | null;
|
|
78
106
|
}
|
|
79
107
|
export interface DehydratedScreen {
|
|
80
108
|
/** Current screen name (from navigation state) */
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 4. Last resort: "Unknown Element".
|
|
13
13
|
*/
|
|
14
14
|
import type { TelemetryService } from './TelemetryService';
|
|
15
|
+
import type { AnalyticsTargetMetadata } from './analyticsLabeling';
|
|
15
16
|
/**
|
|
16
17
|
* Checks if the user is rage-tapping an element.
|
|
17
18
|
*
|
|
@@ -20,12 +21,16 @@ import type { TelemetryService } from './TelemetryService';
|
|
|
20
21
|
* 2. Taps must be on the SAME screen (screen change = not rage, it's navigation)
|
|
21
22
|
* 3. Navigation labels ("Next", "Skip", etc.) are excluded
|
|
22
23
|
*/
|
|
23
|
-
export declare function checkRageClick(
|
|
24
|
+
export declare function checkRageClick(target: AnalyticsTargetMetadata & {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
}, telemetry: TelemetryService): void;
|
|
24
28
|
/**
|
|
25
29
|
* Extract a label from a GestureResponderEvent.
|
|
26
30
|
*
|
|
27
31
|
* @param event - The GestureResponderEvent from onStartShouldSetResponderCapture
|
|
28
32
|
* @returns A descriptive label string for the tapped element
|
|
29
33
|
*/
|
|
34
|
+
export declare function extractTouchTargetMetadata(event: any): AnalyticsTargetMetadata;
|
|
30
35
|
export declare function extractTouchLabel(event: any): string;
|
|
31
36
|
//# sourceMappingURL=TouchAutoCapture.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnalyticsElementKind, AnalyticsLabelConfidence, ElementType } from '../../core/types';
|
|
2
|
+
export type AnalyticsLabelSource = 'accessibility' | 'deep-text' | 'sibling-text' | 'placeholder' | 'title' | 'test-id' | 'icon' | 'context';
|
|
3
|
+
export interface AnalyticsLabelCandidate {
|
|
4
|
+
text?: string | null;
|
|
5
|
+
source: AnalyticsLabelSource;
|
|
6
|
+
isInteractiveContext?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface AnalyticsTargetMetadata {
|
|
9
|
+
label: string | null;
|
|
10
|
+
elementKind: AnalyticsElementKind;
|
|
11
|
+
labelConfidence: AnalyticsLabelConfidence;
|
|
12
|
+
zoneId?: string | null;
|
|
13
|
+
ancestorPath?: string[];
|
|
14
|
+
siblingLabels?: string[];
|
|
15
|
+
componentName?: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function getFallbackAnalyticsLabel(elementKind: AnalyticsElementKind): string | null;
|
|
18
|
+
export declare function getAnalyticsElementKind(elementType?: ElementType | string | null): AnalyticsElementKind;
|
|
19
|
+
export declare function chooseBestAnalyticsTarget(candidates: AnalyticsLabelCandidate[], elementKind: AnalyticsElementKind): AnalyticsTargetMetadata;
|
|
20
|
+
//# sourceMappingURL=analyticsLabeling.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobileai/react-native",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.29",
|
|
4
4
|
"description": "Build autonomous AI agents for React Native and Expo apps. Provides AI-native UI traversal, tool calling, and structured reasoning.",
|
|
5
5
|
"main": "./lib/module/index.js",
|
|
6
6
|
"types": "./lib/typescript/src/index.d.ts",
|
|
@@ -82,14 +82,14 @@
|
|
|
82
82
|
],
|
|
83
83
|
"repository": {
|
|
84
84
|
"type": "git",
|
|
85
|
-
"url": "git+https://github.com/
|
|
85
|
+
"url": "git+https://github.com/mohamed2m2018/react-native-agentic-ai.git"
|
|
86
86
|
},
|
|
87
87
|
"author": "Mohamed Salah <mohamed2m2018@gmail.com> (https://example.com)",
|
|
88
88
|
"license": "SEE LICENSE IN LICENSE",
|
|
89
89
|
"bugs": {
|
|
90
|
-
"url": "https://github.com/
|
|
90
|
+
"url": "https://github.com/mohamed2m2018/react-native-agentic-ai/issues"
|
|
91
91
|
},
|
|
92
|
-
"homepage": "https://github.com/
|
|
92
|
+
"homepage": "https://github.com/mohamed2m2018/react-native-agentic-ai#readme",
|
|
93
93
|
"publishConfig": {
|
|
94
94
|
"registry": "https://registry.npmjs.org/"
|
|
95
95
|
},
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"@babel/traverse": "^7.29.0",
|
|
99
99
|
"@babel/types": "^7.29.0",
|
|
100
100
|
"expo": "~55.0.8",
|
|
101
|
+
"react-native-view-shot": "4.0.3",
|
|
101
102
|
"react": "19.2.0",
|
|
102
103
|
"react-native": "0.83.2"
|
|
103
104
|
},
|
|
@@ -134,8 +135,7 @@
|
|
|
134
135
|
"react": "*",
|
|
135
136
|
"react-native": "*",
|
|
136
137
|
"react-native-audio-api": "*",
|
|
137
|
-
"react-native-screens": "*"
|
|
138
|
-
"react-native-view-shot": "*"
|
|
138
|
+
"react-native-screens": "*"
|
|
139
139
|
},
|
|
140
140
|
"peerDependenciesMeta": {
|
|
141
141
|
"@react-native-async-storage/async-storage": {
|
|
@@ -147,9 +147,6 @@
|
|
|
147
147
|
"react-native-screens": {
|
|
148
148
|
"optional": true
|
|
149
149
|
},
|
|
150
|
-
"react-native-view-shot": {
|
|
151
|
-
"optional": true
|
|
152
|
-
},
|
|
153
150
|
"expo-speech-recognition": {
|
|
154
151
|
"optional": true
|
|
155
152
|
}
|