suna/frontend/src/components/dashboard/sidebar/nav-agents.tsx

292 lines
10 KiB
TypeScript
Raw Normal View History

2025-04-16 02:14:58 +08:00
"use client"
import { useEffect, useState } from "react"
import {
ArrowUpRight,
Link as LinkIcon,
MoreHorizontal,
Trash2,
2025-04-16 02:37:56 +08:00
Plus,
2025-04-16 04:45:46 +08:00
MessagesSquare,
2025-04-16 02:14:58 +08:00
} from "lucide-react"
2025-04-16 08:04:04 +08:00
import { toast } from "sonner"
2025-04-17 01:27:18 +08:00
import { usePathname } from "next/navigation"
2025-04-16 02:14:58 +08:00
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarGroup,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
2025-04-16 15:50:32 +08:00
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from "@/components/ui/tooltip"
2025-04-16 02:14:58 +08:00
import { getProjects, getThreads } from "@/lib/api"
import Link from "next/link"
2025-04-16 17:54:52 +08:00
// Define a type to handle potential database schema/API response differences
type ProjectResponse = {
id: string;
project_id?: string;
name: string;
2025-04-17 01:27:18 +08:00
updated_at?: string;
2025-04-16 17:54:52 +08:00
[key: string]: any; // Allow other properties
}
2025-04-17 01:27:18 +08:00
// Agent type with project ID for easier updating
type Agent = {
projectId: string;
threadId: string;
name: string;
url: string;
updatedAt: string; // Store updated_at for consistent sorting
}
2025-04-16 02:14:58 +08:00
export function NavAgents() {
2025-04-16 04:45:46 +08:00
const { isMobile, state } = useSidebar()
2025-04-17 01:27:18 +08:00
const [agents, setAgents] = useState<Agent[]>([])
2025-04-16 02:14:58 +08:00
const [isLoading, setIsLoading] = useState(true)
2025-04-17 01:27:18 +08:00
const pathname = usePathname()
2025-04-16 02:14:58 +08:00
2025-04-17 01:27:18 +08:00
// Helper to sort agents by updated_at (most recent first)
const sortAgents = (agentsList: Agent[]): Agent[] => {
return [...agentsList].sort((a, b) => {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
});
};
// Function to load agents data
const loadAgents = async (showLoading = true) => {
try {
if (showLoading) {
setIsLoading(true)
}
// Get all projects
const projectsData = await getProjects() as ProjectResponse[]
// Get all threads at once
const allThreads = await getThreads()
// For each project, find its matching threads
const agentsList: Agent[] = []
for (const project of projectsData) {
// Get the project ID (handle potential different field names)
const projectId = project.id || project.project_id || ''
2025-04-16 17:54:52 +08:00
2025-04-17 01:27:18 +08:00
// Get the updated_at timestamp (default to current time if not available)
const updatedAt = project.updated_at || new Date().toISOString()
2025-04-16 02:14:58 +08:00
2025-04-17 01:27:18 +08:00
// Match threads that belong to this project
const projectThreads = allThreads.filter(thread =>
thread.project_id === projectId
)
2025-04-16 17:54:52 +08:00
2025-04-17 01:27:18 +08:00
if (projectThreads.length > 0) {
// For each thread in this project, create an agent entry
for (const thread of projectThreads) {
agentsList.push({
projectId,
threadId: thread.thread_id,
name: project.name || 'Unnamed Project',
url: `/dashboard/agents/${thread.thread_id}`,
updatedAt: thread.updated_at || updatedAt // Use thread update time if available
})
2025-04-16 02:14:58 +08:00
}
}
2025-04-17 01:27:18 +08:00
}
// Set agents, ensuring consistent sort order
setAgents(sortAgents(agentsList))
} catch (err) {
console.error("Error loading agents for sidebar:", err)
} finally {
if (showLoading) {
2025-04-16 02:14:58 +08:00
setIsLoading(false)
}
}
2025-04-17 01:27:18 +08:00
}
// Load agents dynamically from the API on initial load
useEffect(() => {
loadAgents(true)
2025-04-16 02:14:58 +08:00
}, [])
2025-04-17 01:27:18 +08:00
// Listen for project-updated events to update the sidebar without full reload
useEffect(() => {
const handleProjectUpdate = (event: Event) => {
const customEvent = event as CustomEvent;
if (customEvent.detail) {
const { projectId, updatedData } = customEvent.detail;
// Update just the name for the agents with the matching project ID
// Don't update the timestamp here to prevent immediate re-sorting
setAgents(prevAgents => {
const updatedAgents = prevAgents.map(agent =>
agent.projectId === projectId
? {
...agent,
name: updatedData.name,
// Keep the original updatedAt timestamp locally
}
: agent
);
// Return the agents without re-sorting immediately
return updatedAgents;
});
// Silently refresh in background to fetch updated timestamp and re-sort
setTimeout(() => loadAgents(false), 1000);
}
}
// Add event listener
window.addEventListener('project-updated', handleProjectUpdate as EventListener);
// Cleanup
return () => {
window.removeEventListener('project-updated', handleProjectUpdate as EventListener);
}
}, []);
2025-04-16 02:14:58 +08:00
return (
2025-04-16 04:45:46 +08:00
<SidebarGroup>
2025-04-16 02:14:58 +08:00
<div className="flex justify-between items-center">
<SidebarGroupLabel>Agents</SidebarGroupLabel>
2025-04-17 06:10:32 +08:00
{state !== "collapsed" ? (
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/dashboard"
className="text-muted-foreground hover:text-foreground h-8 w-8 flex items-center justify-center rounded-md"
>
<Plus className="h-4 w-4" />
<span className="sr-only">New Agent</span>
</Link>
</TooltipTrigger>
<TooltipContent>New Agent</TooltipContent>
</Tooltip>
) : null}
2025-04-16 02:14:58 +08:00
</div>
2025-04-16 08:04:04 +08:00
<SidebarMenu className="overflow-y-auto max-h-[calc(100vh-200px)]">
2025-04-17 06:10:32 +08:00
{state === "collapsed" && (
<SidebarMenuItem>
<Tooltip>
<TooltipTrigger asChild>
<SidebarMenuButton asChild>
<Link href="/dashboard" className="flex items-center">
<Plus className="h-4 w-4" />
<span>New Agent</span>
</Link>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent>New Agent</TooltipContent>
</Tooltip>
</SidebarMenuItem>
)}
2025-04-16 02:14:58 +08:00
{isLoading ? (
// Show skeleton loaders while loading
Array.from({length: 3}).map((_, index) => (
<SidebarMenuItem key={`skeleton-${index}`}>
<SidebarMenuButton>
<div className="h-4 w-4 bg-sidebar-foreground/10 rounded-md animate-pulse"></div>
<div className="h-3 bg-sidebar-foreground/10 rounded w-3/4 animate-pulse"></div>
</SidebarMenuButton>
</SidebarMenuItem>
))
2025-04-16 08:04:04 +08:00
) : agents.length > 0 ? (
// Show all agents
2025-04-16 02:14:58 +08:00
<>
2025-04-17 01:27:18 +08:00
{agents.map((agent, index) => {
// Check if this agent is currently active
const isActive = pathname.includes(agent.threadId);
return (
<SidebarMenuItem key={`agent-${agent.threadId}`}>
{state === "collapsed" ? (
<Tooltip>
<TooltipTrigger asChild>
<SidebarMenuButton asChild className={isActive ? "bg-accent text-accent-foreground" : ""}>
<Link href={agent.url}>
<MessagesSquare className="h-4 w-4" />
<span>{agent.name}</span>
</Link>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent>{agent.name}</TooltipContent>
</Tooltip>
) : (
<SidebarMenuButton asChild className={isActive ? "bg-accent text-accent-foreground font-medium" : ""}>
<Link href={agent.url}>
<MessagesSquare className="h-4 w-4" />
<span>{agent.name}</span>
</Link>
</SidebarMenuButton>
)}
{state !== "collapsed" && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem onClick={() => {
navigator.clipboard.writeText(window.location.origin + agent.url)
toast.success("Link copied to clipboard")
}}>
<LinkIcon className="text-muted-foreground" />
<span>Copy Link</span>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a href={agent.url} target="_blank" rel="noopener noreferrer">
<ArrowUpRight className="text-muted-foreground" />
<span>Open in New Tab</span>
</a>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
);
})}
2025-04-16 02:14:58 +08:00
</>
) : (
// Empty state
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
2025-04-16 04:45:46 +08:00
<MessagesSquare className="h-4 w-4" />
2025-04-16 02:14:58 +08:00
<span>No agents yet</span>
</SidebarMenuButton>
</SidebarMenuItem>
)}
</SidebarMenu>
</SidebarGroup>
)
}