@mobileai/react-native 0.9.27 → 0.9.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -16
- package/android/build.gradle +17 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
- package/bin/generate-map.cjs +45 -6
- package/ios/MobileAIFloatingOverlayComponentView.h +8 -0
- package/ios/MobileAIFloatingOverlayComponentView.mm +12 -41
- package/ios/Podfile +63 -0
- package/ios/Podfile.lock +2290 -0
- package/ios/Podfile.properties.json +4 -0
- package/ios/mobileaireactnative/AppDelegate.swift +69 -0
- package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
- package/ios/mobileaireactnative/Info.plist +55 -0
- package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
- package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
- package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
- package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
- package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
- package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
- package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
- package/lib/module/components/AIAgent.js +501 -191
- package/lib/module/components/AgentChatBar.js +250 -59
- package/lib/module/components/FloatingOverlayWrapper.js +68 -32
- package/lib/module/config/endpoints.js +22 -1
- package/lib/module/core/AgentRuntime.js +110 -8
- package/lib/module/core/FiberTreeWalker.js +211 -10
- package/lib/module/core/OutcomeVerifier.js +149 -0
- package/lib/module/core/systemPrompt.js +96 -25
- package/lib/module/providers/GeminiProvider.js +9 -3
- package/lib/module/services/telemetry/TelemetryService.js +21 -2
- package/lib/module/services/telemetry/TouchAutoCapture.js +235 -38
- package/lib/module/services/telemetry/analyticsLabeling.js +187 -0
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/lib/module/support/supportPrompt.js +22 -7
- package/lib/module/support/supportStyle.js +55 -0
- package/lib/module/support/types.js +2 -0
- package/lib/module/tools/typeTool.js +20 -0
- package/lib/module/utils/humanizeScreenName.js +49 -0
- package/lib/typescript/src/components/AIAgent.d.ts +6 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
- package/lib/typescript/src/config/endpoints.d.ts +4 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +12 -3
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
- package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
- package/lib/typescript/src/core/types.d.ts +63 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +6 -1
- package/lib/typescript/src/services/telemetry/analyticsLabeling.d.ts +20 -0
- package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +1 -0
- package/lib/typescript/src/support/supportStyle.d.ts +9 -0
- package/lib/typescript/src/support/types.d.ts +3 -0
- package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
- package/package.json +10 -10
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIPilotIntents.swift +0 -51
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
-
import { View, StyleSheet } from 'react-native';
|
|
14
|
+
import { View, StyleSheet, InteractionManager, Platform } from 'react-native';
|
|
15
15
|
import { AgentRuntime } from "../core/AgentRuntime.js";
|
|
16
|
+
import { captureWireframe } from "../core/FiberTreeWalker.js";
|
|
17
|
+
import { humanizeScreenName } from "../utils/humanizeScreenName.js";
|
|
16
18
|
import { createProvider } from "../providers/ProviderFactory.js";
|
|
17
19
|
import { AgentContext } from "../hooks/useAction.js";
|
|
18
20
|
import { AgentChatBar } from "./AgentChatBar.js";
|
|
@@ -26,7 +28,7 @@ import { VoiceService } from "../services/VoiceService.js";
|
|
|
26
28
|
import { AudioInputService } from "../services/AudioInputService.js";
|
|
27
29
|
import { AudioOutputService } from "../services/AudioOutputService.js";
|
|
28
30
|
import { TelemetryService, bindTelemetryService } from "../services/telemetry/index.js";
|
|
29
|
-
import {
|
|
31
|
+
import { extractTouchTargetMetadata, checkRageClick } from "../services/telemetry/TouchAutoCapture.js";
|
|
30
32
|
import { initDeviceId, getDeviceId } from "../services/telemetry/device.js";
|
|
31
33
|
import { AgentErrorBoundary } from "./AgentErrorBoundary.js";
|
|
32
34
|
import { HighlightOverlay } from "./HighlightOverlay.js";
|
|
@@ -41,13 +43,12 @@ import { SupportChatModal } from "../support/SupportChatModal.js";
|
|
|
41
43
|
import { ENDPOINTS } from "../config/endpoints.js";
|
|
42
44
|
import * as ConversationService from "../services/ConversationService.js";
|
|
43
45
|
import { createMobileAIKnowledgeRetriever } from "../services/MobileAIKnowledgeRetriever.js";
|
|
44
|
-
|
|
46
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
45
47
|
// ─── Context ───────────────────────────────────────────────────
|
|
46
48
|
|
|
47
49
|
// ─── AsyncStorage Helper (same pattern as TicketStore) ─────────
|
|
48
50
|
|
|
49
51
|
/** Try to load AsyncStorage for tooltip persistence. Optional peer dep. */
|
|
50
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
51
52
|
function getTooltipStorage() {
|
|
52
53
|
try {
|
|
53
54
|
const origError = console.error;
|
|
@@ -68,6 +69,29 @@ function getTooltipStorage() {
|
|
|
68
69
|
return null;
|
|
69
70
|
}
|
|
70
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
|
+
}
|
|
71
95
|
|
|
72
96
|
// ─── Props ─────────────────────────────────────────────────────
|
|
73
97
|
|
|
@@ -81,6 +105,8 @@ export function AIAgent({
|
|
|
81
105
|
voiceProxyHeaders,
|
|
82
106
|
provider: providerName = 'gemini',
|
|
83
107
|
model,
|
|
108
|
+
supportStyle = 'warm-concise',
|
|
109
|
+
verifier,
|
|
84
110
|
navRef,
|
|
85
111
|
maxSteps = 25,
|
|
86
112
|
showChatBar = true,
|
|
@@ -205,6 +231,9 @@ export function AIAgent({
|
|
|
205
231
|
// ── Onboarding Journey State ────────────────────────────────
|
|
206
232
|
const [isOnboardingActive, setIsOnboardingActive] = useState(false);
|
|
207
233
|
const [currentOnboardingIndex, setCurrentOnboardingIndex] = useState(0);
|
|
234
|
+
const [androidWindowMetrics, setAndroidWindowMetrics] = useState(null);
|
|
235
|
+
const androidWindowMetricsRef = useRef(null);
|
|
236
|
+
const floatingOverlayRef = useRef(null);
|
|
208
237
|
useEffect(() => {
|
|
209
238
|
if (!onboarding?.enabled) return;
|
|
210
239
|
if (onboarding.firstLaunchOnly !== false) {
|
|
@@ -234,7 +263,9 @@ export function AIAgent({
|
|
|
234
263
|
try {
|
|
235
264
|
const AS = getTooltipStorage();
|
|
236
265
|
await AS?.setItem('@mobileai_onboarding_completed', 'true');
|
|
237
|
-
} catch {
|
|
266
|
+
} catch {
|
|
267
|
+
/* graceful */
|
|
268
|
+
}
|
|
238
269
|
})();
|
|
239
270
|
} else {
|
|
240
271
|
setCurrentOnboardingIndex(prev => prev + 1);
|
|
@@ -266,9 +297,54 @@ export function AIAgent({
|
|
|
266
297
|
try {
|
|
267
298
|
const AS = getTooltipStorage();
|
|
268
299
|
await AS?.setItem('@mobileai_tooltip_seen', 'true');
|
|
269
|
-
} catch {
|
|
300
|
+
} catch {
|
|
301
|
+
/* graceful */
|
|
302
|
+
}
|
|
270
303
|
})();
|
|
271
304
|
}, []);
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!showChatBar) {
|
|
307
|
+
androidWindowMetricsRef.current = null;
|
|
308
|
+
setAndroidWindowMetrics(null);
|
|
309
|
+
}
|
|
310
|
+
}, [showChatBar]);
|
|
311
|
+
const handleAndroidWindowMetricsChange = useCallback(metrics => {
|
|
312
|
+
if (Platform.OS !== 'android') {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (!showChatBar) {
|
|
316
|
+
androidWindowMetricsRef.current = null;
|
|
317
|
+
setAndroidWindowMetrics(null);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const previousMetrics = androidWindowMetricsRef.current;
|
|
321
|
+
if (previousMetrics && previousMetrics.x === metrics.x && previousMetrics.y === metrics.y && previousMetrics.width === metrics.width && previousMetrics.height === metrics.height) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
androidWindowMetricsRef.current = metrics;
|
|
325
|
+
if (floatingOverlayRef.current && previousMetrics) {
|
|
326
|
+
floatingOverlayRef.current.setAndroidWindowMetrics(metrics);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
setAndroidWindowMetrics(metrics);
|
|
330
|
+
}, [showChatBar]);
|
|
331
|
+
const handleAndroidWindowDragEnd = useCallback(metrics => {
|
|
332
|
+
if (Platform.OS !== 'android') {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!showChatBar) {
|
|
336
|
+
androidWindowMetricsRef.current = null;
|
|
337
|
+
setAndroidWindowMetrics(null);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
androidWindowMetricsRef.current = metrics;
|
|
341
|
+
setAndroidWindowMetrics(prev => {
|
|
342
|
+
if (prev && prev.x === metrics.x && prev.y === metrics.y && prev.width === metrics.width && prev.height === metrics.height) {
|
|
343
|
+
return prev;
|
|
344
|
+
}
|
|
345
|
+
return metrics;
|
|
346
|
+
});
|
|
347
|
+
}, [showChatBar]);
|
|
272
348
|
|
|
273
349
|
// CRITICAL: clearSupport uses REFS and functional setters — never closure values.
|
|
274
350
|
// This function is captured by long-lived callbacks (escalation sockets, restored
|
|
@@ -385,17 +461,50 @@ export function AIAgent({
|
|
|
385
461
|
});
|
|
386
462
|
});
|
|
387
463
|
}, []);
|
|
388
|
-
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
|
+
}
|
|
389
488
|
const routeName = navRef?.getCurrentRoute?.()?.name;
|
|
390
489
|
if (typeof routeName === 'string' && routeName.trim().length > 0) {
|
|
391
|
-
|
|
490
|
+
const humanized = humanizeScreenName(routeName);
|
|
491
|
+
if (humanized) return humanized;
|
|
392
492
|
}
|
|
393
493
|
const telemetryScreen = telemetryRef.current?.screen;
|
|
394
|
-
if (typeof telemetryScreen === 'string' && telemetryScreen !== 'Unknown') {
|
|
494
|
+
if (typeof telemetryScreen === 'string' && telemetryScreen !== 'Unknown' && telemetryScreen.trim().length > 0) {
|
|
395
495
|
return telemetryScreen;
|
|
396
496
|
}
|
|
397
497
|
return 'unknown';
|
|
398
|
-
}, [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]);
|
|
399
508
|
const resolvedKnowledgeBase = useMemo(() => {
|
|
400
509
|
if (knowledgeBase) return knowledgeBase;
|
|
401
510
|
if (!analyticsKey) return undefined;
|
|
@@ -505,6 +614,7 @@ export function AIAgent({
|
|
|
505
614
|
if (!humanFrtFiredRef.current[ticketId]) {
|
|
506
615
|
humanFrtFiredRef.current[ticketId] = true;
|
|
507
616
|
telemetryRef.current?.track('human_first_response', {
|
|
617
|
+
canonical_type: 'human_first_response_sent',
|
|
508
618
|
ticketId
|
|
509
619
|
});
|
|
510
620
|
}
|
|
@@ -703,7 +813,7 @@ export function AIAgent({
|
|
|
703
813
|
},
|
|
704
814
|
onTypingChange: setIsLiveAgentTyping,
|
|
705
815
|
onTicketClosed: () => clearSupport(ticket.id),
|
|
706
|
-
onError: err => logger.
|
|
816
|
+
onError: err => logger.warn('AIAgent', '★ Restored socket error:', err)
|
|
707
817
|
});
|
|
708
818
|
if (ticket.wsUrl) {
|
|
709
819
|
socket.connect(ticket.wsUrl);
|
|
@@ -715,7 +825,7 @@ export function AIAgent({
|
|
|
715
825
|
logger.info('AIAgent', '★ Single ticket restored and socket cached:', ticket.id);
|
|
716
826
|
}
|
|
717
827
|
} catch (err) {
|
|
718
|
-
logger.
|
|
828
|
+
logger.warn('AIAgent', '★ Failed to restore tickets:', err);
|
|
719
829
|
}
|
|
720
830
|
})();
|
|
721
831
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -949,7 +1059,7 @@ export function AIAgent({
|
|
|
949
1059
|
}
|
|
950
1060
|
clearSupport(ticketId);
|
|
951
1061
|
},
|
|
952
|
-
onError: err => logger.
|
|
1062
|
+
onError: err => logger.warn('AIAgent', '★ Socket error on select:', err)
|
|
953
1063
|
});
|
|
954
1064
|
if (freshWsUrl) {
|
|
955
1065
|
socket.connect(freshWsUrl);
|
|
@@ -1023,16 +1133,50 @@ export function AIAgent({
|
|
|
1023
1133
|
const [pendingApprovalQuestion, setPendingApprovalQuestion] = useState(null);
|
|
1024
1134
|
const overlayVisible = isThinking || !!pendingApprovalQuestion;
|
|
1025
1135
|
const overlayStatusText = pendingApprovalQuestion ? 'Waiting for your approval...' : statusText;
|
|
1136
|
+
const effectiveProxyHeaders = useMemo(() => {
|
|
1137
|
+
if (!analyticsKey) return proxyHeaders;
|
|
1138
|
+
const isAuthMissing = !proxyHeaders || !Object.keys(proxyHeaders).some(k => k.toLowerCase() === 'authorization');
|
|
1139
|
+
if (isAuthMissing) {
|
|
1140
|
+
return {
|
|
1141
|
+
...proxyHeaders,
|
|
1142
|
+
Authorization: `Bearer ${analyticsKey}`
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
return proxyHeaders;
|
|
1146
|
+
}, [proxyHeaders, analyticsKey]);
|
|
1147
|
+
const effectiveVoiceProxyHeaders = useMemo(() => {
|
|
1148
|
+
if (!analyticsKey) return voiceProxyHeaders;
|
|
1149
|
+
const isAuthMissing = !voiceProxyHeaders || !Object.keys(voiceProxyHeaders).some(k => k.toLowerCase() === 'authorization');
|
|
1150
|
+
if (isAuthMissing) {
|
|
1151
|
+
return {
|
|
1152
|
+
...voiceProxyHeaders,
|
|
1153
|
+
Authorization: `Bearer ${analyticsKey}`
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
return voiceProxyHeaders;
|
|
1157
|
+
}, [voiceProxyHeaders, analyticsKey]);
|
|
1158
|
+
const resolvedProxyUrl = useMemo(() => {
|
|
1159
|
+
if (proxyUrl) return proxyUrl;
|
|
1160
|
+
if (analyticsKey) return ENDPOINTS.hostedTextProxy;
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}, [proxyUrl, analyticsKey]);
|
|
1163
|
+
const resolvedVoiceProxyUrl = useMemo(() => {
|
|
1164
|
+
if (voiceProxyUrl) return voiceProxyUrl;
|
|
1165
|
+
if (analyticsKey) return ENDPOINTS.hostedVoiceProxy;
|
|
1166
|
+
return resolvedProxyUrl;
|
|
1167
|
+
}, [voiceProxyUrl, analyticsKey, resolvedProxyUrl]);
|
|
1026
1168
|
|
|
1027
1169
|
// ─── Create Runtime ──────────────────────────────────────────
|
|
1028
1170
|
|
|
1029
1171
|
const config = useMemo(() => ({
|
|
1030
1172
|
apiKey,
|
|
1031
|
-
proxyUrl,
|
|
1173
|
+
proxyUrl: resolvedProxyUrl,
|
|
1032
1174
|
proxyHeaders,
|
|
1033
|
-
voiceProxyUrl,
|
|
1175
|
+
voiceProxyUrl: resolvedVoiceProxyUrl,
|
|
1034
1176
|
voiceProxyHeaders,
|
|
1035
1177
|
model,
|
|
1178
|
+
supportStyle,
|
|
1179
|
+
verifier,
|
|
1036
1180
|
language: 'en',
|
|
1037
1181
|
maxSteps,
|
|
1038
1182
|
interactiveBlacklist,
|
|
@@ -1139,33 +1283,11 @@ export function AIAgent({
|
|
|
1139
1283
|
...(event.data ?? {})
|
|
1140
1284
|
});
|
|
1141
1285
|
}
|
|
1142
|
-
}), [mode, apiKey,
|
|
1286
|
+
}), [mode, apiKey, resolvedProxyUrl, proxyHeaders, resolvedVoiceProxyUrl, voiceProxyHeaders, model, maxSteps, interactiveBlacklist, interactiveWhitelist, onBeforeStep, onAfterStep, onBeforeTask, onAfterTask, transformScreenContent, customTools, instructions, stepDelay, mcpServerUrl, router, pathname, onTokenUsage, resolvedKnowledgeBase, knowledgeMaxTokens, enableUIControl, screenMap, useScreenMap, maxTokenBudget, maxCostUSD, interactionMode, verifier, supportStyle]);
|
|
1143
1287
|
useEffect(() => {
|
|
1144
1288
|
logger.info('AIAgent', `⚙️ Runtime config recomputed: mode=${mode} interactionMode=${interactionMode || 'copilot(default)'} onAskUser=${mode !== 'voice'} mergedTools=${Object.keys(mergedCustomTools).join(', ') || '(none)'}`);
|
|
1145
1289
|
}, [mode, interactionMode, mergedCustomTools]);
|
|
1146
|
-
const
|
|
1147
|
-
if (!analyticsKey) return proxyHeaders;
|
|
1148
|
-
const isAuthMissing = !proxyHeaders || !Object.keys(proxyHeaders).some(k => k.toLowerCase() === 'authorization');
|
|
1149
|
-
if (isAuthMissing) {
|
|
1150
|
-
return {
|
|
1151
|
-
...proxyHeaders,
|
|
1152
|
-
Authorization: `Bearer ${analyticsKey}`
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
return proxyHeaders;
|
|
1156
|
-
}, [proxyHeaders, analyticsKey]);
|
|
1157
|
-
const effectiveVoiceProxyHeaders = useMemo(() => {
|
|
1158
|
-
if (!analyticsKey) return voiceProxyHeaders;
|
|
1159
|
-
const isAuthMissing = !voiceProxyHeaders || !Object.keys(voiceProxyHeaders).some(k => k.toLowerCase() === 'authorization');
|
|
1160
|
-
if (isAuthMissing) {
|
|
1161
|
-
return {
|
|
1162
|
-
...voiceProxyHeaders,
|
|
1163
|
-
Authorization: `Bearer ${analyticsKey}`
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
return voiceProxyHeaders;
|
|
1167
|
-
}, [voiceProxyHeaders, analyticsKey]);
|
|
1168
|
-
const provider = useMemo(() => createProvider(providerName, apiKey, model, proxyUrl, effectiveProxyHeaders), [providerName, apiKey, model, proxyUrl, effectiveProxyHeaders]);
|
|
1290
|
+
const provider = useMemo(() => createProvider(providerName, apiKey, model, resolvedProxyUrl, effectiveProxyHeaders), [providerName, apiKey, model, resolvedProxyUrl, effectiveProxyHeaders]);
|
|
1169
1291
|
const runtime = useMemo(() => new AgentRuntime(provider, config, rootViewRef.current, navRef),
|
|
1170
1292
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1171
1293
|
[provider, config]);
|
|
@@ -1193,12 +1315,12 @@ export function AIAgent({
|
|
|
1193
1315
|
debug,
|
|
1194
1316
|
onEvent: event => {
|
|
1195
1317
|
// Proactive behavior triggers
|
|
1196
|
-
if (event.type === 'rage_tap' || event.type === 'error_screen' || event.type === 'repeated_navigation') {
|
|
1318
|
+
if (event.type === 'rage_tap' || event.type === 'rage_click' || event.type === 'rage_click_detected' || event.type === 'error_screen' || event.type === 'repeated_navigation') {
|
|
1197
1319
|
idleDetectorRef.current?.triggerBehavior(event.type, event.screen);
|
|
1198
1320
|
}
|
|
1199
1321
|
|
|
1200
1322
|
// Customer Success features
|
|
1201
|
-
if (customerSuccess?.enabled && event.type === 'user_interaction' && event.data) {
|
|
1323
|
+
if (customerSuccess?.enabled && (event.type === 'user_interaction' || event.type === 'user_action') && event.data) {
|
|
1202
1324
|
const action = String(event.data.label || event.data.action || '');
|
|
1203
1325
|
|
|
1204
1326
|
// Check milestones
|
|
@@ -1227,15 +1349,14 @@ export function AIAgent({
|
|
|
1227
1349
|
bindTelemetryService(telemetry);
|
|
1228
1350
|
telemetry.start();
|
|
1229
1351
|
// NavigationContainer is a child of AIAgent, so navRef may not be
|
|
1230
|
-
// ready yet when this effect runs. Poll briefly until it
|
|
1352
|
+
// ready yet when this effect runs. Poll briefly until it is.
|
|
1231
1353
|
const resolveInitialScreen = () => {
|
|
1232
1354
|
let attempts = 0;
|
|
1233
1355
|
const maxAttempts = 15; // 15 × 200ms = 3s max wait
|
|
1234
1356
|
const timer = setInterval(() => {
|
|
1235
1357
|
attempts++;
|
|
1236
|
-
const
|
|
1237
|
-
if (
|
|
1238
|
-
telemetry.setScreen(route.name);
|
|
1358
|
+
const cleanName = syncTelemetryScreen();
|
|
1359
|
+
if (cleanName !== 'unknown') {
|
|
1239
1360
|
clearInterval(timer);
|
|
1240
1361
|
} else if (attempts >= maxAttempts) {
|
|
1241
1362
|
clearInterval(timer);
|
|
@@ -1244,22 +1365,45 @@ export function AIAgent({
|
|
|
1244
1365
|
};
|
|
1245
1366
|
resolveInitialScreen();
|
|
1246
1367
|
}); // initDeviceId
|
|
1247
|
-
}, [analyticsKey, analyticsProxyUrl, analyticsProxyHeaders, bindTelemetryService, debug, navRef]);
|
|
1368
|
+
}, [analyticsKey, analyticsProxyUrl, analyticsProxyHeaders, bindTelemetryService, debug, navRef, syncTelemetryScreen]);
|
|
1248
1369
|
|
|
1249
1370
|
// ─── Security warnings ──────────────────────────────────────
|
|
1250
1371
|
|
|
1251
1372
|
useEffect(() => {
|
|
1252
1373
|
// @ts-ignore
|
|
1253
|
-
if (typeof __DEV__ !== 'undefined' && !__DEV__ && apiKey && !
|
|
1374
|
+
if (typeof __DEV__ !== 'undefined' && !__DEV__ && apiKey && !resolvedProxyUrl) {
|
|
1254
1375
|
logger.warn('[MobileAI] ⚠️ SECURITY WARNING: You are using `apiKey` directly in a production build. ' + 'This exposes your LLM provider key in the app binary. ' + 'Use `apiProxyUrl` to route requests through your backend instead. ' + 'See docs for details.');
|
|
1255
1376
|
}
|
|
1256
|
-
}, [apiKey,
|
|
1377
|
+
}, [apiKey, resolvedProxyUrl]);
|
|
1257
1378
|
|
|
1258
1379
|
// Track screen changes via navRef
|
|
1259
1380
|
useEffect(() => {
|
|
1260
1381
|
if (!navRef?.addListener || !telemetryRef.current) return;
|
|
1261
1382
|
const checkScreenMilestone = screenName => {
|
|
1262
1383
|
telemetryRef.current?.setScreen(screenName);
|
|
1384
|
+
|
|
1385
|
+
// Auto-capture wireframe snapshot for privacy-safe heatmaps.
|
|
1386
|
+
// Deferred: wait for all animations/interactions to finish, then
|
|
1387
|
+
// wait one more frame for layout to settle. Zero perf impact.
|
|
1388
|
+
if (rootViewRef.current) {
|
|
1389
|
+
const handle = InteractionManager.runAfterInteractions(() => {
|
|
1390
|
+
requestAnimationFrame(() => {
|
|
1391
|
+
Promise.all([captureWireframe(rootViewRef, {
|
|
1392
|
+
screenName
|
|
1393
|
+
}), captureHeatmapScreenshot(rootViewRef.current)]).then(([wireframe, screenshot]) => {
|
|
1394
|
+
if (wireframe && telemetryRef.current) {
|
|
1395
|
+
telemetryRef.current.trackWireframe({
|
|
1396
|
+
...wireframe,
|
|
1397
|
+
screenshot: screenshot || undefined
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
}).catch(err => {
|
|
1401
|
+
if (debug) logger.debug('AIAgent', 'Wireframe capture failed:', err);
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
});
|
|
1405
|
+
void handle;
|
|
1406
|
+
}
|
|
1263
1407
|
if (customerSuccess?.enabled) {
|
|
1264
1408
|
customerSuccess.successMilestones?.forEach(m => {
|
|
1265
1409
|
if (m.screen && m.screen === screenName) {
|
|
@@ -1293,13 +1437,13 @@ export function AIAgent({
|
|
|
1293
1437
|
}
|
|
1294
1438
|
};
|
|
1295
1439
|
const unsubscribe = navRef.addListener('state', () => {
|
|
1296
|
-
const
|
|
1297
|
-
if (
|
|
1298
|
-
checkScreenMilestone(
|
|
1440
|
+
const cleanName = resolveScreenName();
|
|
1441
|
+
if (cleanName) {
|
|
1442
|
+
checkScreenMilestone(cleanName);
|
|
1299
1443
|
}
|
|
1300
1444
|
});
|
|
1301
1445
|
return () => unsubscribe?.();
|
|
1302
|
-
}, [navRef, customerSuccess, isOnboardingActive, onboarding, currentOnboardingIndex, advanceOnboarding]);
|
|
1446
|
+
}, [navRef, resolveScreenName, customerSuccess, isOnboardingActive, onboarding, currentOnboardingIndex, advanceOnboarding]);
|
|
1303
1447
|
|
|
1304
1448
|
// ─── MCP Bridge ──────────────────────────────────────────────
|
|
1305
1449
|
|
|
@@ -1336,7 +1480,7 @@ export function AIAgent({
|
|
|
1336
1480
|
setProactiveStage('badge');
|
|
1337
1481
|
},
|
|
1338
1482
|
onReset: () => setProactiveStage('hidden'),
|
|
1339
|
-
generateSuggestion: () => proactiveHelp?.generateSuggestion?.(telemetryRef.current?.screen || 'Home') || proactiveHelp?.badgeText ||
|
|
1483
|
+
generateSuggestion: () => proactiveHelp?.generateSuggestion?.(telemetryRef.current?.screen || 'Home') || proactiveHelp?.badgeText || 'Need help with this screen?',
|
|
1340
1484
|
behaviorTriggers: proactiveHelp?.behaviorTriggers
|
|
1341
1485
|
});
|
|
1342
1486
|
return () => {
|
|
@@ -1365,11 +1509,11 @@ export function AIAgent({
|
|
|
1365
1509
|
logger.info('AIAgent', `Registering ${runtimeTools.length} tools with VoiceService: ${runtimeTools.map(t => t.name).join(', ')}`);
|
|
1366
1510
|
// Use voice-adapted system prompt — same core rules as text mode
|
|
1367
1511
|
// but without agent-loop directives that trigger autonomous actions
|
|
1368
|
-
const voicePrompt = buildVoiceSystemPrompt('en', instructions?.system, !!knowledgeBase);
|
|
1512
|
+
const voicePrompt = buildVoiceSystemPrompt('en', instructions?.system, !!knowledgeBase, supportStyle);
|
|
1369
1513
|
logger.info('AIAgent', `📝 Voice system prompt (${voicePrompt.length} chars):\n${voicePrompt}`);
|
|
1370
1514
|
voiceServiceRef.current = new VoiceService({
|
|
1371
1515
|
apiKey,
|
|
1372
|
-
proxyUrl:
|
|
1516
|
+
proxyUrl: resolvedVoiceProxyUrl,
|
|
1373
1517
|
proxyHeaders: effectiveVoiceProxyHeaders || effectiveProxyHeaders,
|
|
1374
1518
|
systemPrompt: voicePrompt,
|
|
1375
1519
|
tools: runtimeTools,
|
|
@@ -1609,7 +1753,7 @@ export function AIAgent({
|
|
|
1609
1753
|
setIsVoiceConnected(false);
|
|
1610
1754
|
};
|
|
1611
1755
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1612
|
-
}, [mode, apiKey,
|
|
1756
|
+
}, [mode, apiKey, resolvedVoiceProxyUrl, effectiveVoiceProxyHeaders, effectiveProxyHeaders, runtime, instructions, supportStyle, knowledgeBase]);
|
|
1613
1757
|
|
|
1614
1758
|
// ─── Stop Voice Session (full cleanup) ─────────────────────
|
|
1615
1759
|
|
|
@@ -1754,12 +1898,15 @@ export function AIAgent({
|
|
|
1754
1898
|
setIsThinking(true);
|
|
1755
1899
|
setStatusText('Thinking...');
|
|
1756
1900
|
setLastResult(null);
|
|
1901
|
+
const requestStartedAt = Date.now();
|
|
1757
1902
|
logger.info('AIAgent', `📨 New user request received in ${mode} mode | interactionMode=${interactionMode || 'copilot(default)'} | text="${message.trim()}"`);
|
|
1758
1903
|
|
|
1759
1904
|
// Telemetry: track agent request
|
|
1760
1905
|
telemetryRef.current?.track('agent_request', {
|
|
1906
|
+
canonical_type: 'ai_question_asked',
|
|
1761
1907
|
query: message.trim(),
|
|
1762
1908
|
transcript: message.trim(),
|
|
1909
|
+
request_topic: message.trim().slice(0, 120),
|
|
1763
1910
|
mode
|
|
1764
1911
|
});
|
|
1765
1912
|
telemetryRef.current?.track('agent_trace', {
|
|
@@ -1778,8 +1925,11 @@ export function AIAgent({
|
|
|
1778
1925
|
if (HIGH_RISK_ESCALATION_REGEX.test(message)) {
|
|
1779
1926
|
logger.warn('AIAgent', 'High-risk support signal detected — auto-escalating to human');
|
|
1780
1927
|
telemetryRef.current?.track('business_escalation', {
|
|
1928
|
+
canonical_type: 'support_escalated',
|
|
1781
1929
|
message,
|
|
1782
|
-
trigger: 'high_risk'
|
|
1930
|
+
trigger: 'high_risk',
|
|
1931
|
+
escalation_reason: 'high_risk',
|
|
1932
|
+
topic: message.trim().slice(0, 120)
|
|
1783
1933
|
});
|
|
1784
1934
|
const escalationResult = await escalateTool.execute({
|
|
1785
1935
|
reason: `Customer needs human support: ${message.trim()}`
|
|
@@ -1880,7 +2030,9 @@ export function AIAgent({
|
|
|
1880
2030
|
if (telemetryRef.current) {
|
|
1881
2031
|
if (!agentFrtFiredRef.current) {
|
|
1882
2032
|
agentFrtFiredRef.current = true;
|
|
1883
|
-
telemetryRef.current.track('agent_first_response'
|
|
2033
|
+
telemetryRef.current.track('agent_first_response', {
|
|
2034
|
+
canonical_type: 'ai_first_response_sent'
|
|
2035
|
+
});
|
|
1884
2036
|
}
|
|
1885
2037
|
for (const step of normalizedResult.steps ?? []) {
|
|
1886
2038
|
telemetryRef.current.track('agent_step', {
|
|
@@ -1894,7 +2046,12 @@ export function AIAgent({
|
|
|
1894
2046
|
});
|
|
1895
2047
|
}
|
|
1896
2048
|
telemetryRef.current.track('agent_complete', {
|
|
2049
|
+
canonical_type: normalizedResult.success ? 'ai_answer_completed' : 'ai_answer_failed',
|
|
1897
2050
|
success: normalizedResult.success,
|
|
2051
|
+
resolved: normalizedResult.success,
|
|
2052
|
+
resolution_type: normalizedResult.success ? 'ai_resolved' : 'needs_follow_up',
|
|
2053
|
+
latency_ms: Date.now() - requestStartedAt,
|
|
2054
|
+
request_topic: message.trim().slice(0, 120),
|
|
1898
2055
|
steps: normalizedResult.steps?.length ?? 0,
|
|
1899
2056
|
tokens: normalizedResult.tokenUsage?.totalTokens ?? 0,
|
|
1900
2057
|
cost: normalizedResult.tokenUsage?.estimatedCostUSD ?? 0,
|
|
@@ -1989,7 +2146,12 @@ export function AIAgent({
|
|
|
1989
2146
|
|
|
1990
2147
|
// Telemetry: track agent failure
|
|
1991
2148
|
telemetryRef.current?.track('agent_complete', {
|
|
2149
|
+
canonical_type: 'ai_answer_failed',
|
|
1992
2150
|
success: false,
|
|
2151
|
+
resolved: false,
|
|
2152
|
+
resolution_type: 'execution_error',
|
|
2153
|
+
latency_ms: Date.now() - requestStartedAt,
|
|
2154
|
+
request_topic: message.trim().slice(0, 120),
|
|
1993
2155
|
error: error.message,
|
|
1994
2156
|
response: `Error: ${error.message}`,
|
|
1995
2157
|
conversation: {
|
|
@@ -2026,8 +2188,7 @@ export function AIAgent({
|
|
|
2026
2188
|
|
|
2027
2189
|
const handleCancel = useCallback(() => {
|
|
2028
2190
|
runtime.cancel();
|
|
2029
|
-
|
|
2030
|
-
setStatusText('');
|
|
2191
|
+
setStatusText('Stopping...');
|
|
2031
2192
|
}, [runtime]);
|
|
2032
2193
|
|
|
2033
2194
|
// ─── Conversation History Handlers ─────────────────────────────
|
|
@@ -2082,24 +2243,46 @@ export function AIAgent({
|
|
|
2082
2243
|
// Skip if the AI agent is currently executing a tool — those are
|
|
2083
2244
|
// already tracked as `agent_step` events with full context.
|
|
2084
2245
|
if (telemetryRef.current && !telemetryRef.current.isAgentActing) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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) {
|
|
2251
|
+
telemetryRef.current.track('user_action', {
|
|
2252
|
+
canonical_type: 'button_tapped',
|
|
2088
2253
|
type: 'tap',
|
|
2089
|
-
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,
|
|
2090
2263
|
actor: 'user',
|
|
2091
|
-
x:
|
|
2092
|
-
y:
|
|
2264
|
+
x: pageX,
|
|
2265
|
+
y: pageY
|
|
2093
2266
|
});
|
|
2094
2267
|
|
|
2095
2268
|
// Track if user is rage-tapping this specific element
|
|
2096
|
-
checkRageClick(
|
|
2269
|
+
checkRageClick({
|
|
2270
|
+
...target,
|
|
2271
|
+
x: pageX,
|
|
2272
|
+
y: pageY
|
|
2273
|
+
}, telemetryRef.current);
|
|
2097
2274
|
} else {
|
|
2098
2275
|
// Tapped an unlabelled/empty area
|
|
2099
2276
|
telemetryRef.current.track('dead_click', {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2277
|
+
canonical_type: 'dead_click_detected',
|
|
2278
|
+
x: pageX,
|
|
2279
|
+
y: pageY,
|
|
2280
|
+
screen: telemetryRef.current.screen,
|
|
2281
|
+
screen_area: 'unknown',
|
|
2282
|
+
zone_id: target.zoneId,
|
|
2283
|
+
ancestor_path: target.ancestorPath,
|
|
2284
|
+
sibling_labels: target.siblingLabels,
|
|
2285
|
+
component_name: target.componentName
|
|
2103
2286
|
});
|
|
2104
2287
|
}
|
|
2105
2288
|
}
|
|
@@ -2115,128 +2298,255 @@ export function AIAgent({
|
|
|
2115
2298
|
},
|
|
2116
2299
|
children: children
|
|
2117
2300
|
})
|
|
2118
|
-
}), /*#__PURE__*/_jsx(FloatingOverlayWrapper, {
|
|
2301
|
+
}), /*#__PURE__*/_jsx(HighlightOverlay, {}), showChatBar && /*#__PURE__*/_jsx(FloatingOverlayWrapper, {
|
|
2302
|
+
ref: Platform.OS === 'android' ? floatingOverlayRef : undefined,
|
|
2303
|
+
androidWindowMetrics: Platform.OS === 'android' ? androidWindowMetricsRef.current ?? androidWindowMetrics : null,
|
|
2304
|
+
onAndroidWindowDragEnd: Platform.OS === 'android' ? handleAndroidWindowDragEnd : undefined,
|
|
2119
2305
|
fallbackStyle: styles.floatingLayer,
|
|
2120
|
-
children: /*#__PURE__*/
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
onConversationSelect: handleConversationSelect,
|
|
2224
|
-
onNewConversation: handleNewConversation
|
|
2225
|
-
})
|
|
2226
|
-
}), /*#__PURE__*/_jsx(AgentOverlay, {
|
|
2227
|
-
visible: overlayVisible,
|
|
2228
|
-
statusText: overlayStatusText,
|
|
2229
|
-
onCancel: handleCancel
|
|
2230
|
-
}), /*#__PURE__*/_jsx(SupportChatModal, {
|
|
2231
|
-
visible: mode === 'human' && !!selectedTicketId,
|
|
2232
|
-
messages: supportMessages,
|
|
2306
|
+
children: Platform.OS === 'android' ? /*#__PURE__*/_jsx(AgentChatBar, {
|
|
2307
|
+
onSend: handleSend,
|
|
2308
|
+
onCancel: handleCancel,
|
|
2309
|
+
isThinking: isThinking,
|
|
2310
|
+
statusText: overlayStatusText,
|
|
2311
|
+
lastResult: lastResult,
|
|
2312
|
+
lastUserMessage: lastUserMessage,
|
|
2313
|
+
chatMessages: messages,
|
|
2314
|
+
pendingApprovalQuestion: pendingApprovalQuestion,
|
|
2315
|
+
onPendingApprovalAction: action => {
|
|
2316
|
+
const resolver = askUserResolverRef.current;
|
|
2317
|
+
logger.info('AIAgent', `🔘 Approval button tapped: action=${action} | resolver=${resolver ? 'EXISTS' : 'NULL'} | pendingApprovalQuestion="${pendingApprovalQuestion}" | pendingAppApprovalRef=${pendingAppApprovalRef.current}`);
|
|
2318
|
+
if (!resolver) {
|
|
2319
|
+
logger.error('AIAgent', '🚫 ABORT: resolver is null when button was tapped — this means ask_user Promise was already resolved without clearing the buttons. This is a state sync bug.');
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
askUserResolverRef.current = null;
|
|
2323
|
+
pendingAskUserKindRef.current = null;
|
|
2324
|
+
pendingAppApprovalRef.current = false;
|
|
2325
|
+
queuedApprovalAnswerRef.current = null;
|
|
2326
|
+
setPendingApprovalQuestion(null);
|
|
2327
|
+
const response = action === 'approve' ? '__APPROVAL_GRANTED__' : '__APPROVAL_REJECTED__';
|
|
2328
|
+
if (action === 'approve') {
|
|
2329
|
+
setIsThinking(true);
|
|
2330
|
+
setStatusText('Working...');
|
|
2331
|
+
}
|
|
2332
|
+
telemetryRef.current?.track('agent_trace', {
|
|
2333
|
+
stage: 'approval_button_pressed',
|
|
2334
|
+
action
|
|
2335
|
+
});
|
|
2336
|
+
resolver(response);
|
|
2337
|
+
},
|
|
2338
|
+
language: 'en',
|
|
2339
|
+
onDismiss: () => {
|
|
2340
|
+
setLastResult(null);
|
|
2341
|
+
setLastUserMessage(null);
|
|
2342
|
+
},
|
|
2343
|
+
theme: accentColor || theme ? {
|
|
2344
|
+
...(accentColor ? {
|
|
2345
|
+
primaryColor: accentColor
|
|
2346
|
+
} : {}),
|
|
2347
|
+
...theme
|
|
2348
|
+
} : undefined,
|
|
2349
|
+
availableModes: availableModes,
|
|
2350
|
+
mode: mode,
|
|
2351
|
+
onModeChange: newMode => {
|
|
2352
|
+
logger.info('AIAgent', '★ onModeChange:', mode, '→', newMode, '| tickets:', tickets.length, 'selectedTicketId:', selectedTicketId);
|
|
2353
|
+
setMode(newMode);
|
|
2354
|
+
},
|
|
2355
|
+
isMicActive: isMicActive,
|
|
2356
|
+
isSpeakerMuted: isSpeakerMuted,
|
|
2357
|
+
isAISpeaking: isAISpeaking,
|
|
2358
|
+
isAgentTyping: isLiveAgentTyping,
|
|
2359
|
+
onStopSession: stopVoiceSession,
|
|
2360
|
+
isVoiceConnected: isVoiceConnected,
|
|
2361
|
+
onMicToggle: active => {
|
|
2362
|
+
if (active && !isVoiceConnected) {
|
|
2363
|
+
logger.warn('AIAgent', 'Cannot toggle mic — VoiceService not connected yet');
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
logger.info('AIAgent', `Mic toggle: ${active ? 'ON' : 'OFF'}`);
|
|
2367
|
+
setIsMicActive(active);
|
|
2368
|
+
if (active) {
|
|
2369
|
+
logger.info('AIAgent', 'Starting AudioInput...');
|
|
2370
|
+
audioInputRef.current?.start().then(ok => {
|
|
2371
|
+
logger.info('AIAgent', `AudioInput start result: ${ok}`);
|
|
2372
|
+
});
|
|
2373
|
+
} else {
|
|
2374
|
+
logger.info('AIAgent', 'Stopping AudioInput...');
|
|
2375
|
+
audioInputRef.current?.stop();
|
|
2376
|
+
}
|
|
2377
|
+
},
|
|
2378
|
+
onSpeakerToggle: muted => {
|
|
2379
|
+
logger.info('AIAgent', `Speaker toggle: ${muted ? 'MUTED' : 'UNMUTED'}`);
|
|
2380
|
+
setIsSpeakerMuted(muted);
|
|
2381
|
+
if (muted) {
|
|
2382
|
+
audioOutputRef.current?.mute();
|
|
2383
|
+
} else {
|
|
2384
|
+
audioOutputRef.current?.unmute();
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
tickets: tickets,
|
|
2388
|
+
selectedTicketId: selectedTicketId,
|
|
2389
|
+
onTicketSelect: handleTicketSelect,
|
|
2390
|
+
onBackToTickets: handleBackToTickets,
|
|
2391
|
+
autoExpandTrigger: autoExpandTrigger,
|
|
2392
|
+
unreadCounts: unreadCounts,
|
|
2393
|
+
totalUnread: totalUnread,
|
|
2394
|
+
showDiscoveryTooltip: tooltipVisible,
|
|
2395
|
+
discoveryTooltipMessage: discoveryTooltipMessage,
|
|
2396
|
+
onTooltipDismiss: handleTooltipDismiss,
|
|
2397
|
+
conversations: conversations,
|
|
2398
|
+
isLoadingHistory: isLoadingHistory,
|
|
2399
|
+
onConversationSelect: handleConversationSelect,
|
|
2400
|
+
onNewConversation: handleNewConversation,
|
|
2401
|
+
renderMode: "android-native-window",
|
|
2402
|
+
windowMetrics: androidWindowMetricsRef.current ?? androidWindowMetrics,
|
|
2403
|
+
onWindowMetricsChange: handleAndroidWindowMetricsChange
|
|
2404
|
+
}) : /*#__PURE__*/_jsx(ProactiveHint, {
|
|
2405
|
+
stage: proactiveStage,
|
|
2406
|
+
badgeText: proactiveBadgeText,
|
|
2407
|
+
onDismiss: () => idleDetectorRef.current?.dismiss(),
|
|
2408
|
+
children: /*#__PURE__*/_jsx(AgentChatBar, {
|
|
2233
2409
|
onSend: handleSend,
|
|
2234
|
-
|
|
2235
|
-
isAgentTyping: isLiveAgentTyping,
|
|
2410
|
+
onCancel: handleCancel,
|
|
2236
2411
|
isThinking: isThinking,
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2412
|
+
statusText: overlayStatusText,
|
|
2413
|
+
lastResult: lastResult,
|
|
2414
|
+
lastUserMessage: lastUserMessage,
|
|
2415
|
+
chatMessages: messages,
|
|
2416
|
+
pendingApprovalQuestion: pendingApprovalQuestion,
|
|
2417
|
+
onPendingApprovalAction: action => {
|
|
2418
|
+
const resolver = askUserResolverRef.current;
|
|
2419
|
+
logger.info('AIAgent', `🔘 Approval button tapped: action=${action} | resolver=${resolver ? 'EXISTS' : 'NULL'} | pendingApprovalQuestion="${pendingApprovalQuestion}" | pendingAppApprovalRef=${pendingAppApprovalRef.current}`);
|
|
2420
|
+
if (!resolver) {
|
|
2421
|
+
logger.error('AIAgent', '🚫 ABORT: resolver is null when button was tapped — this means ask_user Promise was already resolved without clearing the buttons. This is a state sync bug.');
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
askUserResolverRef.current = null;
|
|
2425
|
+
pendingAskUserKindRef.current = null;
|
|
2426
|
+
pendingAppApprovalRef.current = false;
|
|
2427
|
+
queuedApprovalAnswerRef.current = null;
|
|
2428
|
+
setPendingApprovalQuestion(null);
|
|
2429
|
+
const response = action === 'approve' ? '__APPROVAL_GRANTED__' : '__APPROVAL_REJECTED__';
|
|
2430
|
+
if (action === 'approve') {
|
|
2431
|
+
setIsThinking(true);
|
|
2432
|
+
setStatusText('Working...');
|
|
2433
|
+
}
|
|
2434
|
+
telemetryRef.current?.track('agent_trace', {
|
|
2435
|
+
stage: 'approval_button_pressed',
|
|
2436
|
+
action
|
|
2437
|
+
});
|
|
2438
|
+
resolver(response);
|
|
2439
|
+
},
|
|
2440
|
+
language: 'en',
|
|
2441
|
+
onDismiss: () => {
|
|
2442
|
+
setLastResult(null);
|
|
2443
|
+
setLastUserMessage(null);
|
|
2444
|
+
},
|
|
2445
|
+
theme: accentColor || theme ? {
|
|
2446
|
+
...(accentColor ? {
|
|
2447
|
+
primaryColor: accentColor
|
|
2448
|
+
} : {}),
|
|
2449
|
+
...theme
|
|
2450
|
+
} : undefined,
|
|
2451
|
+
availableModes: availableModes,
|
|
2452
|
+
mode: mode,
|
|
2453
|
+
onModeChange: newMode => {
|
|
2454
|
+
logger.info('AIAgent', '★ onModeChange:', mode, '→', newMode, '| tickets:', tickets.length, 'selectedTicketId:', selectedTicketId);
|
|
2455
|
+
setMode(newMode);
|
|
2456
|
+
},
|
|
2457
|
+
isMicActive: isMicActive,
|
|
2458
|
+
isSpeakerMuted: isSpeakerMuted,
|
|
2459
|
+
isAISpeaking: isAISpeaking,
|
|
2460
|
+
isAgentTyping: isLiveAgentTyping,
|
|
2461
|
+
onStopSession: stopVoiceSession,
|
|
2462
|
+
isVoiceConnected: isVoiceConnected,
|
|
2463
|
+
onMicToggle: active => {
|
|
2464
|
+
if (active && !isVoiceConnected) {
|
|
2465
|
+
logger.warn('AIAgent', 'Cannot toggle mic — VoiceService not connected yet');
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
logger.info('AIAgent', `Mic toggle: ${active ? 'ON' : 'OFF'}`);
|
|
2469
|
+
setIsMicActive(active);
|
|
2470
|
+
if (active) {
|
|
2471
|
+
logger.info('AIAgent', 'Starting AudioInput...');
|
|
2472
|
+
audioInputRef.current?.start().then(ok => {
|
|
2473
|
+
logger.info('AIAgent', `AudioInput start result: ${ok}`);
|
|
2474
|
+
});
|
|
2475
|
+
} else {
|
|
2476
|
+
logger.info('AIAgent', 'Stopping AudioInput...');
|
|
2477
|
+
audioInputRef.current?.stop();
|
|
2478
|
+
}
|
|
2479
|
+
},
|
|
2480
|
+
onSpeakerToggle: muted => {
|
|
2481
|
+
logger.info('AIAgent', `Speaker toggle: ${muted ? 'MUTED' : 'UNMUTED'}`);
|
|
2482
|
+
setIsSpeakerMuted(muted);
|
|
2483
|
+
if (muted) {
|
|
2484
|
+
audioOutputRef.current?.mute();
|
|
2485
|
+
} else {
|
|
2486
|
+
audioOutputRef.current?.unmute();
|
|
2487
|
+
}
|
|
2488
|
+
},
|
|
2489
|
+
tickets: tickets,
|
|
2490
|
+
selectedTicketId: selectedTicketId,
|
|
2491
|
+
onTicketSelect: handleTicketSelect,
|
|
2492
|
+
onBackToTickets: handleBackToTickets,
|
|
2493
|
+
autoExpandTrigger: autoExpandTrigger,
|
|
2494
|
+
unreadCounts: unreadCounts,
|
|
2495
|
+
totalUnread: totalUnread,
|
|
2496
|
+
showDiscoveryTooltip: tooltipVisible,
|
|
2497
|
+
discoveryTooltipMessage: discoveryTooltipMessage,
|
|
2498
|
+
onTooltipDismiss: handleTooltipDismiss,
|
|
2499
|
+
conversations: conversations,
|
|
2500
|
+
isLoadingHistory: isLoadingHistory,
|
|
2501
|
+
onConversationSelect: handleConversationSelect,
|
|
2502
|
+
onNewConversation: handleNewConversation,
|
|
2503
|
+
renderMode: "default"
|
|
2504
|
+
})
|
|
2505
|
+
})
|
|
2506
|
+
}), /*#__PURE__*/_jsx(AgentOverlay, {
|
|
2507
|
+
visible: overlayVisible,
|
|
2508
|
+
statusText: overlayStatusText,
|
|
2509
|
+
onCancel: handleCancel
|
|
2510
|
+
}), Platform.OS !== 'android' && /*#__PURE__*/_jsxs(_Fragment, {
|
|
2511
|
+
children: [/*#__PURE__*/_jsx(SupportChatModal, {
|
|
2512
|
+
visible: mode === 'human' && !!selectedTicketId,
|
|
2513
|
+
messages: supportMessages,
|
|
2514
|
+
onSend: handleSend,
|
|
2515
|
+
onClose: handleBackToTickets,
|
|
2516
|
+
isAgentTyping: isLiveAgentTyping,
|
|
2517
|
+
isThinking: isThinking,
|
|
2518
|
+
scrollToEndTrigger: chatScrollTrigger,
|
|
2519
|
+
ticketStatus: tickets.find(t => t.id === selectedTicketId)?.status
|
|
2520
|
+
}), /*#__PURE__*/_jsx(AIConsentDialog, {
|
|
2521
|
+
visible: showConsentDialog,
|
|
2522
|
+
provider: providerName,
|
|
2523
|
+
config: consentConfig,
|
|
2524
|
+
language: 'en',
|
|
2525
|
+
onConsent: async () => {
|
|
2526
|
+
await grantConsent();
|
|
2527
|
+
setShowConsentDialog(false);
|
|
2528
|
+
consentConfig.onConsent?.();
|
|
2529
|
+
logger.info('AIAgent', '✅ AI consent granted by user');
|
|
2530
|
+
},
|
|
2531
|
+
onDecline: () => {
|
|
2532
|
+
setShowConsentDialog(false);
|
|
2533
|
+
consentConfig.onDecline?.();
|
|
2534
|
+
logger.info('AIAgent', '❌ AI consent declined by user');
|
|
2535
|
+
}
|
|
2536
|
+
})]
|
|
2537
|
+
}), Platform.OS === 'android' && /*#__PURE__*/_jsxs(_Fragment, {
|
|
2538
|
+
children: [/*#__PURE__*/_jsx(SupportChatModal, {
|
|
2539
|
+
visible: mode === 'human' && !!selectedTicketId,
|
|
2540
|
+
messages: supportMessages,
|
|
2541
|
+
onSend: handleSend,
|
|
2542
|
+
onClose: handleBackToTickets,
|
|
2543
|
+
isAgentTyping: isLiveAgentTyping,
|
|
2544
|
+
isThinking: isThinking,
|
|
2545
|
+
scrollToEndTrigger: chatScrollTrigger,
|
|
2546
|
+
ticketStatus: tickets.find(t => t.id === selectedTicketId)?.status
|
|
2547
|
+
}), /*#__PURE__*/_jsx(FloatingOverlayWrapper, {
|
|
2548
|
+
fallbackStyle: styles.floatingLayer,
|
|
2549
|
+
children: /*#__PURE__*/_jsx(AIConsentDialog, {
|
|
2240
2550
|
visible: showConsentDialog,
|
|
2241
2551
|
provider: providerName,
|
|
2242
2552
|
config: consentConfig,
|
|
@@ -2252,8 +2562,8 @@ export function AIAgent({
|
|
|
2252
2562
|
consentConfig.onDecline?.();
|
|
2253
2563
|
logger.info('AIAgent', '❌ AI consent declined by user');
|
|
2254
2564
|
}
|
|
2255
|
-
})
|
|
2256
|
-
})
|
|
2565
|
+
})
|
|
2566
|
+
})]
|
|
2257
2567
|
})]
|
|
2258
2568
|
})
|
|
2259
2569
|
});
|