branch backend + ajout du serveur backend, next, et sqlite
This commit is contained in:
104
backendia/dashboard/src/app/contributors/page.tsx
Normal file
104
backendia/dashboard/src/app/contributors/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { fetchApi } from "@/lib/api";
|
||||
import { Users, Award, Camera, Clock } from "lucide-react";
|
||||
|
||||
interface Contributor {
|
||||
wallet_hash: string;
|
||||
photo_count: number;
|
||||
last_upload: string;
|
||||
}
|
||||
|
||||
export default async function ContributorsPage() {
|
||||
let contributors: Contributor[] = [];
|
||||
try {
|
||||
const response = await fetchApi('/api/contributors');
|
||||
contributors = response.contributors;
|
||||
} catch (error) {
|
||||
console.error("Error fetching contributors:", error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Top Contributeurs</h2>
|
||||
<p className="text-slate-400">Classement des utilisateurs selon leur contribution à l'entraînement de l'IA.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Stats Summary */}
|
||||
<div className="lg:col-span-1 space-y-6">
|
||||
<StatCard
|
||||
title="Total Contributeurs"
|
||||
value={contributors.length}
|
||||
icon={<Users className="text-indigo-400" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Photos"
|
||||
value={contributors.reduce((acc, c) => acc + c.photo_count, 0)}
|
||||
icon={<Camera className="text-emerald-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="lg:col-span-3 bg-slate-900 border border-slate-800 rounded-2xl overflow-hidden">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-slate-950/50 border-b border-slate-800">
|
||||
<tr>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-widest">Rang</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-widest">Wallet Hash</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-widest text-center">Photos</th>
|
||||
<th className="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-widest text-right">Dernier Upload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{contributors.map((c, index) => (
|
||||
<tr key={c.wallet_hash} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{index < 3 ? (
|
||||
<Award className={index === 0 ? "text-yellow-500" : index === 1 ? "text-slate-400" : "text-amber-700"} />
|
||||
) : (
|
||||
<span className="text-slate-500 font-mono w-6 text-center">#{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="font-mono text-xs text-slate-300">{c.wallet_hash}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className="px-2 py-1 bg-indigo-500/10 text-indigo-400 rounded-md text-xs font-bold border border-indigo-500/20">
|
||||
{c.photo_count}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-xs text-slate-400 flex items-center justify-end gap-2">
|
||||
<Clock size={14} />
|
||||
{new Date(c.last_upload).toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{contributors.length === 0 && (
|
||||
<div className="py-24 text-center text-slate-500">
|
||||
Aucun contributeur enregistré pour le moment.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ title, value, icon }: any) {
|
||||
return (
|
||||
<div className="bg-slate-900 border border-slate-800 p-6 rounded-2xl flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-slate-500 font-bold uppercase tracking-widest">{title}</p>
|
||||
<p className="text-3xl font-bold">{value}</p>
|
||||
</div>
|
||||
<div className="w-12 h-12 bg-slate-800 rounded-xl flex items-center justify-center shadow-inner">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
backendia/dashboard/src/app/favicon.ico
Normal file
BIN
backendia/dashboard/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
backendia/dashboard/src/app/globals.css
Normal file
26
backendia/dashboard/src/app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
67
backendia/dashboard/src/app/layout.tsx
Normal file
67
backendia/dashboard/src/app/layout.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Link from "next/link";
|
||||
import { LayoutDashboard, Users, Image as ImageIcon } from "lucide-react";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Bully IA Dashboard",
|
||||
description: "Visualisation des données d'analyse de cibles",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr" className="dark">
|
||||
<body className={`${inter.className} bg-slate-950 text-slate-50 min-h-screen flex`}>
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 border-r border-slate-800 bg-slate-900/50 p-6 flex flex-col gap-8">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center font-bold">B</div>
|
||||
<h1 className="text-xl font-bold tracking-tight">Bully IA</h1>
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors text-slate-300 hover:text-white"
|
||||
>
|
||||
<LayoutDashboard size={20} />
|
||||
Tableau de bord
|
||||
</Link>
|
||||
<Link
|
||||
href="/contributors"
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-slate-800 transition-colors text-slate-300 hover:text-white"
|
||||
>
|
||||
<Users size={20} />
|
||||
Contributeurs
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="mt-auto pt-6 border-t border-slate-800">
|
||||
<p className="text-xs text-slate-500 px-2">v1.0.0 Alpha</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 flex flex-col">
|
||||
<header className="h-16 border-b border-slate-800 flex items-center px-8 bg-slate-900/20">
|
||||
<div className="flex items-center gap-2 text-sm text-slate-400">
|
||||
<span>Admin</span>
|
||||
<span>/</span>
|
||||
<span className="text-slate-200">Dashboard</span>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex-1 p-8 overflow-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
105
backendia/dashboard/src/app/page.tsx
Normal file
105
backendia/dashboard/src/app/page.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { fetchApi, API_BASE_URL } from "@/lib/api";
|
||||
import DatasetToolbar from "@/components/DatasetToolbar";
|
||||
import { Calendar, User, Crosshair } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface PlottingData {
|
||||
session_id: string;
|
||||
wallet_hash: string;
|
||||
timestamp: string;
|
||||
plotting: {
|
||||
impacts: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface PhotoData {
|
||||
filename: string;
|
||||
imageUrl: string;
|
||||
data: PlottingData | null;
|
||||
}
|
||||
|
||||
export default async function DashboardPage() {
|
||||
let photos: PhotoData[] = [];
|
||||
try {
|
||||
const response = await fetchApi('/api/all-data');
|
||||
photos = response.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching photos:", error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Galerie des Sessions</h2>
|
||||
<p className="text-slate-400">Visualisez et gérez les données d'entraînement collectées.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DatasetToolbar />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{photos.map((photo) => (
|
||||
<Link
|
||||
key={photo.filename}
|
||||
href={`/photo/${photo.filename}`}
|
||||
className="group relative bg-slate-900 rounded-xl border border-slate-800 overflow-hidden hover:border-indigo-500/50 transition-all hover:shadow-2xl hover:shadow-indigo-500/10"
|
||||
>
|
||||
{/* Image Preview */}
|
||||
<div className="aspect-[3/4] relative overflow-hidden bg-slate-800">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={`${API_BASE_URL}${photo.imageUrl}`}
|
||||
alt={photo.filename}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent opacity-80" />
|
||||
|
||||
{/* Badge impacts */}
|
||||
{photo.data?.plotting?.impacts && (
|
||||
<div className="absolute top-3 right-3 bg-indigo-600/90 backdrop-blur-md px-2 py-1 rounded-md text-[10px] font-bold flex items-center gap-1">
|
||||
<Crosshair size={12} />
|
||||
{photo.data.plotting.impacts.length} IMPACTS
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<Calendar size={14} className="text-indigo-400" />
|
||||
{photo.data ? new Date(photo.data.timestamp).toLocaleDateString() : 'Date inconnue'}
|
||||
</div>
|
||||
{photo.data ? (
|
||||
<span className="text-[10px] bg-emerald-500/10 text-emerald-400 px-1.5 py-0.5 rounded border border-emerald-500/20 font-bold">DATA OK</span>
|
||||
) : (
|
||||
<span className="text-[10px] bg-rose-500/10 text-rose-400 px-1.5 py-0.5 rounded border border-rose-500/20 font-bold">NO DATA</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-slate-300 font-mono truncate">
|
||||
<User size={14} className="text-indigo-400" />
|
||||
{photo.data?.wallet_hash?.substring(0, 12) || 'Anonyme'}...
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-slate-800 flex justify-between items-center">
|
||||
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Session ID</span>
|
||||
<span className="text-xs text-slate-300 font-mono truncate max-w-[120px]">
|
||||
{photo.data?.session_id || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{photos.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-slate-500 gap-4 border-2 border-dashed border-slate-800 rounded-3xl">
|
||||
<Crosshair size={48} className="opacity-20" />
|
||||
<p>Aucune session d'entraînement trouvée.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
backendia/dashboard/src/app/photo/[id]/page.tsx
Normal file
49
backendia/dashboard/src/app/photo/[id]/page.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { fetchApi, API_BASE_URL } from "@/lib/api";
|
||||
import PhotoEditor from "@/components/PhotoEditor";
|
||||
import { ChevronLeft, Download } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function PhotoDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id: encodedId } = await params;
|
||||
const id = decodeURIComponent(encodedId);
|
||||
let photo = null;
|
||||
|
||||
try {
|
||||
const response = await fetchApi('/api/all-data');
|
||||
photo = response.data.find((p: any) => p.filename === id);
|
||||
} catch (error) {
|
||||
console.error("Error fetching photo detail:", error);
|
||||
}
|
||||
|
||||
if (!photo) {
|
||||
return <div className="p-12 text-center">Session non trouvée.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center gap-2 text-slate-400 hover:text-white transition-colors"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
Retour à la galerie
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href={`${API_BASE_URL}/uploads/images/${id}`}
|
||||
download
|
||||
className="flex items-center gap-2 bg-slate-800 hover:bg-slate-700 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
<Download size={18} />
|
||||
Image Originale
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PhotoEditor initialPhoto={photo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user