HyperCluster/Fara-BrowserUse
5
1import { fetchAvailableModels, generateRandomQuestion } from '@/services/api';
2import { selectAvailableModels, selectIsDarkMode, selectIsLoadingModels, selectSelectedModelId, useAgentStore } from '@/stores/agentStore';
3import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
4import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
5import SendIcon from '@mui/icons-material/Send';
6import ShuffleIcon from '@mui/icons-material/Shuffle';
7import SmartToyIcon from '@mui/icons-material/SmartToy';
8import { Box, Button, CircularProgress, Container, FormControl, IconButton, InputLabel, MenuItem, Paper, Select, TextField, Typography } from '@mui/material';
9import React, { useEffect, useRef, useState } from 'react';
10
11interface WelcomeScreenProps {
12 onStartTask: (instruction: string, modelId: string) => void;
13 isConnected: boolean;
14}
15
16export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({ onStartTask, isConnected }) => {
17 const [customTask, setCustomTask] = useState('');
18 const [isTyping, setIsTyping] = useState(false);
19 const [isGeneratingQuestion, setIsGeneratingQuestion] = useState(false);
20 const typingIntervalRef = useRef<NodeJS.Timeout | null>(null);
21
22 const isDarkMode = useAgentStore(selectIsDarkMode);
23 const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode);
24 const selectedModelId = useAgentStore(selectSelectedModelId);
25 const setSelectedModelId = useAgentStore((state) => state.setSelectedModelId);
26 const availableModels = useAgentStore(selectAvailableModels);
27 const isLoadingModels = useAgentStore(selectIsLoadingModels);
28 const setAvailableModels = useAgentStore((state) => state.setAvailableModels);
29 const setIsLoadingModels = useAgentStore((state) => state.setIsLoadingModels);
30
31 // Load available models on mount
32 useEffect(() => {
33 const loadModels = async () => {
34 setIsLoadingModels(true);
35 try {
36 const models = await fetchAvailableModels();
37 setAvailableModels(models);
38
39 // Set first model as default if current selection is not in the list
40 if (models.length > 0 && !models.includes(selectedModelId)) {
41 setSelectedModelId(models[0]);
42 }
43 } catch (error) {
44 console.error('Failed to load models:', error);
45 // Fallback to empty array on error
46 setAvailableModels([]);
47 } finally {
48 setIsLoadingModels(false);
49 }
50 };
51
52 loadModels();
53 }, []); // eslint-disable-line react-hooks/exhaustive-deps
54
55 // Clean up typing interval on unmount
56 useEffect(() => {
57 return () => {
58 if (typingIntervalRef.current) {
59 clearInterval(typingIntervalRef.current);
60 }
61 };
62 }, []);
63
64 const handleWriteRandomTask = async () => {
65 // Clear any existing typing interval
66 if (typingIntervalRef.current) {
67 clearInterval(typingIntervalRef.current);
68 typingIntervalRef.current = null;
69 }
70
71 setIsGeneratingQuestion(true);
72 try {
73 const randomTask = await generateRandomQuestion();
74
75 // Clear current text
76 setCustomTask('');
77 setIsTyping(true);
78
79 // Type effect
80 let currentIndex = 0;
81 typingIntervalRef.current = setInterval(() => {
82 if (currentIndex < randomTask.length) {
83 setCustomTask(randomTask.substring(0, currentIndex + 1));
84 currentIndex++;
85 } else {
86 if (typingIntervalRef.current) {
87 clearInterval(typingIntervalRef.current);
88 typingIntervalRef.current = null;
89 }
90 setIsTyping(false);
91 }
92 }, 10); // 10ms per character
93 } catch (error) {
94 console.error('Failed to generate question:', error);
95 setIsTyping(false);
96 } finally {
97 setIsGeneratingQuestion(false);
98 }
99 };
100
101 const handleCustomTask = () => {
102 if (customTask.trim() && !isTyping) {
103 onStartTask(customTask.trim(), selectedModelId);
104 }
105 };
106
107 return (
108 <>
109 {/* Dark Mode Toggle - Top Right (Absolute to viewport) */}
110 <Box sx={{ position: 'absolute', top: 24, right: 24, zIndex: 1000 }}>
111 <IconButton
112 onClick={toggleDarkMode}
113 size="medium"
114 sx={{
115 color: 'text.primary',
116 backgroundColor: 'background.paper',
117 border: '1px solid',
118 borderColor: 'divider',
119 '&:hover': {
120 backgroundColor: 'action.hover',
121 borderColor: 'primary.main',
122 },
123 }}
124 >
125 {isDarkMode ? <LightModeOutlined /> : <DarkModeOutlined />}
126 </IconButton>
127 </Box>
128
129 <Container
130 maxWidth="md"
131 sx={{
132 display: 'flex',
133 flexDirection: 'column',
134 alignItems: 'center',
135 justifyContent: 'center',
136 minHeight: '100vh',
137 textAlign: 'center',
138 py: 8,
139 }}
140 >
141 {/* Title */}
142 <Typography
143 variant="h2"
144 sx={{
145 fontWeight: 800,
146 mb: 1,
147 color: 'text.primary',
148 }}
149 >
150 FARA Agent
151 </Typography>
152
153 {/* Powered by Microsoft */}
154 <Box
155 sx={{
156 display: 'flex',
157 alignItems: 'center',
158 gap: 1,
159 mb: 2,
160 flexWrap: 'wrap',
161 justifyContent: 'center',
162 }}
163 >
164 <Typography
165 variant="body2"
166 sx={{
167 color: 'text.secondary',
168 fontWeight: 500,
169 }}
170 >
171 Powered by
172 </Typography>
173
174 {/* Microsoft Fara link */}
175 <Box
176 component="a"
177 href="https://github.com/microsoft/fara"
178 target="_blank"
179 rel="noopener noreferrer"
180 sx={{
181 display: 'flex',
182 alignItems: 'center',
183 gap: 0.75,
184 textDecoration: 'none',
185 transition: 'all 0.2s ease',
186 '&:hover': {
187 '& .fara-text': {
188 textDecoration: 'underline',
189 },
190 },
191 }}
192 >
193 <Typography
194 className="fara-text"
195 sx={{
196 color: 'primary.main',
197 fontWeight: 700,
198 fontSize: '1rem',
199 }}
200 >
201 Microsoft Fara-7B
202 </Typography>
203 </Box>
204
205 {/* Separator */}
206 <Typography
207 variant="body2"
208 sx={{
209 color: 'text.secondary',
210 mx: 0.5,
211 }}
212 >
213 &
214 </Typography>
215
216 {/* Modal link */}
217 <Box
218 component="a"
219 href="https://modal.com/"
220 target="_blank"
221 rel="noopener noreferrer"
222 sx={{
223 display: 'flex',
224 alignItems: 'center',
225 gap: 0.75,
226 textDecoration: 'none',
227 transition: 'all 0.2s ease',
228 '&:hover': {
229 '& .modal-text': {
230 textDecoration: 'underline',
231 },
232 },
233 }}
234 >
235 <Typography
236 className="modal-text"
237 sx={{
238 color: 'primary.main',
239 fontWeight: 700,
240 fontSize: '1rem',
241 }}
242 >
243 Modal
244 </Typography>
245 </Box>
246 </Box>
247
248 {/* Subtitle */}
249 <Typography
250 variant="h6"
251 sx={{
252 color: 'text.secondary',
253 fontWeight: 500,
254 mb: 1,
255 }}
256 >
257 AI-Powered Browser Automation
258 </Typography>
259
260 {/* Description */}
261 <Typography
262 variant="body1"
263 sx={{
264 color: 'text.secondary',
265 maxWidth: '650px',
266 mb: 3,
267 lineHeight: 1.7,
268 }}
269 >
270 Experience the future of AI automation as FARA operates your browser in real time to complete complex on-screen tasks.
271 Built with{' '}
272 <Box
273 component="a"
274 href="https://github.com/microsoft/fara"
275 target="_blank"
276 rel="noopener noreferrer"
277 sx={{
278 color: 'primary.main',
279 textDecoration: 'none',
280 fontWeight: 700,
281 '&:hover': {
282 textDecoration: 'underline',
283 },
284 }}
285 >
286 Microsoft Fara-7B
287 </Box>
288 , a vision-language model specifically designed for <strong>computer use and GUI automation</strong>.
289 </Typography>
290
291 {/* Task Input Section */}
292 <Paper
293 elevation={0}
294 sx={{
295 maxWidth: '725px',
296 width: '100%',
297 p: 2.5,
298 border: '2px solid',
299 borderColor: isConnected ? 'primary.main' : 'divider',
300 borderRadius: 2,
301 backgroundColor: 'background.paper',
302 transition: 'all 0.2s ease',
303 '&:hover': isConnected ? {
304 borderColor: 'primary.dark',
305 boxShadow: (theme) => `0 4px 16px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.15)'}`,
306 } : {},
307 }}
308 >
309 {/* Input Field */}
310 <TextField
311 fullWidth
312 placeholder="Describe your task here..."
313 value={customTask}
314 onChange={(e) => setCustomTask(e.target.value)}
315 onKeyPress={(e) => {
316 if (e.key === 'Enter' && !e.shiftKey && isConnected && customTask.trim() && !isTyping) {
317 handleCustomTask();
318 }
319 }}
320 disabled={!isConnected || isTyping}
321 multiline
322 rows={3}
323 sx={{
324 mb: 2,
325 '& .MuiOutlinedInput-root': {
326 borderRadius: 1.5,
327 backgroundColor: 'action.hover',
328 color: 'text.primary',
329 '& fieldset': {
330 borderColor: 'divider',
331 },
332 '&:hover fieldset': {
333 borderColor: 'text.secondary',
334 },
335 '&.Mui-focused fieldset': {
336 borderColor: 'primary.main',
337 borderWidth: '2px',
338 },
339 },
340 '& .MuiInputBase-input': {
341 color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
342 fontWeight: 500,
343 WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
344 },
345 '& .MuiInputBase-input.Mui-disabled': {
346 color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
347 WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
348 },
349 '& .MuiInputBase-input::placeholder': {
350 color: 'text.secondary',
351 opacity: 0.7,
352 },
353 }}
354 />
355
356 {/* Model Selection + Buttons Row */}
357 <Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'space-between' }}>
358 {/* Model Select */}
359 <FormControl size="small" sx={{ minWidth: 240 }}>
360 <InputLabel id="model-select-label">Model</InputLabel>
361 <Select
362 labelId="model-select-label"
363 value={availableModels.length > 0 && availableModels.includes(selectedModelId) ? selectedModelId : ''}
364 label="Model"
365 onChange={(e) => setSelectedModelId(e.target.value)}
366 disabled={!isConnected || isTyping || isLoadingModels}
367 sx={{
368 borderRadius: 1.5,
369 '& .MuiOutlinedInput-notchedOutline': {
370 borderWidth: 2,
371 },
372 }}
373 >
374 {isLoadingModels ? (
375 <MenuItem disabled>
376 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
377 <CircularProgress size={16} />
378 <Typography variant="body2">Loading models...</Typography>
379 </Box>
380 </MenuItem>
381 ) : availableModels.length === 0 ? (
382 <MenuItem disabled>
383 <Typography variant="body2" sx={{ color: 'error.main' }}>
384 No models available
385 </Typography>
386 </MenuItem>
387 ) : (
388 availableModels.map((modelId) => (
389 <MenuItem key={modelId} value={modelId}>
390 <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
391 <SmartToyIcon sx={{ fontSize: '0.9rem', color: 'primary.main' }} />
392 <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.875rem' }}>
393 {modelId.split('/').pop()}
394 </Typography>
395 </Box>
396 </MenuItem>
397 ))
398 )}
399 </Select>
400 </FormControl>
401
402 {/* Buttons on the right */}
403 <Box sx={{ display: 'flex', gap: 1.5 }}>
404 <Button
405 variant="outlined"
406 onClick={handleWriteRandomTask}
407 disabled={!isConnected || isTyping || isGeneratingQuestion}
408 startIcon={isGeneratingQuestion ? <CircularProgress size={16} /> : <ShuffleIcon />}
409 sx={{
410 borderRadius: 1.5,
411 textTransform: 'none',
412 fontWeight: 600,
413 borderWidth: 2,
414 px: 3,
415 '&:hover': {
416 borderWidth: 2,
417 },
418 }}
419 >
420 {isGeneratingQuestion ? 'Generating...' : isTyping ? 'Writing...' : 'Write random task'}
421 </Button>
422
423 <Button
424 variant="contained"
425 onClick={handleCustomTask}
426 disabled={!isConnected || !customTask.trim() || isTyping}
427 sx={{
428 borderRadius: 1.5,
429 textTransform: 'none',
430 fontWeight: 600,
431 px: 4,
432 background: 'linear-gradient(135deg, #4F86C6 0%, #2B5C94 100%)',
433 }}
434 endIcon={<SendIcon />}
435 >
436 Run Task
437 </Button>
438 </Box>
439 </Box>
440 </Paper>
441
442 {/* Research Notice */}
443 <Typography
444 variant="body2"
445 sx={{
446 color: 'text.secondary',
447 maxWidth: '700px',
448 mt: 3,
449 mb: 2,
450 lineHeight: 1.6,
451 fontStyle: 'italic',
452 opacity: 0.8,
453 textAlign: 'center',
454 }}
455 >
456 This is a demo of the FARA computer use agent. The agent will browse the web on your behalf.
457 Cold starts may take upto 1 minute for the first prompt after which each step should take 5-10s.
458 <strong> Please do not enter any personal or sensitive information.</strong>
459 {' '}Task logs will be stored for research purposes.
460 </Typography>
461
462 {/* Credits */}
463 <Typography
464 variant="caption"
465 sx={{
466 color: 'text.secondary',
467 mt: 1,
468 opacity: 0.7,
469 textAlign: 'center',
470 }}
471 >
472 Frontend based on{' '}
473 <Box
474 component="a"
475 href="https://huggingface.co/spaces/smolagents/computer-use-agent"
476 target="_blank"
477 rel="noopener noreferrer"
478 sx={{
479 color: 'primary.main',
480 textDecoration: 'none',
481 '&:hover': {
482 textDecoration: 'underline',
483 },
484 }}
485 >
486 HuggingFace smolagents/computer-use-agent
487 </Box>
488 </Typography>
489
490 {/* Connection status hint */}
491 {!isConnected && (
492 <Typography
493 variant="caption"
494 sx={{
495 mt: 2,
496 color: 'text.secondary',
497 display: 'flex',
498 alignItems: 'center',
499 gap: 1,
500 }}
501 >
502 <Box
503 sx={{
504 width: 8,
505 height: 8,
506 borderRadius: '50%',
507 backgroundColor: 'warning.main',
508 animation: 'pulse 2s ease-in-out infinite',
509 '@keyframes pulse': {
510 '0%, 100%': { opacity: 1 },
511 '50%': { opacity: 0.5 },
512 },
513 }}
514 />
515 Make sure the backend is running on port 8000
516 </Typography>
517 )}
518 </Container>
519 </>
520 );
521};
522 