@mobileai/react-native 0.9.17 → 0.9.19
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 -9
- package/react-native.config.js +12 -0
- package/lib/module/__cli_tmp__.js.map +0 -1
- package/lib/module/components/AIAgent.js.map +0 -1
- package/lib/module/components/AIZone.js.map +0 -1
- package/lib/module/components/AgentChatBar.js.map +0 -1
- package/lib/module/components/AgentErrorBoundary.js.map +0 -1
- package/lib/module/components/AgentOverlay.js.map +0 -1
- package/lib/module/components/DiscoveryTooltip.js.map +0 -1
- package/lib/module/components/HighlightOverlay.js.map +0 -1
- package/lib/module/components/Icons.js.map +0 -1
- package/lib/module/components/ProactiveHint.js.map +0 -1
- package/lib/module/components/cards/InfoCard.js.map +0 -1
- package/lib/module/components/cards/ReviewSummary.js.map +0 -1
- package/lib/module/config/endpoints.js.map +0 -1
- package/lib/module/core/ActionRegistry.js.map +0 -1
- package/lib/module/core/AgentRuntime.js.map +0 -1
- package/lib/module/core/FiberTreeWalker.js.map +0 -1
- package/lib/module/core/IdleDetector.js.map +0 -1
- package/lib/module/core/MCPBridge.js.map +0 -1
- package/lib/module/core/ScreenDehydrator.js.map +0 -1
- package/lib/module/core/ZoneRegistry.js.map +0 -1
- package/lib/module/core/systemPrompt.js.map +0 -1
- package/lib/module/core/types.js.map +0 -1
- package/lib/module/hooks/useAction.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/plugin/withAppIntents.js.map +0 -1
- package/lib/module/providers/GeminiProvider.js.map +0 -1
- package/lib/module/providers/OpenAIProvider.js.map +0 -1
- package/lib/module/providers/ProviderFactory.js.map +0 -1
- package/lib/module/services/AudioInputService.js.map +0 -1
- package/lib/module/services/AudioOutputService.js.map +0 -1
- package/lib/module/services/KnowledgeBaseService.js.map +0 -1
- package/lib/module/services/VoiceService.js.map +0 -1
- package/lib/module/services/flags/FlagService.js.map +0 -1
- package/lib/module/services/telemetry/MobileAI.js.map +0 -1
- package/lib/module/services/telemetry/PiiScrubber.js.map +0 -1
- package/lib/module/services/telemetry/TelemetryService.js.map +0 -1
- package/lib/module/services/telemetry/TouchAutoCapture.js.map +0 -1
- package/lib/module/services/telemetry/device.js.map +0 -1
- package/lib/module/services/telemetry/deviceMetadata.js.map +0 -1
- package/lib/module/services/telemetry/index.js.map +0 -1
- package/lib/module/services/telemetry/types.js.map +0 -1
- package/lib/module/support/CSATSurvey.js.map +0 -1
- package/lib/module/support/EscalationEventSource.js.map +0 -1
- package/lib/module/support/EscalationSocket.js.map +0 -1
- package/lib/module/support/SupportChatModal.js.map +0 -1
- package/lib/module/support/SupportGreeting.js.map +0 -1
- package/lib/module/support/TicketStore.js.map +0 -1
- package/lib/module/support/escalateTool.js.map +0 -1
- package/lib/module/support/index.js.map +0 -1
- package/lib/module/support/supportPrompt.js.map +0 -1
- package/lib/module/support/types.js.map +0 -1
- package/lib/module/tools/datePickerTool.js.map +0 -1
- package/lib/module/tools/guideTool.js.map +0 -1
- package/lib/module/tools/index.js.map +0 -1
- package/lib/module/tools/keyboardTool.js.map +0 -1
- package/lib/module/tools/longPressTool.js.map +0 -1
- package/lib/module/tools/pickerTool.js.map +0 -1
- package/lib/module/tools/restoreTool.js.map +0 -1
- package/lib/module/tools/scrollTool.js.map +0 -1
- package/lib/module/tools/simplifyTool.js.map +0 -1
- package/lib/module/tools/sliderTool.js.map +0 -1
- package/lib/module/tools/tapTool.js.map +0 -1
- package/lib/module/tools/typeTool.js.map +0 -1
- package/lib/module/tools/types.js.map +0 -1
- package/lib/module/types/jsx.d.js.map +0 -1
- package/lib/module/utils/audioUtils.js.map +0 -1
- package/lib/module/utils/logger.js.map +0 -1
- package/lib/typescript/babel.config.d.ts.map +0 -1
- package/lib/typescript/bin/generate-map.d.cts.map +0 -1
- package/lib/typescript/eslint.config.d.mts.map +0 -1
- package/lib/typescript/generate-map.d.ts.map +0 -1
- package/lib/typescript/src/__cli_tmp__.d.ts.map +0 -1
- package/lib/typescript/src/components/AIAgent.d.ts.map +0 -1
- package/lib/typescript/src/components/AIZone.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentErrorBoundary.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentOverlay.d.ts.map +0 -1
- package/lib/typescript/src/components/DiscoveryTooltip.d.ts.map +0 -1
- package/lib/typescript/src/components/HighlightOverlay.d.ts.map +0 -1
- package/lib/typescript/src/components/Icons.d.ts.map +0 -1
- package/lib/typescript/src/components/ProactiveHint.d.ts.map +0 -1
- package/lib/typescript/src/components/cards/InfoCard.d.ts.map +0 -1
- package/lib/typescript/src/components/cards/ReviewSummary.d.ts.map +0 -1
- package/lib/typescript/src/config/endpoints.d.ts.map +0 -1
- package/lib/typescript/src/core/ActionRegistry.d.ts.map +0 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +0 -1
- package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +0 -1
- package/lib/typescript/src/core/IdleDetector.d.ts.map +0 -1
- package/lib/typescript/src/core/MCPBridge.d.ts.map +0 -1
- package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +0 -1
- package/lib/typescript/src/core/ZoneRegistry.d.ts.map +0 -1
- package/lib/typescript/src/core/systemPrompt.d.ts.map +0 -1
- package/lib/typescript/src/core/types.d.ts.map +0 -1
- package/lib/typescript/src/hooks/useAction.d.ts.map +0 -1
- package/lib/typescript/src/index.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withAppIntents.d.ts.map +0 -1
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +0 -1
- package/lib/typescript/src/providers/OpenAIProvider.d.ts.map +0 -1
- package/lib/typescript/src/providers/ProviderFactory.d.ts.map +0 -1
- package/lib/typescript/src/services/AudioInputService.d.ts.map +0 -1
- package/lib/typescript/src/services/AudioOutputService.d.ts.map +0 -1
- package/lib/typescript/src/services/KnowledgeBaseService.d.ts.map +0 -1
- package/lib/typescript/src/services/VoiceService.d.ts.map +0 -1
- package/lib/typescript/src/services/flags/FlagService.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/MobileAI.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/PiiScrubber.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/device.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/index.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/types.d.ts.map +0 -1
- package/lib/typescript/src/support/CSATSurvey.d.ts.map +0 -1
- package/lib/typescript/src/support/EscalationEventSource.d.ts.map +0 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts.map +0 -1
- package/lib/typescript/src/support/SupportChatModal.d.ts.map +0 -1
- package/lib/typescript/src/support/SupportGreeting.d.ts.map +0 -1
- package/lib/typescript/src/support/TicketStore.d.ts.map +0 -1
- package/lib/typescript/src/support/escalateTool.d.ts.map +0 -1
- package/lib/typescript/src/support/index.d.ts.map +0 -1
- package/lib/typescript/src/support/supportPrompt.d.ts.map +0 -1
- package/lib/typescript/src/support/types.d.ts.map +0 -1
- package/lib/typescript/src/tools/datePickerTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/guideTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/index.d.ts.map +0 -1
- package/lib/typescript/src/tools/keyboardTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/longPressTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/pickerTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/restoreTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/scrollTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/simplifyTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/sliderTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/tapTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/typeTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/types.d.ts.map +0 -1
- package/lib/typescript/src/utils/audioUtils.d.ts.map +0 -1
- package/lib/typescript/src/utils/logger.d.ts.map +0 -1
- package/src/__cli_tmp__.tsx +0 -9
- package/src/cli/analyzers/chain-analyzer.ts +0 -183
- package/src/cli/extractors/ai-extractor.ts +0 -6
- package/src/cli/extractors/ast-extractor.ts +0 -551
- package/src/cli/generate-intents.ts +0 -140
- package/src/cli/generate-map.ts +0 -121
- package/src/cli/generate-swift.ts +0 -116
- package/src/cli/scanners/expo-scanner.ts +0 -203
- package/src/cli/scanners/rn-scanner.ts +0 -445
- package/src/components/AIAgent.tsx +0 -1716
- package/src/components/AIZone.tsx +0 -147
- package/src/components/AgentChatBar.tsx +0 -1143
- package/src/components/AgentErrorBoundary.tsx +0 -78
- package/src/components/AgentOverlay.tsx +0 -73
- package/src/components/DiscoveryTooltip.tsx +0 -148
- package/src/components/HighlightOverlay.tsx +0 -136
- package/src/components/Icons.tsx +0 -253
- package/src/components/ProactiveHint.tsx +0 -145
- package/src/components/cards/InfoCard.tsx +0 -58
- package/src/components/cards/ReviewSummary.tsx +0 -76
- package/src/config/endpoints.ts +0 -22
- package/src/core/ActionRegistry.ts +0 -105
- package/src/core/AgentRuntime.ts +0 -1471
- package/src/core/FiberTreeWalker.ts +0 -930
- package/src/core/IdleDetector.ts +0 -72
- package/src/core/MCPBridge.ts +0 -163
- package/src/core/ScreenDehydrator.ts +0 -53
- package/src/core/ZoneRegistry.ts +0 -44
- package/src/core/systemPrompt.ts +0 -431
- package/src/core/types.ts +0 -521
- package/src/hooks/useAction.ts +0 -182
- package/src/index.ts +0 -83
- package/src/plugin/withAppIntents.ts +0 -98
- package/src/providers/GeminiProvider.ts +0 -357
- package/src/providers/OpenAIProvider.ts +0 -379
- package/src/providers/ProviderFactory.ts +0 -36
- package/src/services/AudioInputService.ts +0 -226
- package/src/services/AudioOutputService.ts +0 -236
- package/src/services/KnowledgeBaseService.ts +0 -156
- package/src/services/VoiceService.ts +0 -451
- package/src/services/flags/FlagService.ts +0 -137
- package/src/services/telemetry/MobileAI.ts +0 -66
- package/src/services/telemetry/PiiScrubber.ts +0 -17
- package/src/services/telemetry/TelemetryService.ts +0 -323
- package/src/services/telemetry/TouchAutoCapture.ts +0 -165
- package/src/services/telemetry/device.ts +0 -93
- package/src/services/telemetry/deviceMetadata.ts +0 -13
- package/src/services/telemetry/index.ts +0 -13
- package/src/services/telemetry/types.ts +0 -75
- package/src/support/CSATSurvey.tsx +0 -304
- package/src/support/EscalationEventSource.ts +0 -190
- package/src/support/EscalationSocket.ts +0 -152
- package/src/support/SupportChatModal.tsx +0 -563
- package/src/support/SupportGreeting.tsx +0 -161
- package/src/support/TicketStore.ts +0 -100
- package/src/support/escalateTool.ts +0 -174
- package/src/support/index.ts +0 -29
- package/src/support/supportPrompt.ts +0 -55
- package/src/support/types.ts +0 -155
- package/src/tools/datePickerTool.ts +0 -60
- package/src/tools/guideTool.ts +0 -76
- package/src/tools/index.ts +0 -20
- package/src/tools/keyboardTool.ts +0 -30
- package/src/tools/longPressTool.ts +0 -61
- package/src/tools/pickerTool.ts +0 -115
- package/src/tools/restoreTool.ts +0 -33
- package/src/tools/scrollTool.ts +0 -156
- package/src/tools/simplifyTool.ts +0 -33
- package/src/tools/sliderTool.ts +0 -65
- package/src/tools/tapTool.ts +0 -93
- package/src/tools/typeTool.ts +0 -113
- package/src/tools/types.ts +0 -58
- package/src/types/jsx.d.ts +0 -20
- package/src/utils/audioUtils.ts +0 -54
- package/src/utils/logger.ts +0 -38
package/src/index.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @mobileai/react-native
|
|
3
|
-
*
|
|
4
|
-
* Zero-wrapper AI agent for React Native.
|
|
5
|
-
* Auto-detects interactive elements via React Fiber tree traversal.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// ─── Components ──────────────────────────────────────────────
|
|
9
|
-
export { AIAgent } from './components/AIAgent';
|
|
10
|
-
export { AIZone } from './components/AIZone';
|
|
11
|
-
// Built-in card templates for AIZone injection
|
|
12
|
-
// Note: displayName is set explicitly on each — required for minification-safe template lookup.
|
|
13
|
-
export { InfoCard } from './components/cards/InfoCard';
|
|
14
|
-
export { ReviewSummary } from './components/cards/ReviewSummary';
|
|
15
|
-
|
|
16
|
-
// ─── Providers ───────────────────────────────────────────────
|
|
17
|
-
export { GeminiProvider } from './providers/GeminiProvider';
|
|
18
|
-
export { OpenAIProvider } from './providers/OpenAIProvider';
|
|
19
|
-
export { createProvider } from './providers/ProviderFactory';
|
|
20
|
-
|
|
21
|
-
// ─── Hooks ───────────────────────────────────────────────────
|
|
22
|
-
export { useAction, useAI } from './hooks/useAction';
|
|
23
|
-
|
|
24
|
-
// ─── Services ────────────────────────────────────────────────
|
|
25
|
-
export { VoiceService } from './services/VoiceService';
|
|
26
|
-
export { AudioInputService } from './services/AudioInputService';
|
|
27
|
-
export { AudioOutputService } from './services/AudioOutputService';
|
|
28
|
-
export { KnowledgeBaseService } from './services/KnowledgeBaseService';
|
|
29
|
-
|
|
30
|
-
// ─── Analytics ───────────────────────────────────────────────
|
|
31
|
-
export { MobileAI } from './services/telemetry';
|
|
32
|
-
|
|
33
|
-
// ─── Utilities ───────────────────────────────────────────────
|
|
34
|
-
export { logger } from './utils/logger';
|
|
35
|
-
|
|
36
|
-
// ─── Types ───────────────────────────────────────────────────
|
|
37
|
-
export type {
|
|
38
|
-
AgentConfig,
|
|
39
|
-
AgentMode,
|
|
40
|
-
ExecutionResult,
|
|
41
|
-
InteractiveElement,
|
|
42
|
-
DehydratedScreen,
|
|
43
|
-
ToolDefinition,
|
|
44
|
-
ActionDefinition,
|
|
45
|
-
TokenUsage,
|
|
46
|
-
KnowledgeEntry,
|
|
47
|
-
KnowledgeRetriever,
|
|
48
|
-
KnowledgeBaseConfig,
|
|
49
|
-
ChatBarTheme,
|
|
50
|
-
AIMessage,
|
|
51
|
-
AIProviderName,
|
|
52
|
-
ScreenMap,
|
|
53
|
-
ScreenMapEntry,
|
|
54
|
-
InteractionMode,
|
|
55
|
-
} from './core/types';
|
|
56
|
-
|
|
57
|
-
export type {
|
|
58
|
-
VoiceServiceConfig,
|
|
59
|
-
VoiceServiceCallbacks,
|
|
60
|
-
VoiceStatus,
|
|
61
|
-
} from './services/VoiceService';
|
|
62
|
-
|
|
63
|
-
export type {
|
|
64
|
-
TelemetryConfig,
|
|
65
|
-
TelemetryEvent,
|
|
66
|
-
} from './services/telemetry';
|
|
67
|
-
|
|
68
|
-
// ─── Support Mode ────────────────────────────────────────────
|
|
69
|
-
// SupportGreeting, CSATSurvey, buildSupportPrompt work standalone (no backend)
|
|
70
|
-
// createEscalateTool works with provider='custom' (no backend)
|
|
71
|
-
// EscalationSocket and provider='mobileai' require api.mobileai.dev
|
|
72
|
-
export { SupportGreeting, CSATSurvey, buildSupportPrompt, createEscalateTool, EscalationSocket } from './support';
|
|
73
|
-
|
|
74
|
-
export type {
|
|
75
|
-
SupportModeConfig,
|
|
76
|
-
QuickReply,
|
|
77
|
-
EscalationConfig,
|
|
78
|
-
EscalationContext,
|
|
79
|
-
CSATConfig,
|
|
80
|
-
CSATRating,
|
|
81
|
-
BusinessHoursConfig,
|
|
82
|
-
SupportTicket,
|
|
83
|
-
} from './support';
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import type { ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import { extractIntentsFromAST } from '../cli/generate-intents';
|
|
5
|
-
import { generateSwiftCode } from '../cli/generate-swift';
|
|
6
|
-
|
|
7
|
-
interface PluginOptions {
|
|
8
|
-
/** The source directory to scan for useAction calls. Defaults to 'src' */
|
|
9
|
-
scanDirectory?: string;
|
|
10
|
-
/** App scheme for deep links. Defaults to the scheme in app.json */
|
|
11
|
-
appScheme?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const withAppIntents: ConfigPlugin<PluginOptions | void> = (config, options) => {
|
|
15
|
-
let withXcodeProject: ((config: any, action: (config: any) => any) => any) | undefined;
|
|
16
|
-
try {
|
|
17
|
-
({ withXcodeProject } = require('expo/config-plugins'));
|
|
18
|
-
} catch {
|
|
19
|
-
console.warn(
|
|
20
|
-
'[MobileAI] `withAppIntents` requires `expo/config-plugins`. ' +
|
|
21
|
-
'Skipping App Intents generation because Expo config plugins are not available.'
|
|
22
|
-
);
|
|
23
|
-
return config;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!withXcodeProject) {
|
|
27
|
-
return config;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const applyWithXcodeProject = withXcodeProject;
|
|
31
|
-
|
|
32
|
-
return applyWithXcodeProject(config, async (config) => {
|
|
33
|
-
const project = config.modResults;
|
|
34
|
-
const projectName = config.modRequest.projectName || config.name;
|
|
35
|
-
const projectRoot = config.modRequest.projectRoot;
|
|
36
|
-
|
|
37
|
-
const scanDir = (options as PluginOptions)?.scanDirectory || 'src';
|
|
38
|
-
const appScheme = (options as PluginOptions)?.appScheme || (Array.isArray(config.scheme) ? config.scheme[0] : config.scheme) || 'mobileai';
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// 1. Scan and Extract
|
|
42
|
-
const scanPath = path.resolve(projectRoot, scanDir);
|
|
43
|
-
console.log(`\n🤖 [MobileAI] Scanning ${scanPath} for AI Actions...`);
|
|
44
|
-
const intents = extractIntentsFromAST(scanPath);
|
|
45
|
-
|
|
46
|
-
console.log(`🤖 [MobileAI] Found ${intents.length} actions.`);
|
|
47
|
-
|
|
48
|
-
// 2. Generate Swift Code
|
|
49
|
-
// We write a temporary manifest to disk to use the CLI function,
|
|
50
|
-
// or we can just adapt generateSwiftCode to take the object directly,
|
|
51
|
-
// but the CLI expects a file path. Let's write a temporary file.
|
|
52
|
-
const tmpManifestPath = path.join(projectRoot, '.mobileai-intent-manifest.tmp.json');
|
|
53
|
-
fs.writeFileSync(tmpManifestPath, JSON.stringify(intents, null, 2));
|
|
54
|
-
|
|
55
|
-
const swiftCode = generateSwiftCode(tmpManifestPath, appScheme);
|
|
56
|
-
|
|
57
|
-
// Clean up tmp manifest
|
|
58
|
-
if (fs.existsSync(tmpManifestPath)) {
|
|
59
|
-
fs.unlinkSync(tmpManifestPath);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 3. Write Swift File to iOS Project Directory
|
|
63
|
-
const targetFilePath = path.join(projectRoot, 'ios', projectName, 'MobileAIAppIntents.swift');
|
|
64
|
-
|
|
65
|
-
// Ensure directory exists
|
|
66
|
-
const targetDir = path.dirname(targetFilePath);
|
|
67
|
-
if (!fs.existsSync(targetDir)) {
|
|
68
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
fs.writeFileSync(targetFilePath, swiftCode);
|
|
72
|
-
console.log(`🤖 [MobileAI] Generated ${targetFilePath}`);
|
|
73
|
-
|
|
74
|
-
// 4. Link in Xcode
|
|
75
|
-
const groupKey = project.findPBXGroupKey({ name: projectName });
|
|
76
|
-
if (!groupKey) {
|
|
77
|
-
console.warn(`🤖 [MobileAI] Warning: Could not find main PBXGroup for ${projectName}. You may need to manually add MobileAIAppIntents.swift to Xcode.`);
|
|
78
|
-
return config;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check if already added
|
|
82
|
-
const relativeFilePath = `${projectName}/MobileAIAppIntents.swift`;
|
|
83
|
-
const fileAdded = project.hasFile(relativeFilePath);
|
|
84
|
-
|
|
85
|
-
if (!fileAdded) {
|
|
86
|
-
project.addSourceFile(relativeFilePath, null, groupKey);
|
|
87
|
-
console.log(`🤖 [MobileAI] Linked MobileAIAppIntents.swift to Xcode project.`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
} catch (error) {
|
|
91
|
-
console.error('🤖 [MobileAI] AppIntents generation failed:', error);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return config;
|
|
95
|
-
});
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export default withAppIntents;
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GeminiProvider — Gemini API integration via @google/genai SDK.
|
|
3
|
-
*
|
|
4
|
-
* Uses the official Google GenAI SDK for:
|
|
5
|
-
* - generateContent with structured function calling (agent_step)
|
|
6
|
-
* - inlineData for vision (base64 screenshots)
|
|
7
|
-
* - System instructions
|
|
8
|
-
*
|
|
9
|
-
* Implements the AIProvider interface so it can be swapped
|
|
10
|
-
* with OpenAIProvider, AnthropicProvider, etc.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { GoogleGenAI, FunctionCallingConfigMode, Type } from '@google/genai';
|
|
14
|
-
import { logger } from '../utils/logger';
|
|
15
|
-
import type { AIProvider, ToolDefinition, AgentStep, ProviderResult, AgentReasoning, TokenUsage } from '../core/types';
|
|
16
|
-
|
|
17
|
-
// ─── Constants ─────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const AGENT_STEP_FN = 'agent_step';
|
|
20
|
-
|
|
21
|
-
// Reasoning fields always present in the agent_step schema
|
|
22
|
-
const REASONING_FIELDS = ['previous_goal_eval', 'memory', 'plan'] as const;
|
|
23
|
-
|
|
24
|
-
// ─── Provider ──────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
export class GeminiProvider implements AIProvider {
|
|
27
|
-
private ai: GoogleGenAI;
|
|
28
|
-
private model: string;
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
apiKey?: string,
|
|
32
|
-
model: string = 'gemini-2.5-flash',
|
|
33
|
-
proxyUrl?: string,
|
|
34
|
-
proxyHeaders?: Record<string, string>
|
|
35
|
-
) {
|
|
36
|
-
const config: any = {};
|
|
37
|
-
|
|
38
|
-
if (proxyUrl) {
|
|
39
|
-
config.apiKey = 'proxy-key'; // Dummy key to bypass local validation
|
|
40
|
-
config.httpOptions = {
|
|
41
|
-
baseUrl: proxyUrl,
|
|
42
|
-
headers: proxyHeaders || {},
|
|
43
|
-
};
|
|
44
|
-
} else if (apiKey) {
|
|
45
|
-
config.apiKey = apiKey;
|
|
46
|
-
} else {
|
|
47
|
-
throw new Error('[mobileai] You must provide either "apiKey" or "proxyUrl" to AIAgent.');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
this.ai = new GoogleGenAI(config);
|
|
51
|
-
this.model = model;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async generateContent(
|
|
55
|
-
systemPrompt: string,
|
|
56
|
-
userMessage: string,
|
|
57
|
-
tools: ToolDefinition[],
|
|
58
|
-
history: AgentStep[],
|
|
59
|
-
screenshot?: string,
|
|
60
|
-
): Promise<ProviderResult> {
|
|
61
|
-
|
|
62
|
-
logger.info('GeminiProvider', `Sending request. Model: ${this.model}, Tools: ${tools.length}${screenshot ? ', with screenshot' : ''}`);
|
|
63
|
-
|
|
64
|
-
// Build single agent_step function declaration
|
|
65
|
-
const agentStepDeclaration = this.buildAgentStepDeclaration(tools);
|
|
66
|
-
|
|
67
|
-
// Build contents (user message + optional screenshot)
|
|
68
|
-
const contents = this.buildContents(userMessage, history, screenshot);
|
|
69
|
-
|
|
70
|
-
const startTime = Date.now();
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const response = await this.ai.models.generateContent({
|
|
74
|
-
model: this.model,
|
|
75
|
-
contents,
|
|
76
|
-
config: {
|
|
77
|
-
systemInstruction: systemPrompt,
|
|
78
|
-
tools: [{ functionDeclarations: [agentStepDeclaration] }],
|
|
79
|
-
toolConfig: {
|
|
80
|
-
functionCallingConfig: {
|
|
81
|
-
mode: FunctionCallingConfigMode.ANY,
|
|
82
|
-
allowedFunctionNames: [AGENT_STEP_FN],
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
temperature: 0.2,
|
|
86
|
-
maxOutputTokens: 2048,
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const elapsed = Date.now() - startTime;
|
|
91
|
-
logger.info('GeminiProvider', `Response received in ${elapsed}ms`);
|
|
92
|
-
|
|
93
|
-
// Extract token usage from SDK response
|
|
94
|
-
const tokenUsage = this.extractTokenUsage(response);
|
|
95
|
-
if (tokenUsage) {
|
|
96
|
-
logger.info('GeminiProvider', `Tokens: ${tokenUsage.promptTokens} in / ${tokenUsage.completionTokens} out / $${tokenUsage.estimatedCostUSD.toFixed(6)}`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const result = this.parseAgentStepResponse(response, tools);
|
|
100
|
-
result.tokenUsage = tokenUsage;
|
|
101
|
-
return result;
|
|
102
|
-
} catch (error: any) {
|
|
103
|
-
logger.error('GeminiProvider', 'Request failed:', error.message);
|
|
104
|
-
|
|
105
|
-
if (error.status) {
|
|
106
|
-
throw new Error(this.formatProviderError(error.status, error.message));
|
|
107
|
-
}
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── Build agent_step Declaration ──────────────────────────
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Builds a single `agent_step` function declaration that combines:
|
|
116
|
-
* - Structured reasoning fields (previous_goal_eval, memory, plan)
|
|
117
|
-
* - action_name (enum of all available tool names)
|
|
118
|
-
* - All tool parameter fields as flat top-level properties
|
|
119
|
-
*
|
|
120
|
-
* Flat schema avoids Gemini's "deeply nested schema" rejection in ANY mode.
|
|
121
|
-
*/
|
|
122
|
-
private buildAgentStepDeclaration(tools: ToolDefinition[]): any {
|
|
123
|
-
const toolNames = tools.map(t => t.name);
|
|
124
|
-
|
|
125
|
-
// Collect all unique parameter fields across all tools
|
|
126
|
-
const actionProperties: Record<string, any> = {};
|
|
127
|
-
for (const tool of tools) {
|
|
128
|
-
for (const [paramName, param] of Object.entries(tool.parameters)) {
|
|
129
|
-
if (actionProperties[paramName]) continue;
|
|
130
|
-
actionProperties[paramName] = {
|
|
131
|
-
type: this.mapParamType(param.type),
|
|
132
|
-
description: param.description,
|
|
133
|
-
...(param.enum ? { enum: param.enum } : {}),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Build tool descriptions for the action_name enum
|
|
139
|
-
const toolDescriptions = tools
|
|
140
|
-
.map(t => {
|
|
141
|
-
const params = Object.keys(t.parameters).join(', ');
|
|
142
|
-
return `- ${t.name}(${params}): ${t.description}`;
|
|
143
|
-
})
|
|
144
|
-
.join('\n');
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
name: AGENT_STEP_FN,
|
|
148
|
-
description: `Execute one agent step. Choose an action and provide reasoning.\n\nAvailable actions:\n${toolDescriptions}`,
|
|
149
|
-
parameters: {
|
|
150
|
-
type: Type.OBJECT,
|
|
151
|
-
properties: {
|
|
152
|
-
previous_goal_eval: {
|
|
153
|
-
type: Type.STRING,
|
|
154
|
-
description: 'One-sentence assessment of your last action. State success, failure, or uncertain. Skip on first step.',
|
|
155
|
-
},
|
|
156
|
-
memory: {
|
|
157
|
-
type: Type.STRING,
|
|
158
|
-
description: 'Key facts to remember for future steps: progress made, items found, counters, field values already collected.',
|
|
159
|
-
},
|
|
160
|
-
plan: {
|
|
161
|
-
type: Type.STRING,
|
|
162
|
-
description: 'Your immediate next goal — what action you will take and why.',
|
|
163
|
-
},
|
|
164
|
-
action_name: {
|
|
165
|
-
type: Type.STRING,
|
|
166
|
-
description: 'Which action to execute.',
|
|
167
|
-
enum: toolNames,
|
|
168
|
-
},
|
|
169
|
-
...actionProperties,
|
|
170
|
-
},
|
|
171
|
-
required: ['plan', 'action_name'],
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private mapParamType(type: string): string {
|
|
177
|
-
switch (type) {
|
|
178
|
-
case 'number': return Type.NUMBER;
|
|
179
|
-
case 'integer': return Type.INTEGER;
|
|
180
|
-
case 'boolean': return Type.BOOLEAN;
|
|
181
|
-
case 'string':
|
|
182
|
-
default: return Type.STRING;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ─── Build Contents ────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Builds contents for the generateContent call.
|
|
190
|
-
* Single-turn: user message + optional screenshot as inlineData.
|
|
191
|
-
*/
|
|
192
|
-
private buildContents(userMessage: string, _history: AgentStep[], screenshot?: string): any[] {
|
|
193
|
-
const parts: any[] = [{ text: userMessage }];
|
|
194
|
-
|
|
195
|
-
// Append screenshot as inlineData for Gemini vision
|
|
196
|
-
if (screenshot) {
|
|
197
|
-
parts.push({
|
|
198
|
-
inlineData: {
|
|
199
|
-
mimeType: 'image/jpeg',
|
|
200
|
-
data: screenshot,
|
|
201
|
-
},
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return [{ role: 'user', parts }];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ─── Parse Response ────────────────────────────────────────
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Parses the SDK response expecting a single agent_step function call.
|
|
212
|
-
* Extracts structured reasoning + action.
|
|
213
|
-
*/
|
|
214
|
-
private parseAgentStepResponse(response: any, tools: ToolDefinition[]): ProviderResult {
|
|
215
|
-
const candidates = response.candidates || [];
|
|
216
|
-
|
|
217
|
-
if (candidates.length === 0) {
|
|
218
|
-
logger.warn('GeminiProvider', 'No candidates in response');
|
|
219
|
-
return {
|
|
220
|
-
toolCalls: [{ name: 'done', args: { text: 'No response generated.', success: false } }],
|
|
221
|
-
reasoning: { previousGoalEval: '', memory: '', plan: '' },
|
|
222
|
-
text: 'No response generated.',
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const candidate = candidates[0];
|
|
227
|
-
const parts = candidate.content?.parts || [];
|
|
228
|
-
|
|
229
|
-
// Find the function call part
|
|
230
|
-
const fnCallPart = parts.find((p: any) => p.functionCall);
|
|
231
|
-
const textPart = parts.find((p: any) => p.text);
|
|
232
|
-
|
|
233
|
-
if (!fnCallPart?.functionCall) {
|
|
234
|
-
logger.warn('GeminiProvider', 'No function call in response. Text:', textPart?.text);
|
|
235
|
-
return {
|
|
236
|
-
toolCalls: [{ name: 'done', args: { text: textPart?.text || 'No action taken.', success: false } }],
|
|
237
|
-
reasoning: { previousGoalEval: '', memory: '', plan: '' },
|
|
238
|
-
text: textPart?.text,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const args = fnCallPart.functionCall.args || {};
|
|
243
|
-
|
|
244
|
-
// Extract reasoning fields
|
|
245
|
-
const reasoning: AgentReasoning = {
|
|
246
|
-
previousGoalEval: args.previous_goal_eval || '',
|
|
247
|
-
memory: args.memory || '',
|
|
248
|
-
plan: args.plan || '',
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
// Extract action
|
|
252
|
-
const actionName = args.action_name;
|
|
253
|
-
if (!actionName) {
|
|
254
|
-
logger.warn('GeminiProvider', 'No action_name in agent_step. Falling back to done.');
|
|
255
|
-
return {
|
|
256
|
-
toolCalls: [{ name: 'done', args: { text: 'Agent did not choose an action.', success: false } }],
|
|
257
|
-
reasoning,
|
|
258
|
-
text: textPart?.text,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Build action args: extract only the params that belong to the matched tool
|
|
263
|
-
const actionArgs: Record<string, any> = {};
|
|
264
|
-
const reservedKeys = new Set([...REASONING_FIELDS, 'action_name']);
|
|
265
|
-
|
|
266
|
-
const matchedTool = tools.find(t => t.name === actionName);
|
|
267
|
-
if (matchedTool) {
|
|
268
|
-
for (const paramName of Object.keys(matchedTool.parameters)) {
|
|
269
|
-
if (args[paramName] !== undefined) {
|
|
270
|
-
actionArgs[paramName] = args[paramName];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
} else {
|
|
274
|
-
for (const [key, value] of Object.entries(args)) {
|
|
275
|
-
if (!reservedKeys.has(key)) {
|
|
276
|
-
actionArgs[key] = value;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
logger.info('GeminiProvider', `Parsed: action=${actionName}, plan="${reasoning.plan}"`);
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
toolCalls: [{ name: actionName, args: actionArgs }],
|
|
285
|
-
reasoning,
|
|
286
|
-
text: textPart?.text,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ─── Token Usage Extraction ─────────────────────────────────
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Extracts token usage from SDK response and calculates estimated cost.
|
|
294
|
-
*
|
|
295
|
-
* Pricing (Gemini 2.5 Flash):
|
|
296
|
-
* - Input: $0.30 / 1M tokens
|
|
297
|
-
* - Output: $2.50 / 1M tokens
|
|
298
|
-
*/
|
|
299
|
-
private extractTokenUsage(response: any): TokenUsage | undefined {
|
|
300
|
-
const meta = response?.usageMetadata;
|
|
301
|
-
if (!meta) return undefined;
|
|
302
|
-
|
|
303
|
-
const promptTokens = meta.promptTokenCount ?? 0;
|
|
304
|
-
const completionTokens = meta.candidatesTokenCount ?? 0;
|
|
305
|
-
const totalTokens = meta.totalTokenCount ?? (promptTokens + completionTokens);
|
|
306
|
-
|
|
307
|
-
// Cost estimation based on Gemini 2.5 Flash pricing
|
|
308
|
-
const INPUT_COST_PER_M = 0.30;
|
|
309
|
-
const OUTPUT_COST_PER_M = 2.50;
|
|
310
|
-
|
|
311
|
-
const estimatedCostUSD =
|
|
312
|
-
(promptTokens / 1_000_000) * INPUT_COST_PER_M +
|
|
313
|
-
(completionTokens / 1_000_000) * OUTPUT_COST_PER_M;
|
|
314
|
-
|
|
315
|
-
return { promptTokens, completionTokens, totalTokens, estimatedCostUSD };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ─── Error Formatting ──────────────────────────────────────
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Converts raw API errors into clean, user-friendly messages.
|
|
322
|
-
* Parses JSON error bodies and maps HTTP codes to plain language.
|
|
323
|
-
*/
|
|
324
|
-
private formatProviderError(status: number, rawMessage: string): string {
|
|
325
|
-
// Try to extract the human-readable message from JSON body
|
|
326
|
-
let humanMessage = '';
|
|
327
|
-
try {
|
|
328
|
-
const parsed = JSON.parse(rawMessage);
|
|
329
|
-
humanMessage = parsed?.error?.message || parsed?.message || '';
|
|
330
|
-
} catch {
|
|
331
|
-
// rawMessage may contain JSON embedded in a string like "503: {json}"
|
|
332
|
-
const jsonMatch = rawMessage.match(/\{[\s\S]*\}/);
|
|
333
|
-
if (jsonMatch) {
|
|
334
|
-
try {
|
|
335
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
336
|
-
humanMessage = parsed?.error?.message || parsed?.message || '';
|
|
337
|
-
} catch { /* ignore */ }
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Map status codes to friendly descriptions
|
|
342
|
-
switch (status) {
|
|
343
|
-
case 429:
|
|
344
|
-
return humanMessage || 'Too many requests. Please wait a moment and try again.';
|
|
345
|
-
case 503:
|
|
346
|
-
return humanMessage || 'The AI service is temporarily unavailable. Please try again shortly.';
|
|
347
|
-
case 500:
|
|
348
|
-
return humanMessage || 'The AI service encountered an internal error. Please try again.';
|
|
349
|
-
case 401:
|
|
350
|
-
return 'Authentication failed. Please check your API key.';
|
|
351
|
-
case 403:
|
|
352
|
-
return 'Access denied. Your API key may not have the required permissions.';
|
|
353
|
-
default:
|
|
354
|
-
return humanMessage || `Something went wrong (${status}). Please try again.`;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|