Spaces:
Sleeping
Sleeping
| /** | |
| * Composant Datasets pour AfriDataHub | |
| * Created by BlackBenAI Team - AfriDataHub Platform | |
| */ | |
| import { useState, useEffect, useCallback } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { Button } from '@/components/ui/button' | |
| import DatasetCard from './DatasetCard' | |
| import { | |
| Search, | |
| Filter, | |
| Download, | |
| Upload, | |
| RefreshCw, | |
| Brain, | |
| X | |
| } from 'lucide-react' | |
| import { API_URL } from '../config' | |
| const Datasets = ({ onStartAnalysis }) => { | |
| const [datasets, setDatasets] = useState([]) | |
| const [loading, setLoading] = useState(true) | |
| const [searchTerm, setSearchTerm] = useState('') | |
| const [selectedDomain, setSelectedDomain] = useState('') | |
| const [selectedDataset, setSelectedDataset] = useState(null) | |
| const [isAnalyzing, setIsAnalyzing] = useState(false) | |
| const domains = [ | |
| { value: '', label: 'Tous les domaines' }, | |
| { value: 'agriculture', label: 'Agriculture' }, | |
| { value: 'health', label: 'Santé' }, | |
| { value: 'economy', label: 'Économie' }, | |
| { value: 'weather', label: 'Météorologie' }, | |
| { value: 'energy', label: 'Énergie' }, | |
| { value: 'education', label: 'Éducation' }, | |
| { value: 'population', label: 'Population' }, | |
| { value: 'environment', label: 'Environnement' }, | |
| { value: 'transport', label: 'Transport' }, | |
| { value: 'other', label: 'Autre' }, | |
| ] | |
| useEffect(() => { | |
| fetchDatasets() | |
| }, []) | |
| const fetchDatasets = async () => { | |
| try { | |
| setLoading(true) | |
| const response = await fetch(`${API_URL}datasets/`) | |
| const data = await response.json() | |
| setDatasets(data.results || []) | |
| } catch (error) { | |
| console.error('Erreur lors du chargement des datasets:', error) | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const filteredDatasets = datasets.filter(dataset => { | |
| const matchesSearch = dataset.title.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| dataset.description.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| dataset.source_name.toLowerCase().includes(searchTerm.toLowerCase()) | |
| const matchesDomain = !selectedDomain || dataset.domain === selectedDomain | |
| return matchesSearch && matchesDomain | |
| }) | |
| const handleViewDetails = (dataset) => { | |
| setSelectedDataset(dataset) | |
| } | |
| const handleAnalyze = async (dataset) => { | |
| try { | |
| setIsAnalyzing(true) | |
| // Fermer le modal de détails si ouvert | |
| setSelectedDataset(null) | |
| const response = await fetch(`${API_URL}datasets/${dataset.slug}/analyze/`) | |
| const data = await response.json() | |
| if (response.ok) { | |
| if (onStartAnalysis) { | |
| onStartAnalysis(dataset, data) | |
| } | |
| } else { | |
| console.error('Erreur analyse:', data) | |
| } | |
| } catch (error) { | |
| console.error('Erreur lors de l\'analyse:', error) | |
| } finally { | |
| setIsAnalyzing(false) | |
| } | |
| } | |
| const DatasetDetails = ({ dataset, onClose }) => { | |
| const [dataPoints, setDataPoints] = useState([]) | |
| const [loadingData, setLoadingData] = useState(true) | |
| const fetchDataPoints = useCallback(async () => { | |
| try { | |
| setLoadingData(true) | |
| const response = await fetch(`${API_URL}datasets/${dataset.slug}/data/?limit=50`) | |
| const result = await response.json() | |
| setDataPoints(result.data_points || []) | |
| } catch (error) { | |
| console.error('Erreur lors du chargement des données:', error) | |
| } finally { | |
| setLoadingData(false) | |
| } | |
| }, [dataset.slug]) | |
| useEffect(() => { | |
| fetchDataPoints() | |
| }, [fetchDataPoints]) | |
| const handleDownload = () => { | |
| if (dataPoints.length === 0) return | |
| // Créer le contenu CSV | |
| const headers = ['Pays', 'Date', 'Valeur', 'Unité', 'Source'] | |
| const csvContent = [ | |
| headers.join(','), | |
| ...dataPoints.map(dp => [ | |
| dp.country_name, | |
| dp.date, | |
| dp.value, | |
| dp.unit, | |
| dataset.source_name | |
| ].join(',')) | |
| ].join('\n') | |
| // Créer un blob et télécharger | |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) | |
| const link = document.createElement('a') | |
| const url = URL.createObjectURL(blob) | |
| link.setAttribute('href', url) | |
| link.setAttribute('download', `${dataset.title.replace(/\s+/g, '_')}_preview.csv`) | |
| link.style.visibility = 'hidden' | |
| document.body.appendChild(link) | |
| link.click() | |
| document.body.removeChild(link) | |
| } | |
| return ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" | |
| onClick={onClose} | |
| > | |
| <motion.div | |
| initial={{ scale: 0.9, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| className="bg-card text-card-foreground rounded-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto flex flex-col border border-border shadow-2xl" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| <div className="p-6"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <h2 className="text-2xl font-bold text-foreground">{dataset.title}</h2> | |
| <Button variant="ghost" onClick={onClose} className="p-2 hover:bg-muted text-muted-foreground hover:text-foreground"> | |
| <X className="w-5 h-5" /> | |
| </Button> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> | |
| <div> | |
| <h3 className="font-semibold text-foreground mb-2">Description</h3> | |
| <p className="text-muted-foreground leading-relaxed">{dataset.description}</p> | |
| </div> | |
| <div> | |
| <h3 className="font-semibold text-foreground mb-4">Informations</h3> | |
| <div className="space-y-3 text-sm text-foreground/80"> | |
| <div className="flex items-center justify-between bg-muted/50 p-3 rounded-lg"><span className="font-medium text-muted-foreground w-1/3">Domaine:</span> <span className="font-bold flex-1 text-right">{dataset.domain}</span></div> | |
| <div className="flex items-center justify-between bg-muted/50 p-3 rounded-lg"><span className="font-medium text-muted-foreground w-1/3">Source:</span> <span className="font-bold flex-1 text-right truncate pl-4" title={dataset.source_name}>{dataset.source_name}</span></div> | |
| <div className="flex items-center justify-between bg-muted/50 p-3 rounded-lg"><span className="font-medium text-muted-foreground w-1/3">Points de données:</span> <span className="font-bold flex-1 text-right text-primary">{dataset.data_points_count.toLocaleString()}</span></div> | |
| <div className="flex items-center justify-between bg-muted/50 p-3 rounded-lg"><span className="font-medium text-muted-foreground w-1/3">Mise à jour:</span> <span className="font-bold flex-1 text-right">{new Date(dataset.last_updated).toLocaleDateString('fr-FR')}</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="bg-background border border-border rounded-xl overflow-hidden shadow-inner"> | |
| <div className="px-5 py-4 border-b border-border bg-muted/30 flex justify-between items-center"> | |
| <h3 className="font-semibold text-foreground">Aperçu des données (50 premières lignes)</h3> | |
| <span className="text-xs font-bold px-2 py-1 bg-primary/10 text-primary rounded-md">{dataPoints.length} lignes affichées</span> | |
| </div> | |
| <div className="overflow-x-auto max-h-72 custom-scrollbar"> | |
| <table className="min-w-full divide-y divide-border"> | |
| <thead className="bg-muted/50 sticky top-0 z-10 backdrop-blur-md"> | |
| <tr> | |
| <th className="px-6 py-4 text-left text-xs font-bold text-muted-foreground uppercase tracking-wider">Pays</th> | |
| <th className="px-6 py-4 text-left text-xs font-bold text-muted-foreground uppercase tracking-wider">Date</th> | |
| <th className="px-6 py-4 text-left text-xs font-bold text-muted-foreground uppercase tracking-wider">Valeur</th> | |
| <th className="px-6 py-4 text-left text-xs font-bold text-muted-foreground uppercase tracking-wider">Unité</th> | |
| </tr> | |
| </thead> | |
| <tbody className="bg-background divide-y divide-border/50"> | |
| {loadingData ? ( | |
| <tr> | |
| <td colSpan="4" className="px-6 py-8 text-center text-sm text-muted-foreground"> | |
| <div className="flex items-center justify-center space-x-2"> | |
| <RefreshCw className="h-5 w-5 animate-spin text-primary" /> | |
| <span>Chargement du jeu de données...</span> | |
| </div> | |
| </td> | |
| </tr> | |
| ) : dataPoints.length > 0 ? ( | |
| dataPoints.map((dp, idx) => ( | |
| <tr key={idx} className="hover:bg-muted/30 transition-colors"> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm font-semibold text-foreground">{dp.country_name}</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground font-mono">{dp.date}</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-foreground font-bold">{dp.value}</td> | |
| <td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">{dp.unit}</td> | |
| </tr> | |
| )) | |
| ) : ( | |
| <tr> | |
| <td colSpan="4" className="px-6 py-8 text-center text-sm text-muted-foreground bg-muted/10"> | |
| Aucune donnée expérimentale disponible pour l'aperçu. | |
| </td> | |
| </tr> | |
| )} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div className="flex flex-col sm:flex-row justify-end space-y-3 sm:space-y-0 sm:space-x-4 mt-8 pt-6 border-t border-border"> | |
| <Button variant="outline" className="rounded-xl border-border bg-transparent hover:bg-muted transition-colors font-bold" onClick={handleDownload} disabled={loadingData || dataPoints.length === 0}> | |
| <Download className="h-4 w-4 mr-2" /> | |
| Télécharger CSV | |
| </Button> | |
| <Button | |
| className="bg-primary hover:bg-primary/90 text-primary-foreground rounded-xl shadow-lg shadow-primary/20 hover:shadow-primary/40 transition-all font-bold" | |
| onClick={() => handleAnalyze(dataset)} | |
| disabled={isAnalyzing} | |
| > | |
| {isAnalyzing ? ( | |
| <> | |
| <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> | |
| Analyse en cours... | |
| </> | |
| ) : ( | |
| <> | |
| <Brain className="h-4 w-4 mr-2" /> | |
| Analyser les données | |
| </> | |
| )} | |
| </Button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </motion.div> | |
| ) | |
| } | |
| return ( | |
| <div className="space-y-10 pb-20"> | |
| {/* En-tête */} | |
| <motion.div | |
| initial={{ opacity: 0, y: -20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="flex flex-col md:flex-row md:items-end md:justify-between gap-6" | |
| > | |
| <div> | |
| <h1 className="text-3xl md:text-4xl font-black text-foreground tracking-tight">Bibliothèque de <span className="text-[#fcd34d]">Datasets</span></h1> | |
| <p className="text-muted-foreground font-medium mt-2 text-lg"> | |
| Explorez les {datasets.length} sources de vérité disponibles sur le continent. | |
| </p> | |
| </div> | |
| <div className="flex items-center space-x-3"> | |
| <Button | |
| variant="ghost" | |
| onClick={fetchDatasets} | |
| disabled={loading} | |
| className="rounded-2xl hover:bg-primary/10 text-primary font-bold" | |
| > | |
| <RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> | |
| Actualiser | |
| </Button> | |
| <Button className="rounded-2xl bg-[#166534] hover:bg-[#15803d] text-white font-bold shadow-lg shadow-primary/20 transition-all hover:scale-105"> | |
| <Upload className="h-4 w-4 mr-2" /> | |
| Importer CSV | |
| </Button> | |
| </div> | |
| </motion.div> | |
| {/* Filtres Glassmorphism */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="glass rounded-[1.5rem] md:rounded-[2rem] p-4 md:p-6 shadow-xl" | |
| > | |
| <div className="flex flex-col lg:flex-row gap-6"> | |
| {/* Recherche */} | |
| <div className="flex-1"> | |
| <div className="relative group"> | |
| <Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" /> | |
| <input | |
| type="text" | |
| placeholder="Rechercher par titre, description ou source..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| className="w-full pl-12 pr-6 py-4 bg-background/50 dark:bg-black/20 border border-border rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-medium text-foreground" | |
| /> | |
| </div> | |
| </div> | |
| {/* Filtre par domaine */} | |
| <div className="lg:w-80"> | |
| <div className="relative group"> | |
| <Filter className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground group-focus-within:text-primary transition-colors" /> | |
| <select | |
| value={selectedDomain} | |
| onChange={(e) => setSelectedDomain(e.target.value)} | |
| className="w-full pl-12 pr-10 py-4 bg-background/50 dark:bg-black/20 border border-border rounded-2xl focus:ring-4 focus:ring-primary/10 focus:border-primary/30 transition-all outline-none font-bold text-foreground appearance-none cursor-pointer" | |
| > | |
| {domains.map(domain => ( | |
| <option key={domain.value} value={domain.value}> | |
| {domain.label} | |
| </option> | |
| ))} | |
| </select> | |
| <div className="absolute right-4 top-1/2 transform -translate-y-1/2 pointer-events-none text-muted-foreground"> | |
| <Download className="h-4 w-4 rotate-180" /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </motion.div> | |
| {/* Résultats */} | |
| <div> | |
| <div className="flex items-center justify-between mb-8 px-2"> | |
| <div className="flex items-center space-x-2"> | |
| <div className="w-2 h-2 rounded-full bg-primary animate-pulse"></div> | |
| <p className="text-muted-foreground font-bold uppercase tracking-widest text-xs"> | |
| {filteredDatasets.length} résultat{filteredDatasets.length > 1 ? 's' : ''} trouvé{filteredDatasets.length > 1 ? 's' : ''} | |
| </p> | |
| </div> | |
| </div> | |
| {loading ? ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> | |
| {[...Array(6)].map((_, i) => ( | |
| <div key={i} className="glass rounded-[2rem] p-8 animate-pulse"> | |
| <div className="h-6 bg-primary/10 rounded-xl w-3/4 mb-4"></div> | |
| <div className="h-4 bg-primary/5 rounded-lg w-full mb-2"></div> | |
| <div className="h-4 bg-primary/5 rounded-lg w-5/6 mb-6"></div> | |
| <div className="h-12 bg-primary/10 rounded-2xl w-full"></div> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" | |
| > | |
| {filteredDatasets.map((dataset, index) => ( | |
| <motion.div | |
| key={dataset.slug} | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: index * 0.05 }} | |
| > | |
| <DatasetCard | |
| dataset={dataset} | |
| onViewDetails={handleViewDetails} | |
| /> | |
| </motion.div> | |
| ))} | |
| </motion.div> | |
| )} | |
| {!loading && filteredDatasets.length === 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.9 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| className="text-center py-24 glass rounded-[3rem]" | |
| > | |
| <div className="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6"> | |
| <Search className="h-10 w-10 text-primary" /> | |
| </div> | |
| <h3 className="text-2xl font-black text-foreground mb-2"> | |
| Aucun dataset trouvé | |
| </h3> | |
| <p className="text-muted-foreground font-medium max-w-md mx-auto"> | |
| Nous n'avons trouvé aucun résultat pour votre recherche. Essayez d'utiliser des termes plus larges ou un autre domaine. | |
| </p> | |
| <Button | |
| variant="ghost" | |
| onClick={() => { setSearchTerm(''); setSelectedDomain('') }} | |
| className="mt-6 text-primary font-bold hover:bg-primary/10 rounded-xl" | |
| > | |
| Réinitialiser les filtres | |
| </Button> | |
| </motion.div> | |
| )} | |
| </div> | |
| {/* Modal de détails Premium */} | |
| <AnimatePresence> | |
| {selectedDataset && ( | |
| <DatasetDetails | |
| dataset={selectedDataset} | |
| onClose={() => setSelectedDataset(null)} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| {/* Loader d'analyse global */} | |
| <AnimatePresence> | |
| {isAnalyzing && !selectedDataset && ( | |
| <motion.div | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| className="fixed inset-0 bg-background/80 backdrop-blur-xl flex items-center justify-center z-[100]" | |
| > | |
| <div className="flex flex-col items-center max-w-md text-center p-12"> | |
| <div className="relative w-24 h-24 mb-8"> | |
| <div className="absolute inset-0 border-4 border-primary/20 rounded-full"></div> | |
| <div className="absolute inset-0 border-4 border-primary border-t-transparent rounded-full animate-spin"></div> | |
| <Brain className="absolute inset-0 m-auto h-10 w-10 text-primary animate-pulse" /> | |
| </div> | |
| <h3 className="text-3xl font-black text-foreground mb-4">Intelligence Artificielle en action</h3> | |
| <p className="text-muted-foreground font-medium leading-relaxed"> | |
| Gemini analyse actuellement des milliers de points de données pour générer votre rapport stratégique. | |
| </p> | |
| <div className="mt-8 w-full bg-primary/10 h-2 rounded-full overflow-hidden"> | |
| <motion.div | |
| initial={{ x: '-100%' }} | |
| animate={{ x: '100%' }} | |
| transition={{ repeat: Infinity, duration: 2, ease: "linear" }} | |
| className="w-1/2 h-full bg-primary/50" | |
| /> | |
| </div> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ) | |
| } | |
| export default Datasets | |