ai-toolkit / ui /src /components /JobOverview.tsx
multimodalart's picture
Upload 121 files
f555806 verified
raw
history blame
9.56 kB
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, Cloud, ExternalLink } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import useJobLog from '@/hooks/useJobLog';
import { JobConfig, JobRecord } from '@/types';
import HFJobStatus from './HFJobStatus';
interface JobOverviewProps {
job: JobRecord;
}
export default function JobOverview({ job }: JobOverviewProps) {
// Parse job config to check if it's an HF Job
const jobConfig = useMemo(() => {
try {
return JSON.parse(job.job_config) as JobConfig;
} catch (e) {
return null;
}
}, [job.job_config]);
const isHFJob = jobConfig?.is_hf_job || false;
const hfJobSubmitted = !!jobConfig?.hf_job_id;
const gpuIds = useMemo(() => {
// For HF Jobs, don't parse GPU IDs as they're hardware names
if (isHFJob) return [];
return job.gpu_ids.split(',').map(id => parseInt(id));
}, [job.gpu_ids, isHFJob]);
const { log, setLog, status: statusLog, refresh: refreshLog } = useJobLog(job.id, 2000);
const logRef = useRef<HTMLDivElement>(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 (
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{/* Job Information Panel */}
<div className="col-span-2 bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800 flex flex-col">
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
<h2 className="text-gray-100">
<Info className="w-5 h-5 mr-2 -mt-1 text-amber-400 inline-block" /> {job.info}
</h2>
{isHFJob && hfJobSubmitted && jobConfig?.hf_job_id ? (
<HFJobStatus
hfJobId={jobConfig.hf_job_id}
hfJobUrl={jobConfig.hf_job_url}
/>
) : isHFJob && !hfJobSubmitted ? (
<span className="px-3 py-1 rounded-full text-sm bg-yellow-500/10 text-yellow-500">
Pending Submission
</span>
) : (
<span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(job.status)}`}>
{job.status}
</span>
)}
</div>
<div className="p-4 space-y-6 flex flex-col flex-grow">
{/* Progress Bar */}
{isHFJob && !hfJobSubmitted ? (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Status</span>
<span className="text-yellow-400">Ready for cloud submission</span>
</div>
</div>
) : isHFJob ? (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Cloud Training</span>
<span className="text-gray-200">
Running on {jobConfig?.hardware || job.gpu_ids}
</span>
</div>
</div>
) : (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">Progress</span>
<span className="text-gray-200">
Step {job.step} of {totalSteps}
</span>
</div>
<div className="w-full bg-gray-800 rounded-full h-2">
<div className="h-2 rounded-full bg-blue-500 transition-all" style={{ width: `${progress}%` }} />
</div>
</div>
)}
{/* Job Info Grid */}
<div className="grid gap-4 grid-cols-1 md:grid-cols-3">
<div className="flex items-center space-x-4">
<HardDrive className="w-5 h-5 text-blue-400" />
<div>
<p className="text-xs text-gray-400">Job Name</p>
<p className="text-sm font-medium text-gray-200">{job.name}</p>
</div>
</div>
<div className="flex items-center space-x-4">
{isHFJob ? (
<Cloud className="w-5 h-5 text-blue-400" />
) : (
<Cpu className="w-5 h-5 text-purple-400" />
)}
<div>
<p className="text-xs text-gray-400">
{isHFJob ? 'Hardware' : 'Assigned GPUs'}
</p>
<p className="text-sm font-medium text-gray-200">
{isHFJob ? (jobConfig?.hardware || job.gpu_ids) : `GPUs: ${job.gpu_ids}`}
</p>
</div>
</div>
<div className="flex items-center space-x-4">
<Gauge className="w-5 h-5 text-green-400" />
<div>
<p className="text-xs text-gray-400">Speed</p>
<p className="text-sm font-medium text-gray-200">{job.speed_string == '' ? '?' : job.speed_string}</p>
</div>
</div>
</div>
{/* Log - Now using flex-grow to fill remaining space */}
<div className="bg-gray-950 rounded-lg p-4 relative flex-grow min-h-60">
{isHFJob && hfJobSubmitted && jobConfig?.hf_job_url ? (
<div className="absolute inset-0 p-4 flex flex-col items-center justify-center">
<Cloud className="w-16 h-16 text-blue-400 mb-4" />
<p className="text-sm text-gray-300 mb-4 text-center">
This job is running on HF Jobs. View logs and monitor progress on the HuggingFace platform.
</p>
<a
href={jobConfig.hf_job_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
<ExternalLink className="w-4 h-4 mr-2" />
View on HF Jobs
</a>
</div>
) : isHFJob && !hfJobSubmitted ? (
<div className="absolute inset-0 p-4 flex flex-col items-center justify-center">
<Cloud className="w-16 h-16 text-yellow-400 mb-4" />
<p className="text-sm text-gray-300 mb-4 text-center">
This HF Job is ready for submission. Edit the job clicking on the pen on the top right.
</p>
</div>
) : (
<div
ref={logRef}
className="text-xs text-gray-300 absolute inset-0 p-4 overflow-y-auto"
onScroll={handleScroll}
>
{statusLog === 'loading' && 'Loading log...'}
{statusLog === 'error' && 'Error loading log'}
{['success', 'refreshing'].includes(statusLog) && (
<div>
{logLines.map((line, index) => {
return <pre key={index}>{line}</pre>;
})}
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* GPU Widget Panel */}
<div className="col-span-1">
{!isHFJob && (
<div>{isGPUInfoLoaded && gpuList.length > 0 && <GPUWidget gpu={gpuList[0]} />}</div>
)}
<div className={isHFJob ? '' : 'mt-4'}>
<FilesWidget jobID={job.id} />
</div>
</div>
</div>
);
}