"use client"; import { useState, useEffect, useMemo } from "react"; import { useRouter, usePathname } from "next/navigation"; import { cn } from "@/lib/cn"; import type { ProjectInfo } from "@/lib/project-name"; import { getAttentionLevel, type DashboardSession, type AttentionLevel } from "@/lib/types"; import { isOrchestratorSession } from "@composio/ao-core/types"; import { getSessionTitle } from "@/lib/format"; interface ProjectSidebarProps { projects: ProjectInfo[]; sessions: DashboardSession[]; activeProjectId: string | undefined; activeSessionId: string | undefined; collapsed?: boolean; onToggleCollapsed?: () => void; mobileOpen?: boolean; onMobileClose?: () => void; } type ProjectHealth = "red" | "yellow" | "green" | "gray"; function computeProjectHealth(sessions: DashboardSession[]): ProjectHealth { const workers = sessions.filter((s) => !isOrchestratorSession(s)); if (workers.length === 0) return "gray"; for (const s of workers) { if (getAttentionLevel(s) === "respond") return "red"; } for (const s of workers) { const lvl = getAttentionLevel(s); if (lvl === "review" || lvl === "pending") return "yellow"; } return "green"; } const healthDotColor: Record = { red: "var(--color-status-error)", yellow: "var(--color-status-attention)", green: "var(--color-status-ready)", gray: "var(--color-text-tertiary)", }; const sessionDotColor: Record = { merge: "var(--color-status-ready)", respond: "var(--color-status-error)", review: "var(--color-accent-orange)", pending: "var(--color-status-attention)", working: "var(--color-status-working)", done: "var(--color-text-tertiary)", }; const sessionToneLabel: Record = { merge: "merge", respond: "reply", review: "review", pending: "wait", working: "live", done: "done", }; function SessionDot({ level }: { level: AttentionLevel }) { return (
); } function HealthDot({ health }: { health: ProjectHealth }) { return (
); } export function ProjectSidebar(props: ProjectSidebarProps) { if (props.projects.length <= 1) { return null; } return ; } function ProjectSidebarInner({ projects, sessions, activeProjectId, activeSessionId, collapsed = false, onToggleCollapsed, mobileOpen = false, onMobileClose, }: ProjectSidebarProps) { const router = useRouter(); const pathname = usePathname(); const [expandedProjects, setExpandedProjects] = useState>( () => new Set(activeProjectId && activeProjectId !== "all" ? [activeProjectId] : []), ); useEffect(() => { if (activeProjectId && activeProjectId !== "all") { setExpandedProjects((prev) => new Set([...prev, activeProjectId])); } }, [activeProjectId]); const toggleExpand = (projectId: string) => { setExpandedProjects((prev) => { const next = new Set(prev); if (next.has(projectId)) { next.delete(projectId); } else { next.add(projectId); } return next; }); }; const handleProjectHeaderClick = (projectId: string) => { toggleExpand(projectId); router.push(pathname + `?project=${encodeURIComponent(projectId)}`); }; const sessionsByProject = useMemo(() => { const map = new Map(); let totalWorkers = 0; let needsInput = 0; let reviewLoad = 0; for (const s of sessions) { let entry = map.get(s.projectId); if (!entry) { entry = { all: [], workers: [] }; map.set(s.projectId, entry); } entry.all.push(s); if (!isOrchestratorSession(s)) { entry.workers.push(s); totalWorkers++; } const lvl = getAttentionLevel(s); if (lvl === "respond") needsInput++; if (lvl === "review" || lvl === "pending") reviewLoad++; } return { map, totalWorkers, needsInput, reviewLoad }; }, [sessions]); const { totalWorkers: totalWorkerSessions, needsInput: needsInputCount, reviewLoad: reviewLoadCount } = sessionsByProject; if (collapsed) { return ( ); } return ( <> {mobileOpen && (
)} ); }