add delete chat mutation

This commit is contained in:
thierryverse 2025-11-12 14:12:07 +02:00
parent bd4e5d627d
commit b866170e6a
11 changed files with 486 additions and 445 deletions

View file

@ -2,491 +2,511 @@
import { format } from "date-fns";
import {
Calendar,
ExternalLink,
MessageCircleMore,
MoreHorizontal,
Search,
Tag,
Trash2,
Calendar,
ExternalLink,
MessageCircleMore,
MoreHorizontal,
Search,
Tag,
Trash2,
} from "lucide-react";
import { AnimatePresence, motion, type Variants } from "motion/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { useAtomValue } from "jotai";
import { activeSearchSpaceChatsAtom } from "@/atoms/chats/queries/active-search-space-chats.atom";
import { useAtom, useAtomValue } from "jotai";
import { activeSearchSpaceChatsAtom } from "@/atoms/chats/queries/active-search-space-chats.query.atom";
import { deleteChatMutationAtom } from "@/atoms/chats/mutations/delete-chat.mutation.atom";
export interface Chat {
created_at: string;
id: number;
type: "DOCUMENT" | "CHAT";
title: string;
search_space_id: number;
state_version: number;
created_at: string;
id: number;
type: "DOCUMENT" | "CHAT";
title: string;
search_space_id: number;
state_version: number;
}
export interface ChatDetails {
type: "DOCUMENT" | "CHAT";
title: string;
initial_connectors: string[];
messages: any[];
created_at: string;
id: number;
search_space_id: number;
state_version: number;
type: "DOCUMENT" | "CHAT";
title: string;
initial_connectors: string[];
messages: any[];
created_at: string;
id: number;
search_space_id: number;
state_version: number;
}
interface ChatsPageClientProps {
searchSpaceId: string;
searchSpaceId: string;
}
const pageVariants: Variants = {
initial: { opacity: 0 },
enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } },
exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } },
initial: { opacity: 0 },
enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } },
exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } },
};
const chatCardVariants: Variants = {
initial: { y: 20, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -20, opacity: 0 },
initial: { y: 20, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -20, opacity: 0 },
};
const MotionCard = motion(Card);
export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) {
const router = useRouter();
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedType, setSelectedType] = useState<string>("all");
const [sortOrder, setSortOrder] = useState<string>("newest");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; title: string } | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const {isFetching , data : chats, error} = useAtomValue(activeSearchSpaceChatsAtom);
export default function ChatsPageClient({
searchSpaceId,
}: ChatsPageClientProps) {
const router = useRouter();
const [filteredChats, setFilteredChats] = useState<Chat[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [selectedType, setSelectedType] = useState<string>("all");
const [sortOrder, setSortOrder] = useState<string>("newest");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{
id: number;
title: string;
} | null>(null);
const {
isFetching: isFetchingChats,
data: chats,
error: fetchError,
} = useAtomValue(activeSearchSpaceChatsAtom);
const [
{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }
] = useAtom(deleteChatMutationAtom);
const chatsPerPage = 9;
const searchParams = useSearchParams();
const chatsPerPage = 9;
const searchParams = useSearchParams();
// Get initial page from URL params if it exists
useEffect(() => {
const pageParam = searchParams.get("page");
if (pageParam) {
const pageNumber = parseInt(pageParam, 10);
if (!Number.isNaN(pageNumber) && pageNumber > 0) {
setCurrentPage(pageNumber);
}
}
}, [searchParams]);
// Get initial page from URL params if it exists
useEffect(() => {
const pageParam = searchParams.get("page");
if (pageParam) {
const pageNumber = parseInt(pageParam, 10);
if (!Number.isNaN(pageNumber) && pageNumber > 0) {
setCurrentPage(pageNumber);
}
}
}, [searchParams]);
useEffect(() => {
if (fetchError) {
console.error("Error fetching chats:", fetchError);
}
}, [fetchError]);
useEffect(() => {
if (error) {
console.error("Error fetching chats:", error);
}
}, [error]);
useEffect(() => {
if (deleteError) {
console.error("Error deleting chat:", deleteError);
}
}, [deleteError]);
// Filter and sort chats based on search query, type, and sort order
useEffect(() => {
let result = [...(chats || [])];
// Filter and sort chats based on search query, type, and sort order
useEffect(() => {
let result = [...(chats || [])];
// Filter by search term
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter((chat) => chat.title.toLowerCase().includes(query));
}
// Filter by search term
if (searchQuery) {
const query = searchQuery.toLowerCase();
result = result.filter((chat) =>
chat.title.toLowerCase().includes(query)
);
}
// Filter by type
if (selectedType !== "all") {
result = result.filter((chat) => chat.type === selectedType);
}
// Filter by type
if (selectedType !== "all") {
result = result.filter((chat) => chat.type === selectedType);
}
// Sort chats
result.sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
// Sort chats
result.sort((a, b) => {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
return sortOrder === "newest" ? dateB - dateA : dateA - dateB;
});
return sortOrder === "newest" ? dateB - dateA : dateA - dateB;
});
setFilteredChats(result);
setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage)));
setFilteredChats(result);
setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage)));
// Reset to first page when filters change
if (currentPage !== 1 && (searchQuery || selectedType !== "all" || sortOrder !== "newest")) {
setCurrentPage(1);
}
}, [chats, searchQuery, selectedType, sortOrder, currentPage]);
// Reset to first page when filters change
if (
currentPage !== 1 &&
(searchQuery || selectedType !== "all" || sortOrder !== "newest")
) {
setCurrentPage(1);
}
}, [chats, searchQuery, selectedType, sortOrder, currentPage]);
// Function to handle chat deletion
const handleDeleteChat = async () => {
// if (!chatToDelete) return;
// Function to handle chat deletion
const handleDeleteChat = async () => {
if (!chatToDelete) return;
// setIsDeleting(true);
// try {
// const token = localStorage.getItem("surfsense_bearer_token");
// if (!token) {
// setIsDeleting(false);
// return;
// }
await deleteChat(chatToDelete.id);
// const response = await fetch(
// `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatToDelete.id}`,
// {
// method: "DELETE",
// headers: {
// Authorization: `Bearer ${token}`,
// "Content-Type": "application/json",
// },
// }
// );
setDeleteDialogOpen(false);
setChatToDelete(null);
};
// if (!response.ok) {
// throw new Error(`Failed to delete chat: ${response.statusText}`);
// }
// Calculate pagination
const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page
const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page
const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat);
// // Close dialog and refresh chats
// setDeleteDialogOpen(false);
// setChatToDelete(null);
// Get unique chat types for filter dropdown
const chatTypes = chats
? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))]
: [];
// // Update local state by removing the deleted chat
// setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatToDelete.id));
// } catch (error) {
// console.error("Error deleting chat:", error);
// } finally {
// setIsDeleting(false);
// }
};
return (
<motion.div
className="container p-6 mx-auto"
initial="initial"
animate="enter"
exit="exit"
variants={pageVariants}
>
<div className="flex flex-col space-y-4 md:space-y-6">
<div className="flex flex-col space-y-2">
<h1 className="text-3xl font-bold tracking-tight">All Chats</h1>
<p className="text-muted-foreground">
View, search, and manage all your chats.
</p>
</div>
// Calculate pagination
const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page
const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page
const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat);
{/* Filter and Search Bar */}
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
<div className="flex flex-1 items-center gap-2">
<div className="relative w-full md:w-80">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search chats..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
// Get unique chat types for filter dropdown
const chatTypes = chats ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] : [];
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-full md:w-40">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{chatTypes.map((type) => (
<SelectItem key={type} value={type}>
{type === "all"
? "All Types"
: type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
return (
<motion.div
className="container p-6 mx-auto"
initial="initial"
animate="enter"
exit="exit"
variants={pageVariants}
>
<div className="flex flex-col space-y-4 md:space-y-6">
<div className="flex flex-col space-y-2">
<h1 className="text-3xl font-bold tracking-tight">All Chats</h1>
<p className="text-muted-foreground">View, search, and manage all your chats.</p>
</div>
<div className="flex items-center gap-2">
<Select value={sortOrder} onValueChange={setSortOrder}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Sort order" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="newest">Newest First</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
{/* Filter and Search Bar */}
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:justify-between md:space-y-0">
<div className="flex flex-1 items-center gap-2">
<div className="relative w-full md:w-80">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder="Search chats..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{/* Status Messages */}
{isFetchingChats && (
<div className="flex items-center justify-center h-40">
<div className="flex flex-col items-center gap-2">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<p className="text-sm text-muted-foreground">Loading chats...</p>
</div>
</div>
)}
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-full md:w-40">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{chatTypes.map((type) => (
<SelectItem key={type} value={type}>
{type === "all" ? "All Types" : type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
{fetchError && !isFetchingChats && (
<div className="border border-destructive/50 text-destructive p-4 rounded-md">
<h3 className="font-medium">Error loading chats</h3>
<p className="text-sm">{fetchError.message}</p>
</div>
)}
<div className="flex items-center gap-2">
<Select value={sortOrder} onValueChange={setSortOrder}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Sort order" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="newest">Newest First</SelectItem>
<SelectItem value="oldest">Oldest First</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
{!isFetchingChats && !fetchError && filteredChats.length === 0 && (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-center">
<MessageCircleMore className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No chats found</h3>
<p className="text-sm text-muted-foreground">
{searchQuery || selectedType !== "all"
? "Try adjusting your search filters"
: "Start a new chat to get started"}
</p>
</div>
)}
{/* Status Messages */}
{isFetching && (
<div className="flex items-center justify-center h-40">
<div className="flex flex-col items-center gap-2">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
<p className="text-sm text-muted-foreground">Loading chats...</p>
</div>
</div>
)}
{/* Chat Grid */}
{!isFetchingChats && !fetchError && filteredChats.length > 0 && (
<AnimatePresence mode="wait">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{currentChats.map((chat, index) => (
<MotionCard
key={chat.id}
variants={chatCardVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.2, delay: index * 0.05 }}
className="overflow-hidden hover:shadow-md transition-shadow"
>
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<div className="space-y-1">
<CardTitle className="line-clamp-1">
{chat.title || `Chat ${chat.id}`}
</CardTitle>
<CardDescription>
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
<span>
{format(new Date(chat.created_at), "MMM d, yyyy")}
</span>
</span>
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
}
>
<ExternalLink className="mr-2 h-4 w-4" />
<span>View Chat</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation();
setChatToDelete({
id: chat.id,
title: chat.title || `Chat ${chat.id}`,
});
setDeleteDialogOpen(true);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
{error && !isFetching && (
<div className="border border-destructive/50 text-destructive p-4 rounded-md">
<h3 className="font-medium">Error loading chats</h3>
<p className="text-sm">{error.message}</p>
</div>
)}
<CardFooter className="flex items-center justify-between gap-2 w-full">
<Badge variant="secondary" className="text-xs">
<Tag className="mr-1 h-3 w-3" />
{chat.type || "Unknown"}
</Badge>
<Button
size="sm"
onClick={() =>
router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
}
>
<MessageCircleMore className="h-4 w-4" />
<span>View Chat</span>
</Button>
</CardFooter>
</MotionCard>
))}
</div>
</AnimatePresence>
)}
{!isFetching && !error && filteredChats.length === 0 && (
<div className="flex flex-col items-center justify-center h-40 gap-2 text-center">
<MessageCircleMore className="h-8 w-8 text-muted-foreground" />
<h3 className="font-medium">No chats found</h3>
<p className="text-sm text-muted-foreground">
{searchQuery || selectedType !== "all"
? "Try adjusting your search filters"
: "Start a new chat to get started"}
</p>
</div>
)}
{/* Pagination */}
{!isFetchingChats && !fetchError && totalPages > 1 && (
<Pagination className="mt-8">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${Math.max(1, currentPage - 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={
currentPage <= 1 ? "pointer-events-none opacity-50" : ""
}
/>
</PaginationItem>
{/* Chat Grid */}
{!isFetching && !error && filteredChats.length > 0 && (
<AnimatePresence mode="wait">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{currentChats.map((chat, index) => (
<MotionCard
key={chat.id}
variants={chatCardVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.2, delay: index * 0.05 }}
className="overflow-hidden hover:shadow-md transition-shadow"
>
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<div className="space-y-1">
<CardTitle className="line-clamp-1">
{chat.title || `Chat ${chat.id}`}
</CardTitle>
<CardDescription>
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
<span>{format(new Date(chat.created_at), "MMM d, yyyy")}</span>
</span>
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() =>
router.push(
`/dashboard/${chat.search_space_id}/researcher/${chat.id}`
)
}
>
<ExternalLink className="mr-2 h-4 w-4" />
<span>View Chat</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={(e) => {
e.stopPropagation();
setChatToDelete({
id: chat.id,
title: chat.title || `Chat ${chat.id}`,
});
setDeleteDialogOpen(true);
}}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete Chat</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
{Array.from({ length: totalPages }).map((_, index) => {
const pageNumber = index + 1;
const isVisible =
pageNumber === 1 ||
pageNumber === totalPages ||
(pageNumber >= currentPage - 1 &&
pageNumber <= currentPage + 1);
<CardFooter className="flex items-center justify-between gap-2 w-full">
<Badge variant="secondary" className="text-xs">
<Tag className="mr-1 h-3 w-3" />
{chat.type || "Unknown"}
</Badge>
<Button
size="sm"
onClick={() =>
router.push(`/dashboard/${chat.search_space_id}/researcher/${chat.id}`)
}
>
<MessageCircleMore className="h-4 w-4" />
<span>View Chat</span>
</Button>
</CardFooter>
</MotionCard>
))}
</div>
</AnimatePresence>
)}
if (!isVisible) {
// Show ellipsis at appropriate positions
if (pageNumber === 2 || pageNumber === totalPages - 1) {
return (
<PaginationItem key={pageNumber}>
<span className="flex h-9 w-9 items-center justify-center">
...
</span>
</PaginationItem>
);
}
return null;
}
{/* Pagination */}
{!isFetching && !error && totalPages > 1 && (
<Pagination className="mt-8">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={`?page=${Math.max(1, currentPage - 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setCurrentPage(currentPage - 1);
}}
className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
return (
<PaginationItem key={pageNumber}>
<PaginationLink
href={`?page=${pageNumber}`}
onClick={(e) => {
e.preventDefault();
setCurrentPage(pageNumber);
}}
isActive={pageNumber === currentPage}
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
{Array.from({ length: totalPages }).map((_, index) => {
const pageNumber = index + 1;
const isVisible =
pageNumber === 1 ||
pageNumber === totalPages ||
(pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1);
<PaginationItem>
<PaginationNext
href={`?page=${Math.min(totalPages, currentPage + 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages)
setCurrentPage(currentPage + 1);
}}
className={
currentPage >= totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
if (!isVisible) {
// Show ellipsis at appropriate positions
if (pageNumber === 2 || pageNumber === totalPages - 1) {
return (
<PaginationItem key={pageNumber}>
<span className="flex h-9 w-9 items-center justify-center">...</span>
</PaginationItem>
);
}
return null;
}
return (
<PaginationItem key={pageNumber}>
<PaginationLink
href={`?page=${pageNumber}`}
onClick={(e) => {
e.preventDefault();
setCurrentPage(pageNumber);
}}
isActive={pageNumber === currentPage}
>
{pageNumber}
</PaginationLink>
</PaginationItem>
);
})}
<PaginationItem>
<PaginationNext
href={`?page=${Math.min(totalPages, currentPage + 1)}`}
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
}}
className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
<span>Delete Chat</span>
</DialogTitle>
<DialogDescription>
Are you sure you want to delete{" "}
<span className="font-medium">{chatToDelete?.title}</span>? This action cannot be
undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={isDeleting}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeleting}
className="gap-2"
>
{isDeleting ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Deleting...
</>
) : (
<>
<Trash2 className="h-4 w-4" />
Delete
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</motion.div>
);
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
<span>Delete Chat</span>
</DialogTitle>
<DialogDescription>
Are you sure you want to delete{" "}
<span className="font-medium">{chatToDelete?.title}</span>? This
action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-2 sm:justify-end">
<Button
variant="outline"
onClick={() => setDeleteDialogOpen(false)}
disabled={isDeletingChat}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleDeleteChat}
disabled={isDeletingChat}
className="gap-2"
>
{isDeletingChat ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
Deleting...
</>
) : (
<>
<Trash2 className="h-4 w-4" />
Delete
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</motion.div>
);
}

View file

@ -27,8 +27,8 @@ import {
} from "@/components/ui/sidebar";
import { useLLMPreferences } from "@/hooks/use-llm-configs";
import { cn } from "@/lib/utils";
import { activeChatIdAtom } from "@/atoms/chats/queries/active-chat.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat-ui.atom";
import { activeChatIdAtom } from "@/atoms/chats/queries/active-chat.query.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat.atom";
import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/active-seach-space.atom";
export function DashboardClientLayout({

View file

@ -0,0 +1,33 @@
import { atomWithMutation } from "jotai-tanstack-query";
import { deleteChat } from "@/lib/apis/chat-apis";
import { activeSearchSpaceIdAtom } from "../../seach-spaces/active-seach-space.atom";
import { queryClient } from "@/lib/query-client/client";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { toast } from "sonner";
export const deleteChatMutationAtom = atomWithMutation((get) => {
const searchSpaceId = get(activeSearchSpaceIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
return {
mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken,
mutationFn: async (chatId: number) => {
if (!authToken) {
throw new Error("No authentication token found");
}
if (!searchSpaceId) {
throw new Error("No search space id found");
}
return deleteChat(chatId, authToken);
},
onSuccess: () => {
toast.success("Chat deleted successfully");
queryClient.invalidateQueries({
queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId!),
});
},
};
});

View file

@ -18,18 +18,8 @@ export const activeChatAtom = atomWithQuery<ActiveChatState>((get) => {
const activeChatId = get(activeChatIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
if (!activeChatId) {
return {
queryKey: [],
enabled: false,
queryFn: async () => {
return { chatId: null, chatDetails: null, podcast: null };
},
};
}
return {
queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId),
queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""),
enabled: !!activeChatId && !!authToken,
queryFn: async () => {
if (!authToken) {

View file

@ -1,13 +1,14 @@
import { atomWithQuery } from "jotai-tanstack-query";
import { fetchChatsBySearchSpace } from "@/lib/apis/chat-apis";
import { activeSearchSpaceIdAtom } from "../../seach-spaces/active-seach-space.atom";
import { cacheKeys } from "@/lib/query-client/cache-keys";
export const activeSearchSpaceChatsAtom = atomWithQuery((get) => {
const searchSpaceId = get(activeSearchSpaceIdAtom);
const authToken = localStorage.getItem("surfsense_bearer_token");
return {
queryKey: ["chatsBySearchSpace", searchSpaceId],
queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
enabled: !!searchSpaceId && !!authToken,
queryFn: async () => {
if (!authToken) {

View file

@ -4,8 +4,8 @@ import { LoaderIcon, PanelRight, TriangleAlert } from "lucide-react";
import { toast } from "sonner";
import { generatePodcast } from "@/lib/apis/podcast-apis";
import { cn } from "@/lib/utils";
import { activeChatAtom, activeChatIdAtom } from "@/atoms/chats/queries/active-chat.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat-ui.atom";
import { activeChatAtom, activeChatIdAtom } from "@/atoms/chats/queries/active-chat.query.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat.atom";
import { ChatPanelView } from "./ChatPanelView";
export interface GeneratePodcastRequest {

View file

@ -5,8 +5,8 @@ import { AlertCircle, Play, RefreshCw, Sparkles } from "lucide-react";
import { motion } from "motion/react";
import { useCallback } from "react";
import { cn } from "@/lib/utils";
import { activeChatAtom } from "@/atoms/chats/queries/active-chat.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat-ui.atom";
import { activeChatAtom } from "@/atoms/chats/queries/active-chat.query.atom";
import { chatUIAtom } from "@/atoms/chats/active-chat.atom";
import { getPodcastStalenessMessage, isPodcastStale } from "../PodcastUtils";
import type { GeneratePodcastRequest } from "./ChatPanelContainer";
import { ConfigModal } from "./ConfigModal";

View file

@ -4,7 +4,7 @@ import { useAtomValue } from "jotai";
import { Pencil } from "lucide-react";
import { useCallback, useContext, useState } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { activeChatAtom } from "@/atoms/chats/queries/active-chat.atom";
import { activeChatAtom } from "@/atoms/chats/queries/active-chat.query.atom";
import type { GeneratePodcastRequest } from "./ChatPanelContainer";
interface ConfigModalProps {

View file

@ -72,9 +72,7 @@ export const deleteChat = async (chatId: number, authToken: string) => {
throw new Error(`Failed to delete chat: ${response.statusText}`);
}
return true;
} catch (err) {
console.error("Error deleting chat:", err);
return false;
}
};

View file

@ -2,6 +2,5 @@ export const cacheKeys = {
activeSearchSpace: {
chats : (searchSpaceId: string) => ["active-search-space", "chats", searchSpaceId] as const,
activeChat : (chatId: string) => ["active-search-space", "active-chat", chatId] as const,
deleteChat : ( searchSpaceId: string, chatId: string) => ["active-search-space", "chats", searchSpaceId, "delete", chatId] as const,
},
};