Major restyling of frontend

Signed-off-by: Felipe Cardoso <felipe.cardoso@hotmail.it>
This commit is contained in:
2025-01-23 14:12:12 +01:00
parent 756bd999f7
commit a81a07d6d8
4 changed files with 173 additions and 102 deletions

View File

@@ -2,45 +2,47 @@ import {TrainingProgress} from '@/components/TrainingProgress'
import {SamplesGallery} from '@/components/SamplesGallery' import {SamplesGallery} from '@/components/SamplesGallery'
import {LogViewer} from '@/components/LogViewer' import {LogViewer} from '@/components/LogViewer'
import {Suspense} from 'react' import {Suspense} from 'react'
import {TrainingInfo} from "@/components/TrainingInfo";
export default function DashboardPage() { export default function DashboardPage() {
return ( return (
<main className="min-h-screen p-4 bg-gray-50"> <main className="min-h-screen p-4 bg-gray-900">
<div className="max-w-7xl mx-auto space-y-6"> <div className="max-w-7xl mx-auto space-y-6">
<header className="flex justify-between items-center"> <header className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Training Monitor</h1> <h1 className="text-3xl font-bold text-gray-100">Training Monitor</h1>
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-400">
Live monitoring Live monitoring
</div> </div>
</header> </header>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Training Status Section */} {/* Training Status Section */}
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-6"> <div className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6">
<h2 className="text-xl font-semibold mb-4 text-gray-500">Training Progress</h2> <h2 className="text-xl font-semibold mb-4 text-gray-300">Training Progress</h2>
<Suspense fallback={<div>Loading training status...</div>}> <Suspense fallback={<div className="text-gray-400">Loading training status...</div>}>
<TrainingProgress/> <TrainingProgress/>
</Suspense> </Suspense>
</div> </div>
{/* Latest Samples Section */} {/* Latest Samples Section */}
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-6"> <div className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-500">Latest Samples</h2> <h2 className="text-xl font-semibold text-gray-300">Latest Samples</h2>
<span className="text-sm text-gray-500">Auto-updating</span> <span className="text-sm text-gray-400">Auto-updating</span>
</div> </div>
<Suspense fallback={<div>Loading samples...</div>}> <Suspense fallback={<div className="text-gray-400">Loading samples...</div>}>
<SamplesGallery/> <SamplesGallery/>
</Suspense> </Suspense>
</div> </div>
{/* Log Viewer Section - Full Width */} {/* Log Viewer Section - Full Width */}
<div className="bg-white rounded-lg shadow-sm border border-gray-100 p-6 lg:col-span-2 h-[400px]"> <div
className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6 lg:col-span-2 h-[400px]">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold text-gray-500">Training Logs</h2> <h2 className="text-xl font-semibold text-gray-300">Training Logs</h2>
<span className="text-sm text-gray-500">Real-time updates</span> <span className="text-sm text-gray-400">Real-time updates</span>
</div> </div>
<Suspense fallback={<div>Loading logs...</div>}> <Suspense fallback={<div className="text-gray-400">Loading logs...</div>}>
<LogViewer/> <LogViewer/>
</Suspense> </Suspense>
</div> </div>

View File

@@ -3,94 +3,125 @@
import {useSamples} from '@/contexts/SamplesContext' import {useSamples} from '@/contexts/SamplesContext'
import Image from 'next/image' import Image from 'next/image'
import Link from "next/link"
import {useMemo} from 'react' import {useMemo} from 'react'
interface ParsedSample { // Helper function to parse sample information
filename: string const parseSampleInfo = (filename: string) => {
timestamp: number const [timestamp, info] = filename.split('__')
batch: number const [batch, index] = info.split('.')[0].split('_')
index: number return {
url: string timestamp: parseInt(timestamp),
created_at: string batch: parseInt(batch),
index: parseInt(index)
}
} }
export default function SamplesPage() { export default function SamplesPage() {
const {samples, isLoading, error} = useSamples() const {samples, isLoading, error} = useSamples()
// Group samples by batch number using a memoized calculation
const groupedSamples = useMemo(() => { const groupedSamples = useMemo(() => {
if (!samples?.length) return new Map() if (!samples?.length) return new Map()
const parsed: ParsedSample[] = samples.map(sample => { // Create groups based on batch numbers
console.debug('sample', sample) const groups = samples.reduce((acc, sample) => {
const [timestamp, info] = sample.filename.split('__') const {batch} = parseSampleInfo(sample.filename)
const [batch, index] = info.split('_') const group = acc.get(batch) || []
return { group.push({
...sample, ...sample,
timestamp: parseInt(timestamp), ...parseSampleInfo(sample.filename)
batch: parseInt(batch), })
index: parseInt(index.replace('.jpg', '')), acc.set(batch, group)
}
})
// Group by batch
const groups = parsed.reduce((acc, sample) => {
const group = acc.get(sample.batch) || []
group.push(sample)
acc.set(sample.batch, group)
return acc return acc
}, new Map<number, ParsedSample[]>()) }, new Map())
// Sort within each group // Sort samples within each group by index
for (const [batch, items] of groups) { for (const [batch, items] of groups) {
groups.set(batch, items.sort((a, b) => b.index - a.index)) groups.set(batch, items.sort((a: any, b: any) => a.index - b.index))
} }
// return new Map([...groups].sort((a, b) => b[0] - a[0])) // Return groups sorted by batch number (descending)
return new Map([...groups]) return new Map([...groups].sort((a, b) => b[0] - a[0]))
}, [samples]) }, [samples])
if (isLoading) return <div>Loading samples...</div> // Handle loading and error states with appropriate styling
if (error) return <div>Error: {error.message}</div> if (isLoading) return (
<div className="min-h-screen bg-gray-900 p-4">
return ( <div className="max-w-7xl mx-auto text-gray-400">Loading samples...</div>
<div className="p-6 space-y-8">
<h1 className="text-2xl font-bold">Samples Gallery</h1>
{Array.from(groupedSamples).map(([batch, items]) => (
<div key={batch} className="space-y-2">
<h2 className="text-xl font-semibold">
Step {batch}
</h2>
<div className="overflow-x-auto">
<div className="flex gap-4 min-w-full pb-4">
{items.sort((a: any, b: any) => a.index - b.index).map((sample: any) => (
<div key={sample.filename} className="flex-shrink-0 w-48">
<Image
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
alt={`Sample ${sample.index}`}
width={200}
height={200}
className="rounded-lg shadow-sm object-cover w-full h-48"
/>
<div className="mt-2 text-sm">
<div className="text-gray-600">
{new Date(sample.created_at).toLocaleString()}
</div>
<a
href={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Sample {sample.index}
</a>
</div>
</div>
))}
</div>
</div>
</div>
))}
</div> </div>
) )
if (error) return (
<div className="min-h-screen bg-gray-900 p-4">
<div className="max-w-7xl mx-auto text-red-400">Error: {error.message}</div>
</div>
)
return (
<main className="min-h-screen bg-gray-900 p-4">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header with navigation */}
<header className="flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-100">Sample Gallery</h1>
<Link
href="/"
className="text-gray-400 hover:text-gray-200 transition-colors duration-200 flex items-center gap-2 group"
>
Back to Dashboard
</Link>
</header>
{/* Sample groups */}
<div className="space-y-8">
{Array.from(groupedSamples).map(([batch, items]) => (
<div key={batch} className="bg-gray-800 rounded-lg shadow-lg border border-gray-700 p-6">
<h2 className="text-xl font-semibold text-gray-300 mb-4">
Step {batch}
</h2>
{/* Scrollable container for samples */}
<div className="overflow-x-auto">
<div className="flex gap-4 min-w-full pb-4">
{items.map((sample: any) => (
<div
key={sample.filename}
className="flex-shrink-0 group"
>
{/* Image container with hover effects */}
<div
className="w-48 overflow-hidden rounded-lg bg-gray-700 shadow-md transition-all duration-200 group-hover:shadow-lg">
<Image
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
alt={`Sample ${sample.index}`}
width={200}
height={200}
className="object-cover w-full h-48 rounded-lg transition-transform duration-200 group-hover:scale-105"
/>
</div>
{/* Sample information */}
<div className="mt-2 space-y-1">
<div className="text-sm text-gray-400">
{new Date(sample.created_at).toLocaleString()}
</div>
<a
href={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-400 hover:text-gray-200 transition-colors duration-200 font-mono"
>
Sample {sample.index}
</a>
</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
</div>
</main>
)
} }

View File

@@ -1,28 +1,53 @@
"use client" "use client"
import {useSamples} from '@/contexts/SamplesContext' import {useSamples} from '@/contexts/SamplesContext'
import Image from 'next/image' import Image from 'next/image'
import Link from "next/link"
export function SamplesGallery() { export function SamplesGallery() {
const {latestSamples, isLoading, error, refreshSamples} = useSamples() const {latestSamples, isLoading, error, refreshSamples} = useSamples()
if (isLoading) return <div>Loading samples...</div> // First, let's handle our loading and error states with appropriate dark mode styling
if (error) return <div>Error loading samples: {error.message}</div> if (isLoading) return <div className="text-gray-400">Loading samples...</div>
if (latestSamples.length === 0) return <div>No samples available</div> if (error) return <div className="text-red-400">Error loading samples: {error.message}</div>
if (latestSamples.length === 0) return <div className="text-gray-400">No samples available</div>
return ( return (
<div className="grid grid-cols-5 gap-4"> <div>
{latestSamples.map((sample) => ( {/* Style the link to match our dark theme while maintaining accessibility */}
<div key={sample.filename}> <Link
<Image href="/samples"
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`} className="text-gray-400 hover:text-gray-200 transition-colors duration-200 flex items-center gap-2 group"
alt={sample.filename} >
width={200} Go to All samples
height={200} <span className="group-hover:translate-x-1 transition-transform duration-200"></span>
className="object-cover rounded" </Link>
/>
<p className="text-sm mt-1">{sample.url.split('__')[1]}</p> {/* Update the grid layout with dark mode appropriate spacing and styling */}
</div> <div className="mt-4 grid grid-cols-5 gap-4">
))} {latestSamples.map((sample) => (
<div
key={sample.filename}
className="group relative"
>
{/* Add a subtle hover effect to the image container */}
<div
className="overflow-hidden rounded-lg bg-gray-700 shadow-md transition-transform duration-200 group-hover:shadow-lg">
<Image
src={`${process.env.NEXT_PUBLIC_API_URL}${sample.url}`}
alt={sample.filename}
width={200}
height={200}
className="object-cover rounded-lg transition-transform duration-200 group-hover:scale-105"
/>
</div>
{/* Style the filename with appropriate dark mode colors */}
<p className="text-sm mt-2 text-gray-400 font-mono transition-colors duration-200 group-hover:text-gray-200">
{sample.url.split('__')[1]}
</p>
</div>
))}
</div>
</div> </div>
) )
} }

View File

@@ -1,9 +1,11 @@
"use client" "use client"
import {createContext, useContext, useEffect, useState} from 'react' import {createContext, useContext, useEffect, useState} from 'react'
import type {TrainingStatus} from '@/types/api' import type {TrainingStatus} from '@/types/api'
import {Config} from "@/types/config";
interface TrainingContextType { interface TrainingContextType {
status: TrainingStatus | null status: TrainingStatus | null
config: Config | null;
logs: string[] logs: string[]
isLoading: boolean isLoading: boolean
error: Error | null error: Error | null
@@ -13,6 +15,7 @@ const TrainingContext = createContext<TrainingContextType | undefined>(undefined
export function TrainingProvider({children}: { children: React.ReactNode }) { export function TrainingProvider({children}: { children: React.ReactNode }) {
const [status, setStatus] = useState<TrainingStatus | null>(null) const [status, setStatus] = useState<TrainingStatus | null>(null)
const [config, setConfig] = useState<Config | null>(null)
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<Error | null>(null) const [error, setError] = useState<Error | null>(null)
@@ -47,6 +50,16 @@ export function TrainingProvider({children}: { children: React.ReactNode }) {
return result; return result;
}; };
const fetchConfig = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/config/`)
const data = await response.json()
setConfig(data)
} catch (err) {
setError(err as Error)
}
}
const fetchStatus = async () => { const fetchStatus = async () => {
try { try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/training/status`) const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/training/status`)
@@ -64,7 +77,6 @@ export function TrainingProvider({children}: { children: React.ReactNode }) {
const data = await response.json() const data = await response.json()
const filteredLogs = filterLogsKeepLatestProgress(data) const filteredLogs = filterLogsKeepLatestProgress(data)
setLogs(filteredLogs) setLogs(filteredLogs)
setIsLoading(false)
} catch (err) { } catch (err) {
setError(err as Error) setError(err as Error)
} }
@@ -74,6 +86,7 @@ export function TrainingProvider({children}: { children: React.ReactNode }) {
// Initial fetch // Initial fetch
fetchConfig()
fetchStatus() fetchStatus()
fetchLogs() fetchLogs()
@@ -87,7 +100,7 @@ export function TrainingProvider({children}: { children: React.ReactNode }) {
}, []) }, [])
return ( return (
<TrainingContext.Provider value={{status, logs, isLoading, error}}> <TrainingContext.Provider value={{status, config, logs, isLoading, error}}>
{children} {children}
</TrainingContext.Provider> </TrainingContext.Provider>
) )