From acd37d2d75eb56618b008728623c4aba35e9c223 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Thu, 23 Jan 2025 12:39:22 +0100 Subject: [PATCH] Add complete base frontend components Signed-off-by: Felipe Cardoso --- backend/app/services/training_monitor.py | 2 +- frontend/next.config.ts | 3 + frontend/src/app/layout.tsx | 9 +- frontend/src/app/page.tsx | 145 ++++++------------- frontend/src/components/LogViewer.tsx | 36 +++++ frontend/src/components/SamplesGallery.tsx | 28 ++++ frontend/src/components/TrainingProgress.tsx | 19 +++ frontend/src/contexts/SamplesContext.tsx | 52 +++++++ frontend/src/contexts/TrainingContext.tsx | 102 +++++++++++++ frontend/src/types/api.ts | 22 +++ 10 files changed, 318 insertions(+), 100 deletions(-) create mode 100644 frontend/src/components/LogViewer.tsx create mode 100644 frontend/src/components/SamplesGallery.tsx create mode 100644 frontend/src/components/TrainingProgress.tsx create mode 100644 frontend/src/contexts/SamplesContext.tsx create mode 100644 frontend/src/contexts/TrainingContext.tsx create mode 100644 frontend/src/types/api.ts diff --git a/backend/app/services/training_monitor.py b/backend/app/services/training_monitor.py index 33b028d..9cfa5a8 100644 --- a/backend/app/services/training_monitor.py +++ b/backend/app/services/training_monitor.py @@ -175,7 +175,7 @@ class TrainingMonitor: logger.error(f"Monitor error: {str(e)}") await asyncio.sleep(5) - async def get_log(self, lines: int = 50) -> List[str]: + async def get_log(self, lines: int = 100) -> List[str]: """Get recent log entries""" return self.recent_logs[-lines:] diff --git a/frontend/next.config.ts b/frontend/next.config.ts index cb6eef4..4769445 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -2,6 +2,9 @@ import type {NextConfig} from "next"; const nextConfig: NextConfig = { /* config options here */ + images: { + domains: ['localhost'], + } }; export default nextConfig; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 1affe3a..68f61d7 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,8 @@ import type {Metadata} from "next"; import {Geist, Geist_Mono} from "next/font/google"; import "./globals.css"; +import {TrainingProvider} from "@/contexts/TrainingContext"; +import {SamplesProvider} from "@/contexts/SamplesContext"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,7 +29,12 @@ export default function RootLayout({ - {children} + + + {children} + + + ); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8cd606b..85df6e5 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,102 +1,51 @@ -import Image from "next/image"; +import {TrainingProgress} from '@/components/TrainingProgress' +import {SamplesGallery} from '@/components/SamplesGallery' +import {LogViewer} from '@/components/LogViewer' +import {Suspense} from 'react' -export default function Home() { +export default function DashboardPage() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
+
+
+
+

Training Monitor

+
+ Live monitoring +
+
-
- - Vercel logomark - Deploy now - - - Read our docs - +
+ {/* Training Status Section */} +
+

Training Progress

+ Loading training status...
}> + + +
+ + {/* Latest Samples Section */} +
+
+

Latest Samples

+ Auto-updating +
+ Loading samples...
}> + + +
+ + {/* Log Viewer Section - Full Width */} +
+
+

Training Logs

