tfrere's picture
tfrere HF Staff
Front update (#5)
67b042f
raw
history blame
15.3 kB
import React, { useState, useEffect, useRef } from 'react';
import { Box, Typography, Button, Container, Paper, TextField, IconButton, Select, MenuItem, FormControl, InputLabel, CircularProgress } from '@mui/material';
import ShuffleIcon from '@mui/icons-material/Shuffle';
import SendIcon from '@mui/icons-material/Send';
import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
import SmartToyIcon from '@mui/icons-material/SmartToy';
import { useAgentStore, selectSelectedModelId, selectIsDarkMode, selectAvailableModels, selectIsLoadingModels } from '@/stores/agentStore';
import { fetchAvailableModels, generateRandomQuestion } from '@/services/api';
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(selectedModelId);
// 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);
}
}, 30); // 30ms 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',
}}
>
CUA2 Agent
</Typography>
{/* Powered by smolagents */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
}}
>
<Typography
variant="body2"
sx={{
color: 'text.secondary',
fontWeight: 500,
}}
>
Powered by
</Typography>
<Box
component="a"
href="https://github.com/huggingface/smolagents"
target="_blank"
rel="noopener noreferrer"
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
textDecoration: 'none',
transition: 'all 0.2s ease',
'&:hover': {
'& .smolagents-text': {
textDecoration: 'underline',
},
},
}}
>
{/* Hugging Face Official Logo */}
<Box
component="img"
src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg"
alt="Hugging Face"
sx={{
width: 24,
height: 24,
flexShrink: 0,
}}
/>
<Typography
className="smolagents-text"
sx={{
color: 'primary.main',
fontWeight: 700,
fontSize: '1rem',
}}
>
smolagents
</Typography>
{/* GitHub stars badge */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.25,
backgroundColor: (theme) =>
theme.palette.mode === 'dark'
? 'rgba(144, 202, 249, 0.08)'
: 'rgba(25, 118, 210, 0.08)',
borderRadius: 1,
border: '1px solid',
borderColor: 'primary.main',
}}
>
<Box component="span" sx={{ fontSize: '0.75rem' }}></Box>
<Typography
variant="caption"
sx={{
fontWeight: 700,
color: 'primary.main',
fontSize: '0.75rem',
}}
>
23.7k
</Typography>
</Box>
</Box>
</Box>
{/* Subtitle */}
<Typography
variant="h6"
sx={{
color: 'text.secondary',
fontWeight: 500,
mb: 1,
}}
>
AI-Powered Computer Use Automation
</Typography>
{/* Description */}
<Typography
variant="body1"
sx={{
color: 'text.secondary',
maxWidth: '650px',
mb: 6,
lineHeight: 1.7,
}}
>
Watch in real-time as AI agents write and execute Python code to complete tasks.
Built by Hugging Face, <strong>smolagents</strong> is LLM-agnostic and uses <strong>30% fewer steps</strong> than traditional agents.
</Typography>
{/* Task Input Section */}
<Paper
elevation={0}
sx={{
maxWidth: '700px',
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>
{/* 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>
</>
);
};