|
|
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>
|
|
|
</>
|
|
|
);
|
|
|
};
|
|
|
|