import { Job } from '@prisma/client'; import useGPUInfo from '@/hooks/useGPUInfo'; import GPUWidget from '@/components/GPUWidget'; import FilesWidget from '@/components/FilesWidget'; import { getTotalSteps } from '@/utils/jobs'; import { Cpu, HardDrive, Info, Gauge } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import useJobLog from '@/hooks/useJobLog'; interface JobOverviewProps { job: Job; } export default function JobOverview({ job }: JobOverviewProps) { const gpuIds = useMemo(() => job.gpu_ids.split(',').map(id => parseInt(id)), [job.gpu_ids]); const { log, setLog, status: statusLog, refresh: refreshLog } = useJobLog(job.id, 2000); const logRef = useRef(null); // Track whether we should auto-scroll to bottom const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); const { gpuList, isGPUInfoLoaded } = useGPUInfo(gpuIds, 5000); const totalSteps = getTotalSteps(job); const progress = (job.step / totalSteps) * 100; const isStopping = job.stop && job.status === 'running'; const logLines: string[] = useMemo(() => { // split at line breaks on \n or \r\n but not \r let splits: string[] = log.split(/\n|\r\n/); splits = splits.map(line => { return line.split(/\r/).pop(); }) as string[]; // only return last 100 lines max const maxLines = 1000; if (splits.length > maxLines) { splits = splits.slice(splits.length - maxLines); } return splits; }, [log]); // Handle scroll events to determine if user has scrolled away from bottom const handleScroll = () => { if (logRef.current) { const { scrollTop, scrollHeight, clientHeight } = logRef.current; // Consider "at bottom" if within 10 pixels of the bottom const isAtBottom = scrollHeight - scrollTop - clientHeight < 10; setIsScrolledToBottom(isAtBottom); } }; // Auto-scroll to bottom only if we were already at the bottom useEffect(() => { if (logRef.current && isScrolledToBottom) { logRef.current.scrollTop = logRef.current.scrollHeight; } }, [log, isScrolledToBottom]); const getStatusColor = (status: string) => { switch (status.toLowerCase()) { case 'running': return 'bg-emerald-500/10 text-emerald-500'; case 'stopping': return 'bg-amber-500/10 text-amber-500'; case 'stopped': return 'bg-gray-500/10 text-gray-400'; case 'completed': return 'bg-blue-500/10 text-blue-500'; case 'error': return 'bg-rose-500/10 text-rose-500'; default: return 'bg-gray-500/10 text-gray-400'; } }; let status = job.status; if (isStopping) { status = 'stopping'; } return (
{/* Job Information Panel */}

{job.info}

{job.status}
{/* Progress Bar */}
Progress Step {job.step} of {totalSteps}
{/* Job Info Grid */}

Job Name

{job.name}

Assigned GPUs

GPUs: {job.gpu_ids}

Speed

{job.speed_string == '' ? '?' : job.speed_string}

{/* Log - Now using flex-grow to fill remaining space */}
{statusLog === 'loading' && 'Loading log...'} {statusLog === 'error' && 'Error loading log'} {['success', 'refreshing'].includes(statusLog) && (
{logLines.map((line, index) => { return
{line}
; })}
)}
{/* GPU Widget Panel */}
{isGPUInfoLoaded && gpuList.length > 0 && }
); }