Major restyling of frontend
Signed-off-by: Felipe Cardoso <felipe.cardoso@hotmail.it>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user