danielle2035's picture
Add file
d32e728
Raw
History Blame Contribute Delete
15.8 kB
import React, { useState, useMemo } from "react";
import axios from "axios";
import {
Box,
Button,
Container,
Typography,
Paper,
ThemeProvider,
createTheme,
IconButton,
Card,
CardMedia,
Chip,
AppBar,
Toolbar,
TextField,
Select,
FormControl,
InputLabel,
MenuItem,
LinearProgress,
Collapse,
Tooltip,
Divider
} from "@mui/material";
import {
CloudUpload as CloudUploadIcon,
Landscape as LandscapeIcon,
DarkMode as DarkModeIcon,
LightMode as LightModeIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
GitHub as GitHubIcon,
LinkedIn as LinkedInIcon
} from "@mui/icons-material";
const LANGUAGES = [
{ code: "en", flag: "en", label: "English" },
{ code: "fr", flag: "fr", label: "Français" },
{ code: "wo", flag: "wo", label: "Wolof" },
];
function App() {
const [mode, setMode] = useState("light");
const theme = useMemo(() =>
createTheme({
palette: {
mode,
primary: { main: "#2e7d32" },
},
shape: { borderRadius: 16 },
components: {
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 20,
boxShadow: mode === "dark"
? "0 8px 32px rgba(0,0,0,0.6)"
: "0 8px 32px rgba(0,0,0,0.12)",
transition: "transform 0.25s ease, box-shadow 0.25s ease",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: mode === "dark"
? "0 16px 48px rgba(0,0,0,0.7)"
: "0 16px 48px rgba(0,0,0,0.18)",
},
},
},
},
MuiCard: {
styleOverrides: {
root: {
borderRadius: 20,
boxShadow: mode === "dark"
? "0 8px 32px rgba(0,0,0,0.6)"
: "0 8px 32px rgba(0,0,0,0.12)",
transition: "transform 0.25s ease, box-shadow 0.25s ease",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: mode === "dark"
? "0 16px 48px rgba(0,0,0,0.7)"
: "0 16px 48px rgba(0,0,0,0.18)",
},
},
},
},
MuiButton: {
styleOverrides: {
root: { borderRadius: 10, textTransform: "none", fontWeight: 600 },
},
},
},
}),
[mode]
);
const [language, setLanguage] = useState("en");
const texts = {
en: {
title: "Intel Image Classifier",
subtitle: "Classify natural scenes",
upload: "Upload Image",
urlBtn: "Load image from URL",
classify: "Classify",
result: "Result",
confidence: "Confidence",
selectModel: "Select Model",
processing: "Processing...",
selectImage: "Please provide an image",
classes: "Possible Classes",
reset: "Reset",
details: "Details",
hideDetails: "Hide",
unknown: "Image not recognized",
},
fr: {
title: "Classificateur Intel",
subtitle: "Classifiez des scènes naturelles avec le deep learning",
upload: "Télécharger Image",
urlBtn: "Charger image depuis URL",
classify: "Classer",
result: "Résultat",
confidence: "Confiance",
selectModel: "Choisir modèle",
processing: "Traitement...",
selectImage: "Veuillez fournir une image",
classes: "Classes possibles",
reset: "Réinitialiser",
details: "Détails",
hideDetails: "Masquer",
unknown: "Image non reconnue",
},
wo: {
title: "Intel Xët-Nataal (IA)",
subtitle: "Jëfandikoo IA ngir xool nataal yi",
upload: "Yeb Nataal bi",
urlBtn: "Yeb nataal ci URL",
classify: "Wone",
result: "Njëg",
confidence: "Loo xam ne",
selectModel: "Tànn modil",
processing: "Di liggéey...",
selectImage: "Tànnal ab nataal",
classes: "Yëgël yi",
reset: "Tàkku",
details: "Xam ci kanam",
hideDetails: "Planque",
unknown: "Nataal xamul",
}
};
const t = texts[language];
const CLASS_LABELS = {
buildings: { en: "Buildings", fr: "Bâtiments", wo: "Kër yi" },
forest: { en: "Forest", fr: "Forêt", wo: "Géej bu wees" },
glacier: { en: "Glacier", fr: "Glacier", wo: "Dëkk bu sedd" },
mountain: { en: "Mountain", fr: "Montagne", wo: "Tund bi" },
sea: { en: "Sea", fr: "Mer", wo: "Géej bi" },
street: { en: "Street", fr: "Rue", wo: "Yoon bi" },
};
const getLabel = (cls) => CLASS_LABELS[cls]?.[language] ?? cls;
const [selectedImage, setSelectedImage] = useState(null);
const [imageUrl, setImageUrl] = useState("");
const [preview, setPreview] = useState(null);
const [showUrlInput, setShowUrlInput] = useState(false);
const [model, setModel] = useState("pytorch");
const [result, setResult] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState(null);
const [showDetails, setShowDetails] = useState(false);
const handleImageUpload = (event) => {
const file = event.target.files[0];
if (!file) return;
setSelectedImage(file);
setPreview(URL.createObjectURL(file));
setImageUrl("");
setResult(null);
setError(null);
setShowDetails(false);
};
const resetAll = () => {
setSelectedImage(null);
setImageUrl("");
setPreview(null);
setResult(null);
setError(null);
setShowDetails(false);
};
const processImage = async () => {
if (!selectedImage && !imageUrl) {
setError(t.selectImage);
return;
}
setIsProcessing(true);
setError(null);
try {
const formData = new FormData();
if (selectedImage) {
formData.append("image", selectedImage);
} else {
formData.append("image_url", imageUrl);
}
formData.append("model", model);
const response = await axios.post(
"http://127.0.0.1:8000/api/classify/",
formData,
{ headers: { "Content-Type": "multipart/form-data" } }
);
const data = response.data;
const conf = Math.round((parseFloat(data.confidence) || 0) * 100);
const allProbs = (data.all_probabilities || []).map(item => ({
class: item.class,
probability: parseFloat(item.probability) || 0
}));
setResult({
predictedClass: conf < 50 ? "unknown" : (data.predicted_class || "unknown"),
confidence: conf,
allProbabilities: allProbs,
modelUsed: data.model_used || model
});
} catch (err) {
console.error(err);
setError("Classification error. Please try again.");
} finally {
setIsProcessing(false);
}
};
return (
<ThemeProvider theme={theme}>
<Box sx={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
bgcolor: "background.default",
color: "text.primary"
}}>
{/* HEADER */}
<AppBar position="static" color="transparent" elevation={0}
sx={{ borderBottom: "1px solid", borderColor: "divider" }}>
<Toolbar>
<LandscapeIcon sx={{ mr: 2, color: "primary.main" }} />
<Typography variant="h6" sx={{ flexGrow: 1, fontWeight: 800 }}>
{t.title}
</Typography>
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mr: 1 }}>
{LANGUAGES.map((lang) => (
<Tooltip key={lang.code} title={lang.label}>
<IconButton
onClick={() => setLanguage(lang.code)}
size="small"
sx={{
fontSize: "1.4rem",
opacity: language === lang.code ? 1 : 0.35,
transition: "opacity 0.2s",
p: "4px",
"&:hover": { opacity: 0.8 }
}}
>
{lang.flag}
</IconButton>
</Tooltip>
))}
</Box>
<Tooltip title={mode === "light" ? "Dark mode" : "Light mode"}>
<IconButton onClick={() => setMode(mode === "light" ? "dark" : "light")}>
{mode === "light" ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton>
</Tooltip>
</Toolbar>
</AppBar>
{/* MAIN */}
<Container maxWidth="md" sx={{ mt: 4, flex: 1 }}>
{/* INPUT BOX */}
<Paper sx={{ p: 4 }} elevation={3}>
<Typography variant="h4" align="center"
sx={{ fontWeight: 800, mb: 1 }}>
{t.title}
</Typography>
<Typography align="center" color="text.secondary" sx={{ mb: 3 }}>
{t.subtitle}
</Typography>
<FormControl fullWidth sx={{ mt: 1 }}>
<InputLabel>{t.selectModel}</InputLabel>
<Select
value={model}
label={t.selectModel}
onChange={(e) => setModel(e.target.value)}
>
<MenuItem value="pytorch">PyTorch CNN</MenuItem>
<MenuItem value="tensorflow">TensorFlow CNN</MenuItem>
</Select>
</FormControl>
<Box sx={{ mt: 3, display: "flex", gap: 3, flexWrap: "wrap" }}>
<Box sx={{ flex: 1, minWidth: "250px" }}>
<Typography gutterBottom>{t.upload}</Typography>
<Button fullWidth variant="outlined"
startIcon={<CloudUploadIcon />} component="label">
{t.upload}
<input type="file" hidden accept="image/*" onChange={handleImageUpload} />
</Button>
</Box>
<Box sx={{ flex: 1, minWidth: "250px" }}>
<Typography gutterBottom>{t.urlBtn}</Typography>
<Button fullWidth variant="outlined" color="primary"
startIcon={<CloudUploadIcon />}
onClick={() => setShowUrlInput(!showUrlInput)}>
{t.urlBtn}
</Button>
{showUrlInput && (
<TextField fullWidth label="URL" value={imageUrl}
onChange={(e) => {
setImageUrl(e.target.value);
setSelectedImage(null);
setPreview(e.target.value);
}}
sx={{ mt: 2 }} />
)}
</Box>
</Box>
<Box sx={{ mt: 3 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
{t.classes}:
</Typography>
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
{Object.keys(CLASS_LABELS).map((cls) => (
<Chip key={cls} label={getLabel(cls)} size="small" variant="outlined" />
))}
</Box>
</Box>
</Paper>
{/* IMAGE PREVIEW */}
{preview && (
<Card sx={{ mt: 3, p: 2, textAlign: "center" }}>
<CardMedia component="img" height="300" image={preview}
sx={{ objectFit: "contain", borderRadius: 2 }} />
{isProcessing && <LinearProgress sx={{ mt: 1 }} color="primary" />}
<Box sx={{ mt: 2 }}>
<Button variant="contained" color="primary"
onClick={processImage} disabled={isProcessing} size="large">
{isProcessing ? t.processing : t.classify}
</Button>
</Box>
</Card>
)}
{/* ERROR */}
{error && (
<Paper sx={{ mt: 2, p: 2, backgroundColor: "#ffebee" }}>
<Typography color="error">{error}</Typography>
</Paper>
)}
{/* RESULT */}
{result && (
<Paper sx={{ mt: 3, p: 3, textAlign: "center" }}>
<Typography variant="h6" fontWeight={700} gutterBottom>
{t.result}
</Typography>
<Chip
label={result.predictedClass === "unknown"
? t.unknown
: getLabel(result.predictedClass)}
color={result.predictedClass === "unknown" ? "error" : "success"}
sx={{ fontSize: "1.1rem", px: 2, py: 2.5, mt: 1, fontWeight: 700 }}
/>
<Typography sx={{ mt: 2 }}>
{t.confidence}: <strong>{result.confidence}%</strong>
</Typography>
<Typography variant="caption" color="text.secondary">
Model: {result.modelUsed}
</Typography>
<Collapse in={showDetails}>
<Box sx={{ mt: 2, textAlign: "left" }}>
{result.allProbabilities.map((item) => (
<Box key={item.class} sx={{ mb: 1.5 }}>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5 }}>
<Typography variant="body2" fontWeight={600}>
{getLabel(item.class)}
</Typography>
<Typography variant="body2">
{Math.round(item.probability * 100)}%
</Typography>
</Box>
<LinearProgress variant="determinate"
value={Math.round(item.probability * 100)}
sx={{ height: 8, borderRadius: 4 }} color="primary" />
</Box>
))}
</Box>
</Collapse>
<Box sx={{ mt: 3, display: "flex", justifyContent: "center", gap: 2 }}>
<Button variant="outlined" onClick={resetAll}>{t.reset}</Button>
<Button variant="outlined" color="primary"
onClick={() => setShowDetails(!showDetails)}
endIcon={showDetails ? <ExpandLessIcon /> : <ExpandMoreIcon />}>
{showDetails ? t.hideDetails : t.details}
</Button>
</Box>
</Paper>
)}
</Container>
{/* FOOTER */}
<Box sx={{ mt: "auto" }}>
<Divider />
<Box sx={{
py: 1.5, px: 4,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
gap: 1
}}>
<Typography variant="body2" color="text.secondary">
© 2026 Intel Image Classifier By <strong>Tsemo Danielle</strong>
</Typography>
<Box sx={{ display: "flex", gap: 1 }}>
<Tooltip title="GitHub">
<IconButton size="small"
onClick={() => window.open("https://github.com/nguemtchuengdanielle/")}>
<GitHubIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="LinkedIn">
<IconButton size="small"
onClick={() => window.open("https://linkedin.com/in/danielle-tsemo3")}
sx={{ color: "#0077b5" }}>
<LinkedInIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
</Box>
</Box>
</ThemeProvider>
);
}
export default App;