Fara-BrowserUse / src /components /WelcomeScreen.tsx
VyoJ's picture
Upload 78 files
7fcdb70 verified
import { fetchAvailableModels, generateRandomQuestion } from '@/services/api';
import { selectAvailableModels, selectIsDarkMode, selectIsLoadingModels, selectSelectedModelId, useAgentStore } from '@/stores/agentStore';
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
import SendIcon from '@mui/icons-material/Send';
import ShuffleIcon from '@mui/icons-material/Shuffle';
import SmartToyIcon from '@mui/icons-material/SmartToy';
import { Box, Button, CircularProgress, Container, FormControl, IconButton, InputLabel, MenuItem, Paper, Select, TextField, Typography } from '@mui/material';
import React, { useEffect, useRef, useState } from 'react';
interface WelcomeScreenProps {
onStartTask: (instruction: string, modelId: string) => void;
isConnected: boolean;
}
export const WelcomeScreen: React.FC<WelcomeScreenProps> = ({ onStartTask, isConnected }) => {
const [customTask, setCustomTask] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [isGeneratingQuestion, setIsGeneratingQuestion] = useState(false);
const typingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const isDarkMode = useAgentStore(selectIsDarkMode);
const toggleDarkMode = useAgentStore((state) => state.toggleDarkMode);
const selectedModelId = useAgentStore(selectSelectedModelId);
const setSelectedModelId = useAgentStore((state) => state.setSelectedModelId);
const availableModels = useAgentStore(selectAvailableModels);
const isLoadingModels = useAgentStore(selectIsLoadingModels);
const setAvailableModels = useAgentStore((state) => state.setAvailableModels);
const setIsLoadingModels = useAgentStore((state) => state.setIsLoadingModels);
// Load available models on mount
useEffect(() => {
const loadModels = async () => {
setIsLoadingModels(true);
try {
const models = await fetchAvailableModels();
setAvailableModels(models);
// Set first model as default if current selection is not in the list
if (models.length > 0 && !models.includes(selectedModelId)) {
setSelectedModelId(models[0]);
}
} catch (error) {
console.error('Failed to load models:', error);
// Fallback to empty array on error
setAvailableModels([]);
} finally {
setIsLoadingModels(false);
}
};
loadModels();
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Clean up typing interval on unmount
useEffect(() => {
return () => {
if (typingIntervalRef.current) {
clearInterval(typingIntervalRef.current);
}
};
}, []);
const handleWriteRandomTask = async () => {
// Clear any existing typing interval
if (typingIntervalRef.current) {
clearInterval(typingIntervalRef.current);
typingIntervalRef.current = null;
}
setIsGeneratingQuestion(true);
try {
const randomTask = await generateRandomQuestion();
// Clear current text
setCustomTask('');
setIsTyping(true);
// Type effect
let currentIndex = 0;
typingIntervalRef.current = setInterval(() => {
if (currentIndex < randomTask.length) {
setCustomTask(randomTask.substring(0, currentIndex + 1));
currentIndex++;
} else {
if (typingIntervalRef.current) {
clearInterval(typingIntervalRef.current);
typingIntervalRef.current = null;
}
setIsTyping(false);
}
}, 10); // 10ms per character
} catch (error) {
console.error('Failed to generate question:', error);
setIsTyping(false);
} finally {
setIsGeneratingQuestion(false);
}
};
const handleCustomTask = () => {
if (customTask.trim() && !isTyping) {
onStartTask(customTask.trim(), selectedModelId);
}
};
return (
<>
{/* Dark Mode Toggle - Top Right (Absolute to viewport) */}
<Box sx={{ position: 'absolute', top: 24, right: 24, zIndex: 1000 }}>
<IconButton
onClick={toggleDarkMode}
size="medium"
sx={{
color: 'text.primary',
backgroundColor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
'&:hover': {
backgroundColor: 'action.hover',
borderColor: 'primary.main',
},
}}
>
{isDarkMode ? <LightModeOutlined /> : <DarkModeOutlined />}
</IconButton>
</Box>
<Container
maxWidth="md"
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
textAlign: 'center',
py: 8,
}}
>
{/* Title */}
<Typography
variant="h2"
sx={{
fontWeight: 800,
mb: 1,
color: 'text.primary',
}}
>
FARA Agent
</Typography>
{/* Powered by Microsoft */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
<Typography
variant="body2"
sx={{
color: 'text.secondary',
fontWeight: 500,
}}
>
Powered by
</Typography>
{/* Microsoft Fara link */}
<Box
component="a"
href="https://github.com/microsoft/fara"
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
textDecoration: 'none',
transition: 'all 0.2s ease',
'&:hover': {
'& .fara-text': {
textDecoration: 'underline',
},
},
}}
>
<Typography
className="fara-text"
sx={{
color: 'primary.main',
fontWeight: 700,
fontSize: '1rem',
}}
>
Microsoft Fara-7B
</Typography>
</Box>
{/* Separator */}
<Typography
variant="body2"
sx={{
color: 'text.secondary',
mx: 0.5,
}}
>
&
</Typography>
{/* Modal link */}
<Box
component="a"
href="https://modal.com/"
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
textDecoration: 'none',
transition: 'all 0.2s ease',
'&:hover': {
'& .modal-text': {
textDecoration: 'underline',
},
},
}}
>
<Typography
className="modal-text"
sx={{
color: 'primary.main',
fontWeight: 700,
fontSize: '1rem',
}}
>
Modal
</Typography>
</Box>
</Box>
{/* Subtitle */}
<Typography
variant="h6"
sx={{
color: 'text.secondary',
fontWeight: 500,
mb: 1,
}}
>
AI-Powered Browser Automation
</Typography>
{/* Description */}
<Typography
variant="body1"
sx={{
color: 'text.secondary',
maxWidth: '650px',
mb: 3,
lineHeight: 1.7,
}}
>
Experience the future of AI automation as FARA operates your browser in real time to complete complex on-screen tasks.
Built with{' '}
<Box
component="a"
href="https://github.com/microsoft/fara"
target="_blank"
rel="noopener noreferrer"
sx={{
color: 'primary.main',
textDecoration: 'none',
fontWeight: 700,
'&:hover': {
textDecoration: 'underline',
},
}}
>
Microsoft Fara-7B
</Box>
, a vision-language model specifically designed for <strong>computer use and GUI automation</strong>.
</Typography>
{/* Task Input Section */}
<Paper
elevation={0}
sx={{
maxWidth: '725px',
width: '100%',
p: 2.5,
border: '2px solid',
borderColor: isConnected ? 'primary.main' : 'divider',
borderRadius: 2,
backgroundColor: 'background.paper',
transition: 'all 0.2s ease',
'&:hover': isConnected ? {
borderColor: 'primary.dark',
boxShadow: (theme) => `0 4px 16px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.15)'}`,
} : {},
}}
>
{/* Input Field */}
<TextField
fullWidth
placeholder="Describe your task here..."
value={customTask}
onChange={(e) => setCustomTask(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && !e.shiftKey && isConnected && customTask.trim() && !isTyping) {
handleCustomTask();
}
}}
disabled={!isConnected || isTyping}
multiline
rows={3}
sx={{
mb: 2,
'& .MuiOutlinedInput-root': {
borderRadius: 1.5,
backgroundColor: 'action.hover',
color: 'text.primary',
'& fieldset': {
borderColor: 'divider',
},
'&:hover fieldset': {
borderColor: 'text.secondary',
},
'&.Mui-focused fieldset': {
borderColor: 'primary.main',
borderWidth: '2px',
},
},
'& .MuiInputBase-input': {
color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
fontWeight: 500,
WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
},
'& .MuiInputBase-input.Mui-disabled': {
color: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
WebkitTextFillColor: (theme) => theme.palette.mode === 'dark' ? '#FFFFFF !important' : '#000000 !important',
},
'& .MuiInputBase-input::placeholder': {
color: 'text.secondary',
opacity: 0.7,
},
}}
/>
{/* Model Selection + Buttons Row */}
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'space-between' }}>
{/* Model Select */}
<FormControl size="small" sx={{ minWidth: 240 }}>
<InputLabel id="model-select-label">Model</InputLabel>
<Select
labelId="model-select-label"
value={availableModels.length > 0 && availableModels.includes(selectedModelId) ? selectedModelId : ''}
label="Model"
onChange={(e) => setSelectedModelId(e.target.value)}
disabled={!isConnected || isTyping || isLoadingModels}
sx={{
borderRadius: 1.5,
'& .MuiOutlinedInput-notchedOutline': {
borderWidth: 2,
},
}}
>
{isLoadingModels ? (
<MenuItem disabled>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="body2">Loading models...</Typography>
</Box>
</MenuItem>
) : availableModels.length === 0 ? (
<MenuItem disabled>
<Typography variant="body2" sx={{ color: 'error.main' }}>
No models available
</Typography>
</MenuItem>
) : (
availableModels.map((modelId) => (
<MenuItem key={modelId} value={modelId}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SmartToyIcon sx={{ fontSize: '0.9rem', color: 'primary.main' }} />
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.875rem' }}>
{modelId.split('/').pop()}
</Typography>
</Box>
</MenuItem>
))
)}
</Select>
</FormControl>
{/* Buttons on the right */}
<Box sx={{ display: 'flex', gap: 1.5 }}>
<Button
variant="outlined"
onClick={handleWriteRandomTask}
disabled={!isConnected || isTyping || isGeneratingQuestion}
startIcon={isGeneratingQuestion ? <CircularProgress size={16} /> : <ShuffleIcon />}
sx={{
borderRadius: 1.5,
textTransform: 'none',
fontWeight: 600,
borderWidth: 2,
px: 3,
'&:hover': {
borderWidth: 2,
},
}}
>
{isGeneratingQuestion ? 'Generating...' : isTyping ? 'Writing...' : 'Write random task'}
</Button>
<Button
variant="contained"
onClick={handleCustomTask}
disabled={!isConnected || !customTask.trim() || isTyping}
sx={{
borderRadius: 1.5,
textTransform: 'none',
fontWeight: 600,
px: 4,
background: 'linear-gradient(135deg, #4F86C6 0%, #2B5C94 100%)',
}}
endIcon={<SendIcon />}
>
Run Task
</Button>
</Box>
</Box>
</Paper>
{/* Research Notice */}
<Typography
variant="body2"
sx={{
color: 'text.secondary',
maxWidth: '700px',
mt: 3,
mb: 2,
lineHeight: 1.6,
fontStyle: 'italic',
opacity: 0.8,
textAlign: 'center',
}}
>
This is a demo of the FARA computer use agent. The agent will browse the web on your behalf.
Cold starts may take upto 1 minute for the first prompt after which each step should take 5-10s.
<strong> Please do not enter any personal or sensitive information.</strong>
{' '}Task logs will be stored for research purposes.
</Typography>
{/* Credits */}
<Typography
variant="caption"
sx={{
color: 'text.secondary',
mt: 1,
opacity: 0.7,
textAlign: 'center',
}}
>
Frontend based on{' '}
<Box
component="a"
href="https://huggingface.co/spaces/smolagents/computer-use-agent"
target="_blank"
rel="noopener noreferrer"
sx={{
color: 'primary.main',
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
}}
>
HuggingFace smolagents/computer-use-agent
</Box>
</Typography>
{/* Connection status hint */}
{!isConnected && (
<Typography
variant="caption"
sx={{
mt: 2,
color: 'text.secondary',
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
backgroundColor: 'warning.main',
animation: 'pulse 2s ease-in-out infinite',
'@keyframes pulse': {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0.5 },
},
}}
/>
Make sure the backend is running on port 8000
</Typography>
)}
</Container>
</>
);
};