| | 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);
|
| |
|
| |
|
| | useEffect(() => {
|
| | const loadModels = async () => {
|
| | setIsLoadingModels(true);
|
| | try {
|
| | const models = await fetchAvailableModels();
|
| | setAvailableModels(models);
|
| |
|
| |
|
| | if (models.length > 0 && !models.includes(selectedModelId)) {
|
| | setSelectedModelId(models[0]);
|
| | }
|
| | } catch (error) {
|
| | console.error('Failed to load models:', error);
|
| |
|
| | setAvailableModels([]);
|
| | } finally {
|
| | setIsLoadingModels(false);
|
| | }
|
| | };
|
| |
|
| | loadModels();
|
| | }, []);
|
| |
|
| |
|
| | useEffect(() => {
|
| | return () => {
|
| | if (typingIntervalRef.current) {
|
| | clearInterval(typingIntervalRef.current);
|
| | }
|
| | };
|
| | }, []);
|
| |
|
| | const handleWriteRandomTask = async () => {
|
| |
|
| | if (typingIntervalRef.current) {
|
| | clearInterval(typingIntervalRef.current);
|
| | typingIntervalRef.current = null;
|
| | }
|
| |
|
| | setIsGeneratingQuestion(true);
|
| | try {
|
| | const randomTask = await generateRandomQuestion();
|
| |
|
| |
|
| | setCustomTask('');
|
| | setIsTyping(true);
|
| |
|
| |
|
| | 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);
|
| | } 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>
|
| | </>
|
| | );
|
| | };
|
| |
|