mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 17:56:25 +02:00
refactor: remove old chat components and implement new sidebar structure
- Deleted `chats-client.tsx` and `page.tsx` as part of the chat management overhaul. - Introduced `AllChatsSidebar` and `NavChats` components for improved chat navigation and management. - Updated sidebar to integrate new chat components and removed the deprecated `NavProjects`. - Enhanced chat deletion handling and added loading states for better user experience. - Added new translation keys for chat-related UI strings.
This commit is contained in:
parent
90b4ce6e43
commit
79e552520a
11 changed files with 500 additions and 672 deletions
241
surfsense_web/components/sidebar/all-chats-sidebar.tsx
Normal file
241
surfsense_web/components/sidebar/all-chats-sidebar.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Loader2, MessageCircleMore, MoreHorizontal, Search, Trash2, X } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
||||
import { chatsApiService } from "@/lib/apis/chats-api.service";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AllChatsSidebarProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
searchSpaceId: string;
|
||||
}
|
||||
|
||||
export function AllChatsSidebar({
|
||||
open,
|
||||
onOpenChange,
|
||||
searchSpaceId,
|
||||
}: AllChatsSidebarProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [deletingChatId, setDeletingChatId] = useState<number | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
||||
|
||||
// Fetch all chats
|
||||
const {
|
||||
data: chatsData,
|
||||
error: chatsError,
|
||||
isLoading: isLoadingChats,
|
||||
} = useQuery({
|
||||
queryKey: ["all-chats", searchSpaceId],
|
||||
queryFn: () =>
|
||||
chatsApiService.getChats({
|
||||
queryParams: {
|
||||
search_space_id: Number(searchSpaceId),
|
||||
},
|
||||
}),
|
||||
enabled: !!searchSpaceId && open,
|
||||
});
|
||||
|
||||
// Handle chat navigation
|
||||
const handleChatClick = useCallback(
|
||||
(chatId: number, chatSearchSpaceId: number) => {
|
||||
router.push(`/dashboard/${chatSearchSpaceId}/researcher/${chatId}`);
|
||||
onOpenChange(false);
|
||||
},
|
||||
[router, onOpenChange]
|
||||
);
|
||||
|
||||
// Handle chat deletion
|
||||
const handleDeleteChat = useCallback(
|
||||
async (chatId: number) => {
|
||||
setDeletingChatId(chatId);
|
||||
try {
|
||||
await chatsApiService.deleteChat({ id: chatId });
|
||||
toast.success(t("chat_deleted") || "Chat deleted successfully");
|
||||
// Invalidate queries to refresh the list
|
||||
queryClient.invalidateQueries({ queryKey: ["all-chats", searchSpaceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["chats"] });
|
||||
} catch (error) {
|
||||
console.error("Error deleting chat:", error);
|
||||
toast.error(t("error_deleting_chat") || "Failed to delete chat");
|
||||
} finally {
|
||||
setDeletingChatId(null);
|
||||
}
|
||||
},
|
||||
[queryClient, searchSpaceId, t]
|
||||
);
|
||||
|
||||
// Clear search
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchQuery("");
|
||||
}, []);
|
||||
|
||||
// Filter chats based on search query (client-side filtering)
|
||||
const chats = useMemo(() => {
|
||||
const allChats = chatsData ?? [];
|
||||
if (!debouncedSearchQuery) {
|
||||
return allChats;
|
||||
}
|
||||
const query = debouncedSearchQuery.toLowerCase();
|
||||
return allChats.filter((chat) =>
|
||||
chat.title.toLowerCase().includes(query)
|
||||
);
|
||||
}, [chatsData, debouncedSearchQuery]);
|
||||
|
||||
const isSearchMode = !!debouncedSearchQuery;
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="left" className="w-80 p-0 flex flex-col">
|
||||
<SheetHeader className="px-4 py-4 border-b space-y-3">
|
||||
<SheetTitle>{t("all_chats") || "All Chats"}</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
{t("all_chats_description") || "Browse and manage all your chats"}
|
||||
</SheetDescription>
|
||||
|
||||
{/* Search Input */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("search_chats") || "Search chats..."}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-9 pr-8 h-9"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
||||
onClick={handleClearSearch}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
<span className="sr-only">Clear search</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2">
|
||||
{isLoadingChats ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : chatsError ? (
|
||||
<div className="text-center py-8 text-sm text-destructive">
|
||||
{t("error_loading_chats") || "Error loading chats"}
|
||||
</div>
|
||||
) : chats.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{chats.map((chat) => {
|
||||
const isDeleting = deletingChatId === chat.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={chat.id}
|
||||
className={cn(
|
||||
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
||||
"hover:bg-accent hover:text-accent-foreground",
|
||||
"transition-colors cursor-pointer",
|
||||
isDeleting && "opacity-50 pointer-events-none"
|
||||
)}
|
||||
>
|
||||
{/* Main clickable area for navigation */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleChatClick(chat.id, chat.search_space_id)}
|
||||
disabled={isDeleting}
|
||||
className="flex items-center gap-2 flex-1 min-w-0 text-left"
|
||||
>
|
||||
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span className="truncate">{chat.title}</span>
|
||||
</button>
|
||||
|
||||
{/* Actions dropdown - separate from main click area */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6 shrink-0",
|
||||
"opacity-0 group-hover:opacity-100 focus:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className="sr-only">More options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDeleteChat(chat.id)}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : isSearchMode ? (
|
||||
<div className="text-center py-8">
|
||||
<Search className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("no_chats_found") || "No chats found"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
{t("try_different_search") || "Try a different search term"}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<MessageCircleMore className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("no_chats") || "No chats yet"}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-1">
|
||||
{t("start_new_chat_hint") || "Start a new chat from the researcher"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
|
@ -113,9 +113,9 @@ function UserAvatar({ email, size = 32 }: { email: string; size?: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
import { NavChats } from "@/components/sidebar/nav-chats";
|
||||
import { NavMain } from "@/components/sidebar/nav-main";
|
||||
import { NavNotes } from "@/components/sidebar/nav-notes";
|
||||
import { NavProjects } from "@/components/sidebar/nav-projects";
|
||||
import { NavSecondary } from "@/components/sidebar/nav-secondary";
|
||||
import { PageUsageDisplay } from "@/components/sidebar/page-usage-display";
|
||||
import {
|
||||
|
|
@ -446,11 +446,12 @@ export const AppSidebar = memo(function AppSidebar({
|
|||
<SidebarContent className="space-y-6">
|
||||
<NavMain items={processedNavMain} />
|
||||
|
||||
{processedRecentChats.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<NavProjects chats={processedRecentChats} />
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<NavChats
|
||||
chats={processedRecentChats}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<NavNotes
|
||||
|
|
|
|||
230
surfsense_web/components/sidebar/nav-chats.tsx
Normal file
230
surfsense_web/components/sidebar/nav-chats.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ChevronRight,
|
||||
FolderOpen,
|
||||
type LucideIcon,
|
||||
Loader2,
|
||||
MessageCircleMore,
|
||||
MoreHorizontal,
|
||||
Search,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AllChatsSidebar } from "./all-chats-sidebar";
|
||||
|
||||
interface ChatAction {
|
||||
name: string;
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ChatItem {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
id?: number;
|
||||
search_space_id?: number;
|
||||
actions?: ChatAction[];
|
||||
}
|
||||
|
||||
interface NavChatsProps {
|
||||
chats: ChatItem[];
|
||||
defaultOpen?: boolean;
|
||||
searchSpaceId?: string;
|
||||
}
|
||||
|
||||
// Map of icon names to their components
|
||||
const actionIconMap: Record<string, LucideIcon> = {
|
||||
MessageCircleMore,
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
};
|
||||
|
||||
export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const router = useRouter();
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
|
||||
|
||||
// Handle chat deletion with loading state
|
||||
const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => {
|
||||
setIsDeleting(chatId);
|
||||
try {
|
||||
await deleteAction();
|
||||
} finally {
|
||||
setIsDeleting(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle chat navigation
|
||||
const handleChatClick = useCallback(
|
||||
(url: string) => {
|
||||
router.push(url);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<div className="flex items-center group/header">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarGroupLabel className="cursor-pointer rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center gap-1.5 flex-1">
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 text-muted-foreground transition-all duration-200 shrink-0",
|
||||
isOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
<span>{t("recent_chats") || "Recent Chats"}</span>
|
||||
</SidebarGroupLabel>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
{/* Action buttons - always visible on hover */}
|
||||
<div className="flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity pr-1">
|
||||
{searchSpaceId && chats.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsAllChatsSidebarOpen(true);
|
||||
}}
|
||||
aria-label="View all chats"
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{chats.length > 0 ? (
|
||||
chats.map((chat) => {
|
||||
const isDeletingChat = isDeleting === chat.id;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={chat.id || chat.name} className="group/chat">
|
||||
{/* Main navigation button */}
|
||||
<SidebarMenuButton
|
||||
onClick={() => handleChatClick(chat.url)}
|
||||
disabled={isDeletingChat}
|
||||
className={cn(
|
||||
"pr-8", // Make room for the action button
|
||||
isDeletingChat && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<chat.icon className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{chat.name}</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
{/* Actions dropdown - positioned absolutely */}
|
||||
{chat.actions && chat.actions.length > 0 && (
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"h-6 w-6",
|
||||
"opacity-0 group-hover/chat:opacity-100 focus:opacity-100",
|
||||
"data-[state=open]:opacity-100",
|
||||
"transition-opacity"
|
||||
)}
|
||||
disabled={isDeletingChat}
|
||||
>
|
||||
{isDeletingChat ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span className="sr-only">More options</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" side="right" className="w-40">
|
||||
{chat.actions.map((action, actionIndex) => {
|
||||
const ActionIcon = actionIconMap[action.icon] || MessageCircleMore;
|
||||
const isDeleteAction = action.name.toLowerCase().includes("delete");
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${action.name}-${actionIndex}`}
|
||||
onClick={() => {
|
||||
if (isDeleteAction) {
|
||||
handleDeleteChat(chat.id || 0, action.onClick);
|
||||
} else {
|
||||
action.onClick();
|
||||
}
|
||||
}}
|
||||
disabled={isDeletingChat}
|
||||
className={
|
||||
isDeleteAction
|
||||
? "text-destructive focus:text-destructive"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<ActionIcon className="mr-2 h-4 w-4" />
|
||||
<span>
|
||||
{isDeletingChat && isDeleteAction
|
||||
? "Deleting..."
|
||||
: action.name}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton disabled className="text-muted-foreground text-xs">
|
||||
<Search className="h-4 w-4" />
|
||||
<span>{t("no_recent_chats") || "No recent chats"}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* All Chats Sheet */}
|
||||
{searchSpaceId && (
|
||||
<AllChatsSidebar
|
||||
open={isAllChatsSidebarOpen}
|
||||
onOpenChange={setIsAllChatsSidebarOpen}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
)}
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ExternalLink,
|
||||
Folder,
|
||||
type LucideIcon,
|
||||
MoreHorizontal,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Share,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarInput,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
|
||||
// Map of icon names to their components
|
||||
const actionIconMap: Record<string, LucideIcon> = {
|
||||
ExternalLink,
|
||||
Folder,
|
||||
Share,
|
||||
Trash2,
|
||||
MoreHorizontal,
|
||||
Search,
|
||||
RefreshCw,
|
||||
};
|
||||
|
||||
interface ChatAction {
|
||||
name: string;
|
||||
icon: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface ChatItem {
|
||||
name: string;
|
||||
url: string;
|
||||
icon: LucideIcon;
|
||||
id?: number;
|
||||
search_space_id?: number;
|
||||
actions?: ChatAction[];
|
||||
}
|
||||
|
||||
export function NavProjects({ chats }: { chats: ChatItem[] }) {
|
||||
const t = useTranslations("sidebar");
|
||||
const { isMobile } = useSidebar();
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isDeleting, setIsDeleting] = useState<number | null>(null);
|
||||
|
||||
const searchSpaceId = chats[0]?.search_space_id || "";
|
||||
|
||||
// Memoized filtered chats
|
||||
const filteredChats = useMemo(() => {
|
||||
if (!searchQuery.trim()) return chats;
|
||||
|
||||
return chats.filter((chat) => chat.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
}, [chats, searchQuery]);
|
||||
|
||||
// Handle chat deletion with loading state
|
||||
const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => {
|
||||
setIsDeleting(chatId);
|
||||
try {
|
||||
await deleteAction();
|
||||
} finally {
|
||||
setIsDeleting(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Enhanced chat item component
|
||||
const ChatItemComponent = useCallback(
|
||||
({ chat }: { chat: ChatItem }) => {
|
||||
const isDeletingChat = isDeleting === chat.id;
|
||||
|
||||
return (
|
||||
<SidebarMenuItem key={chat.id ? `chat-${chat.id}` : `chat-${chat.name}`}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => router.push(chat.url)}
|
||||
disabled={isDeletingChat}
|
||||
className={isDeletingChat ? "opacity-50" : ""}
|
||||
>
|
||||
<chat.icon />
|
||||
<span className={isDeletingChat ? "opacity-50" : ""}>{chat.name}</span>
|
||||
</SidebarMenuButton>
|
||||
|
||||
{chat.actions && chat.actions.length > 0 && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction showOnHover>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">More</span>
|
||||
</SidebarMenuAction>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-48"
|
||||
side={isMobile ? "bottom" : "right"}
|
||||
align={isMobile ? "end" : "start"}
|
||||
>
|
||||
{chat.actions.map((action, actionIndex) => {
|
||||
const ActionIcon = actionIconMap[action.icon] || Folder;
|
||||
const isDeleteAction = action.name.toLowerCase().includes("delete");
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={`${action.name}-${actionIndex}`}
|
||||
onClick={() => {
|
||||
if (isDeleteAction) {
|
||||
handleDeleteChat(chat.id || 0, action.onClick);
|
||||
} else {
|
||||
action.onClick();
|
||||
}
|
||||
}}
|
||||
disabled={isDeletingChat}
|
||||
className={isDeleteAction ? "text-destructive" : ""}
|
||||
>
|
||||
<ActionIcon className="text-muted-foreground" />
|
||||
<span>{isDeletingChat && isDeleteAction ? "Deleting..." : action.name}</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
},
|
||||
[isDeleting, router, isMobile, handleDeleteChat]
|
||||
);
|
||||
|
||||
// Show search input if there are chats
|
||||
const showSearch = chats.length > 0;
|
||||
|
||||
return (
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>{t("recent_chats")}</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
{/* Chat Items */}
|
||||
{filteredChats.length > 0 ? (
|
||||
filteredChats.map((chat) => <ChatItemComponent key={chat.id || chat.name} chat={chat} />)
|
||||
) : (
|
||||
/* No results state */
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton disabled className="text-muted-foreground">
|
||||
<Search className="h-4 w-4" />
|
||||
<span>{searchQuery ? t("no_chats_found") : t("no_recent_chats")}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
|
||||
{/* View All Chats */}
|
||||
{chats.length > 0 && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton onClick={() => router.push(`/dashboard/${searchSpaceId}/chats`)}>
|
||||
<MoreHorizontal />
|
||||
<span>{t("view_all_chats")}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue