"use client"; import { useEffect, useState, useRef } from "react"; import Link from "next/link"; import { Button } from "./ui/Button"; import { Badge } from "./ui/Badge"; import { ProgressBar } from "./ui/ProgressBar"; interface Job { id: string; library_id: string | null; type: string; status: string; current_file: string | null; progress_percent: number | null; processed_files: number | null; total_files: number | null; stats_json: { scanned_files: number; indexed_files: number; errors: number; } | null; } // Icons const JobsIcon = ({ className }: { className?: string }) => ( ); const SpinnerIcon = ({ className }: { className?: string }) => ( ); const ChevronIcon = ({ className }: { className?: string }) => ( ); export function JobsIndicator() { const [activeJobs, setActiveJobs] = useState([]); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); useEffect(() => { const fetchActiveJobs = async () => { try { const response = await fetch("/api/jobs/active"); if (response.ok) { const jobs = await response.json(); setActiveJobs(jobs); } } catch (error) { console.error("Failed to fetch jobs:", error); } }; fetchActiveJobs(); const interval = setInterval(fetchActiveJobs, 2000); return () => clearInterval(interval); }, []); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, []); const runningJobs = activeJobs.filter(j => j.status === "running"); const pendingJobs = activeJobs.filter(j => j.status === "pending"); const totalCount = activeJobs.length; // Calculate overall progress const totalProgress = runningJobs.reduce((acc, job) => { return acc + (job.progress_percent || 0); }, 0) / (runningJobs.length || 1); if (totalCount === 0) { return ( ); } return (
{/* Popin/Dropdown with glassmorphism */} {isOpen && (
{/* Header */}
📊

Active Jobs

{runningJobs.length > 0 ? `${runningJobs.length} running, ${pendingJobs.length} pending` : `${pendingJobs.length} job${pendingJobs.length !== 1 ? 's' : ''} pending` }

setIsOpen(false)} > View All →
{/* Overall progress bar if running */} {runningJobs.length > 0 && (
Overall Progress {Math.round(totalProgress)}%
)} {/* Job List */}
{activeJobs.length === 0 ? (

No active jobs

) : (
    {activeJobs.map(job => (
  • setIsOpen(false)} >
    {job.status === "running" && } {job.status === "pending" && }
    {job.id.slice(0, 8)} {job.type}
    {job.status === "running" && job.progress_percent !== null && (
    {job.progress_percent}%
    )} {job.current_file && (

    📄 {job.current_file}

    )} {job.stats_json && (
    ✓ {job.stats_json.indexed_files} {job.stats_json.errors > 0 && ( ⚠ {job.stats_json.errors} )}
    )}
  • ))}
)}
{/* Footer */}

Auto-refreshing every 2s

)}
); } // Mini progress bar for dropdown function MiniProgressBar({ value }: { value: number }) { return (
); }