@mobileai/react-native 0.9.27 → 0.9.28
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 +24 -11
- 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/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 +405 -168
- 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 +103 -1
- package/lib/module/core/FiberTreeWalker.js +98 -0
- 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 +45 -35
- 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 +9 -0
- 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 +35 -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/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 +5 -2
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
- package/ios/MobileAIPilotIntents.swift +0 -51
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Does not block underlying UI natively.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useState, useRef, useEffect } from 'react';
|
|
9
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
10
10
|
import { View, TextInput, Pressable, Text, StyleSheet, Animated, PanResponder, ScrollView, Keyboard, Platform, useWindowDimensions } from 'react-native';
|
|
11
11
|
import { MicIcon, SpeakerIcon, SendArrowIcon, StopIcon, LoadingDots, AIBadge, HistoryIcon, NewChatIcon, CloseIcon } from "./Icons.js";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
@@ -170,6 +170,7 @@ function TextInputRow({
|
|
|
170
170
|
text,
|
|
171
171
|
setText,
|
|
172
172
|
onSend,
|
|
173
|
+
onCancel,
|
|
173
174
|
isThinking,
|
|
174
175
|
isArabic,
|
|
175
176
|
theme
|
|
@@ -182,6 +183,14 @@ function TextInputRow({
|
|
|
182
183
|
// render batch.
|
|
183
184
|
inputRef.current?.clear();
|
|
184
185
|
};
|
|
186
|
+
const handlePrimaryAction = () => {
|
|
187
|
+
if (isThinking) {
|
|
188
|
+
onCancel?.();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!text.trim()) return;
|
|
192
|
+
handleSendWithClear();
|
|
193
|
+
};
|
|
185
194
|
return /*#__PURE__*/_jsxs(View, {
|
|
186
195
|
style: styles.inputRow,
|
|
187
196
|
children: [/*#__PURE__*/_jsx(TextInput, {
|
|
@@ -208,10 +217,10 @@ function TextInputRow({
|
|
|
208
217
|
style: [styles.sendButton, isThinking && styles.sendButtonDisabled, theme?.primaryColor ? {
|
|
209
218
|
backgroundColor: theme.primaryColor
|
|
210
219
|
} : undefined],
|
|
211
|
-
onPress:
|
|
212
|
-
disabled: isThinking
|
|
213
|
-
accessibilityLabel:
|
|
214
|
-
children: isThinking ? /*#__PURE__*/_jsx(
|
|
220
|
+
onPress: handlePrimaryAction,
|
|
221
|
+
disabled: !isThinking && !text.trim(),
|
|
222
|
+
accessibilityLabel: isThinking ? 'Stop AI Agent request' : 'Send request to AI Agent',
|
|
223
|
+
children: isThinking ? /*#__PURE__*/_jsx(StopIcon, {
|
|
215
224
|
size: 18,
|
|
216
225
|
color: theme?.textColor || '#fff'
|
|
217
226
|
}) : /*#__PURE__*/_jsx(SendArrowIcon, {
|
|
@@ -288,6 +297,7 @@ function VoiceControlsRow({
|
|
|
288
297
|
|
|
289
298
|
export function AgentChatBar({
|
|
290
299
|
onSend,
|
|
300
|
+
onCancel,
|
|
291
301
|
isThinking,
|
|
292
302
|
statusText,
|
|
293
303
|
lastResult,
|
|
@@ -318,7 +328,10 @@ export function AgentChatBar({
|
|
|
318
328
|
onConversationSelect,
|
|
319
329
|
onNewConversation,
|
|
320
330
|
pendingApprovalQuestion,
|
|
321
|
-
onPendingApprovalAction
|
|
331
|
+
onPendingApprovalAction,
|
|
332
|
+
renderMode = 'default',
|
|
333
|
+
onWindowMetricsChange,
|
|
334
|
+
windowMetrics
|
|
322
335
|
}) {
|
|
323
336
|
const [text, setText] = useState('');
|
|
324
337
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
@@ -336,6 +349,85 @@ export function AgentChatBar({
|
|
|
336
349
|
const preKeyboardYRef = useRef(null);
|
|
337
350
|
const previousThinkingRef = useRef(false);
|
|
338
351
|
const autoCollapsedForThinkingRef = useRef(false);
|
|
352
|
+
const dragOriginRef = useRef({
|
|
353
|
+
x: 0,
|
|
354
|
+
y: 0
|
|
355
|
+
});
|
|
356
|
+
const panPositionRef = useRef({
|
|
357
|
+
x: 10,
|
|
358
|
+
y: height - 200
|
|
359
|
+
});
|
|
360
|
+
const panelHeightRef = useRef(0);
|
|
361
|
+
const isExpandedRef = useRef(false);
|
|
362
|
+
const isAndroidNativeWindow = renderMode === 'android-native-window';
|
|
363
|
+
const metricsFrameRef = useRef(null);
|
|
364
|
+
const pendingMetricsRef = useRef(null);
|
|
365
|
+
const getExpandedWindowHeight = useCallback(measuredPanelHeight => {
|
|
366
|
+
const minExpandedHeight = mode === 'voice' ? 150 : showHistory ? 280 : mode === 'human' ? 240 : pendingApprovalQuestion ? 220 : 164;
|
|
367
|
+
const maxExpandedHeight = Math.min(height * 0.65, 520);
|
|
368
|
+
const naturalHeight = measuredPanelHeight > 0 ? measuredPanelHeight : minExpandedHeight;
|
|
369
|
+
return Math.max(minExpandedHeight, Math.min(naturalHeight, maxExpandedHeight));
|
|
370
|
+
}, [height, mode, showHistory]);
|
|
371
|
+
const getWindowSize = useCallback((expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
|
|
372
|
+
return {
|
|
373
|
+
width: expanded ? 340 : 60,
|
|
374
|
+
height: expanded ? getExpandedWindowHeight(measuredPanelHeight) : 60
|
|
375
|
+
};
|
|
376
|
+
}, [getExpandedWindowHeight]);
|
|
377
|
+
const clampWindowPosition = useCallback((x, y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
|
|
378
|
+
const screenInset = 10;
|
|
379
|
+
const bottomInset = 24;
|
|
380
|
+
const {
|
|
381
|
+
width: windowWidth,
|
|
382
|
+
height: windowHeight
|
|
383
|
+
} = getWindowSize(expanded, measuredPanelHeight);
|
|
384
|
+
const maxX = Math.max(screenInset, width - windowWidth - screenInset);
|
|
385
|
+
const maxY = Math.max(screenInset, height - windowHeight - bottomInset);
|
|
386
|
+
return {
|
|
387
|
+
x: Math.min(Math.max(x, screenInset), maxX),
|
|
388
|
+
y: Math.min(Math.max(y, screenInset), maxY)
|
|
389
|
+
};
|
|
390
|
+
}, [getWindowSize, height, width]);
|
|
391
|
+
const publishWindowMetrics = useCallback((x = panPositionRef.current.x, y = panPositionRef.current.y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
|
|
392
|
+
if (!onWindowMetricsChange) return;
|
|
393
|
+
const {
|
|
394
|
+
x: clampedX,
|
|
395
|
+
y: clampedY
|
|
396
|
+
} = clampWindowPosition(x, y, expanded, measuredPanelHeight);
|
|
397
|
+
const {
|
|
398
|
+
width: resolvedWidth,
|
|
399
|
+
height: resolvedHeight
|
|
400
|
+
} = getWindowSize(expanded, measuredPanelHeight);
|
|
401
|
+
const nextMetrics = {
|
|
402
|
+
x: Math.round(clampedX),
|
|
403
|
+
y: Math.round(clampedY),
|
|
404
|
+
width: resolvedWidth,
|
|
405
|
+
height: Math.round(resolvedHeight)
|
|
406
|
+
};
|
|
407
|
+
onWindowMetricsChange(nextMetrics);
|
|
408
|
+
}, [clampWindowPosition, getWindowSize, onWindowMetricsChange]);
|
|
409
|
+
const publishNativeWindowPosition = useCallback((x, y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
|
|
410
|
+
const clampedPosition = clampWindowPosition(x, y, expanded, measuredPanelHeight);
|
|
411
|
+
panPositionRef.current = clampedPosition;
|
|
412
|
+
publishWindowMetrics(clampedPosition.x, clampedPosition.y, expanded, measuredPanelHeight);
|
|
413
|
+
}, [clampWindowPosition, publishWindowMetrics]);
|
|
414
|
+
const scheduleWindowMetricsPublish = useCallback((x = panPositionRef.current.x, y = panPositionRef.current.y, expanded = isExpandedRef.current, measuredPanelHeight = panelHeightRef.current) => {
|
|
415
|
+
if (!onWindowMetricsChange) return;
|
|
416
|
+
pendingMetricsRef.current = {
|
|
417
|
+
x,
|
|
418
|
+
y,
|
|
419
|
+
expanded,
|
|
420
|
+
measuredPanelHeight
|
|
421
|
+
};
|
|
422
|
+
if (metricsFrameRef.current != null) return;
|
|
423
|
+
metricsFrameRef.current = requestAnimationFrame(() => {
|
|
424
|
+
metricsFrameRef.current = null;
|
|
425
|
+
const pending = pendingMetricsRef.current;
|
|
426
|
+
pendingMetricsRef.current = null;
|
|
427
|
+
if (!pending) return;
|
|
428
|
+
publishWindowMetrics(pending.x, pending.y, pending.expanded, pending.measuredPanelHeight);
|
|
429
|
+
});
|
|
430
|
+
}, [onWindowMetricsChange, publishWindowMetrics]);
|
|
339
431
|
|
|
340
432
|
// Track incoming AI messages while collapsed
|
|
341
433
|
useEffect(() => {
|
|
@@ -350,6 +442,13 @@ export function AgentChatBar({
|
|
|
350
442
|
useEffect(() => {
|
|
351
443
|
if (autoExpandTrigger > 0) setIsExpanded(true);
|
|
352
444
|
}, [autoExpandTrigger]);
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
return () => {
|
|
447
|
+
if (metricsFrameRef.current != null) {
|
|
448
|
+
cancelAnimationFrame(metricsFrameRef.current);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}, []);
|
|
353
452
|
useEffect(() => {
|
|
354
453
|
const wasThinking = previousThinkingRef.current;
|
|
355
454
|
if (pendingApprovalQuestion) {
|
|
@@ -365,17 +464,54 @@ export function AgentChatBar({
|
|
|
365
464
|
x: 10,
|
|
366
465
|
y: height - 200
|
|
367
466
|
})).current;
|
|
467
|
+
useEffect(() => {
|
|
468
|
+
if (!isAndroidNativeWindow || !windowMetrics) return;
|
|
469
|
+
const nextPosition = {
|
|
470
|
+
x: windowMetrics.x,
|
|
471
|
+
y: windowMetrics.y
|
|
472
|
+
};
|
|
473
|
+
if (panPositionRef.current.x === nextPosition.x && panPositionRef.current.y === nextPosition.y) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
panPositionRef.current = nextPosition;
|
|
477
|
+
pan.setValue(nextPosition);
|
|
478
|
+
}, [isAndroidNativeWindow, pan, windowMetrics]);
|
|
368
479
|
const tooltipSide = fabX < width / 2 ? 'right' : 'left';
|
|
480
|
+
const expandedContentMinHeight = getExpandedWindowHeight(0);
|
|
369
481
|
useEffect(() => {
|
|
370
|
-
|
|
482
|
+
isExpandedRef.current = isExpanded;
|
|
483
|
+
const clampedPosition = clampWindowPosition(panPositionRef.current.x, panPositionRef.current.y, isExpanded);
|
|
484
|
+
if (clampedPosition.x !== panPositionRef.current.x || clampedPosition.y !== panPositionRef.current.y) {
|
|
485
|
+
panPositionRef.current = clampedPosition;
|
|
486
|
+
pan.setValue(clampedPosition);
|
|
487
|
+
if (!isAndroidNativeWindow) {
|
|
488
|
+
setFabX(clampedPosition.x);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
scheduleWindowMetricsPublish(clampedPosition.x, clampedPosition.y, isExpanded);
|
|
492
|
+
}, [clampWindowPosition, isAndroidNativeWindow, isExpanded, pan, scheduleWindowMetricsPublish]);
|
|
493
|
+
useEffect(() => {
|
|
494
|
+
if (isAndroidNativeWindow) return;
|
|
495
|
+
const xListenerId = pan.x.addListener(({
|
|
496
|
+
value
|
|
497
|
+
}) => {
|
|
498
|
+
panPositionRef.current.x = value;
|
|
499
|
+
if (!isAndroidNativeWindow) {
|
|
500
|
+
setFabX(value);
|
|
501
|
+
}
|
|
502
|
+
scheduleWindowMetricsPublish(value, panPositionRef.current.y);
|
|
503
|
+
});
|
|
504
|
+
const yListenerId = pan.y.addListener(({
|
|
371
505
|
value
|
|
372
506
|
}) => {
|
|
373
|
-
|
|
507
|
+
panPositionRef.current.y = value;
|
|
508
|
+
scheduleWindowMetricsPublish(panPositionRef.current.x, value);
|
|
374
509
|
});
|
|
375
510
|
return () => {
|
|
376
|
-
pan.x.removeListener(
|
|
511
|
+
pan.x.removeListener(xListenerId);
|
|
512
|
+
pan.y.removeListener(yListenerId);
|
|
377
513
|
};
|
|
378
|
-
}, [pan.x]);
|
|
514
|
+
}, [isAndroidNativeWindow, pan.x, pan.y, scheduleWindowMetricsPublish]);
|
|
379
515
|
|
|
380
516
|
// ─── Keyboard Handling ──────────────────────────────────────
|
|
381
517
|
useEffect(() => {
|
|
@@ -384,6 +520,14 @@ export function AgentChatBar({
|
|
|
384
520
|
const keyboardMargin = 12;
|
|
385
521
|
const showSub = Keyboard.addListener(showEvent, e => {
|
|
386
522
|
if (!isExpanded || mode !== 'text' || panelHeight <= 0) return;
|
|
523
|
+
if (isAndroidNativeWindow) {
|
|
524
|
+
const currentY = panPositionRef.current.y;
|
|
525
|
+
const targetY = Math.max(keyboardMargin, height - e.endCoordinates.height - panelHeight - keyboardMargin);
|
|
526
|
+
preKeyboardYRef.current = currentY;
|
|
527
|
+
if (currentY <= targetY) return;
|
|
528
|
+
publishNativeWindowPosition(panPositionRef.current.x, targetY);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
387
531
|
pan.y.stopAnimation(currentY => {
|
|
388
532
|
const targetY = Math.max(keyboardMargin, height - e.endCoordinates.height - panelHeight - keyboardMargin);
|
|
389
533
|
|
|
@@ -403,6 +547,10 @@ export function AgentChatBar({
|
|
|
403
547
|
const restoreY = preKeyboardYRef.current;
|
|
404
548
|
if (restoreY == null) return;
|
|
405
549
|
preKeyboardYRef.current = null;
|
|
550
|
+
if (isAndroidNativeWindow) {
|
|
551
|
+
publishNativeWindowPosition(panPositionRef.current.x, restoreY);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
406
554
|
Animated.timing(pan.y, {
|
|
407
555
|
toValue: restoreY,
|
|
408
556
|
duration: 200,
|
|
@@ -413,31 +561,58 @@ export function AgentChatBar({
|
|
|
413
561
|
showSub.remove();
|
|
414
562
|
hideSub.remove();
|
|
415
563
|
};
|
|
416
|
-
}, [height, isExpanded, mode, pan.y, panelHeight]);
|
|
417
|
-
const panResponder =
|
|
564
|
+
}, [height, isAndroidNativeWindow, isExpanded, mode, pan.y, panelHeight, publishNativeWindowPosition]);
|
|
565
|
+
const panResponder = useMemo(() => PanResponder.create({
|
|
418
566
|
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
419
567
|
return Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5;
|
|
420
568
|
},
|
|
421
569
|
onPanResponderGrant: () => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
570
|
+
if (isAndroidNativeWindow) {
|
|
571
|
+
dragOriginRef.current = {
|
|
572
|
+
...panPositionRef.current
|
|
573
|
+
};
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
pan.extractOffset();
|
|
426
577
|
pan.setValue({
|
|
427
578
|
x: 0,
|
|
428
579
|
y: 0
|
|
429
580
|
});
|
|
430
581
|
},
|
|
431
|
-
onPanResponderMove:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
582
|
+
onPanResponderMove: (event, gestureState) => {
|
|
583
|
+
if (isAndroidNativeWindow) {
|
|
584
|
+
publishNativeWindowPosition(dragOriginRef.current.x + gestureState.dx, dragOriginRef.current.y + gestureState.dy);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
Animated.event([null, {
|
|
588
|
+
dx: pan.x,
|
|
589
|
+
dy: pan.y
|
|
590
|
+
}], {
|
|
591
|
+
useNativeDriver: false
|
|
592
|
+
})(event, gestureState);
|
|
593
|
+
},
|
|
437
594
|
onPanResponderRelease: () => {
|
|
595
|
+
if (isAndroidNativeWindow) {
|
|
596
|
+
publishNativeWindowPosition(panPositionRef.current.x, panPositionRef.current.y);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
438
599
|
pan.flattenOffset();
|
|
600
|
+
const clampedPosition = clampWindowPosition(panPositionRef.current.x, panPositionRef.current.y);
|
|
601
|
+
Animated.spring(pan, {
|
|
602
|
+
toValue: clampedPosition,
|
|
603
|
+
useNativeDriver: false,
|
|
604
|
+
tension: 120,
|
|
605
|
+
friction: 14
|
|
606
|
+
}).start(() => {
|
|
607
|
+
panPositionRef.current = clampedPosition;
|
|
608
|
+
if (!isAndroidNativeWindow) {
|
|
609
|
+
setFabX(clampedPosition.x);
|
|
610
|
+
}
|
|
611
|
+
scheduleWindowMetricsPublish(clampedPosition.x, clampedPosition.y);
|
|
612
|
+
});
|
|
439
613
|
}
|
|
440
|
-
}))
|
|
614
|
+
}), [clampWindowPosition, isAndroidNativeWindow, pan, publishNativeWindowPosition, scheduleWindowMetricsPublish]);
|
|
615
|
+
const jsDragHandlers = isAndroidNativeWindow ? undefined : panResponder.panHandlers;
|
|
441
616
|
const handleSend = () => {
|
|
442
617
|
if (text.trim() && !isThinking) {
|
|
443
618
|
onSend(text.trim());
|
|
@@ -452,8 +627,8 @@ export function AgentChatBar({
|
|
|
452
627
|
|
|
453
628
|
if (!isExpanded) {
|
|
454
629
|
return /*#__PURE__*/_jsxs(Animated.View, {
|
|
455
|
-
style: [styles.fabContainer, pan.getLayout()],
|
|
456
|
-
...
|
|
630
|
+
style: [styles.fabContainer, isAndroidNativeWindow ? styles.fabContainerNativeWindow : pan.getLayout()],
|
|
631
|
+
...(jsDragHandlers ?? {}),
|
|
457
632
|
children: [/*#__PURE__*/_jsx(Pressable, {
|
|
458
633
|
style: [styles.fab, theme?.primaryColor ? {
|
|
459
634
|
backgroundColor: theme.primaryColor
|
|
@@ -471,13 +646,13 @@ export function AgentChatBar({
|
|
|
471
646
|
}) : /*#__PURE__*/_jsx(AIBadge, {
|
|
472
647
|
size: 28
|
|
473
648
|
})
|
|
474
|
-
}), showDiscoveryTooltip && /*#__PURE__*/_jsx(DiscoveryTooltip, {
|
|
649
|
+
}), showDiscoveryTooltip && !isAndroidNativeWindow && /*#__PURE__*/_jsx(DiscoveryTooltip, {
|
|
475
650
|
language: language,
|
|
476
651
|
primaryColor: theme?.primaryColor,
|
|
477
652
|
message: discoveryTooltipMessage,
|
|
478
653
|
side: tooltipSide,
|
|
479
654
|
onDismiss: () => onTooltipDismiss?.()
|
|
480
|
-
}), localUnread > 0 && chatMessages.length > 0 && /*#__PURE__*/_jsxs(Pressable, {
|
|
655
|
+
}), localUnread > 0 && chatMessages.length > 0 && !isAndroidNativeWindow && /*#__PURE__*/_jsxs(Pressable, {
|
|
481
656
|
style: [styles.unreadPopup, isArabic ? styles.unreadPopupRTL : styles.unreadPopupLTR],
|
|
482
657
|
onPress: () => {
|
|
483
658
|
onTooltipDismiss?.();
|
|
@@ -503,7 +678,7 @@ export function AgentChatBar({
|
|
|
503
678
|
children: displayUnread > 99 ? '99+' : displayUnread
|
|
504
679
|
})
|
|
505
680
|
})]
|
|
506
|
-
}), isThinking && !pendingApprovalQuestion && /*#__PURE__*/_jsxs(Pressable, {
|
|
681
|
+
}), isThinking && !pendingApprovalQuestion && !isAndroidNativeWindow && /*#__PURE__*/_jsxs(Pressable, {
|
|
507
682
|
style: [styles.statusPopup, isArabic ? styles.unreadPopupRTL : styles.unreadPopupLTR],
|
|
508
683
|
onPress: () => {
|
|
509
684
|
autoCollapsedForThinkingRef.current = false;
|
|
@@ -533,7 +708,9 @@ export function AgentChatBar({
|
|
|
533
708
|
// ─── Expanded Widget ───────────────────────────────────────
|
|
534
709
|
|
|
535
710
|
return /*#__PURE__*/_jsxs(Animated.View, {
|
|
536
|
-
style: [styles.expandedContainer, pan.getLayout(), {
|
|
711
|
+
style: [styles.expandedContainer, isAndroidNativeWindow ? styles.expandedContainerNativeWindow : pan.getLayout(), isAndroidNativeWindow ? {
|
|
712
|
+
minHeight: expandedContentMinHeight
|
|
713
|
+
} : null, {
|
|
537
714
|
maxHeight: height * 0.65
|
|
538
715
|
}, theme?.backgroundColor ? {
|
|
539
716
|
backgroundColor: theme.backgroundColor
|
|
@@ -541,11 +718,13 @@ export function AgentChatBar({
|
|
|
541
718
|
onLayout: event => {
|
|
542
719
|
const nextHeight = event.nativeEvent.layout.height;
|
|
543
720
|
if (Math.abs(nextHeight - panelHeight) > 1) {
|
|
721
|
+
panelHeightRef.current = nextHeight;
|
|
544
722
|
setPanelHeight(nextHeight);
|
|
723
|
+
scheduleWindowMetricsPublish(panPositionRef.current.x, panPositionRef.current.y, true, nextHeight);
|
|
545
724
|
}
|
|
546
725
|
},
|
|
547
726
|
children: [/*#__PURE__*/_jsx(View, {
|
|
548
|
-
...
|
|
727
|
+
...(jsDragHandlers ?? {}),
|
|
549
728
|
style: styles.dragHandleArea,
|
|
550
729
|
accessibilityLabel: "Drag AI Agent",
|
|
551
730
|
children: /*#__PURE__*/_jsx(View, {
|
|
@@ -589,34 +768,36 @@ export function AgentChatBar({
|
|
|
589
768
|
children: "+"
|
|
590
769
|
})
|
|
591
770
|
})]
|
|
592
|
-
}), /*#__PURE__*/
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
771
|
+
}), !isAndroidNativeWindow && /*#__PURE__*/_jsxs(_Fragment, {
|
|
772
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
773
|
+
...panResponder.panHandlers,
|
|
774
|
+
style: [styles.cornerHandle, styles.cornerTL],
|
|
775
|
+
pointerEvents: "box-only",
|
|
776
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
777
|
+
style: [styles.cornerIndicator, styles.cornerIndicatorTL]
|
|
778
|
+
})
|
|
779
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
780
|
+
...panResponder.panHandlers,
|
|
781
|
+
style: [styles.cornerHandle, styles.cornerTR],
|
|
782
|
+
pointerEvents: "box-only",
|
|
783
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
784
|
+
style: [styles.cornerIndicator, styles.cornerIndicatorTR]
|
|
785
|
+
})
|
|
786
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
787
|
+
...panResponder.panHandlers,
|
|
788
|
+
style: [styles.cornerHandle, styles.cornerBL],
|
|
789
|
+
pointerEvents: "box-only",
|
|
790
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
791
|
+
style: [styles.cornerIndicator, styles.cornerIndicatorBL]
|
|
792
|
+
})
|
|
793
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
794
|
+
...panResponder.panHandlers,
|
|
795
|
+
style: [styles.cornerHandle, styles.cornerBR],
|
|
796
|
+
pointerEvents: "box-only",
|
|
797
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
798
|
+
style: [styles.cornerIndicator, styles.cornerIndicatorBR]
|
|
799
|
+
})
|
|
800
|
+
})]
|
|
620
801
|
}), !showHistory && /*#__PURE__*/_jsx(ModeSelector, {
|
|
621
802
|
modes: availableModes,
|
|
622
803
|
activeMode: mode,
|
|
@@ -811,6 +992,7 @@ export function AgentChatBar({
|
|
|
811
992
|
text: text,
|
|
812
993
|
setText: setText,
|
|
813
994
|
onSend: handleSend,
|
|
995
|
+
onCancel: onCancel,
|
|
814
996
|
isThinking: isThinking,
|
|
815
997
|
isArabic: isArabic,
|
|
816
998
|
theme: theme
|
|
@@ -869,6 +1051,9 @@ const styles = StyleSheet.create({
|
|
|
869
1051
|
position: 'absolute',
|
|
870
1052
|
zIndex: 9999
|
|
871
1053
|
},
|
|
1054
|
+
fabContainerNativeWindow: {
|
|
1055
|
+
position: 'relative'
|
|
1056
|
+
},
|
|
872
1057
|
fab: {
|
|
873
1058
|
width: 60,
|
|
874
1059
|
height: 60,
|
|
@@ -970,6 +1155,9 @@ const styles = StyleSheet.create({
|
|
|
970
1155
|
shadowOpacity: 0.4,
|
|
971
1156
|
shadowRadius: 10
|
|
972
1157
|
},
|
|
1158
|
+
expandedContainerNativeWindow: {
|
|
1159
|
+
position: 'relative'
|
|
1160
|
+
},
|
|
973
1161
|
dragHandleArea: {
|
|
974
1162
|
width: '100%',
|
|
975
1163
|
height: 30,
|
|
@@ -1052,7 +1240,8 @@ const styles = StyleSheet.create({
|
|
|
1052
1240
|
borderBottomRightRadius: 1
|
|
1053
1241
|
},
|
|
1054
1242
|
messageList: {
|
|
1055
|
-
marginBottom: 12
|
|
1243
|
+
marginBottom: 12,
|
|
1244
|
+
flexShrink: 1
|
|
1056
1245
|
},
|
|
1057
1246
|
messageBubble: {
|
|
1058
1247
|
padding: 12,
|
|
@@ -1094,6 +1283,8 @@ const styles = StyleSheet.create({
|
|
|
1094
1283
|
alignItems: 'flex-end',
|
|
1095
1284
|
gap: 8,
|
|
1096
1285
|
justifyContent: 'center',
|
|
1286
|
+
minHeight: 48,
|
|
1287
|
+
flexShrink: 0,
|
|
1097
1288
|
paddingBottom: 2 // Slight padding so buttons don't clip against bottom edge
|
|
1098
1289
|
},
|
|
1099
1290
|
approvalPanel: {
|
|
@@ -11,13 +11,10 @@
|
|
|
11
11
|
* Renders ABOVE all native Modals, system alerts, and navigation chrome.
|
|
12
12
|
* 2. Falls back to plain View if react-native-screens is not installed.
|
|
13
13
|
*
|
|
14
|
-
* Android
|
|
15
|
-
* 1.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* No SYSTEM_ALERT_WINDOW permission needed — scoped to app's own window.
|
|
19
|
-
* 2. Falls back to plain View if the app hasn't been rebuilt after install
|
|
20
|
-
* (graceful degradation with DEV warning).
|
|
14
|
+
* Android:
|
|
15
|
+
* 1. Uses a native panel dialog window when explicit bounds are provided.
|
|
16
|
+
* This keeps the floating agent compact and above native modal surfaces.
|
|
17
|
+
* 2. Falls back to a plain View otherwise.
|
|
21
18
|
*
|
|
22
19
|
* Usage:
|
|
23
20
|
* <FloatingOverlayWrapper fallbackStyle={styles.floatingLayer}>
|
|
@@ -31,14 +28,14 @@
|
|
|
31
28
|
* but passing StyleSheet.absoluteFill is often necessary to prevent dimensions collapsing conditionally.
|
|
32
29
|
*/
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
32
|
+
import { DeviceEventEmitter, Platform, View, StyleSheet, findNodeHandle } from 'react-native';
|
|
35
33
|
|
|
36
34
|
// ─── iOS: FullWindowOverlay (react-native-screens optional peer dep) ──────────
|
|
37
|
-
|
|
35
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
38
36
|
let FullWindowOverlay = null;
|
|
39
37
|
if (Platform.OS === 'ios') {
|
|
40
38
|
try {
|
|
41
|
-
// Literal string required by Metro bundler — try/catch handles MODULE_NOT_FOUND
|
|
42
39
|
const screens = require('react-native-screens');
|
|
43
40
|
FullWindowOverlay = screens.FullWindowOverlay ?? null;
|
|
44
41
|
} catch {
|
|
@@ -46,43 +43,66 @@ if (Platform.OS === 'ios') {
|
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
// ───
|
|
46
|
+
// ─── Export: whether a true elevated overlay is active ───────────────────────
|
|
50
47
|
|
|
51
48
|
let NativeFloatingOverlay = null;
|
|
52
49
|
if (Platform.OS === 'android') {
|
|
53
50
|
try {
|
|
54
|
-
|
|
55
|
-
requireNativeComponent
|
|
56
|
-
} = require('react-native');
|
|
57
|
-
// Throws if ViewManager is not registered (app hasn't been rebuilt after install)
|
|
58
|
-
NativeFloatingOverlay = requireNativeComponent('MobileAIFloatingOverlay');
|
|
51
|
+
NativeFloatingOverlay = require('../specs/FloatingOverlayNativeComponent.ts').default;
|
|
59
52
|
} catch {
|
|
60
|
-
|
|
61
|
-
console.warn('[MobileAI] MobileAIFloatingOverlay native module not found on Android.\n' + 'The chat bar may appear behind native Modals.\n' + 'Fix: rebuild the app with `npx react-native run-android` or `npx expo run:android`.');
|
|
62
|
-
}
|
|
53
|
+
// Falls back to a plain View when the native Android overlay isn't available.
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
// ─── Export: whether a true elevated overlay is active ───────────────────────
|
|
67
|
-
|
|
68
57
|
/**
|
|
69
58
|
* True when a native elevated overlay is available on the current platform.
|
|
70
59
|
* Used by AIConsentDialog to decide whether to render as View vs Modal.
|
|
71
60
|
*
|
|
72
61
|
* iOS + react-native-screens installed → true
|
|
73
|
-
* Android + native rebuild done → true
|
|
74
62
|
* Everything else (fallback) → false
|
|
75
63
|
*/
|
|
76
|
-
export const isNativeOverlayActive = Platform.OS === 'ios' && !!FullWindowOverlay
|
|
64
|
+
export const isNativeOverlayActive = Platform.OS === 'ios' && !!FullWindowOverlay;
|
|
65
|
+
const ANDROID_WINDOW_DRAG_END_EVENT = 'mobileaiFloatingOverlayDragEnd';
|
|
77
66
|
|
|
78
67
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
79
68
|
|
|
80
|
-
|
|
81
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
82
|
-
export function FloatingOverlayWrapper({
|
|
69
|
+
export const FloatingOverlayWrapper = /*#__PURE__*/forwardRef(function FloatingOverlayWrapper({
|
|
83
70
|
children,
|
|
71
|
+
androidWindowMetrics,
|
|
72
|
+
onAndroidWindowDragEnd,
|
|
84
73
|
fallbackStyle
|
|
85
|
-
}) {
|
|
74
|
+
}, ref) {
|
|
75
|
+
const nativeOverlayRef = useRef(null);
|
|
76
|
+
useImperativeHandle(ref, () => ({
|
|
77
|
+
setAndroidWindowMetrics: metrics => {
|
|
78
|
+
nativeOverlayRef.current?.setNativeProps?.({
|
|
79
|
+
windowX: metrics.x,
|
|
80
|
+
windowY: metrics.y,
|
|
81
|
+
windowWidth: metrics.width,
|
|
82
|
+
windowHeight: metrics.height
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}), []);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (Platform.OS !== 'android' || !onAndroidWindowDragEnd) return;
|
|
88
|
+
const subscription = DeviceEventEmitter.addListener(ANDROID_WINDOW_DRAG_END_EVENT, event => {
|
|
89
|
+
const nativeHandle = findNodeHandle(nativeOverlayRef.current);
|
|
90
|
+
if (nativeHandle != null && event.viewId != null && event.viewId !== nativeHandle) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof event.x !== 'number' || typeof event.y !== 'number' || typeof event.width !== 'number' || typeof event.height !== 'number') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
onAndroidWindowDragEnd({
|
|
97
|
+
x: event.x,
|
|
98
|
+
y: event.y,
|
|
99
|
+
width: event.width,
|
|
100
|
+
height: event.height
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return () => subscription.remove();
|
|
104
|
+
}, [onAndroidWindowDragEnd]);
|
|
105
|
+
|
|
86
106
|
// iOS: FullWindowOverlay — separate UIWindow above everything
|
|
87
107
|
if (Platform.OS === 'ios' && FullWindowOverlay) {
|
|
88
108
|
// @ts-ignore - Some versions of react-native-screens don't type 'style'
|
|
@@ -91,10 +111,15 @@ export function FloatingOverlayWrapper({
|
|
|
91
111
|
children: children
|
|
92
112
|
});
|
|
93
113
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
if (Platform.OS === 'android' && NativeFloatingOverlay && androidWindowMetrics) {
|
|
115
|
+
const NativeFloatingOverlayComponent = NativeFloatingOverlay;
|
|
116
|
+
return /*#__PURE__*/_jsx(NativeFloatingOverlayComponent, {
|
|
117
|
+
ref: nativeOverlayRef,
|
|
118
|
+
style: styles.androidAnchor,
|
|
119
|
+
windowX: androidWindowMetrics.x,
|
|
120
|
+
windowY: androidWindowMetrics.y,
|
|
121
|
+
windowWidth: androidWindowMetrics.width,
|
|
122
|
+
windowHeight: androidWindowMetrics.height,
|
|
98
123
|
children: children
|
|
99
124
|
});
|
|
100
125
|
}
|
|
@@ -102,7 +127,18 @@ export function FloatingOverlayWrapper({
|
|
|
102
127
|
// Fallback: regular View — same behavior as before this overlay feature
|
|
103
128
|
return /*#__PURE__*/_jsx(View, {
|
|
104
129
|
style: fallbackStyle,
|
|
130
|
+
pointerEvents: "box-none",
|
|
105
131
|
children: children
|
|
106
132
|
});
|
|
107
|
-
}
|
|
133
|
+
});
|
|
134
|
+
FloatingOverlayWrapper.displayName = 'FloatingOverlayWrapper';
|
|
135
|
+
const styles = StyleSheet.create({
|
|
136
|
+
androidAnchor: {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
top: 0,
|
|
139
|
+
left: 0,
|
|
140
|
+
width: 1,
|
|
141
|
+
height: 1
|
|
142
|
+
}
|
|
143
|
+
});
|
|
108
144
|
//# sourceMappingURL=FloatingOverlayWrapper.js.map
|
|
@@ -10,8 +10,29 @@
|
|
|
10
10
|
* to route telemetry through your own backend without touching this file.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import { Platform } from 'react-native';
|
|
14
|
+
function resolveMobileAIBase() {
|
|
15
|
+
const configuredBase = process.env.EXPO_PUBLIC_MOBILEAI_BASE_URL || process.env.NEXT_PUBLIC_MOBILEAI_BASE_URL || 'https://mobileai.cloud';
|
|
16
|
+
|
|
17
|
+
// Android emulators cannot reach the host machine via localhost/127.0.0.1.
|
|
18
|
+
// Translate those hostnames to 10.0.2.2 so the Expo example can talk to the
|
|
19
|
+
// local dashboard/backend without affecting iOS.
|
|
20
|
+
if (Platform.OS === 'android') {
|
|
21
|
+
return configuredBase.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, 'http://10.0.2.2');
|
|
22
|
+
}
|
|
23
|
+
return configuredBase;
|
|
24
|
+
}
|
|
25
|
+
const MOBILEAI_BASE = resolveMobileAIBase();
|
|
26
|
+
function toWebSocketBase(url) {
|
|
27
|
+
if (url.startsWith('https://')) return `wss://${url.slice('https://'.length)}`;
|
|
28
|
+
if (url.startsWith('http://')) return `ws://${url.slice('http://'.length)}`;
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
14
31
|
export const ENDPOINTS = {
|
|
32
|
+
/** Hosted MobileAI text proxy — used by default when analyticsKey is set */
|
|
33
|
+
hostedTextProxy: `${MOBILEAI_BASE}/api/v1/hosted-proxy/text`,
|
|
34
|
+
/** Hosted MobileAI voice proxy — used by default when analyticsKey is set */
|
|
35
|
+
hostedVoiceProxy: `${toWebSocketBase(MOBILEAI_BASE)}/ws/hosted-proxy/voice`,
|
|
15
36
|
/** Telemetry event ingest — receives batched SDK events */
|
|
16
37
|
telemetryIngest: `${MOBILEAI_BASE}/api/v1/events`,
|
|
17
38
|
/** Feature flag sync — fetches remote flags for this analyticsKey */
|