import { useState, useMemo, useCallback, useEffect } from "react"; import { Plus, LayoutGrid, Filter, ArrowUpDown, Activity, CheckCircle, AlertCircle, Folder, Inbox } from "lucide-react"; import type { ProjectInfo, ProjectHealth, NodeInfo } from "../api"; import type { ProjectStatus } from "@fusion/core"; import { ProjectCard } from "./ProjectCard"; import { ProjectGridSkeleton } from "./ProjectGridSkeleton"; import { useProjectHealth } from "../hooks/useProjectHealth"; export interface ProjectOverviewProps { projects: ProjectInfo[]; loading?: boolean; onSelectProject: (project: ProjectInfo) => void; onAddProject: () => void; onPauseProject: (project: ProjectInfo) => void; onResumeProject: (project: ProjectInfo) => void; onRemoveProject: (project: ProjectInfo) => void; onViewAllProjects?: () => void; nodes?: NodeInfo[]; } type FilterTab = "all" | "active" | "paused" | "errored"; type SortOption = "name" | "activity" | "status"; interface ProjectWithHealth { project: ProjectInfo; health: ProjectHealth | null; } /** * ProjectOverview - Multi-project grid view with stats and filtering * * Displays all projects in a responsive grid with: * - Header stats: total projects, active tasks, completed tasks * - Filter tabs: All, Active, Paused, Errored * - Sort dropdown: Name, Last Activity, Status * - Project cards with health indicators * - Empty state when no projects */ export function ProjectOverview({ projects, loading = false, onSelectProject, onAddProject, onPauseProject, onResumeProject, onRemoveProject, nodes = [], }: ProjectOverviewProps) { const [activeFilter, setActiveFilter] = useState("all"); const [sortBy, setSortBy] = useState("activity"); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); // Track recently accessed projects for quick selection useEffect(() => { if (typeof window === "undefined") return; // Load recently accessed from localStorage const recent = localStorage.getItem("kb-dashboard-recent-projects"); if (recent) { try { const parsed = JSON.parse(recent) as string[]; setRecentProjectIds(parsed); } catch { // Ignore parse errors } } }, []); const [recentProjectIds, setRecentProjectIds] = useState([]); // Fetch health for all projects const projectIds = useMemo(() => projects.map((p) => p.id), [projects]); const { healthMap, loading: healthLoading } = useProjectHealth(projectIds); // Combine projects with their health data const projectsWithHealth: ProjectWithHealth[] = useMemo(() => { return projects.map((project) => ({ project, health: healthMap[project.id] || null, })); }, [projects, healthMap]); // Filter projects const filteredProjects = useMemo(() => { let filtered = [...projectsWithHealth]; if (activeFilter !== "all") { filtered = filtered.filter(({ project }) => project.status === activeFilter); } return filtered; }, [projectsWithHealth, activeFilter]); // Sort projects const sortedProjects = useMemo(() => { const sorted = [...filteredProjects]; sorted.sort((a, b) => { let comparison = 0; switch (sortBy) { case "name": comparison = a.project.name.localeCompare(b.project.name); break; case "activity": const aTime = a.project.lastActivityAt || a.health?.lastActivityAt || a.project.updatedAt; const bTime = b.project.lastActivityAt || b.health?.lastActivityAt || b.project.updatedAt; comparison = new Date(bTime).getTime() - new Date(aTime).getTime(); break; case "status": const statusOrder: Record = { errored: 0, initializing: 1, paused: 2, active: 3, }; comparison = statusOrder[a.project.status] - statusOrder[b.project.status]; break; } return sortDirection === "asc" ? comparison : -comparison; }); return sorted; }, [filteredProjects, sortBy, sortDirection]); // Calculate stats const stats = useMemo(() => { const totalProjects = projects.length; const activeProjects = projects.filter((p) => p.status === "active").length; const erroredProjects = projects.filter((p) => p.status === "errored").length; let totalActiveTasks = 0; let totalCompletedTasks = 0; let totalInFlightAgents = 0; Object.values(healthMap).forEach((health) => { if (health) { totalActiveTasks += health.activeTaskCount; totalCompletedTasks += health.totalTasksCompleted; totalInFlightAgents += health.inFlightAgentCount; } }); return { totalProjects, activeProjects, erroredProjects, totalActiveTasks, totalCompletedTasks, totalInFlightAgents, }; }, [projects, healthMap]); // Filter counts const filterCounts = useMemo(() => { return { all: projects.length, active: projects.filter((p) => p.status === "active").length, paused: projects.filter((p) => p.status === "paused").length, errored: projects.filter((p) => p.status === "errored").length, }; }, [projects]); // Handle sort change const handleSort = useCallback((option: SortOption) => { if (sortBy === option) { setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); } else { setSortBy(option); setSortDirection(option === "name" ? "asc" : "desc"); } }, [sortBy]); // Handle project selection const handleSelectProject = useCallback((project: ProjectInfo) => { // Update recent projects in localStorage const updated = [project.id, ...recentProjectIds.filter((id) => id !== project.id)].slice(0, 3); setRecentProjectIds(updated); if (typeof window !== "undefined") { localStorage.setItem("kb-dashboard-recent-projects", JSON.stringify(updated)); } onSelectProject(project); }, [onSelectProject, recentProjectIds]); // Determine if we need to show skeleton // Show skeleton for initial load if: // 1. Projects list is still loading, OR // 2. Projects exist but we haven't fetched health data yet (healthLoading with no data) // Don't show skeleton during background health polling when health data already exists const needsInitialSkeleton = loading || (healthLoading && projects.length > 0 && Object.keys(healthMap).length === 0); // Show skeleton while loading if (needsInitialSkeleton) { return ; } // Empty state when no projects if (projects.length === 0) { return (

No Projects Found

Get started by adding your first project. Projects allow you to organize and track tasks across multiple repositories.

); } return (
{/* Header with stats */}

Projects

{stats.totalProjects} Total
{stats.totalActiveTasks} Active Tasks
{stats.totalCompletedTasks} Completed
{stats.erroredProjects > 0 && (
{stats.erroredProjects} Errored
)}
{/* Filter tabs */}
{/* Sort dropdown */}
{/* Project grid */}
{sortedProjects.map(({ project, health }) => { const projectNode = nodes.find((node) => node.id === project.nodeId); return ( ); })}
{/* No results state */} {sortedProjects.length === 0 && (

No projects match the current filter

)}
); }