mirror of https://github.com/kortix-ai/suna.git
tasks time grouping
This commit is contained in:
parent
cd3ebb5e78
commit
3535c25c83
|
@ -44,10 +44,147 @@ import { DeleteConfirmationDialog } from "@/components/thread/DeleteConfirmation
|
|||
import { useDeleteOperation } from '@/contexts/DeleteOperationContext'
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { ThreadWithProject } from '@/hooks/react-query/sidebar/use-sidebar';
|
||||
import { processThreadsWithProjects, useDeleteMultipleThreads, useDeleteThread, useProjects, useThreads } from '@/hooks/react-query/sidebar/use-sidebar';
|
||||
import { ThreadWithProject, GroupedThreads } from '@/hooks/react-query/sidebar/use-sidebar';
|
||||
import { processThreadsWithProjects, useDeleteMultipleThreads, useDeleteThread, useProjects, useThreads, groupThreadsByDate } from '@/hooks/react-query/sidebar/use-sidebar';
|
||||
import { projectKeys, threadKeys } from '@/hooks/react-query/sidebar/keys';
|
||||
|
||||
// Component for date group headers
|
||||
const DateGroupHeader: React.FC<{ dateGroup: string; count: number }> = ({ dateGroup, count }) => {
|
||||
return (
|
||||
<div className="px-2 py-1 mb-1 mt-3 first:mt-0">
|
||||
<div className="text-xs font-medium text-muted-foreground/80 uppercase tracking-wider">
|
||||
{dateGroup} ({count})
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Component for individual thread item
|
||||
const ThreadItem: React.FC<{
|
||||
thread: ThreadWithProject;
|
||||
isActive: boolean;
|
||||
isThreadLoading: boolean;
|
||||
isSelected: boolean;
|
||||
selectedThreads: Set<string>;
|
||||
loadingThreadId: string | null;
|
||||
pathname: string | null;
|
||||
isMobile: boolean;
|
||||
handleThreadClick: (e: React.MouseEvent<HTMLAnchorElement>, threadId: string, url: string) => void;
|
||||
toggleThreadSelection: (threadId: string, e?: React.MouseEvent) => void;
|
||||
handleDeleteThread: (threadId: string, threadName: string) => void;
|
||||
setSelectedItem: (item: { threadId: string; projectId: string } | null) => void;
|
||||
setShowShareModal: (show: boolean) => void;
|
||||
}> = ({
|
||||
thread,
|
||||
isActive,
|
||||
isThreadLoading,
|
||||
isSelected,
|
||||
handleThreadClick,
|
||||
toggleThreadSelection,
|
||||
handleDeleteThread,
|
||||
setSelectedItem,
|
||||
setShowShareModal,
|
||||
isMobile
|
||||
}) => {
|
||||
return (
|
||||
<SidebarMenuItem key={`thread-${thread.threadId}`} className="group/row">
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className={`relative ${isActive
|
||||
? 'bg-accent text-accent-foreground font-medium'
|
||||
: isSelected
|
||||
? 'bg-primary/10'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center w-full">
|
||||
<Link
|
||||
href={thread.url}
|
||||
onClick={(e) =>
|
||||
handleThreadClick(e, thread.threadId, thread.url)
|
||||
}
|
||||
prefetch={false}
|
||||
className="flex items-center flex-1 min-w-0 touch-manipulation"
|
||||
>
|
||||
{isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2 flex-shrink-0" />
|
||||
) : null}
|
||||
<span className="truncate">{thread.projectName}</span>
|
||||
</Link>
|
||||
|
||||
{/* Checkbox - only visible on hover of this specific area */}
|
||||
<div
|
||||
className="mr-1 flex-shrink-0 w-4 h-4 flex items-center justify-center group/checkbox"
|
||||
onClick={(e) => toggleThreadSelection(thread.threadId, e)}
|
||||
>
|
||||
<div
|
||||
className={`h-4 w-4 border rounded cursor-pointer transition-all duration-150 flex items-center justify-center ${isSelected
|
||||
? 'opacity-100 bg-primary border-primary hover:bg-primary/90'
|
||||
: 'opacity-0 group-hover/checkbox:opacity-100 border-muted-foreground/30 bg-background hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Menu - inline with content */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="cursor-pointer flex-shrink-0 w-4 h-4 flex items-center justify-center hover:bg-muted/50 rounded transition-all duration-150 text-muted-foreground hover:text-foreground opacity-0 group-hover/row:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Ensure pointer events are enabled when dropdown opens
|
||||
document.body.style.pointerEvents = 'auto';
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More actions</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align={isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
setSelectedItem({ threadId: thread?.threadId, projectId: thread?.projectId })
|
||||
setShowShareModal(true)
|
||||
}}>
|
||||
<Share2 className="text-muted-foreground" />
|
||||
<span>Share Chat</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href={thread.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRight className="text-muted-foreground" />
|
||||
<span>Open in New Tab</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleDeleteThread(
|
||||
thread.threadId,
|
||||
thread.projectName,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export function NavAgents() {
|
||||
const { isMobile, state, setOpenMobile } = useSidebar()
|
||||
const [loadingThreadId, setLoadingThreadId] = useState<string | null>(null)
|
||||
|
@ -88,6 +225,8 @@ export function NavAgents() {
|
|||
!isProjectsLoading && !isThreadsLoading ?
|
||||
processThreadsWithProjects(threads, projects) : [];
|
||||
|
||||
const groupedThreads: GroupedThreads = groupThreadsByDate(combinedThreads);
|
||||
|
||||
const handleDeletionProgress = (completed: number, total: number) => {
|
||||
const percentage = (completed / total) * 100;
|
||||
setDeleteProgress(percentage);
|
||||
|
@ -401,112 +540,37 @@ export function NavAgents() {
|
|||
</SidebarMenuItem>
|
||||
))
|
||||
) : combinedThreads.length > 0 ? (
|
||||
// Show all threads with project info
|
||||
// Show threads grouped by date
|
||||
<>
|
||||
{combinedThreads.map((thread) => {
|
||||
// Check if this thread is currently active
|
||||
const isActive = pathname?.includes(thread.threadId) || false;
|
||||
const isThreadLoading = loadingThreadId === thread.threadId;
|
||||
const isSelected = selectedThreads.has(thread.threadId);
|
||||
{Object.entries(groupedThreads).map(([dateGroup, threadsInGroup]) => (
|
||||
<div key={dateGroup}>
|
||||
<DateGroupHeader dateGroup={dateGroup} count={threadsInGroup.length} />
|
||||
{threadsInGroup.map((thread) => {
|
||||
const isActive = pathname?.includes(thread.threadId) || false;
|
||||
const isThreadLoading = loadingThreadId === thread.threadId;
|
||||
const isSelected = selectedThreads.has(thread.threadId);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={`thread-${thread.threadId}`} className="group/row">
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className={`relative ${isActive
|
||||
? 'bg-accent text-accent-foreground font-medium'
|
||||
: isSelected
|
||||
? 'bg-primary/10'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center w-full">
|
||||
<Link
|
||||
href={thread.url}
|
||||
onClick={(e) =>
|
||||
handleThreadClick(e, thread.threadId, thread.url)
|
||||
}
|
||||
prefetch={false}
|
||||
className="flex items-center flex-1 min-w-0 touch-manipulation"
|
||||
>
|
||||
{isThreadLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2 flex-shrink-0" />
|
||||
) : null}
|
||||
<span className="truncate">{thread.projectName}</span>
|
||||
</Link>
|
||||
|
||||
{/* Checkbox - only visible on hover of this specific area */}
|
||||
<div
|
||||
className="mr-1 flex-shrink-0 w-4 h-4 flex items-center justify-center group/checkbox"
|
||||
onClick={(e) => toggleThreadSelection(thread.threadId, e)}
|
||||
>
|
||||
<div
|
||||
className={`h-4 w-4 border rounded cursor-pointer transition-all duration-150 flex items-center justify-center ${isSelected
|
||||
? 'opacity-100 bg-primary border-primary hover:bg-primary/90'
|
||||
: 'opacity-0 group-hover/checkbox:opacity-100 border-muted-foreground/30 bg-background hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dropdown Menu - inline with content */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="cursor-pointer flex-shrink-0 w-4 h-4 flex items-center justify-center hover:bg-muted/50 rounded transition-all duration-150 text-muted-foreground hover:text-foreground opacity-0 group-hover/row:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Ensure pointer events are enabled when dropdown opens
|
||||
document.body.style.pointerEvents = 'auto';
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More actions</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-56 rounded-lg"
|
||||
side={isMobile ? 'bottom' : 'right'}
|
||||
align={isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => {
|
||||
setSelectedItem({ threadId: thread?.threadId, projectId: thread?.projectId })
|
||||
setShowShareModal(true)
|
||||
}}>
|
||||
<Share2 className="text-muted-foreground" />
|
||||
<span>Share Chat</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<a
|
||||
href={thread.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRight className="text-muted-foreground" />
|
||||
<span>Open in New Tab</span>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleDeleteThread(
|
||||
thread.threadId,
|
||||
thread.projectName,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 className="text-muted-foreground" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<ThreadItem
|
||||
key={`thread-${thread.threadId}`}
|
||||
thread={thread}
|
||||
isActive={isActive}
|
||||
isThreadLoading={isThreadLoading}
|
||||
isSelected={isSelected}
|
||||
selectedThreads={selectedThreads}
|
||||
loadingThreadId={loadingThreadId}
|
||||
pathname={pathname}
|
||||
isMobile={isMobile}
|
||||
handleThreadClick={handleThreadClick}
|
||||
toggleThreadSelection={toggleThreadSelection}
|
||||
handleDeleteThread={handleDeleteThread}
|
||||
setSelectedItem={setSelectedItem}
|
||||
setShowShareModal={setShowShareModal}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<SidebarMenuItem>
|
||||
|
|
|
@ -132,4 +132,45 @@ export const sortThreads = (
|
|||
return [...threadsList].sort((a, b) => {
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
};
|
||||
|
||||
export type GroupedThreads = {
|
||||
[dateGroup: string]: ThreadWithProject[];
|
||||
};
|
||||
|
||||
export const groupThreadsByDate = (
|
||||
threadsList: ThreadWithProject[]
|
||||
): GroupedThreads => {
|
||||
const sortedThreads = sortThreads(threadsList);
|
||||
const grouped: GroupedThreads = {};
|
||||
const now = new Date();
|
||||
|
||||
sortedThreads.forEach(thread => {
|
||||
const threadDate = new Date(thread.updatedAt);
|
||||
const diffInMs = now.getTime() - threadDate.getTime();
|
||||
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
let dateGroup: string;
|
||||
|
||||
if (diffInDays === 0) {
|
||||
dateGroup = 'Today';
|
||||
} else if (diffInDays === 1) {
|
||||
dateGroup = 'Yesterday';
|
||||
} else if (diffInDays <= 7) {
|
||||
dateGroup = 'This Week';
|
||||
} else if (diffInDays <= 30) {
|
||||
dateGroup = 'This Month';
|
||||
} else if (diffInDays <= 90) {
|
||||
dateGroup = 'Last 3 Months';
|
||||
} else {
|
||||
dateGroup = 'Older';
|
||||
}
|
||||
|
||||
if (!grouped[dateGroup]) {
|
||||
grouped[dateGroup] = [];
|
||||
}
|
||||
grouped[dateGroup].push(thread);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
};
|
Loading…
Reference in New Issue