+ Real-time updates +
+ Loading logs...
}> + + +
- - - - ); -} + + + ) +} \ No newline at end of file diff --git a/frontend/src/components/LogViewer.tsx b/frontend/src/components/LogViewer.tsx new file mode 100644 index 0000000..6d18323 --- /dev/null +++ b/frontend/src/components/LogViewer.tsx @@ -0,0 +1,36 @@ +"use client" +import {useTraining} from '@/contexts/TrainingContext' +import {useEffect, useRef} from 'react' + +export function LogViewer() { + const {logs, isLoading} = useTraining() + const scrollRef = useRef(null) + + // Auto-scroll to bottom when new logs arrive + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight + } + }, [logs]) + + if (isLoading) return
Loading logs...
+ + return ( +
+ {logs.map((log, index) => ( +
+ {log} +
+ ))} + {logs.length === 0 && ( +
No logs available
+ )} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/SamplesGallery.tsx b/frontend/src/components/SamplesGallery.tsx new file mode 100644 index 0000000..47e4f66 --- /dev/null +++ b/frontend/src/components/SamplesGallery.tsx @@ -0,0 +1,28 @@ +"use client" +import {useSamples} from '@/contexts/SamplesContext' +import Image from 'next/image' + +export function SamplesGallery() { + const {samples, isLoading, error, refreshSamples} = useSamples() + + if (isLoading) return
Loading samples...
+ if (error) return
Error loading samples: {error.message}
+ if (samples.length === 0) return
No samples available
+ + return ( +
+ {samples.map((sample) => ( +
+ {sample.filename} +

{sample.filename}

+
+ ))} +
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/TrainingProgress.tsx b/frontend/src/components/TrainingProgress.tsx new file mode 100644 index 0000000..b2f296f --- /dev/null +++ b/frontend/src/components/TrainingProgress.tsx @@ -0,0 +1,19 @@ +"use client" +import {useTraining} from '@/contexts/TrainingContext' + +export function TrainingProgress() { + const {status, isLoading, error} = useTraining() + + if (isLoading) return
Loading...
+ if (error) return
Error: {error.message}
+ if (!status) return
No training data available
+ + + return ( +
+

Progress: {status.percentage.toFixed(2)}%

+

Step: {status.current_step} / {status.total_steps}

+

Loss: {status.loss.toFixed(6)}

+
+ ) +} \ No newline at end of file diff --git a/frontend/src/contexts/SamplesContext.tsx b/frontend/src/contexts/SamplesContext.tsx new file mode 100644 index 0000000..0269177 --- /dev/null +++ b/frontend/src/contexts/SamplesContext.tsx @@ -0,0 +1,52 @@ +"use client" +import {createContext, useContext, useEffect, useState} from 'react' +import type {Sample} from '@/types/api' + +interface SamplesContextType { + samples: Sample[] + isLoading: boolean + error: Error | null + refreshSamples: () => Promise +} + +const SamplesContext = createContext(undefined) + +export function SamplesProvider({children}: { children: React.ReactNode }) { + const [samples, setSamples] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchSamples = async () => { + try { + setIsLoading(true) + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/samples/list`) + const data = await response.json() + setSamples(data) + } catch (err) { + setError(err as Error) + } finally { + setIsLoading(false) + } + } + + useEffect(() => { + fetchSamples() + // Poll for new samples less frequently than training status + const interval = setInterval(fetchSamples, 180000) + return () => clearInterval(interval) + }, []) + + return ( + + {children} + + ) +} + +export function useSamples() { + const context = useContext(SamplesContext) + if (context === undefined) { + throw new Error('useSamples must be used within a SamplesProvider') + } + return context +} \ No newline at end of file diff --git a/frontend/src/contexts/TrainingContext.tsx b/frontend/src/contexts/TrainingContext.tsx new file mode 100644 index 0000000..73183cf --- /dev/null +++ b/frontend/src/contexts/TrainingContext.tsx @@ -0,0 +1,102 @@ +"use client" +import {createContext, useContext, useEffect, useState} from 'react' +import type {TrainingStatus} from '@/types/api' + +interface TrainingContextType { + status: TrainingStatus | null + logs: string[] + isLoading: boolean + error: Error | null +} + +const TrainingContext = createContext(undefined) + +export function TrainingProvider({children}: { children: React.ReactNode }) { + const [status, setStatus] = useState(null) + const [logs, setLogs] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const filterLogsKeepLatestProgress = (logs: string[]): string[] => { + const result: string[] = []; + const progressLogMap = new Map(); // Map to store the latest progress log for each progress key + + logs.forEach((log) => { + if (log.trim() === "") return; // Skip empty strings + + // Detect if the log contains a progress bar + const isProgressLog = /^(\S+):\s+\d+%\|/.test(log); + + if (isProgressLog) { + // Extract a unique key based on the identifier and the progress percentage + const match = log.match(/^(\S+):\s+\d+%/); // Match the identifier (e.g., "ovs_bangel_001") + const progressKey = match ? match[1] : null; // Extract the relevant progress key + + if (progressKey) { + progressLogMap.set(progressKey, log); // Update the map with the latest progress log for this key + } + } else { + result.push(log); // Add non-progress logs immediately + } + }); + + // Add the latest progress logs to the result + progressLogMap.forEach((log) => { + result.push(log); + }); + + return result; + }; + const fetchStatus = async () => { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/training/status`) + const data = await response.json() + setStatus(data) + setIsLoading(false) + } catch (err) { + setError(err as Error) + } + } + + const fetchLogs = async () => { + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/training/log`) + const data = await response.json() + const filteredLogs = filterLogsKeepLatestProgress(data) + setLogs(filteredLogs) + setIsLoading(false) + } catch (err) { + setError(err as Error) + } + } + + useEffect(() => { + + + // Initial fetch + fetchStatus() + fetchLogs() + + // Set up polling + const interval = setInterval(fetchStatus, 10000) + const intervalLogs = setInterval(fetchLogs, 10000) + return () => { + clearInterval(interval) + clearInterval(intervalLogs) + } + }, []) + + return ( + + {children} + + ) +} + +export function useTraining() { + const context = useContext(TrainingContext) + if (context === undefined) { + throw new Error('useTraining must be used within a TrainingProvider') + } + return context +} \ No newline at end of file diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..1284fc9 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,22 @@ +// src/types/api.ts +export interface TrainingStatus { + current_step: number; + total_steps: number; + loss: number; + learning_rate: number; + percentage: number; + eta_seconds: number; + steps_per_second: number; + updated_at: string; + source: string; + source_path: string; +} + +export interface Sample { + filename: string; + url: string; + created_at: string; + source: string; + source_path: string; + size: number; +} \ No newline at end of file