Spaces:
Sleeping
Sleeping
| 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; |