CoolFace
Apppublic

HyperCluster/Fara-BrowserUse

sourceHugging Facemitupdated 10mo agoView on Hugging Face
5likes
WelcomeScreen.tsx522 linesDownload Raw Back to components
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