rinogeek's picture
Update for deployment
62fe6d4
/**
* 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