react-native-agentic-ai 0.0.2 → 0.2.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 +253 -14
- package/lib/module/components/AIAgent.js +185 -0
- package/lib/module/components/AIAgent.js.map +1 -0
- package/lib/module/components/AgentChatBar.js +268 -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 +640 -0
- package/lib/module/core/AgentRuntime.js.map +1 -0
- package/lib/module/core/FiberTreeWalker.js +362 -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/systemPrompt.js +164 -0
- package/lib/module/core/systemPrompt.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 +294 -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 +65 -0
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -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 +53 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +31 -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/systemPrompt.d.ts +9 -0
- package/lib/typescript/src/core/systemPrompt.d.ts.map +1 -0
- package/lib/typescript/src/core/types.d.ts +176 -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 +43 -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 +135 -12
- package/src/components/AIAgent.tsx +262 -0
- package/src/components/AgentChatBar.tsx +258 -0
- package/src/components/AgentOverlay.tsx +48 -0
- package/src/core/AgentRuntime.ts +661 -0
- package/src/core/FiberTreeWalker.ts +404 -0
- package/src/core/MCPBridge.ts +110 -0
- package/src/core/ScreenDehydrator.ts +53 -0
- package/src/core/systemPrompt.ts +162 -0
- package/src/core/types.ts +233 -0
- package/src/hooks/useAction.ts +40 -0
- package/src/index.ts +22 -0
- package/src/providers/GeminiProvider.ts +283 -0
- package/src/utils/logger.ts +21 -0
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
/** Expo Router instance (from useRouter()) */
|
|
79
|
+
router?: {
|
|
80
|
+
push: (href: string) => void;
|
|
81
|
+
replace: (href: string) => void;
|
|
82
|
+
back: () => void;
|
|
83
|
+
};
|
|
84
|
+
/** Expo Router pathname (from usePathname()) */
|
|
85
|
+
pathname?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Component ─────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export function AIAgent({
|
|
91
|
+
apiKey,
|
|
92
|
+
model = 'gemini-2.5-flash',
|
|
93
|
+
navRef,
|
|
94
|
+
language = 'en',
|
|
95
|
+
maxSteps = 10,
|
|
96
|
+
showChatBar = true,
|
|
97
|
+
children,
|
|
98
|
+
onResult,
|
|
99
|
+
// Security props
|
|
100
|
+
interactiveBlacklist,
|
|
101
|
+
interactiveWhitelist,
|
|
102
|
+
onBeforeStep,
|
|
103
|
+
onAfterStep,
|
|
104
|
+
onBeforeTask,
|
|
105
|
+
onAfterTask,
|
|
106
|
+
transformScreenContent,
|
|
107
|
+
customTools,
|
|
108
|
+
instructions,
|
|
109
|
+
stepDelay,
|
|
110
|
+
mcpServerUrl,
|
|
111
|
+
router,
|
|
112
|
+
pathname,
|
|
113
|
+
}: AIAgentProps) {
|
|
114
|
+
const rootViewRef = useRef<any>(null);
|
|
115
|
+
const [isThinking, setIsThinking] = useState(false);
|
|
116
|
+
const [statusText, setStatusText] = useState('');
|
|
117
|
+
const [lastResult, setLastResult] = useState<ExecutionResult | null>(null);
|
|
118
|
+
|
|
119
|
+
// Ref-based resolver for ask_user — stays alive across renders
|
|
120
|
+
const askUserResolverRef = useRef<((answer: string) => void) | null>(null);
|
|
121
|
+
|
|
122
|
+
// ─── Create Runtime ──────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
const config: AgentConfig = useMemo(() => ({
|
|
125
|
+
apiKey,
|
|
126
|
+
model,
|
|
127
|
+
language,
|
|
128
|
+
maxSteps,
|
|
129
|
+
interactiveBlacklist,
|
|
130
|
+
interactiveWhitelist,
|
|
131
|
+
onBeforeStep,
|
|
132
|
+
onAfterStep,
|
|
133
|
+
onBeforeTask,
|
|
134
|
+
onAfterTask,
|
|
135
|
+
transformScreenContent,
|
|
136
|
+
customTools,
|
|
137
|
+
instructions,
|
|
138
|
+
stepDelay,
|
|
139
|
+
mcpServerUrl,
|
|
140
|
+
router,
|
|
141
|
+
pathname,
|
|
142
|
+
onStatusUpdate: setStatusText,
|
|
143
|
+
// Page-agent pattern: block the agent loop until user responds
|
|
144
|
+
onAskUser: (question: string) => {
|
|
145
|
+
return new Promise<string>((resolve) => {
|
|
146
|
+
askUserResolverRef.current = resolve;
|
|
147
|
+
// Show question in chat bar, allow user input
|
|
148
|
+
setLastResult({ success: true, message: `❓ ${question}`, steps: [] });
|
|
149
|
+
setIsThinking(false);
|
|
150
|
+
setStatusText('');
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
}), [
|
|
154
|
+
apiKey, model, language, maxSteps,
|
|
155
|
+
interactiveBlacklist, interactiveWhitelist,
|
|
156
|
+
onBeforeStep, onAfterStep, onBeforeTask, onAfterTask,
|
|
157
|
+
transformScreenContent, customTools, instructions, stepDelay,
|
|
158
|
+
mcpServerUrl, router, pathname,
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const provider = useMemo(() => new GeminiProvider(apiKey, model), [apiKey, model]);
|
|
162
|
+
|
|
163
|
+
const runtime = useMemo(
|
|
164
|
+
() => new AgentRuntime(provider, config, rootViewRef.current, navRef),
|
|
165
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
166
|
+
[provider, config],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Update refs when they change
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
runtime.updateRefs(rootViewRef.current, navRef);
|
|
172
|
+
}, [runtime, navRef]);
|
|
173
|
+
|
|
174
|
+
// ─── MCP Bridge ──────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!mcpServerUrl) return;
|
|
178
|
+
|
|
179
|
+
logger.info('AIAgent', `Setting up MCP bridge at ${mcpServerUrl}`);
|
|
180
|
+
const bridge = new MCPBridge(mcpServerUrl, runtime);
|
|
181
|
+
|
|
182
|
+
return () => {
|
|
183
|
+
bridge.destroy();
|
|
184
|
+
};
|
|
185
|
+
}, [mcpServerUrl, runtime]);
|
|
186
|
+
|
|
187
|
+
// ─── Execute ──────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
const handleSend = useCallback(async (message: string) => {
|
|
190
|
+
if (!message.trim()) return;
|
|
191
|
+
|
|
192
|
+
logger.info('AIAgent', `User message: "${message}"`);
|
|
193
|
+
|
|
194
|
+
// If there's a pending ask_user, resolve it instead of starting a new execution
|
|
195
|
+
if (askUserResolverRef.current) {
|
|
196
|
+
const resolver = askUserResolverRef.current;
|
|
197
|
+
askUserResolverRef.current = null;
|
|
198
|
+
setIsThinking(true);
|
|
199
|
+
setStatusText('Processing your answer...');
|
|
200
|
+
setLastResult(null);
|
|
201
|
+
resolver(message);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Normal execution — new task
|
|
206
|
+
setIsThinking(true);
|
|
207
|
+
setStatusText('Thinking...');
|
|
208
|
+
setLastResult(null);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Ensure we have the latest Fiber tree ref
|
|
212
|
+
runtime.updateRefs(rootViewRef.current, navRef);
|
|
213
|
+
|
|
214
|
+
const result = await runtime.execute(message);
|
|
215
|
+
|
|
216
|
+
setLastResult(result);
|
|
217
|
+
onResult?.(result);
|
|
218
|
+
|
|
219
|
+
logger.info('AIAgent', `Result: ${result.success ? '✅' : '❌'} ${result.message}`);
|
|
220
|
+
} catch (error: any) {
|
|
221
|
+
logger.error('AIAgent', 'Execution failed:', error);
|
|
222
|
+
setLastResult({
|
|
223
|
+
success: false,
|
|
224
|
+
message: `Error: ${error.message}`,
|
|
225
|
+
steps: [],
|
|
226
|
+
});
|
|
227
|
+
} finally {
|
|
228
|
+
setIsThinking(false);
|
|
229
|
+
setStatusText('');
|
|
230
|
+
}
|
|
231
|
+
}, [runtime, navRef, onResult]);
|
|
232
|
+
|
|
233
|
+
// ─── Render ──────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<AgentContext.Provider value={runtime}>
|
|
237
|
+
<View ref={rootViewRef} style={styles.root} collapsable={false}>
|
|
238
|
+
{children}
|
|
239
|
+
</View>
|
|
240
|
+
|
|
241
|
+
{/* Overlay (shown while thinking) */}
|
|
242
|
+
<AgentOverlay visible={isThinking} statusText={statusText} />
|
|
243
|
+
|
|
244
|
+
{/* Chat bar */}
|
|
245
|
+
{showChatBar && (
|
|
246
|
+
<AgentChatBar
|
|
247
|
+
onSend={handleSend}
|
|
248
|
+
isThinking={isThinking}
|
|
249
|
+
lastResult={lastResult}
|
|
250
|
+
language={language}
|
|
251
|
+
onDismiss={() => setLastResult(null)}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
</AgentContext.Provider>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const styles = StyleSheet.create({
|
|
259
|
+
root: {
|
|
260
|
+
flex: 1,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentChatBar — Floating, draggable, compressible chat widget.
|
|
3
|
+
* Does not block underlying UI natively.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useRef } from 'react';
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
TextInput,
|
|
10
|
+
Pressable,
|
|
11
|
+
Text,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Animated,
|
|
14
|
+
PanResponder,
|
|
15
|
+
useWindowDimensions,
|
|
16
|
+
} from 'react-native';
|
|
17
|
+
import type { ExecutionResult } from '../core/types';
|
|
18
|
+
|
|
19
|
+
interface AgentChatBarProps {
|
|
20
|
+
onSend: (message: string) => void;
|
|
21
|
+
isThinking: boolean;
|
|
22
|
+
lastResult: ExecutionResult | null;
|
|
23
|
+
language: 'en' | 'ar';
|
|
24
|
+
onDismiss?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function AgentChatBar({ onSend, isThinking, lastResult, language, onDismiss }: AgentChatBarProps) {
|
|
28
|
+
const [text, setText] = useState('');
|
|
29
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
30
|
+
const { height } = useWindowDimensions();
|
|
31
|
+
const isArabic = language === 'ar';
|
|
32
|
+
|
|
33
|
+
// Initial position: Bottom right for FAB, Bottom center for Expanded
|
|
34
|
+
// For simplicity, we just initialize to a safe generic spot on screen.
|
|
35
|
+
const pan = useRef(new Animated.ValueXY({ x: 10, y: height - 200 })).current;
|
|
36
|
+
|
|
37
|
+
// PanResponder for dragging the widget
|
|
38
|
+
const panResponder = useRef(
|
|
39
|
+
PanResponder.create({
|
|
40
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
41
|
+
// Only trigger drag if moving more than 5px (allows taps to register inside)
|
|
42
|
+
return Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5;
|
|
43
|
+
},
|
|
44
|
+
onPanResponderGrant: () => {
|
|
45
|
+
pan.setOffset({
|
|
46
|
+
x: (pan.x as any)._value,
|
|
47
|
+
y: (pan.y as any)._value,
|
|
48
|
+
});
|
|
49
|
+
pan.setValue({ x: 0, y: 0 });
|
|
50
|
+
},
|
|
51
|
+
onPanResponderMove: Animated.event(
|
|
52
|
+
[null, { dx: pan.x, dy: pan.y }],
|
|
53
|
+
{ useNativeDriver: false }
|
|
54
|
+
),
|
|
55
|
+
onPanResponderRelease: () => {
|
|
56
|
+
pan.flattenOffset();
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
).current;
|
|
60
|
+
|
|
61
|
+
const handleSend = () => {
|
|
62
|
+
if (text.trim() && !isThinking) {
|
|
63
|
+
onSend(text.trim());
|
|
64
|
+
setText('');
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ─── Compressed State (FAB) ───
|
|
69
|
+
if (!isExpanded) {
|
|
70
|
+
return (
|
|
71
|
+
<Animated.View style={[styles.fabContainer, pan.getLayout()]} {...panResponder.panHandlers}>
|
|
72
|
+
<Pressable
|
|
73
|
+
style={styles.fab}
|
|
74
|
+
onPress={() => setIsExpanded(true)}
|
|
75
|
+
accessibilityLabel="Open AI Agent Chat"
|
|
76
|
+
>
|
|
77
|
+
<Text style={styles.fabIcon}>{isThinking ? '⏳' : '🤖'}</Text>
|
|
78
|
+
</Pressable>
|
|
79
|
+
</Animated.View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Expanded State (Widget) ───
|
|
84
|
+
return (
|
|
85
|
+
<Animated.View style={[styles.expandedContainer, pan.getLayout()]}>
|
|
86
|
+
{/* Drag Handle Area */}
|
|
87
|
+
<View {...panResponder.panHandlers} style={styles.dragHandleArea} accessibilityLabel="Drag AI Agent">
|
|
88
|
+
<View style={styles.dragGrip} />
|
|
89
|
+
<Pressable onPress={() => setIsExpanded(false)} style={styles.minimizeBtn} accessibilityLabel="Minimize AI Agent">
|
|
90
|
+
<Text style={styles.minimizeText}>—</Text>
|
|
91
|
+
</Pressable>
|
|
92
|
+
</View>
|
|
93
|
+
|
|
94
|
+
{/* Result message */}
|
|
95
|
+
{lastResult && (
|
|
96
|
+
<View style={[styles.resultBubble, lastResult.success ? styles.resultSuccess : styles.resultError]}>
|
|
97
|
+
<Text style={styles.resultText}>{lastResult.message}</Text>
|
|
98
|
+
{onDismiss && (
|
|
99
|
+
<Pressable style={styles.dismissButton} onPress={onDismiss} hitSlop={12}>
|
|
100
|
+
<Text style={styles.dismissText}>✕</Text>
|
|
101
|
+
</Pressable>
|
|
102
|
+
)}
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Input row */}
|
|
107
|
+
<View style={styles.inputRow}>
|
|
108
|
+
<TextInput
|
|
109
|
+
style={[styles.input, isArabic && styles.inputRTL]}
|
|
110
|
+
placeholder={isArabic ? 'اكتب طلبك...' : 'Ask AI...'}
|
|
111
|
+
placeholderTextColor="#999"
|
|
112
|
+
value={text}
|
|
113
|
+
onChangeText={setText}
|
|
114
|
+
onSubmitEditing={handleSend}
|
|
115
|
+
returnKeyType="send"
|
|
116
|
+
editable={!isThinking}
|
|
117
|
+
multiline={false}
|
|
118
|
+
/>
|
|
119
|
+
<Pressable
|
|
120
|
+
style={[styles.sendButton, isThinking && styles.sendButtonDisabled]}
|
|
121
|
+
onPress={handleSend}
|
|
122
|
+
disabled={isThinking || !text.trim()}
|
|
123
|
+
accessibilityLabel="Send request to AI Agent"
|
|
124
|
+
>
|
|
125
|
+
<Text style={styles.sendButtonText}>
|
|
126
|
+
{isThinking ? '⏳' : '🚀'}
|
|
127
|
+
</Text>
|
|
128
|
+
</Pressable>
|
|
129
|
+
</View>
|
|
130
|
+
</Animated.View>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const styles = StyleSheet.create({
|
|
135
|
+
// FAB Styles
|
|
136
|
+
fabContainer: {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
zIndex: 9999,
|
|
139
|
+
},
|
|
140
|
+
fab: {
|
|
141
|
+
width: 60,
|
|
142
|
+
height: 60,
|
|
143
|
+
borderRadius: 30,
|
|
144
|
+
backgroundColor: '#1a1a2e',
|
|
145
|
+
justifyContent: 'center',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
elevation: 5,
|
|
148
|
+
shadowColor: '#000',
|
|
149
|
+
shadowOffset: { width: 0, height: 4 },
|
|
150
|
+
shadowOpacity: 0.3,
|
|
151
|
+
shadowRadius: 6,
|
|
152
|
+
},
|
|
153
|
+
fabIcon: {
|
|
154
|
+
fontSize: 28,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Expanded Styles
|
|
158
|
+
expandedContainer: {
|
|
159
|
+
position: 'absolute',
|
|
160
|
+
zIndex: 9999,
|
|
161
|
+
width: 340,
|
|
162
|
+
backgroundColor: 'rgba(26, 26, 46, 0.95)',
|
|
163
|
+
borderRadius: 24,
|
|
164
|
+
padding: 16,
|
|
165
|
+
paddingTop: 8,
|
|
166
|
+
elevation: 8,
|
|
167
|
+
shadowColor: '#000',
|
|
168
|
+
shadowOffset: { width: 0, height: 8 },
|
|
169
|
+
shadowOpacity: 0.4,
|
|
170
|
+
shadowRadius: 10,
|
|
171
|
+
},
|
|
172
|
+
dragHandleArea: {
|
|
173
|
+
width: '100%',
|
|
174
|
+
height: 30,
|
|
175
|
+
justifyContent: 'center',
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
marginBottom: 8,
|
|
178
|
+
},
|
|
179
|
+
dragGrip: {
|
|
180
|
+
width: 40,
|
|
181
|
+
height: 5,
|
|
182
|
+
backgroundColor: 'rgba(255, 255, 255, 0.3)',
|
|
183
|
+
borderRadius: 4,
|
|
184
|
+
},
|
|
185
|
+
minimizeBtn: {
|
|
186
|
+
position: 'absolute',
|
|
187
|
+
right: 0,
|
|
188
|
+
top: 0,
|
|
189
|
+
padding: 8,
|
|
190
|
+
},
|
|
191
|
+
minimizeText: {
|
|
192
|
+
color: '#fff',
|
|
193
|
+
fontSize: 18,
|
|
194
|
+
fontWeight: 'bold',
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Results & Input
|
|
198
|
+
resultBubble: {
|
|
199
|
+
borderRadius: 12,
|
|
200
|
+
padding: 12,
|
|
201
|
+
marginBottom: 12,
|
|
202
|
+
flexDirection: 'row',
|
|
203
|
+
alignItems: 'flex-start',
|
|
204
|
+
},
|
|
205
|
+
resultSuccess: {
|
|
206
|
+
backgroundColor: 'rgba(40, 167, 69, 0.2)',
|
|
207
|
+
},
|
|
208
|
+
resultError: {
|
|
209
|
+
backgroundColor: 'rgba(220, 53, 69, 0.2)',
|
|
210
|
+
},
|
|
211
|
+
resultText: {
|
|
212
|
+
color: '#fff',
|
|
213
|
+
fontSize: 14,
|
|
214
|
+
lineHeight: 20,
|
|
215
|
+
flex: 1,
|
|
216
|
+
},
|
|
217
|
+
dismissButton: {
|
|
218
|
+
marginLeft: 8,
|
|
219
|
+
padding: 2,
|
|
220
|
+
},
|
|
221
|
+
dismissText: {
|
|
222
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
223
|
+
fontSize: 14,
|
|
224
|
+
fontWeight: 'bold',
|
|
225
|
+
},
|
|
226
|
+
inputRow: {
|
|
227
|
+
flexDirection: 'row',
|
|
228
|
+
alignItems: 'center',
|
|
229
|
+
gap: 8,
|
|
230
|
+
},
|
|
231
|
+
input: {
|
|
232
|
+
flex: 1,
|
|
233
|
+
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
234
|
+
borderRadius: 20,
|
|
235
|
+
paddingHorizontal: 16,
|
|
236
|
+
paddingVertical: 10,
|
|
237
|
+
color: '#fff',
|
|
238
|
+
fontSize: 16,
|
|
239
|
+
},
|
|
240
|
+
inputRTL: {
|
|
241
|
+
textAlign: 'right',
|
|
242
|
+
writingDirection: 'rtl',
|
|
243
|
+
},
|
|
244
|
+
sendButton: {
|
|
245
|
+
width: 40,
|
|
246
|
+
height: 40,
|
|
247
|
+
borderRadius: 20,
|
|
248
|
+
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
|
249
|
+
justifyContent: 'center',
|
|
250
|
+
alignItems: 'center',
|
|
251
|
+
},
|
|
252
|
+
sendButtonDisabled: {
|
|
253
|
+
opacity: 0.5,
|
|
254
|
+
},
|
|
255
|
+
sendButtonText: {
|
|
256
|
+
fontSize: 18,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
@@ -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
|
+
});
|