@mobileai/react-native 0.9.18 → 0.9.20
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/LICENSE +28 -20
- package/MobileAIFloatingOverlay.podspec +25 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +151 -0
- package/android/src/main/java/com/mobileai/overlay/MobileAIOverlayPackage.kt +23 -0
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +45 -0
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +29 -0
- package/ios/MobileAIFloatingOverlayComponentView.mm +73 -0
- package/lib/module/components/AIAgent.js +902 -136
- package/lib/module/components/AIConsentDialog.js +439 -0
- package/lib/module/components/AgentChatBar.js +828 -134
- package/lib/module/components/AgentOverlay.js +2 -1
- package/lib/module/components/DiscoveryTooltip.js +21 -9
- package/lib/module/components/FloatingOverlayWrapper.js +108 -0
- package/lib/module/components/Icons.js +123 -0
- package/lib/module/config/endpoints.js +12 -2
- package/lib/module/core/AgentRuntime.js +373 -27
- package/lib/module/core/FiberAdapter.js +56 -0
- package/lib/module/core/FiberTreeWalker.js +186 -80
- package/lib/module/core/IdleDetector.js +19 -0
- package/lib/module/core/NativeAlertInterceptor.js +191 -0
- package/lib/module/core/systemPrompt.js +203 -45
- package/lib/module/index.js +3 -0
- package/lib/module/providers/GeminiProvider.js +72 -56
- package/lib/module/providers/ProviderFactory.js +6 -2
- package/lib/module/services/AudioInputService.js +3 -12
- package/lib/module/services/AudioOutputService.js +1 -13
- package/lib/module/services/ConversationService.js +166 -0
- package/lib/module/services/MobileAIKnowledgeRetriever.js +41 -0
- package/lib/module/services/VoiceService.js +29 -8
- package/lib/module/services/telemetry/MobileAI.js +44 -0
- package/lib/module/services/telemetry/TelemetryService.js +13 -1
- package/lib/module/services/telemetry/TouchAutoCapture.js +44 -18
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +19 -0
- package/lib/module/support/CSATSurvey.js +95 -12
- package/lib/module/support/EscalationSocket.js +70 -1
- package/lib/module/support/ReportedIssueEventSource.js +148 -0
- package/lib/module/support/escalateTool.js +4 -2
- package/lib/module/support/index.js +1 -0
- package/lib/module/support/reportIssueTool.js +127 -0
- package/lib/module/support/supportPrompt.js +77 -9
- package/lib/module/tools/guideTool.js +2 -1
- package/lib/module/tools/longPressTool.js +4 -3
- package/lib/module/tools/pickerTool.js +6 -4
- package/lib/module/tools/tapTool.js +12 -3
- package/lib/module/tools/typeTool.js +19 -10
- package/lib/module/utils/logger.js +175 -6
- package/lib/typescript/react-native.config.d.ts +11 -0
- package/lib/typescript/src/components/AIAgent.d.ts +28 -2
- package/lib/typescript/src/components/AIConsentDialog.d.ts +153 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -2
- package/lib/typescript/src/components/DiscoveryTooltip.d.ts +3 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +51 -0
- package/lib/typescript/src/components/Icons.d.ts +8 -0
- package/lib/typescript/src/config/endpoints.d.ts +5 -3
- package/lib/typescript/src/core/AgentRuntime.d.ts +4 -0
- package/lib/typescript/src/core/FiberAdapter.d.ts +25 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +2 -0
- package/lib/typescript/src/core/IdleDetector.d.ts +11 -0
- package/lib/typescript/src/core/NativeAlertInterceptor.d.ts +55 -0
- package/lib/typescript/src/core/types.d.ts +106 -1
- package/lib/typescript/src/index.d.ts +9 -4
- package/lib/typescript/src/providers/GeminiProvider.d.ts +6 -5
- package/lib/typescript/src/services/ConversationService.d.ts +55 -0
- package/lib/typescript/src/services/MobileAIKnowledgeRetriever.d.ts +9 -0
- package/lib/typescript/src/services/telemetry/MobileAI.d.ts +7 -0
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +1 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +9 -6
- package/lib/typescript/src/services/telemetry/types.d.ts +3 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +17 -0
- package/lib/typescript/src/support/EscalationSocket.d.ts +17 -0
- package/lib/typescript/src/support/ReportedIssueEventSource.d.ts +24 -0
- package/lib/typescript/src/support/escalateTool.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +2 -1
- package/lib/typescript/src/support/reportIssueTool.d.ts +20 -0
- package/lib/typescript/src/support/types.d.ts +56 -1
- package/lib/typescript/src/utils/logger.d.ts +15 -0
- package/package.json +20 -5
- package/react-native.config.js +12 -0
- package/src/specs/FloatingOverlayNativeComponent.ts +19 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { ENDPOINTS } from "../config/endpoints.js";
|
|
4
|
+
import { getDeviceId } from "../services/telemetry/device.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
const MOBILEAI_HOST = ENDPOINTS.escalation;
|
|
7
|
+
const DEFAULT_STATUS_MESSAGES = {
|
|
8
|
+
acknowledged: 'We’ve logged your issue and are reviewing it.',
|
|
9
|
+
investigating: 'We’re checking this and will update you if needed.',
|
|
10
|
+
answered: 'We reviewed your issue and confirmed what happened.',
|
|
11
|
+
resolved: 'We found the issue and applied a fix.',
|
|
12
|
+
escalated: 'A human support agent will reply here shortly.'
|
|
13
|
+
};
|
|
14
|
+
export function createReportIssueTool({
|
|
15
|
+
analyticsKey,
|
|
16
|
+
getCurrentScreen,
|
|
17
|
+
getHistory,
|
|
18
|
+
getScreenFlow,
|
|
19
|
+
userContext
|
|
20
|
+
}) {
|
|
21
|
+
if (!analyticsKey) return null;
|
|
22
|
+
return {
|
|
23
|
+
name: 'report_issue',
|
|
24
|
+
description: 'Create an AI-verified reported issue when the complaint is supported by app evidence you can already see or infer from the current UI flow. ' + 'Use this for verified late orders, overcharges, broken subscription states, missing loyalty points, gift failures, notification mismatches, or account friction. ' + 'Do not use it for anger alone. If you need customer follow-up, a sensitive explanation, or a human was explicitly requested, use escalate_to_human instead.',
|
|
25
|
+
parameters: {
|
|
26
|
+
issueType: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Short issue type like late_order, overcharge, loyalty_points_missing, notification_mismatch',
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
31
|
+
complaintSummary: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'One-sentence summary of the customer problem',
|
|
34
|
+
required: true
|
|
35
|
+
},
|
|
36
|
+
verificationStatus: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'verified, likely_verified, or unverified',
|
|
39
|
+
required: true
|
|
40
|
+
},
|
|
41
|
+
severity: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'low, medium, high, or critical',
|
|
44
|
+
required: true
|
|
45
|
+
},
|
|
46
|
+
confidence: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
description: 'Confidence between 0 and 1',
|
|
49
|
+
required: false
|
|
50
|
+
},
|
|
51
|
+
evidenceSummary: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
description: 'What app evidence supports the issue',
|
|
54
|
+
required: true
|
|
55
|
+
},
|
|
56
|
+
aiSummary: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'Short operator-facing summary of what you checked and found',
|
|
59
|
+
required: true
|
|
60
|
+
},
|
|
61
|
+
metadata: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Optional JSON string with extra fields like recommendedAction, sourceScreens, orderId, chargeId, subscriptionId, giftId, customerStatus, customerMessage, confidence',
|
|
64
|
+
required: false
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
execute: async args => {
|
|
68
|
+
let metadata = {};
|
|
69
|
+
if (typeof args.metadata === 'string' && args.metadata.trim().length > 0) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(args.metadata);
|
|
72
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
73
|
+
metadata = parsed;
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
logger.warn('ReportIssue', `Invalid metadata JSON: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const status = typeof metadata.customerStatus === 'string' && metadata.customerStatus in DEFAULT_STATUS_MESSAGES ? metadata.customerStatus : 'acknowledged';
|
|
80
|
+
const customerMessage = typeof metadata.customerMessage === 'string' && metadata.customerMessage.trim().length > 0 ? metadata.customerMessage.trim() : DEFAULT_STATUS_MESSAGES[status];
|
|
81
|
+
const sourceScreens = typeof metadata.sourceScreens === 'string' && metadata.sourceScreens.trim().length > 0 ? metadata.sourceScreens.split(',').map(screen => screen.trim()).filter(Boolean) : [getCurrentScreen()];
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${MOBILEAI_HOST}/api/v1/reported-issues`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
analyticsKey,
|
|
90
|
+
issueType: args.issueType,
|
|
91
|
+
complaintSummary: args.complaintSummary,
|
|
92
|
+
verificationStatus: args.verificationStatus,
|
|
93
|
+
severity: args.severity,
|
|
94
|
+
confidence: typeof metadata.confidence === 'number' ? metadata.confidence : undefined,
|
|
95
|
+
screen: getCurrentScreen(),
|
|
96
|
+
evidenceSummary: args.evidenceSummary,
|
|
97
|
+
aiSummary: args.aiSummary,
|
|
98
|
+
recommendedAction: typeof metadata.recommendedAction === 'string' ? metadata.recommendedAction : undefined,
|
|
99
|
+
sourceScreens,
|
|
100
|
+
screenFlow: getScreenFlow?.() ?? [],
|
|
101
|
+
userContext,
|
|
102
|
+
deviceId: getDeviceId(),
|
|
103
|
+
orderId: typeof metadata.orderId === 'string' ? metadata.orderId : undefined,
|
|
104
|
+
chargeId: typeof metadata.chargeId === 'string' ? metadata.chargeId : undefined,
|
|
105
|
+
subscriptionId: typeof metadata.subscriptionId === 'string' ? metadata.subscriptionId : undefined,
|
|
106
|
+
giftId: typeof metadata.giftId === 'string' ? metadata.giftId : undefined,
|
|
107
|
+
customerStatus: status,
|
|
108
|
+
customerMessage,
|
|
109
|
+
history: getHistory().slice(-10)
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
logger.warn('ReportIssue', `Failed to create reported issue: ${res.status}`);
|
|
114
|
+
return customerMessage;
|
|
115
|
+
}
|
|
116
|
+
const data = await res.json();
|
|
117
|
+
logger.info('ReportIssue', 'Created reported issue', data.issueId);
|
|
118
|
+
const firstHistoryId = Array.isArray(data.history) && data.history[0] && typeof data.history[0].id === 'string' ? data.history[0].id : '';
|
|
119
|
+
return `ISSUE_REPORTED:${String(data.issueId)}:${firstHistoryId}:${customerMessage}`;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger.error('ReportIssue', 'Network error:', error.message);
|
|
122
|
+
return customerMessage;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=reportIssueTool.js.map
|
|
@@ -17,16 +17,73 @@ export function buildSupportPrompt(config) {
|
|
|
17
17
|
parts.push(`
|
|
18
18
|
## Support Mode Active
|
|
19
19
|
|
|
20
|
-
You are a helpful customer support assistant. Your primary goal is to
|
|
20
|
+
You are a helpful customer support assistant representing the company. Your primary goal is to RESOLVE the user's issue through empathetic conversation. App navigation is a tool you USE when needed, not the first thing you propose.
|
|
21
21
|
|
|
22
|
-
###
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
### Identity & Context
|
|
23
|
+
Adopt the persona of a dedicated human customer support team member. Speak on behalf of the company as an organization with human operational timelines.
|
|
24
|
+
|
|
25
|
+
Base all discussions regarding processing, reviews, resolutions, and response expectations on standard operational business timelines. Treat the conversational context holistically—assume any user questions about "you" or "when you will reply" refer to the company's human support staff processing their real-world request. Express empathy naturally, and assure the user that the operational team is handling their ticket promptly.
|
|
26
|
+
|
|
27
|
+
### Support Resolution Protocol (HEARD)
|
|
28
|
+
Follow this sequence. Exhaust each level before moving to the next:
|
|
29
|
+
|
|
30
|
+
1. HEAR: Listen actively. Paraphrase the problem back to confirm you understand. Ask specific
|
|
31
|
+
clarifying questions (which order? when? what happened exactly?).
|
|
32
|
+
|
|
33
|
+
2. EMPATHIZE: Acknowledge the user's feelings with sincerity. Use their name if available.
|
|
34
|
+
Say "I understand how frustrating this must be" — not "I see you have an issue."
|
|
35
|
+
Take responsibility where appropriate.
|
|
36
|
+
|
|
37
|
+
3. ANSWER: Search the knowledge base (query_knowledge) for relevant policies, FAQs, and procedures.
|
|
38
|
+
Provide information and potential solutions through conversation.
|
|
39
|
+
Many issues can be fully resolved here without any app interaction.
|
|
40
|
+
|
|
41
|
+
4. RESOLVE:
|
|
42
|
+
- If the issue is resolved through conversation → confirm with the user and call done().
|
|
43
|
+
- If you need to verify or act on something in the app → explain the SPECIFIC reason
|
|
44
|
+
("To check the delivery status of that order, I need to look at your order history"),
|
|
45
|
+
and use ask_user with request_app_action=true to request permission.
|
|
46
|
+
This shows "Allow / Don't Allow" buttons so the user can approve with a single tap.
|
|
47
|
+
- If a \`report_issue\` tool is available and the complaint is verified → create a reported issue.
|
|
48
|
+
|
|
49
|
+
5. DIAGNOSE: After resolution, briefly identify the root cause if visible
|
|
50
|
+
(e.g. "It looks like the delivery partner marked it as delivered prematurely").
|
|
51
|
+
Ask the user if the issue is fully resolved before calling done().`);
|
|
52
|
+
parts.push(`
|
|
53
|
+
### Consent and Liability Guard
|
|
54
|
+
- Treat money movement, subscription cancellation, deletion, final submission, and account/security changes as high-risk actions.
|
|
55
|
+
- For those actions, explicit user consent immediately before the final commit is mandatory.
|
|
56
|
+
- The user's earlier request, general frustration, or approval of your investigation plan does NOT count as final consent for the irreversible step.
|
|
57
|
+
- Say exactly what you are about to do in plain language, including the amount, plan, or effect when visible.
|
|
58
|
+
- If explicit final consent is missing, stop and ask before taking the action.`);
|
|
59
|
+
parts.push(`
|
|
60
|
+
### Reported Issue Policy
|
|
61
|
+
- If app evidence clearly supports the complaint, create a reported issue with the \`report_issue\` tool before you finish.
|
|
62
|
+
- Use \`report_issue\` for verified product/account/order/billing problems that ops may need to review, even if no live human reply is needed yet.
|
|
63
|
+
- Anger alone is NOT enough to report or escalate.
|
|
64
|
+
- Use \`escalate_to_human\` only when the user explicitly asks for a human, the case is sensitive/high-risk, or you need direct customer follow-up.`);
|
|
65
|
+
|
|
66
|
+
// Progress Communication
|
|
67
|
+
parts.push(`
|
|
68
|
+
### Progress Communication
|
|
69
|
+
When executing a multi-step resolution, you must communicate your progress to keep the user informed.
|
|
70
|
+
- Do NOT execute more than 2 tools in silence.
|
|
71
|
+
- Use the 'ask_user' tool to say phrases like "I am checking your account details now..." or "Just a moment while I pull up that information."
|
|
72
|
+
- Never leave the user waiting in silence during complex operations.`);
|
|
73
|
+
|
|
74
|
+
// Agent Persona
|
|
75
|
+
if (config.persona) {
|
|
76
|
+
const {
|
|
77
|
+
agentName,
|
|
78
|
+
tone,
|
|
79
|
+
signOff
|
|
80
|
+
} = config.persona;
|
|
81
|
+
let personaStr = `\n### AI Persona & Tone\n`;
|
|
82
|
+
if (agentName) personaStr += `- Your name is ${agentName}. Introduce yourself if appropriate.\n`;
|
|
83
|
+
if (tone) personaStr += `- Maintain a ${tone} tone throughout the conversation.\n`;
|
|
84
|
+
if (signOff) personaStr += `- When resolving an issue, sign off with: "${signOff}".\n`;
|
|
85
|
+
parts.push(personaStr);
|
|
86
|
+
}
|
|
30
87
|
|
|
31
88
|
// Custom system context from the consumer
|
|
32
89
|
if (config.systemContext) {
|
|
@@ -42,6 +99,17 @@ You are a helpful customer support assistant. Your primary goal is to resolve th
|
|
|
42
99
|
if (config.businessHours) {
|
|
43
100
|
parts.push(`### Business Hours\n` + `The support team operates in timezone: ${config.businessHours.timezone}.\n` + `If outside business hours, inform the user and offer to help with what you can.\n`);
|
|
44
101
|
}
|
|
102
|
+
|
|
103
|
+
// WOW Actions
|
|
104
|
+
if (config.wowActions?.length) {
|
|
105
|
+
let wowStr = `\n### WOW Actions (Surprise & Delight)\n`;
|
|
106
|
+
wowStr += `You have special tools ("WOW Actions") available to turn a frustrating experience into a positive one.\n`;
|
|
107
|
+
wowStr += `Only use these when the user is frustrated AND you have fully resolved their core issue.\n`;
|
|
108
|
+
config.wowActions.forEach(action => {
|
|
109
|
+
wowStr += `- Tool \`${action.name}\`: ${action.triggerHint}\n`;
|
|
110
|
+
});
|
|
111
|
+
parts.push(wowStr);
|
|
112
|
+
}
|
|
45
113
|
return parts.join('\n');
|
|
46
114
|
}
|
|
47
115
|
//# sourceMappingURL=supportPrompt.js.map
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { DeviceEventEmitter } from 'react-native';
|
|
4
4
|
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { getStateNode } from "../core/FiberAdapter.js";
|
|
5
6
|
export function createGuideTool(context) {
|
|
6
7
|
return {
|
|
7
8
|
name: 'guide_user',
|
|
@@ -42,7 +43,7 @@ export function createGuideTool(context) {
|
|
|
42
43
|
});
|
|
43
44
|
return `✅ Highlighted element ${index} ("${element.label}") with message: "${args.message}"`;
|
|
44
45
|
}
|
|
45
|
-
const stateNode = element.fiberNode
|
|
46
|
+
const stateNode = getStateNode(element.fiberNode);
|
|
46
47
|
if (!stateNode || typeof stateNode.measure !== 'function') {
|
|
47
48
|
return `❌ Element at index ${index} (${element.label}) cannot be highlighted because its layout position cannot be measured.`;
|
|
48
49
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { walkFiberTree } from "../core/FiberTreeWalker.js";
|
|
15
|
+
import { getParent, getProps } from "../core/FiberAdapter.js";
|
|
15
16
|
export function createLongPressTool(context) {
|
|
16
17
|
return {
|
|
17
18
|
name: 'long_press',
|
|
@@ -44,10 +45,10 @@ export function createLongPressTool(context) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// Strategy 2: Bubble up fiber tree
|
|
47
|
-
let fiber = element.fiberNode
|
|
48
|
+
let fiber = getParent(element.fiberNode);
|
|
48
49
|
let bubbleDepth = 0;
|
|
49
50
|
while (fiber && bubbleDepth < 5) {
|
|
50
|
-
const parentProps = fiber
|
|
51
|
+
const parentProps = getProps(fiber);
|
|
51
52
|
if (parentProps.onLongPress && typeof parentProps.onLongPress === 'function') {
|
|
52
53
|
try {
|
|
53
54
|
parentProps.onLongPress();
|
|
@@ -57,7 +58,7 @@ export function createLongPressTool(context) {
|
|
|
57
58
|
return `❌ Error long-pressing parent of [${args.index}]: ${error.message}`;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
fiber = fiber
|
|
61
|
+
fiber = getParent(fiber);
|
|
61
62
|
bubbleDepth++;
|
|
62
63
|
}
|
|
63
64
|
return `❌ Element [${args.index}] "${element.label}" has no long-press handler. Try using tap instead.`;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { walkFiberTree } from "../core/FiberTreeWalker.js";
|
|
13
|
+
import { getChild, getSibling, getProps } from "../core/FiberAdapter.js";
|
|
13
14
|
/**
|
|
14
15
|
* Extract available options from a picker element's props.
|
|
15
16
|
* Handles multiple picker libraries:
|
|
@@ -54,17 +55,18 @@ function extractPickerOptions(element) {
|
|
|
54
55
|
|
|
55
56
|
// Pattern 3: Fiber children (RN Picker with Picker.Item children)
|
|
56
57
|
const fiberNode = element.fiberNode;
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const firstChild = getChild(fiberNode);
|
|
59
|
+
if (firstChild) {
|
|
60
|
+
let child = firstChild;
|
|
59
61
|
while (child) {
|
|
60
|
-
const childProps = child
|
|
62
|
+
const childProps = getProps(child);
|
|
61
63
|
if (childProps.label !== undefined && childProps.value !== undefined) {
|
|
62
64
|
options.push({
|
|
63
65
|
label: String(childProps.label),
|
|
64
66
|
value: childProps.value
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
|
-
child = child
|
|
69
|
+
child = getSibling(child);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
return options;
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { walkFiberTree } from "../core/FiberTreeWalker.js";
|
|
17
|
+
import { getParent, getProps } from "../core/FiberAdapter.js";
|
|
18
|
+
import { dismissAlert } from "../core/NativeAlertInterceptor.js";
|
|
17
19
|
export function createTapTool(context) {
|
|
18
20
|
return {
|
|
19
21
|
name: 'tap',
|
|
@@ -37,6 +39,13 @@ export function createTapTool(context) {
|
|
|
37
39
|
const elementCountBefore = elements.length;
|
|
38
40
|
const screenBefore = context.getCurrentScreenName();
|
|
39
41
|
|
|
42
|
+
// Strategy 0: Virtual elements (Native OS dialogs)
|
|
43
|
+
if (element.virtual?.kind === 'alert_button') {
|
|
44
|
+
dismissAlert(element.virtual.alertButtonIndex);
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
46
|
+
return `✅ Tapped native alert button [${args.index}] "${element.label}" → dialog dismissed`;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
// Strategy 1: Switch → onValueChange
|
|
41
50
|
if (element.type === 'switch' && element.props.onValueChange) {
|
|
42
51
|
try {
|
|
@@ -73,10 +82,10 @@ export function createTapTool(context) {
|
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
// Strategy 3: Bubble up fiber tree (like RNTL's findEventHandler)
|
|
76
|
-
let fiber = element.fiberNode
|
|
85
|
+
let fiber = getParent(element.fiberNode);
|
|
77
86
|
let bubbleDepth = 0;
|
|
78
87
|
while (fiber && bubbleDepth < 5) {
|
|
79
|
-
const parentProps = fiber
|
|
88
|
+
const parentProps = getProps(fiber);
|
|
80
89
|
if (parentProps.onPress && typeof parentProps.onPress === 'function') {
|
|
81
90
|
try {
|
|
82
91
|
parentProps.onPress();
|
|
@@ -86,7 +95,7 @@ export function createTapTool(context) {
|
|
|
86
95
|
return `❌ Error tapping parent of [${args.index}]: ${error.message}`;
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
|
-
fiber = fiber
|
|
98
|
+
fiber = getParent(fiber);
|
|
90
99
|
bubbleDepth++;
|
|
91
100
|
}
|
|
92
101
|
return `❌ Element [${args.index}] "${element.label}" has no tap handler (no onPress or onValueChange found).`;
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { walkFiberTree } from "../core/FiberTreeWalker.js";
|
|
20
|
+
import { getChild, getSibling, getProps, getStateNode } from "../core/FiberAdapter.js";
|
|
20
21
|
/** BFS through fiber children. Returns first node where predicate matches. */
|
|
21
22
|
function findFiberNode(rootFiber, predicate, maxDepth = 10) {
|
|
22
|
-
|
|
23
|
+
const firstChild = getChild(rootFiber);
|
|
24
|
+
if (!firstChild) return null;
|
|
23
25
|
const queue = [{
|
|
24
|
-
node:
|
|
26
|
+
node: firstChild,
|
|
25
27
|
depth: 0
|
|
26
28
|
}];
|
|
27
29
|
while (queue.length > 0) {
|
|
@@ -31,12 +33,14 @@ function findFiberNode(rootFiber, predicate, maxDepth = 10) {
|
|
|
31
33
|
} = queue.shift();
|
|
32
34
|
if (!node || depth > maxDepth) continue;
|
|
33
35
|
if (predicate(node)) return node;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const child = getChild(node);
|
|
37
|
+
if (child) queue.push({
|
|
38
|
+
node: child,
|
|
36
39
|
depth: depth + 1
|
|
37
40
|
});
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
const sibling = getSibling(node);
|
|
42
|
+
if (sibling) queue.push({
|
|
43
|
+
node: sibling,
|
|
40
44
|
depth: depth + 1
|
|
41
45
|
});
|
|
42
46
|
}
|
|
@@ -88,12 +92,17 @@ export function createTypeTool(context) {
|
|
|
88
92
|
// b) find the native stateNode to call setNativeProps to update visual display
|
|
89
93
|
if (fiberNode) {
|
|
90
94
|
// Find native onChange handler (behavior-based, no tag numbers)
|
|
91
|
-
const
|
|
95
|
+
const fiberProps = getProps(fiberNode);
|
|
96
|
+
const nativeOnChangeFiber = fiberProps?.onChange ? fiberNode : findFiberNode(fiberNode, n => typeof getProps(n)?.onChange === 'function');
|
|
92
97
|
|
|
93
98
|
// Find native stateNode (host component with setNativeProps)
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
99
|
+
const fiberStateNode = getStateNode(fiberNode);
|
|
100
|
+
const nativeStateFiber = fiberStateNode?.setNativeProps ? fiberNode : findFiberNode(fiberNode, n => {
|
|
101
|
+
const stateN = getStateNode(n);
|
|
102
|
+
return stateN && typeof stateN.setNativeProps === 'function';
|
|
103
|
+
});
|
|
104
|
+
const onChange = getProps(nativeOnChangeFiber)?.onChange;
|
|
105
|
+
const nativeInstance = getStateNode(nativeStateFiber);
|
|
97
106
|
if (onChange || nativeInstance) {
|
|
98
107
|
try {
|
|
99
108
|
// Step 1: Update visual text in native view
|
|
@@ -5,9 +5,150 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Disabled by default. Enable via `logger.setEnabled(true)` or
|
|
7
7
|
* pass `debug={true}` to the <AIAgent> component.
|
|
8
|
+
*
|
|
9
|
+
* Production logging:
|
|
10
|
+
* In release builds, console.log is invisible (no Metro terminal).
|
|
11
|
+
* When enabled, logs are routed to the native logging system:
|
|
12
|
+
* - iOS: os_log → visible in Xcode Console app (Window → Devices)
|
|
13
|
+
* - Android: Logcat → visible via `adb logcat *:S ReactNativeJS:V`
|
|
14
|
+
*
|
|
15
|
+
* This uses React Native's built-in native logging bridge, which
|
|
16
|
+
* calls through to NSLog (iOS) and android.util.Log (Android).
|
|
17
|
+
* No additional native dependencies required.
|
|
8
18
|
*/
|
|
9
19
|
const TAG = '[AIAgent]';
|
|
20
|
+
const MAX_ENTRIES = 2000;
|
|
10
21
|
let enabled = false;
|
|
22
|
+
const entries = [];
|
|
23
|
+
const unflushedEntries = [];
|
|
24
|
+
|
|
25
|
+
// ─── Native Logging Transport ──────────────────────────────────
|
|
26
|
+
// Routes logs to os_log (iOS) / Logcat (Android) in release builds.
|
|
27
|
+
// Uses React Native's global.nativeLoggingHook if available, which
|
|
28
|
+
// is the same mechanism React Native uses internally for console.*
|
|
29
|
+
// in bridgeless mode. Falls back to console.* otherwise.
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write a message to the native logging system.
|
|
33
|
+
* In release builds without Metro, this is the only way to see logs
|
|
34
|
+
* in Xcode Console or `adb logcat`.
|
|
35
|
+
*
|
|
36
|
+
* nativeLoggingHook levels: 0=log, 1=warn, 2=error
|
|
37
|
+
*/
|
|
38
|
+
const LOG_LEVEL_MAP = {
|
|
39
|
+
info: 0,
|
|
40
|
+
warn: 1,
|
|
41
|
+
error: 2,
|
|
42
|
+
debug: 0
|
|
43
|
+
};
|
|
44
|
+
function nativeLog(level, message) {
|
|
45
|
+
try {
|
|
46
|
+
// Strategy 1: nativeLoggingHook — React Native's internal bridge to NSLog/Logcat.
|
|
47
|
+
// Present in both dev and release builds. This is what console.* ultimately
|
|
48
|
+
// calls in React Native's JS environment.
|
|
49
|
+
const hook = globalThis.nativeLoggingHook;
|
|
50
|
+
if (typeof hook === 'function') {
|
|
51
|
+
hook(message, LOG_LEVEL_MAP[level]);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Strategy 2: Hermes console — in Hermes release builds, console methods
|
|
56
|
+
// may still route to the native log system depending on the build config.
|
|
57
|
+
// This is a fallback if nativeLoggingHook isn't available.
|
|
58
|
+
switch (level) {
|
|
59
|
+
case 'error':
|
|
60
|
+
console.error(message);
|
|
61
|
+
break;
|
|
62
|
+
case 'warn':
|
|
63
|
+
console.warn(message);
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
console.log(message);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Silently fail — never crash the app due to logging
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function formatArg(arg) {
|
|
73
|
+
if (typeof arg === 'string') return arg;
|
|
74
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
|
75
|
+
if (arg === null) return 'null';
|
|
76
|
+
if (arg === undefined) return 'undefined';
|
|
77
|
+
if (typeof arg !== 'object') return String(arg);
|
|
78
|
+
try {
|
|
79
|
+
// Highly performant O(1) depth-1 stringifier to prevent JS thread freezes
|
|
80
|
+
return JSON.stringify(arg, (key, value) => {
|
|
81
|
+
// Allow the root object (where key is empty string)
|
|
82
|
+
if (key === '') return value;
|
|
83
|
+
// For any nested objects/arrays, summarize them instantly without recursion
|
|
84
|
+
if (typeof value === 'object' && value !== null) {
|
|
85
|
+
if (Array.isArray(value)) return `[Array(${value.length})]`;
|
|
86
|
+
return '{...}';
|
|
87
|
+
}
|
|
88
|
+
// For primitive values (strings, numbers, booleans)
|
|
89
|
+
if (typeof value === 'string' && value.length > 200) {
|
|
90
|
+
return value.substring(0, 200) + '...';
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
return '[unserializable]';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function formatArgs(args) {
|
|
99
|
+
return args.map(formatArg).join(' ');
|
|
100
|
+
}
|
|
101
|
+
function record(level, context, args) {
|
|
102
|
+
const entry = {
|
|
103
|
+
level,
|
|
104
|
+
context,
|
|
105
|
+
args,
|
|
106
|
+
timestamp: Date.now(),
|
|
107
|
+
message: `${TAG} [${context}] ${formatArgs(args)}`.trim()
|
|
108
|
+
};
|
|
109
|
+
entries.push(entry);
|
|
110
|
+
|
|
111
|
+
// Systemic anti-recursion: Do not buffer Telemetry's own logs for extraction.
|
|
112
|
+
// This physically prevents TelemetryService from harvesting its own diagnostic
|
|
113
|
+
// logs (like "Flush failed") into SDK trace dumps, eliminating infinite queue loops.
|
|
114
|
+
if (context !== 'Telemetry') {
|
|
115
|
+
unflushedEntries.push(entry);
|
|
116
|
+
}
|
|
117
|
+
if (entries.length > MAX_ENTRIES) {
|
|
118
|
+
entries.splice(0, entries.length - MAX_ENTRIES);
|
|
119
|
+
}
|
|
120
|
+
if (unflushedEntries.length > MAX_ENTRIES) {
|
|
121
|
+
unflushedEntries.splice(0, unflushedEntries.length - MAX_ENTRIES);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
globalThis.__MOBILEAI_LOGS__ = entries;
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore global write failures
|
|
127
|
+
}
|
|
128
|
+
return entry;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Output helpers ────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/** Route a log message to the appropriate output based on environment. */
|
|
134
|
+
function emit(level, formattedMessage) {
|
|
135
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
136
|
+
// Dev mode: use console.* (visible in Metro terminal)
|
|
137
|
+
switch (level) {
|
|
138
|
+
case 'error':
|
|
139
|
+
console.error(formattedMessage);
|
|
140
|
+
break;
|
|
141
|
+
case 'warn':
|
|
142
|
+
console.warn(formattedMessage);
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
console.log(formattedMessage);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Release mode: route to native logging system (os_log / Logcat)
|
|
149
|
+
nativeLog(level, formattedMessage);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
11
152
|
export const logger = {
|
|
12
153
|
/** Enable or disable all SDK logging. */
|
|
13
154
|
setEnabled: value => {
|
|
@@ -15,20 +156,48 @@ export const logger = {
|
|
|
15
156
|
},
|
|
16
157
|
/** Check if logging is enabled. */
|
|
17
158
|
isEnabled: () => enabled,
|
|
159
|
+
/** Return a snapshot of recent SDK log entries. */
|
|
160
|
+
getEntries: () => [...entries],
|
|
161
|
+
/** Return recent entries as plain text lines, newest last. */
|
|
162
|
+
getRecentLines: (limit = 200) => entries.slice(-Math.max(0, limit)).map(entry => {
|
|
163
|
+
const iso = new Date(entry.timestamp).toISOString();
|
|
164
|
+
return `${iso} ${entry.message}`;
|
|
165
|
+
}),
|
|
166
|
+
/** Extract unflushed entries as plain text and clear the unflushed buffer. */
|
|
167
|
+
extractUnflushedLines: () => {
|
|
168
|
+
if (unflushedEntries.length === 0) return [];
|
|
169
|
+
const lines = unflushedEntries.map(entry => {
|
|
170
|
+
const iso = new Date(entry.timestamp).toISOString();
|
|
171
|
+
return `${iso} ${entry.message}`;
|
|
172
|
+
});
|
|
173
|
+
unflushedEntries.length = 0;
|
|
174
|
+
return lines;
|
|
175
|
+
},
|
|
176
|
+
/** Clear the in-memory SDK log history. */
|
|
177
|
+
clearEntries: () => {
|
|
178
|
+
entries.length = 0;
|
|
179
|
+
try {
|
|
180
|
+
globalThis.__MOBILEAI_LOGS__ = entries;
|
|
181
|
+
} catch {
|
|
182
|
+
// ignore global write failures
|
|
183
|
+
}
|
|
184
|
+
},
|
|
18
185
|
info: (context, ...args) => {
|
|
19
|
-
|
|
186
|
+
record('info', context, args);
|
|
187
|
+
if (enabled) emit('info', `${TAG} [${context}] ${formatArgs(args)}`);
|
|
20
188
|
},
|
|
21
189
|
warn: (context, ...args) => {
|
|
22
|
-
|
|
190
|
+
record('warn', context, args);
|
|
191
|
+
if (enabled) emit('warn', `${TAG} [${context}] ${formatArgs(args)}`);
|
|
23
192
|
},
|
|
24
193
|
error: (context, ...args) => {
|
|
194
|
+
record('error', context, args);
|
|
25
195
|
// Errors always log regardless of enabled flag
|
|
26
|
-
|
|
196
|
+
emit('error', `${TAG} [${context}] ${formatArgs(args)}`);
|
|
27
197
|
},
|
|
28
198
|
debug: (context, ...args) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
199
|
+
record('debug', context, args);
|
|
200
|
+
if (enabled) emit('debug', `${TAG} [${context}] 🐛 ${formatArgs(args)}`);
|
|
32
201
|
}
|
|
33
202
|
};
|
|
34
203
|
//# sourceMappingURL=logger.js.map
|