@mobileai/react-native 0.1.0
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 +20 -0
- package/README.md +190 -0
- package/lib/module/components/AIAgent.js +149 -0
- package/lib/module/components/AIAgent.js.map +1 -0
- package/lib/module/components/AgentChatBar.js +120 -0
- package/lib/module/components/AgentChatBar.js.map +1 -0
- package/lib/module/components/AgentOverlay.js +53 -0
- package/lib/module/components/AgentOverlay.js.map +1 -0
- package/lib/module/core/AgentRuntime.js +498 -0
- package/lib/module/core/AgentRuntime.js.map +1 -0
- package/lib/module/core/FiberTreeWalker.js +308 -0
- package/lib/module/core/FiberTreeWalker.js.map +1 -0
- package/lib/module/core/MCPBridge.js +98 -0
- package/lib/module/core/MCPBridge.js.map +1 -0
- package/lib/module/core/ScreenDehydrator.js +46 -0
- package/lib/module/core/ScreenDehydrator.js.map +1 -0
- package/lib/module/core/types.js +2 -0
- package/lib/module/core/types.js.map +1 -0
- package/lib/module/hooks/useAction.js +32 -0
- package/lib/module/hooks/useAction.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/providers/GeminiProvider.js +178 -0
- package/lib/module/providers/GeminiProvider.js.map +1 -0
- package/lib/module/utils/logger.js +17 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/AIAgent.d.ts +57 -0
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts +14 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -0
- package/lib/typescript/src/components/AgentOverlay.d.ts +10 -0
- package/lib/typescript/src/components/AgentOverlay.d.ts.map +1 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +37 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +26 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -0
- package/lib/typescript/src/core/MCPBridge.d.ts +23 -0
- package/lib/typescript/src/core/MCPBridge.d.ts.map +1 -0
- package/lib/typescript/src/core/ScreenDehydrator.d.ts +20 -0
- package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +1 -0
- package/lib/typescript/src/core/types.d.ts +138 -0
- package/lib/typescript/src/core/types.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useAction.d.ts +13 -0
- package/lib/typescript/src/hooks/useAction.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/providers/GeminiProvider.d.ts +23 -0
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +7 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/package.json +143 -0
- package/src/components/AIAgent.tsx +222 -0
- package/src/components/AgentChatBar.tsx +136 -0
- package/src/components/AgentOverlay.tsx +48 -0
- package/src/core/AgentRuntime.ts +505 -0
- package/src/core/FiberTreeWalker.ts +349 -0
- package/src/core/MCPBridge.ts +110 -0
- package/src/core/ScreenDehydrator.ts +53 -0
- package/src/core/types.ts +185 -0
- package/src/hooks/useAction.ts +40 -0
- package/src/index.ts +22 -0
- package/src/providers/GeminiProvider.ts +210 -0
- package/src/utils/logger.ts +21 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const logger: {
|
|
2
|
+
info: (context: string, ...args: any[]) => void;
|
|
3
|
+
warn: (context: string, ...args: any[]) => void;
|
|
4
|
+
error: (context: string, ...args: any[]) => void;
|
|
5
|
+
debug: (context: string, ...args: any[]) => void;
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../../../src/utils/logger.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,MAAM;oBACD,MAAM,WAAW,GAAG,EAAE;oBAGtB,MAAM,WAAW,GAAG,EAAE;qBAGrB,MAAM,WAAW,GAAG,EAAE;qBAGtB,MAAM,WAAW,GAAG,EAAE;CAKxC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mobileai/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Autonomous AI agent for React Native — reads your UI via Fiber tree, understands layout context, and interacts with your app. Zero wrappers.",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"source": "./src/index.ts",
|
|
7
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"source": "./src/index.ts",
|
|
11
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
12
|
+
"default": "./lib/module/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"lib",
|
|
19
|
+
"android",
|
|
20
|
+
"ios",
|
|
21
|
+
"cpp",
|
|
22
|
+
"*.podspec",
|
|
23
|
+
"react-native.config.js",
|
|
24
|
+
"!ios/build",
|
|
25
|
+
"!android/build",
|
|
26
|
+
"!android/gradle",
|
|
27
|
+
"!android/gradlew",
|
|
28
|
+
"!android/gradlew.bat",
|
|
29
|
+
"!android/local.properties",
|
|
30
|
+
"!**/__tests__",
|
|
31
|
+
"!**/__fixtures__",
|
|
32
|
+
"!**/__mocks__",
|
|
33
|
+
"!**/__mocks__",
|
|
34
|
+
"!**/.*",
|
|
35
|
+
"!mcp-server"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"example": "yarn workspace @mobileai/react-native-example",
|
|
39
|
+
"clean": "del-cli lib",
|
|
40
|
+
"prepare": "bob build",
|
|
41
|
+
"typecheck": "tsc",
|
|
42
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
43
|
+
"test": "jest"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"react-native",
|
|
47
|
+
"ai",
|
|
48
|
+
"agent",
|
|
49
|
+
"agentic",
|
|
50
|
+
"gemini",
|
|
51
|
+
"llm",
|
|
52
|
+
"autonomous",
|
|
53
|
+
"fiber-tree",
|
|
54
|
+
"page-agent",
|
|
55
|
+
"mcp",
|
|
56
|
+
"ios",
|
|
57
|
+
"android"
|
|
58
|
+
],
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/mohamed2m2018/react-native-ai-agent.git"
|
|
62
|
+
},
|
|
63
|
+
"author": "Mohamed Salah <mohamedsalah@example.com> (https://example.com)",
|
|
64
|
+
"license": "MIT",
|
|
65
|
+
"bugs": {
|
|
66
|
+
"url": "https://github.com/mohamed2m2018/react-native-ai-agent/issues"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/mohamed2m2018/react-native-ai-agent#readme",
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"registry": "https://registry.npmjs.org/"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@eslint/compat": "^1.3.2",
|
|
74
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
75
|
+
"@eslint/js": "^9.35.0",
|
|
76
|
+
"@react-native/babel-preset": "0.83.0",
|
|
77
|
+
"@react-native/eslint-config": "0.83.0",
|
|
78
|
+
"@types/jest": "^29.5.14",
|
|
79
|
+
"@types/react": "^19.1.12",
|
|
80
|
+
"@types/react-native": "^0.72.8",
|
|
81
|
+
"del-cli": "^6.0.0",
|
|
82
|
+
"eslint": "^9.35.0",
|
|
83
|
+
"eslint-config-prettier": "^10.1.8",
|
|
84
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
85
|
+
"jest": "^29.7.0",
|
|
86
|
+
"prettier": "^2.8.8",
|
|
87
|
+
"react": "19.2.0",
|
|
88
|
+
"react-native": "0.83.2",
|
|
89
|
+
"react-native-builder-bob": "^0.40.18",
|
|
90
|
+
"turbo": "^2.5.6",
|
|
91
|
+
"typescript": "^5.9.2"
|
|
92
|
+
},
|
|
93
|
+
"peerDependencies": {
|
|
94
|
+
"react": "*",
|
|
95
|
+
"react-native": "*"
|
|
96
|
+
},
|
|
97
|
+
"workspaces": [
|
|
98
|
+
"example"
|
|
99
|
+
],
|
|
100
|
+
"packageManager": "yarn@4.11.0",
|
|
101
|
+
"react-native-builder-bob": {
|
|
102
|
+
"source": "src",
|
|
103
|
+
"output": "lib",
|
|
104
|
+
"targets": [
|
|
105
|
+
[
|
|
106
|
+
"module",
|
|
107
|
+
{
|
|
108
|
+
"esm": true
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"typescript",
|
|
113
|
+
{
|
|
114
|
+
"project": "tsconfig.build.json"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"prettier": {
|
|
120
|
+
"quoteProps": "consistent",
|
|
121
|
+
"singleQuote": true,
|
|
122
|
+
"tabWidth": 2,
|
|
123
|
+
"trailingComma": "es5",
|
|
124
|
+
"useTabs": false
|
|
125
|
+
},
|
|
126
|
+
"jest": {
|
|
127
|
+
"preset": "react-native",
|
|
128
|
+
"modulePathIgnorePatterns": [
|
|
129
|
+
"<rootDir>/example/node_modules",
|
|
130
|
+
"<rootDir>/lib/"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"create-react-native-library": {
|
|
134
|
+
"type": "library",
|
|
135
|
+
"languages": "js",
|
|
136
|
+
"tools": [
|
|
137
|
+
"eslint",
|
|
138
|
+
"jest"
|
|
139
|
+
],
|
|
140
|
+
"version": "0.57.2"
|
|
141
|
+
},
|
|
142
|
+
"dependencies": {}
|
|
143
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIAgent — Root provider component for the AI agent.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the app and provides:
|
|
5
|
+
* - Fiber tree root ref for element auto-detection
|
|
6
|
+
* - Navigation ref for auto-navigation
|
|
7
|
+
* - Floating chat bar for user input
|
|
8
|
+
* - Agent runtime context for useAction hooks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, {
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
} from 'react';
|
|
18
|
+
import { View, StyleSheet } from 'react-native';
|
|
19
|
+
import { AgentRuntime } from '../core/AgentRuntime';
|
|
20
|
+
import { GeminiProvider } from '../providers/GeminiProvider';
|
|
21
|
+
import { AgentContext } from '../hooks/useAction';
|
|
22
|
+
import { AgentChatBar } from './AgentChatBar';
|
|
23
|
+
import { AgentOverlay } from './AgentOverlay';
|
|
24
|
+
import { logger } from '../utils/logger';
|
|
25
|
+
import { MCPBridge } from '../core/MCPBridge';
|
|
26
|
+
import type { AgentConfig, ExecutionResult, ToolDefinition, AgentStep } from '../core/types';
|
|
27
|
+
|
|
28
|
+
// ─── Context ───────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// ─── Props ─────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
interface AIAgentProps {
|
|
34
|
+
/** Gemini API key */
|
|
35
|
+
apiKey: string;
|
|
36
|
+
/** Gemini model name */
|
|
37
|
+
model?: string;
|
|
38
|
+
/** Navigation container ref (from useNavigationContainerRef) */
|
|
39
|
+
navRef?: any;
|
|
40
|
+
/** UI language */
|
|
41
|
+
language?: 'en' | 'ar';
|
|
42
|
+
/** Max agent steps per request */
|
|
43
|
+
maxSteps?: number;
|
|
44
|
+
/** Show/hide the chat bar */
|
|
45
|
+
showChatBar?: boolean;
|
|
46
|
+
/** Children — the actual app */
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
/** Callback when agent completes */
|
|
49
|
+
onResult?: (result: ExecutionResult) => void;
|
|
50
|
+
|
|
51
|
+
// ── Security (mirrors page-agent.js) ──────────────────────
|
|
52
|
+
|
|
53
|
+
/** Refs of elements the AI must NOT interact with */
|
|
54
|
+
interactiveBlacklist?: React.RefObject<any>[];
|
|
55
|
+
/** If set, AI can ONLY interact with these elements */
|
|
56
|
+
interactiveWhitelist?: React.RefObject<any>[];
|
|
57
|
+
/** Called before each step */
|
|
58
|
+
onBeforeStep?: (stepCount: number) => Promise<void> | void;
|
|
59
|
+
/** Called after each step */
|
|
60
|
+
onAfterStep?: (history: AgentStep[]) => Promise<void> | void;
|
|
61
|
+
/** Called before task starts */
|
|
62
|
+
onBeforeTask?: () => Promise<void> | void;
|
|
63
|
+
/** Called after task completes */
|
|
64
|
+
onAfterTask?: (result: ExecutionResult) => Promise<void> | void;
|
|
65
|
+
/** Transform screen content before LLM sees it (for data masking) */
|
|
66
|
+
transformScreenContent?: (content: string) => Promise<string> | string;
|
|
67
|
+
/** Override or remove built-in tools (null = remove) */
|
|
68
|
+
customTools?: Record<string, ToolDefinition | null>;
|
|
69
|
+
/** Instructions to guide agent behavior */
|
|
70
|
+
instructions?: {
|
|
71
|
+
system?: string;
|
|
72
|
+
getScreenInstructions?: (screenName: string) => string | undefined | null;
|
|
73
|
+
};
|
|
74
|
+
/** Delay between steps in ms */
|
|
75
|
+
stepDelay?: number;
|
|
76
|
+
/** WebSocket URL to companion MCP server bridge (e.g., ws://localhost:3101) */
|
|
77
|
+
mcpServerUrl?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Component ─────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function AIAgent({
|
|
83
|
+
apiKey,
|
|
84
|
+
model = 'gemini-2.5-flash',
|
|
85
|
+
navRef,
|
|
86
|
+
language = 'en',
|
|
87
|
+
maxSteps = 10,
|
|
88
|
+
showChatBar = true,
|
|
89
|
+
children,
|
|
90
|
+
onResult,
|
|
91
|
+
// Security props
|
|
92
|
+
interactiveBlacklist,
|
|
93
|
+
interactiveWhitelist,
|
|
94
|
+
onBeforeStep,
|
|
95
|
+
onAfterStep,
|
|
96
|
+
onBeforeTask,
|
|
97
|
+
onAfterTask,
|
|
98
|
+
transformScreenContent,
|
|
99
|
+
customTools,
|
|
100
|
+
instructions,
|
|
101
|
+
stepDelay,
|
|
102
|
+
mcpServerUrl,
|
|
103
|
+
}: AIAgentProps) {
|
|
104
|
+
const rootViewRef = useRef<any>(null);
|
|
105
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
106
|
+
const [statusText, setStatusText] = useState('');
|
|
107
|
+
const [lastResult, setLastResult] = useState<ExecutionResult | null>(null);
|
|
108
|
+
|
|
109
|
+
// ─── Create Runtime ──────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const config: AgentConfig = useMemo(() => ({
|
|
112
|
+
apiKey,
|
|
113
|
+
model,
|
|
114
|
+
language,
|
|
115
|
+
maxSteps,
|
|
116
|
+
interactiveBlacklist,
|
|
117
|
+
interactiveWhitelist,
|
|
118
|
+
onBeforeStep,
|
|
119
|
+
onAfterStep,
|
|
120
|
+
onBeforeTask,
|
|
121
|
+
onAfterTask,
|
|
122
|
+
transformScreenContent,
|
|
123
|
+
customTools,
|
|
124
|
+
instructions,
|
|
125
|
+
stepDelay,
|
|
126
|
+
mcpServerUrl,
|
|
127
|
+
}), [
|
|
128
|
+
apiKey, model, language, maxSteps,
|
|
129
|
+
interactiveBlacklist, interactiveWhitelist,
|
|
130
|
+
onBeforeStep, onAfterStep, onBeforeTask, onAfterTask,
|
|
131
|
+
transformScreenContent, customTools, instructions, stepDelay,
|
|
132
|
+
mcpServerUrl,
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const provider = useMemo(() => new GeminiProvider(apiKey, model), [apiKey, model]);
|
|
136
|
+
|
|
137
|
+
const runtime = useMemo(
|
|
138
|
+
() => new AgentRuntime(provider, config, rootViewRef.current, navRef),
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
+
[provider, config],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Update refs when they change
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
runtime.updateRefs(rootViewRef.current, navRef);
|
|
146
|
+
}, [runtime, navRef]);
|
|
147
|
+
|
|
148
|
+
// ─── MCP Bridge ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!mcpServerUrl) return;
|
|
152
|
+
|
|
153
|
+
logger.info('AIAgent', `Setting up MCP bridge at ${mcpServerUrl}`);
|
|
154
|
+
const bridge = new MCPBridge(mcpServerUrl, runtime);
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
bridge.destroy();
|
|
158
|
+
};
|
|
159
|
+
}, [mcpServerUrl, runtime]);
|
|
160
|
+
|
|
161
|
+
// ─── Execute ──────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const handleSend = useCallback(async (message: string) => {
|
|
164
|
+
if (!message.trim()) return;
|
|
165
|
+
|
|
166
|
+
logger.info('AIAgent', `User message: "${message}"`);
|
|
167
|
+
setIsThinking(true);
|
|
168
|
+
setStatusText('Thinking...');
|
|
169
|
+
setLastResult(null);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Ensure we have the latest Fiber tree ref
|
|
173
|
+
runtime.updateRefs(rootViewRef.current, navRef);
|
|
174
|
+
|
|
175
|
+
const result = await runtime.execute(message);
|
|
176
|
+
|
|
177
|
+
setLastResult(result);
|
|
178
|
+
onResult?.(result);
|
|
179
|
+
|
|
180
|
+
logger.info('AIAgent', `Result: ${result.success ? '✅' : '❌'} ${result.message}`);
|
|
181
|
+
} catch (error: any) {
|
|
182
|
+
logger.error('AIAgent', 'Execution failed:', error);
|
|
183
|
+
setLastResult({
|
|
184
|
+
success: false,
|
|
185
|
+
message: `Error: ${error.message}`,
|
|
186
|
+
steps: [],
|
|
187
|
+
});
|
|
188
|
+
} finally {
|
|
189
|
+
setIsThinking(false);
|
|
190
|
+
setStatusText('');
|
|
191
|
+
}
|
|
192
|
+
}, [runtime, navRef, onResult]);
|
|
193
|
+
|
|
194
|
+
// ─── Render ──────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<AgentContext.Provider value={runtime}>
|
|
198
|
+
<View ref={rootViewRef} style={styles.root} collapsable={false}>
|
|
199
|
+
{children}
|
|
200
|
+
</View>
|
|
201
|
+
|
|
202
|
+
{/* Overlay (shown while thinking) */}
|
|
203
|
+
<AgentOverlay visible={isThinking} statusText={statusText} />
|
|
204
|
+
|
|
205
|
+
{/* Chat bar */}
|
|
206
|
+
{showChatBar && (
|
|
207
|
+
<AgentChatBar
|
|
208
|
+
onSend={handleSend}
|
|
209
|
+
isThinking={isThinking}
|
|
210
|
+
lastResult={lastResult}
|
|
211
|
+
language={language}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</AgentContext.Provider>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const styles = StyleSheet.create({
|
|
219
|
+
root: {
|
|
220
|
+
flex: 1,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentChatBar — Floating chat input at the bottom of the screen.
|
|
3
|
+
* User sends messages here, and results are shown inline.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
TextInput,
|
|
10
|
+
Pressable,
|
|
11
|
+
Text,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
KeyboardAvoidingView,
|
|
14
|
+
Platform,
|
|
15
|
+
} from 'react-native';
|
|
16
|
+
import type { ExecutionResult } from '../core/types';
|
|
17
|
+
|
|
18
|
+
interface AgentChatBarProps {
|
|
19
|
+
onSend: (message: string) => void;
|
|
20
|
+
isThinking: boolean;
|
|
21
|
+
lastResult: ExecutionResult | null;
|
|
22
|
+
language: 'en' | 'ar';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function AgentChatBar({ onSend, isThinking, lastResult, language }: AgentChatBarProps) {
|
|
26
|
+
const [text, setText] = useState('');
|
|
27
|
+
const isArabic = language === 'ar';
|
|
28
|
+
|
|
29
|
+
const handleSend = () => {
|
|
30
|
+
if (text.trim() && !isThinking) {
|
|
31
|
+
onSend(text.trim());
|
|
32
|
+
setText('');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<KeyboardAvoidingView
|
|
38
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
39
|
+
style={styles.container}
|
|
40
|
+
>
|
|
41
|
+
{/* Result message */}
|
|
42
|
+
{lastResult && (
|
|
43
|
+
<View style={[styles.resultBubble, lastResult.success ? styles.resultSuccess : styles.resultError]}>
|
|
44
|
+
<Text style={styles.resultText}>{lastResult.message}</Text>
|
|
45
|
+
</View>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{/* Input row */}
|
|
49
|
+
<View style={styles.inputRow}>
|
|
50
|
+
<TextInput
|
|
51
|
+
style={[styles.input, isArabic && styles.inputRTL]}
|
|
52
|
+
placeholder={isArabic ? 'اكتب طلبك...' : 'Ask the AI agent...'}
|
|
53
|
+
placeholderTextColor="#999"
|
|
54
|
+
value={text}
|
|
55
|
+
onChangeText={setText}
|
|
56
|
+
onSubmitEditing={handleSend}
|
|
57
|
+
returnKeyType="send"
|
|
58
|
+
editable={!isThinking}
|
|
59
|
+
multiline={false}
|
|
60
|
+
/>
|
|
61
|
+
<Pressable
|
|
62
|
+
style={[styles.sendButton, isThinking && styles.sendButtonDisabled]}
|
|
63
|
+
onPress={handleSend}
|
|
64
|
+
disabled={isThinking || !text.trim()}
|
|
65
|
+
>
|
|
66
|
+
<Text style={styles.sendButtonText}>
|
|
67
|
+
{isThinking ? '⏳' : '🚀'}
|
|
68
|
+
</Text>
|
|
69
|
+
</Pressable>
|
|
70
|
+
</View>
|
|
71
|
+
</KeyboardAvoidingView>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const styles = StyleSheet.create({
|
|
76
|
+
container: {
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
bottom: 0,
|
|
79
|
+
left: 0,
|
|
80
|
+
right: 0,
|
|
81
|
+
paddingHorizontal: 12,
|
|
82
|
+
paddingBottom: Platform.OS === 'ios' ? 34 : 12,
|
|
83
|
+
paddingTop: 8,
|
|
84
|
+
backgroundColor: 'rgba(26, 26, 46, 0.95)',
|
|
85
|
+
borderTopLeftRadius: 20,
|
|
86
|
+
borderTopRightRadius: 20,
|
|
87
|
+
},
|
|
88
|
+
resultBubble: {
|
|
89
|
+
borderRadius: 12,
|
|
90
|
+
padding: 12,
|
|
91
|
+
marginBottom: 8,
|
|
92
|
+
},
|
|
93
|
+
resultSuccess: {
|
|
94
|
+
backgroundColor: 'rgba(40, 167, 69, 0.2)',
|
|
95
|
+
},
|
|
96
|
+
resultError: {
|
|
97
|
+
backgroundColor: 'rgba(220, 53, 69, 0.2)',
|
|
98
|
+
},
|
|
99
|
+
resultText: {
|
|
100
|
+
color: '#fff',
|
|
101
|
+
fontSize: 14,
|
|
102
|
+
lineHeight: 20,
|
|
103
|
+
},
|
|
104
|
+
inputRow: {
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
gap: 8,
|
|
108
|
+
},
|
|
109
|
+
input: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
112
|
+
borderRadius: 24,
|
|
113
|
+
paddingHorizontal: 16,
|
|
114
|
+
paddingVertical: 12,
|
|
115
|
+
color: '#fff',
|
|
116
|
+
fontSize: 16,
|
|
117
|
+
},
|
|
118
|
+
inputRTL: {
|
|
119
|
+
textAlign: 'right',
|
|
120
|
+
writingDirection: 'rtl',
|
|
121
|
+
},
|
|
122
|
+
sendButton: {
|
|
123
|
+
width: 44,
|
|
124
|
+
height: 44,
|
|
125
|
+
borderRadius: 22,
|
|
126
|
+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
|
127
|
+
justifyContent: 'center',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
},
|
|
130
|
+
sendButtonDisabled: {
|
|
131
|
+
opacity: 0.5,
|
|
132
|
+
},
|
|
133
|
+
sendButtonText: {
|
|
134
|
+
fontSize: 20,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentOverlay — Subtle thinking indicator shown while the AI agent is processing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
|
|
6
|
+
|
|
7
|
+
interface AgentOverlayProps {
|
|
8
|
+
visible: boolean;
|
|
9
|
+
statusText: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AgentOverlay({ visible, statusText }: AgentOverlayProps) {
|
|
13
|
+
if (!visible) return null;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<View style={styles.overlay} pointerEvents="none">
|
|
17
|
+
<View style={styles.pill}>
|
|
18
|
+
<ActivityIndicator size="small" color="#fff" />
|
|
19
|
+
<Text style={styles.text}>{statusText || 'Thinking...'}</Text>
|
|
20
|
+
</View>
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const styles = StyleSheet.create({
|
|
26
|
+
overlay: {
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
top: 60,
|
|
29
|
+
left: 0,
|
|
30
|
+
right: 0,
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
zIndex: 9999,
|
|
33
|
+
},
|
|
34
|
+
pill: {
|
|
35
|
+
flexDirection: 'row',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
gap: 8,
|
|
38
|
+
backgroundColor: 'rgba(26, 26, 46, 0.9)',
|
|
39
|
+
paddingHorizontal: 16,
|
|
40
|
+
paddingVertical: 10,
|
|
41
|
+
borderRadius: 20,
|
|
42
|
+
},
|
|
43
|
+
text: {
|
|
44
|
+
color: '#fff',
|
|
45
|
+
fontSize: 14,
|
|
46
|
+
fontWeight: '500',
|
|
47
|
+
},
|
|
48
|
+
});
|